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, ProcedureStep,
17 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 pub memory_budget_bytes: Option<u64>,
1424 pub temp_disk_budget_bytes: Option<u64>,
1429 pub offline_validation: bool,
1436}
1437
1438impl OpenOptions {
1439 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1442 self.lock_timeout_ms = ms;
1443 self
1444 }
1445
1446 pub fn with_memory_budget_bytes(mut self, bytes: u64) -> Self {
1448 self.memory_budget_bytes = Some(bytes);
1449 self
1450 }
1451
1452 pub fn with_temp_disk_budget_bytes(mut self, bytes: u64) -> Self {
1454 self.temp_disk_budget_bytes = Some(bytes);
1455 self
1456 }
1457
1458 pub fn with_offline_validation(mut self, offline_validation: bool) -> Self {
1461 self.offline_validation = offline_validation;
1462 self
1463 }
1464}
1465
1466#[derive(Debug, Clone)]
1470pub(crate) enum OpenModeGate {
1471 Normal,
1474 OfflineValidation,
1476 ClusterRuntime {
1480 cluster_id: mongreldb_types::ids::ClusterId,
1482 node_id: mongreldb_types::ids::NodeId,
1484 database_id: mongreldb_types::ids::DatabaseId,
1486 },
1487 Create(crate::storage_mode::StorageMode),
1489}
1490
1491pub const DEFAULT_MEMORY_BUDGET_BYTES: u64 = 1024 * 1024 * 1024;
1495
1496pub const DEFAULT_TEMP_DISK_BUDGET_BYTES: u64 = 4 * 1024 * 1024 * 1024;
1499
1500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1504pub(crate) struct CoreResourceConfig {
1505 pub memory_budget_bytes: u64,
1506 pub temp_disk_budget_bytes: u64,
1507}
1508
1509impl Default for CoreResourceConfig {
1510 fn default() -> Self {
1511 Self {
1512 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
1513 temp_disk_budget_bytes: DEFAULT_TEMP_DISK_BUDGET_BYTES,
1514 }
1515 }
1516}
1517
1518impl CoreResourceConfig {
1519 fn from_options(options: &OpenOptions) -> Result<Self> {
1520 let config = Self {
1521 memory_budget_bytes: options
1522 .memory_budget_bytes
1523 .unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES),
1524 temp_disk_budget_bytes: options
1525 .temp_disk_budget_bytes
1526 .unwrap_or(DEFAULT_TEMP_DISK_BUDGET_BYTES),
1527 };
1528 if config.memory_budget_bytes == 0 {
1529 return Err(MongrelError::InvalidArgument(
1530 "memory_budget_bytes must be nonzero".into(),
1531 ));
1532 }
1533 if config.temp_disk_budget_bytes == 0 {
1534 return Err(MongrelError::InvalidArgument(
1535 "temp_disk_budget_bytes must be nonzero".into(),
1536 ));
1537 }
1538 Ok(config)
1539 }
1540}
1541
1542#[derive(Debug, Clone)]
1545pub struct TablePinsReport {
1546 pub table_id: u64,
1548 pub table: String,
1550 pub pins: crate::retention::PinsReport,
1552}
1553
1554pub struct DatabaseCore {
1567 root: PathBuf,
1568 durable_root: Arc<crate::durable_file::DurableRoot>,
1569 lifecycle: Arc<crate::core::LifecycleController>,
1572 registry: std::sync::OnceLock<crate::manager::CoreRegistration>,
1576 read_only: bool,
1578 catalog: RwLock<Catalog>,
1579 security_coordinator: Arc<SecurityCoordinator>,
1580 security_catalog_disk_reads: AtomicU64,
1581 rls_cache: Mutex<RlsCache>,
1582 epoch: Arc<EpochAuthority>,
1583 snapshots: Arc<SnapshotRegistry>,
1584 memory_governor: crate::memory::MemoryGovernor,
1589 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1590 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1591 spill_manager: crate::spill::SpillManager,
1595 resource_groups: crate::resource::ResourceGroupRegistry,
1599 embedding_providers: crate::embedding::EmbeddingProviderRegistry,
1603 job_registry: Arc<crate::jobs::JobRegistry>,
1606 commit_lock: Arc<Mutex<()>>,
1607 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1612 next_txn_id: Arc<Mutex<u64>>,
1616 tables: RwLock<HashMap<u64, TableHandle>>,
1617 kek: Option<Arc<crate::encryption::Kek>>,
1618 ddl_lock: Mutex<()>,
1621 meta_dek: Option<[u8; META_DEK_LEN]>,
1622 spill_threshold: std::sync::atomic::AtomicU64,
1625 conflicts: crate::txn::ConflictIndex,
1628 active_txns: crate::txn::ActiveTxns,
1631 lock_manager: Arc<crate::locks::LockManager>,
1637 poisoned: Arc<std::sync::atomic::AtomicBool>,
1640 group: Arc<crate::txn::GroupCommit>,
1644 commit_log: Arc<crate::commit_log::StandaloneCommitLog>,
1649 hlc: Arc<mongreldb_types::hlc::HlcClock>,
1655 idempotency: crate::txn::IdempotencyLedger,
1658 commit_ts_ledger: Mutex<std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp>>,
1666 active_spills: Arc<crate::retention::ActiveSpills>,
1669 replication_barrier: parking_lot::RwLock<()>,
1672 replication_wal_retention_segments: AtomicUsize,
1674 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1677 #[doc(hidden)]
1681 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1682 #[doc(hidden)]
1684 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1685 #[doc(hidden)]
1688 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1689 #[doc(hidden)]
1692 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1693 #[doc(hidden)]
1699 fk_lock_hook: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
1700 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1701 trigger_recursive: AtomicBool,
1702 trigger_max_depth: AtomicU32,
1703 trigger_max_loop_iterations: AtomicU32,
1704 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
1707 change_wake: tokio::sync::broadcast::Sender<()>,
1710 _lock: Mutex<Option<ExclusiveDatabaseLease>>,
1714}
1715
1716pub struct Database {
1726 core: Arc<DatabaseCore>,
1727 principal: RwLock<Option<crate::auth::Principal>>,
1737 auth_state: crate::auth_state::AuthState,
1743 shared: bool,
1750}
1751
1752impl std::ops::Deref for Database {
1753 type Target = DatabaseCore;
1754
1755 fn deref(&self) -> &DatabaseCore {
1756 &self.core
1757 }
1758}
1759
1760struct RunPins {
1761 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1762 runs: Vec<(u64, u128)>,
1763}
1764
1765struct TxnLockGuard<'a> {
1770 locks: &'a crate::locks::LockManager,
1771 txn_id: u64,
1772}
1773
1774impl Drop for TxnLockGuard<'_> {
1775 fn drop(&mut self) {
1776 self.locks.release_all(self.txn_id);
1777 }
1778}
1779
1780struct BackupFilePins {
1781 root: PathBuf,
1782}
1783
1784struct PendingTableDir {
1785 path: PathBuf,
1786 armed: bool,
1787}
1788
1789impl PendingTableDir {
1790 fn new(path: PathBuf) -> Self {
1791 Self { path, armed: true }
1792 }
1793
1794 fn disarm(&mut self) {
1795 self.armed = false;
1796 }
1797}
1798
1799impl Drop for PendingTableDir {
1800 fn drop(&mut self) {
1801 if self.armed {
1802 let _ = std::fs::remove_dir_all(&self.path);
1803 }
1804 }
1805}
1806
1807impl Drop for BackupFilePins {
1808 fn drop(&mut self) {
1809 let _ = std::fs::remove_dir_all(&self.root);
1810 }
1811}
1812
1813impl Drop for RunPins {
1814 fn drop(&mut self) {
1815 let mut pins = self.pins.lock();
1816 for run in &self.runs {
1817 if let Some(count) = pins.get_mut(run) {
1818 *count -= 1;
1819 if *count == 0 {
1820 pins.remove(run);
1821 }
1822 }
1823 }
1824 }
1825}
1826
1827#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1830pub struct ChangeEvent {
1831 pub id: Option<String>,
1832 pub channel: String,
1833 pub table_id: Option<u64>,
1834 pub table: String,
1835 pub op: String,
1836 pub epoch: u64,
1837 pub txn_id: Option<u64>,
1838 pub message: Option<String>,
1839 pub data: Option<serde_json::Value>,
1840}
1841
1842#[derive(Debug, Clone)]
1843pub struct CdcBatch {
1844 pub events: Vec<ChangeEvent>,
1845 pub current_epoch: u64,
1846 pub earliest_epoch: Option<u64>,
1847 pub gap: bool,
1848}
1849
1850impl std::fmt::Debug for Database {
1857 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1858 let cat = self.catalog.read();
1859 let principal_guard = self.principal.read();
1860 let principal: &str = principal_guard
1861 .as_ref()
1862 .map(|p| p.username.as_str())
1863 .unwrap_or("<none>");
1864 f.debug_struct("Database")
1865 .field("root", &self.root)
1866 .field("db_epoch", &cat.db_epoch)
1867 .field("open_generation", &"sidecar")
1868 .field("tables", &cat.tables.len())
1869 .field("visible_epoch", &self.epoch.visible().0)
1870 .field("encrypted", &self.kek.is_some())
1871 .field("require_auth", &cat.require_auth)
1872 .field("principal", &principal)
1873 .finish()
1874 }
1875}
1876
1877impl std::fmt::Debug for DatabaseCore {
1880 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1881 let cat = self.catalog.read();
1882 f.debug_struct("DatabaseCore")
1883 .field("root", &self.root)
1884 .field("lifecycle", &self.lifecycle.state())
1885 .field("db_epoch", &cat.db_epoch)
1886 .field("tables", &cat.tables.len())
1887 .field("visible_epoch", &self.epoch.visible().0)
1888 .field("encrypted", &self.kek.is_some())
1889 .field("require_auth", &cat.require_auth)
1890 .finish()
1891 }
1892}
1893
1894impl DatabaseCore {
1895 fn ensure_owner_process(&self) -> Result<()> {
1896 let current_pid = std::process::id();
1897 let owner_pid = self
1898 ._lock
1899 .lock()
1900 .as_ref()
1901 .map(|lease| lease.owner_pid)
1902 .unwrap_or(current_pid);
1903 if current_pid == owner_pid {
1904 Ok(())
1905 } else {
1906 Err(MongrelError::ForkedProcess {
1907 owner_pid,
1908 current_pid,
1909 })
1910 }
1911 }
1912
1913 pub fn root(&self) -> &Path {
1915 self.durable_root.canonical_path()
1916 }
1917
1918 pub fn file_identity(&self) -> Result<crate::core::DatabaseFileIdentity> {
1920 crate::core::DatabaseFileIdentity::from_durable_root(&self.durable_root)
1921 }
1922
1923 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
1925 self.lifecycle.state()
1926 }
1927
1928 pub fn is_open(&self) -> bool {
1930 self.lifecycle.is_open()
1931 }
1932
1933 pub fn operation_guard(self: &Arc<Self>) -> Result<crate::core::OperationGuard> {
1937 self.lifecycle.begin_operation()
1938 }
1939
1940 pub(crate) fn set_registry(
1944 &self,
1945 registration: crate::manager::CoreRegistration,
1946 ) -> Result<()> {
1947 self.registry
1948 .set(registration)
1949 .map_err(|_| MongrelError::Conflict("database core is already registry-bound".into()))
1950 }
1951
1952 pub fn shutdown(self: &Arc<Self>, drain_deadline: std::time::Duration) -> Result<()> {
1972 self.ensure_owner_process()?;
1973 let initiated = self.lifecycle.begin_shutdown();
1974 if !initiated {
1975 match self.lifecycle.state() {
1976 crate::core::LifecycleState::Closed => return Ok(()),
1977 crate::core::LifecycleState::Draining => {}
1980 state => {
1981 return Err(MongrelError::Conflict(format!(
1982 "database core cannot shut down from lifecycle state {state}"
1983 )))
1984 }
1985 }
1986 }
1987 if let Some(registration) = self.registry.get() {
1988 registration
1989 .manager
1990 .mark_closing(®istration.identity, self);
1991 }
1992 self.lifecycle.wait_drained(drain_deadline)?;
1993 let sync = self.shared_wal.lock().group_sync().map(|_| ());
1994 self.lifecycle.mark_closing();
1995 let lease = self._lock.lock().take();
1996 drop(lease);
1997 if let Some(registration) = self.registry.get() {
1998 registration
1999 .manager
2000 .entry_closed(®istration.identity, self);
2001 }
2002 self.lifecycle.mark_closed();
2003 sync
2004 }
2005}
2006
2007impl Database {
2008 pub fn open_metrics() -> DatabaseOpenMetrics {
2009 DatabaseOpenMetrics {
2010 lock_waits: DATABASE_OPEN_WAIT_COUNT.load(Ordering::Relaxed),
2011 failures: DATABASE_OPEN_FAILURE_COUNT.load(Ordering::Relaxed),
2012 }
2013 }
2014
2015 pub fn core(&self) -> Arc<DatabaseCore> {
2017 Arc::clone(&self.core)
2018 }
2019
2020 pub fn identity(&self) -> crate::handle::HandleIdentity {
2023 match self.principal.read().as_ref() {
2024 Some(principal) => crate::handle::HandleIdentity::CatalogUser {
2025 username: principal.username.clone(),
2026 user_id: principal.user_id,
2027 created_version: principal.created_epoch,
2028 },
2029 None => crate::handle::HandleIdentity::Credentialless,
2030 }
2031 }
2032
2033 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2035 self.core.lifecycle_state()
2036 }
2037
2038 pub fn operation_guard(&self) -> Result<crate::core::OperationGuard> {
2042 self.core.operation_guard()
2043 }
2044
2045 pub(crate) fn from_core(
2049 core: Arc<DatabaseCore>,
2050 principal: Option<crate::auth::Principal>,
2051 shared: bool,
2052 ) -> Self {
2053 let require_auth = core.catalog.read().require_auth;
2054 Self {
2055 core,
2056 principal: RwLock::new(principal.clone()),
2057 auth_state: crate::auth_state::AuthState::new(require_auth, principal),
2058 shared,
2059 }
2060 }
2061
2062 pub(crate) fn into_core(self) -> Arc<DatabaseCore> {
2064 self.core
2065 }
2066
2067 pub(crate) fn open_for_shared_core(root: impl AsRef<Path>) -> Result<Self> {
2072 Self::open_inner_with_lock_timeout(
2073 root,
2074 None,
2075 None,
2076 0,
2077 CoreResourceConfig::default(),
2078 OpenModeGate::Normal,
2079 )
2080 }
2081
2082 pub fn shutdown(self: Arc<Self>) -> Result<()> {
2088 if self.shared {
2089 return Err(MongrelError::Conflict(
2090 "shared-core facades do not own storage; use DatabaseHandle::shutdown".into(),
2091 ));
2092 }
2093 match Arc::try_unwrap(self) {
2094 Ok(database) => {
2095 database.ensure_owner_process()?;
2096 database.core.shutdown(std::time::Duration::from_secs(30))?;
2100 drop(database);
2101 Ok(())
2102 }
2103 Err(database) => Err(MongrelError::DatabaseBusy {
2104 strong_handles: Arc::strong_count(&database),
2105 }),
2106 }
2107 }
2108
2109 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
2110 if let Ok(canonical) = root.canonicalize() {
2111 let lock_dir = canonical.parent().ok_or_else(|| {
2112 std::io::Error::new(
2113 std::io::ErrorKind::InvalidInput,
2114 "database root must have a parent directory",
2115 )
2116 })?;
2117 return Ok((canonical.clone(), lock_dir.to_path_buf()));
2118 }
2119
2120 let absolute = if root.is_absolute() {
2121 root.to_path_buf()
2122 } else {
2123 std::env::current_dir()?.join(root)
2124 };
2125 let mut cursor = absolute.as_path();
2126 let mut suffix = Vec::new();
2127 while !cursor.exists() {
2128 let name = cursor.file_name().ok_or_else(|| {
2129 std::io::Error::new(
2130 std::io::ErrorKind::NotFound,
2131 format!("no existing ancestor for database root {}", root.display()),
2132 )
2133 })?;
2134 suffix.push(name.to_os_string());
2135 cursor = cursor.parent().ok_or_else(|| {
2136 std::io::Error::new(
2137 std::io::ErrorKind::NotFound,
2138 format!("no existing ancestor for database root {}", root.display()),
2139 )
2140 })?;
2141 }
2142 let lock_dir = cursor.canonicalize()?;
2143 let mut canonical = lock_dir.clone();
2144 for component in suffix.iter().rev() {
2145 canonical.push(component);
2146 }
2147 Ok((canonical, lock_dir))
2148 }
2149
2150 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<ExclusiveDatabaseLease> {
2151 use std::hash::{Hash, Hasher};
2152
2153 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
2154 let reservation =
2155 OpenReservation::acquire(DatabaseOpenKey::IntendedPath(canonical_path.clone()), root)?;
2156
2157 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2158 canonical_path.hash(&mut hasher);
2159 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
2160 let file = std::fs::OpenOptions::new()
2161 .create(true)
2162 .truncate(false)
2163 .write(true)
2164 .open(lock_path)?;
2165 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2166 return Err(MongrelError::DatabaseLocked {
2167 path: root.to_path_buf(),
2168 message: error.to_string(),
2169 });
2170 }
2171 Ok(reservation.into_lease(file, canonical_path))
2172 }
2173
2174 fn acquire_legacy_database_lock(
2175 lock: &mut ExclusiveDatabaseLease,
2176 root: &Path,
2177 timeout_ms: u32,
2178 ) -> Result<()> {
2179 let durable_root = lock
2180 .durable_root
2181 .as_ref()
2182 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
2183 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
2184 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2185 return Err(MongrelError::DatabaseLocked {
2186 path: root.to_path_buf(),
2187 message: error.to_string(),
2188 });
2189 }
2190 lock.legacy_file = Some(file);
2191 Ok(())
2192 }
2193
2194 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
2195 if path.exists() {
2196 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
2197 }
2198 let mut ancestor = path;
2199 while !ancestor.exists() {
2200 ancestor = ancestor.parent().ok_or_else(|| {
2201 MongrelError::NotFound(format!(
2202 "no existing ancestor for database root {}",
2203 path.display()
2204 ))
2205 })?;
2206 }
2207 let relative = path.strip_prefix(ancestor).map_err(|error| {
2208 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
2209 })?;
2210 crate::durable_file::DurableRoot::open(ancestor)?
2211 .create_directory_all_pinned(relative)
2212 .map_err(Into::into)
2213 }
2214
2215 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2216 let requested_root = root.as_ref();
2217 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
2218 let root = lock.canonical_path.clone();
2219 Self::reject_existing_database(&root)?;
2220 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
2221 if durable_root.canonical_path() != lock.canonical_path {
2222 return Err(MongrelError::Conflict(
2223 "database root changed while it was being created".into(),
2224 ));
2225 }
2226 lock.claim_root_identity(&durable_root)?;
2227 durable_root.create_directory_all(META_DIR)?;
2228 lock.durable_root = Some(durable_root);
2229 let io_root = lock
2230 .durable_root
2231 .as_ref()
2232 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2233 .io_path()?;
2234 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
2235 Self::reject_existing_database(&io_root)?;
2236 Ok((io_root, lock))
2237 }
2238
2239 fn begin_open(
2240 root: impl AsRef<Path>,
2241 lock_timeout_ms: u32,
2242 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2243 let root = root.as_ref();
2244 let canonical_root = root.canonicalize().map_err(|error| {
2245 if error.kind() == std::io::ErrorKind::NotFound {
2246 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
2247 } else {
2248 error.into()
2249 }
2250 })?;
2251 let durable_root = crate::durable_file::DurableRoot::open(&canonical_root)?;
2252 Self::begin_open_durable(durable_root, lock_timeout_ms)
2253 }
2254
2255 fn begin_open_durable(
2256 durable_root: crate::durable_file::DurableRoot,
2257 lock_timeout_ms: u32,
2258 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2259 let io_root = durable_root.io_path()?;
2260 let current_root = io_root.canonicalize()?;
2261 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
2262 lock.claim_root_identity(&durable_root)?;
2263 lock.durable_root = Some(Arc::new(durable_root));
2264 let io_root = lock
2265 .durable_root
2266 .as_ref()
2267 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2268 .io_path()?;
2269 if lock
2270 .durable_root
2271 .as_ref()
2272 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2273 .open_directory(META_DIR)
2274 .is_err()
2275 {
2276 return Err(MongrelError::NotFound(format!(
2277 "no database metadata found at {:?}",
2278 current_root
2279 )));
2280 }
2281 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
2282 Ok((io_root, lock))
2283 }
2284
2285 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
2287 let (root, lock) = Self::begin_create(root)?;
2288 Self::create_inner(
2289 root,
2290 None,
2291 lock,
2292 crate::storage_mode::StorageMode::Standalone,
2293 )
2294 }
2295
2296 #[cfg(feature = "encryption")]
2299 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2300 let (root, lock) = Self::begin_create(root)?;
2301 let salt = crate::encryption::random_salt()?;
2302 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2303 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2304 Self::create_inner(
2305 root,
2306 Some(kek),
2307 lock,
2308 crate::storage_mode::StorageMode::Standalone,
2309 )
2310 }
2311
2312 #[cfg(feature = "encryption")]
2315 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2316 let (root, lock) = Self::begin_create(root)?;
2317 let salt = crate::encryption::random_salt()?;
2318 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2319 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2320 Self::create_inner(
2321 root,
2322 Some(kek),
2323 lock,
2324 crate::storage_mode::StorageMode::Standalone,
2325 )
2326 }
2327
2328 pub fn create_cluster_replica(
2336 root: impl AsRef<Path>,
2337 cluster_id: mongreldb_types::ids::ClusterId,
2338 node_id: mongreldb_types::ids::NodeId,
2339 database_id: mongreldb_types::ids::DatabaseId,
2340 ) -> Result<Self> {
2341 let (root, lock) = Self::begin_create(root)?;
2342 Self::create_inner(
2343 root,
2344 None,
2345 lock,
2346 crate::storage_mode::StorageMode::ClusterReplica {
2347 cluster_id,
2348 node_id,
2349 database_id,
2350 },
2351 )
2352 }
2353
2354 pub fn open_cluster_replica(
2361 root: impl AsRef<Path>,
2362 expected: &crate::storage_mode::StorageMode,
2363 ) -> Result<Self> {
2364 let Some((cluster_id, node_id, database_id)) = expected.cluster_identity() else {
2365 return Err(MongrelError::InvalidArgument(format!(
2366 "open_cluster_replica requires a ClusterReplica identity, got {expected:?}"
2367 )));
2368 };
2369 let (root, lock) = Self::begin_open(root, 0)?;
2370 Self::open_inner_locked(
2371 root,
2372 None,
2373 lock,
2374 CoreResourceConfig::default(),
2375 OpenModeGate::ClusterRuntime {
2376 cluster_id,
2377 node_id,
2378 database_id,
2379 },
2380 )
2381 }
2382
2383 fn create_inner(
2384 root: PathBuf,
2385 kek: Option<Arc<crate::encryption::Kek>>,
2386 lock: ExclusiveDatabaseLease,
2387 storage_mode: crate::storage_mode::StorageMode,
2388 ) -> Result<Self> {
2389 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2390 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2391 let cat = Catalog::empty();
2392 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2393 Self::finish_open(
2394 root,
2395 cat,
2396 kek,
2397 meta_dek,
2398 false,
2399 None,
2400 None,
2401 None,
2402 lock,
2403 CoreResourceConfig::default(),
2404 OpenModeGate::Create(storage_mode),
2405 )
2406 }
2407
2408 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
2410 Self::open_inner(root, None, None)
2411 }
2412
2413 #[cfg(feature = "encryption")]
2415 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2416 let (root, lock) = Self::begin_open(root, 0)?;
2417 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2418 MongrelError::Other("database root descriptor was not pinned".into())
2419 })?)?;
2420 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2421 Self::open_inner_locked(
2422 root,
2423 Some(kek),
2424 lock,
2425 CoreResourceConfig::default(),
2426 OpenModeGate::Normal,
2427 )
2428 }
2429
2430 #[cfg(feature = "encryption")]
2433 pub fn open_encrypted_with_options(
2434 root: impl AsRef<Path>,
2435 passphrase: &str,
2436 options: OpenOptions,
2437 ) -> Result<Self> {
2438 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2439 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2440 MongrelError::Other("database root descriptor was not pinned".into())
2441 })?)?;
2442 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2443 let gate = if options.offline_validation {
2444 OpenModeGate::OfflineValidation
2445 } else {
2446 OpenModeGate::Normal
2447 };
2448 Self::open_inner_locked(
2449 root,
2450 Some(kek),
2451 lock,
2452 CoreResourceConfig::from_options(&options)?,
2453 gate,
2454 )
2455 }
2456
2457 #[cfg(feature = "encryption")]
2459 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2460 let (root, lock) = Self::begin_open(root, 0)?;
2461 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2462 MongrelError::Other("database root descriptor was not pinned".into())
2463 })?)?;
2464 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2465 Self::open_inner_locked(
2466 root,
2467 Some(kek),
2468 lock,
2469 CoreResourceConfig::default(),
2470 OpenModeGate::Normal,
2471 )
2472 }
2473
2474 pub fn open_with_credentials(
2486 root: impl AsRef<Path>,
2487 username: &str,
2488 password: &str,
2489 ) -> Result<Self> {
2490 Self::open_inner_with_credentials(root, None, username, password)
2491 }
2492
2493 pub fn open_with_credentials_and_options(
2497 root: impl AsRef<Path>,
2498 username: &str,
2499 password: &str,
2500 options: OpenOptions,
2501 ) -> Result<Self> {
2502 let gate = if options.offline_validation {
2503 OpenModeGate::OfflineValidation
2504 } else {
2505 OpenModeGate::Normal
2506 };
2507 Self::open_inner_with_credentials_and_lock_timeout(
2508 root,
2509 None,
2510 username,
2511 password,
2512 options.lock_timeout_ms,
2513 CoreResourceConfig::from_options(&options)?,
2514 gate,
2515 )
2516 }
2517
2518 #[cfg(feature = "encryption")]
2521 pub fn open_encrypted_with_credentials(
2522 root: impl AsRef<Path>,
2523 passphrase: &str,
2524 username: &str,
2525 password: &str,
2526 ) -> Result<Self> {
2527 let (root, lock) = Self::begin_open(root, 0)?;
2528 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2529 MongrelError::Other("database root descriptor was not pinned".into())
2530 })?)?;
2531 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2532 Self::open_inner_with_credentials_locked(
2533 root,
2534 Some(kek),
2535 username,
2536 password,
2537 lock,
2538 CoreResourceConfig::default(),
2539 OpenModeGate::Normal,
2540 )
2541 }
2542
2543 #[cfg(feature = "encryption")]
2547 pub fn open_encrypted_with_credentials_and_options(
2548 root: impl AsRef<Path>,
2549 passphrase: &str,
2550 username: &str,
2551 password: &str,
2552 options: OpenOptions,
2553 ) -> Result<Self> {
2554 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2555 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2556 MongrelError::Other("database root descriptor was not pinned".into())
2557 })?)?;
2558 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2559 let gate = if options.offline_validation {
2560 OpenModeGate::OfflineValidation
2561 } else {
2562 OpenModeGate::Normal
2563 };
2564 Self::open_inner_with_credentials_locked(
2565 root,
2566 Some(kek),
2567 username,
2568 password,
2569 lock,
2570 CoreResourceConfig::from_options(&options)?,
2571 gate,
2572 )
2573 }
2574
2575 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
2582 let gate = if options.offline_validation {
2585 OpenModeGate::OfflineValidation
2586 } else {
2587 OpenModeGate::Normal
2588 };
2589 Self::open_inner_with_lock_timeout(
2590 root,
2591 None,
2592 None,
2593 options.lock_timeout_ms,
2594 CoreResourceConfig::from_options(&options)?,
2595 gate,
2596 )
2597 }
2598
2599 fn open_inner_with_lock_timeout(
2600 root: impl AsRef<Path>,
2601 kek: Option<Arc<crate::encryption::Kek>>,
2602 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2603 lock_timeout_ms: u32,
2604 resources: CoreResourceConfig,
2605 mode_gate: OpenModeGate,
2606 ) -> Result<Self> {
2607 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
2608 Self::open_inner_locked(root, kek, lock, resources, mode_gate)
2609 }
2610
2611 fn open_inner_locked(
2612 root: PathBuf,
2613 kek: Option<Arc<crate::encryption::Kek>>,
2614 lock: ExclusiveDatabaseLease,
2615 resources: CoreResourceConfig,
2616 mode_gate: OpenModeGate,
2617 ) -> Result<Self> {
2618 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2619 let mut cat = catalog::read_durable(
2620 lock.durable_root.as_deref().ok_or_else(|| {
2621 MongrelError::Other("database root descriptor was not pinned".into())
2622 })?,
2623 meta_dek.as_ref(),
2624 )?
2625 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2626 let recovery_checkpoint = cat.clone();
2627
2628 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2631 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2632 lock.durable_root.as_deref().ok_or_else(|| {
2633 MongrelError::Other("database root descriptor was not pinned".into())
2634 })?,
2635 wal_dek.as_ref(),
2636 )?;
2637 recover_ddl_from_records(
2638 &root,
2639 Some(lock.durable_root.as_deref().ok_or_else(|| {
2640 MongrelError::Other("database root descriptor was not pinned".into())
2641 })?),
2642 &mut cat,
2643 meta_dek.as_ref(),
2644 false,
2645 None,
2646 &recovery_records,
2647 )?;
2648 Self::finish_open(
2649 root,
2650 cat,
2651 kek,
2652 meta_dek,
2653 true,
2654 Some(recovery_checkpoint),
2655 Some(recovery_records),
2656 None,
2657 lock,
2658 resources,
2659 mode_gate,
2660 )
2661 }
2662
2663 fn open_inner_with_credentials(
2670 root: impl AsRef<Path>,
2671 kek: Option<Arc<crate::encryption::Kek>>,
2672 username: &str,
2673 password: &str,
2674 ) -> Result<Self> {
2675 Self::open_inner_with_credentials_and_lock_timeout(
2676 root,
2677 kek,
2678 username,
2679 password,
2680 0,
2681 CoreResourceConfig::default(),
2682 OpenModeGate::Normal,
2683 )
2684 }
2685
2686 #[allow(clippy::too_many_arguments)]
2690 fn open_inner_with_credentials_and_lock_timeout(
2691 root: impl AsRef<Path>,
2692 kek: Option<Arc<crate::encryption::Kek>>,
2693 username: &str,
2694 password: &str,
2695 lock_timeout_ms: u32,
2696 resources: CoreResourceConfig,
2697 mode_gate: OpenModeGate,
2698 ) -> Result<Self> {
2699 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
2700 Self::open_inner_with_credentials_locked(
2701 root, kek, username, password, lock, resources, mode_gate,
2702 )
2703 }
2704
2705 #[allow(clippy::too_many_arguments)]
2706 fn open_inner_with_credentials_locked(
2707 root: PathBuf,
2708 kek: Option<Arc<crate::encryption::Kek>>,
2709 username: &str,
2710 password: &str,
2711 lock: ExclusiveDatabaseLease,
2712 resources: CoreResourceConfig,
2713 mode_gate: OpenModeGate,
2714 ) -> Result<Self> {
2715 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2716 let mut cat = catalog::read_durable(
2717 lock.durable_root.as_deref().ok_or_else(|| {
2718 MongrelError::Other("database root descriptor was not pinned".into())
2719 })?,
2720 meta_dek.as_ref(),
2721 )?
2722 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2723 let recovery_checkpoint = cat.clone();
2724
2725 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2728 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2729 lock.durable_root.as_deref().ok_or_else(|| {
2730 MongrelError::Other("database root descriptor was not pinned".into())
2731 })?,
2732 wal_dek.as_ref(),
2733 )?;
2734 recover_ddl_from_records(
2735 &root,
2736 Some(lock.durable_root.as_deref().ok_or_else(|| {
2737 MongrelError::Other("database root descriptor was not pinned".into())
2738 })?),
2739 &mut cat,
2740 meta_dek.as_ref(),
2741 false,
2742 None,
2743 &recovery_records,
2744 )?;
2745
2746 if !cat.require_auth {
2749 return Err(MongrelError::AuthNotRequired);
2750 }
2751
2752 let user = cat
2757 .users
2758 .iter()
2759 .find(|u| u.username == username)
2760 .filter(|u| !u.password_hash.is_empty())
2761 .ok_or_else(|| MongrelError::InvalidCredentials {
2762 username: username.to_string(),
2763 })?;
2764 let password_ok = crate::auth::verify_password(password, &user.password_hash)
2765 .map_err(MongrelError::Other)?;
2766 if !password_ok {
2767 return Err(MongrelError::InvalidCredentials {
2768 username: username.to_string(),
2769 });
2770 }
2771
2772 let principal =
2774 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
2775 MongrelError::InvalidCredentials {
2776 username: username.to_string(),
2777 }
2778 })?;
2779
2780 Self::finish_open(
2781 root,
2782 cat,
2783 kek,
2784 meta_dek,
2785 true,
2786 Some(recovery_checkpoint),
2787 Some(recovery_records),
2788 Some(principal),
2789 lock,
2790 resources,
2791 mode_gate,
2792 )
2793 }
2794
2795 pub fn create_with_credentials(
2805 root: impl AsRef<Path>,
2806 admin_username: &str,
2807 admin_password: &str,
2808 ) -> Result<Self> {
2809 let (root, lock) = Self::begin_create(root)?;
2810 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
2811 }
2812
2813 #[cfg(feature = "encryption")]
2817 pub fn create_encrypted_with_credentials(
2818 root: impl AsRef<Path>,
2819 passphrase: &str,
2820 admin_username: &str,
2821 admin_password: &str,
2822 ) -> Result<Self> {
2823 let (root, lock) = Self::begin_create(root)?;
2824 let salt = crate::encryption::random_salt()?;
2825 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2826 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2827 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
2828 }
2829
2830 fn create_inner_with_credentials(
2831 root: PathBuf,
2832 kek: Option<Arc<crate::encryption::Kek>>,
2833 admin_username: &str,
2834 admin_password: &str,
2835 lock: ExclusiveDatabaseLease,
2836 ) -> Result<Self> {
2837 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2838 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2839
2840 let password_hash =
2842 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2843 let mut cat = Catalog::empty();
2844 cat.require_auth = true;
2845 cat.next_user_id = 2;
2846 cat.users.push(crate::auth::UserEntry {
2847 id: 1,
2848 username: admin_username.to_string(),
2849 password_hash,
2850 roles: Vec::new(),
2851 is_admin: true,
2852 created_epoch: 0,
2853 });
2854 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2855
2856 let admin_principal = crate::auth::Principal {
2859 user_id: 1,
2860 created_epoch: 0,
2861 username: admin_username.to_string(),
2862 is_admin: true,
2863 roles: Vec::new(),
2864 permissions: Vec::new(),
2865 };
2866 Self::finish_open(
2867 root,
2868 cat,
2869 kek,
2870 meta_dek,
2871 false,
2872 None,
2873 None,
2874 Some(admin_principal),
2875 lock,
2876 CoreResourceConfig::default(),
2877 OpenModeGate::Create(crate::storage_mode::StorageMode::Standalone),
2878 )
2879 }
2880
2881 fn reject_existing_database(root: &Path) -> Result<()> {
2882 if root.join(catalog::CATALOG_FILENAME).exists() {
2885 return Err(MongrelError::InvalidArgument(format!(
2886 "database already exists at {}; use Database::open() to open it, \
2887 or remove the directory first",
2888 root.display()
2889 )));
2890 }
2891 Ok(())
2892 }
2893
2894 fn open_inner(
2895 root: impl AsRef<Path>,
2896 kek: Option<Arc<crate::encryption::Kek>>,
2897 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2898 ) -> Result<Self> {
2899 Self::open_inner_with_lock_timeout(
2900 root,
2901 kek,
2902 None,
2903 0,
2904 CoreResourceConfig::default(),
2905 OpenModeGate::Normal,
2906 )
2907 }
2908
2909 pub(crate) fn open_offline_validation(root: impl AsRef<Path>) -> Result<Self> {
2912 Self::open_inner_with_lock_timeout(
2913 root,
2914 None,
2915 None,
2916 0,
2917 CoreResourceConfig::default(),
2918 OpenModeGate::OfflineValidation,
2919 )
2920 }
2921
2922 pub(crate) fn open_replica_recovery_durable(
2926 root: &crate::durable_file::DurableRoot,
2927 ) -> Result<Self> {
2928 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2929 Self::open_replica_recovery_inner(root, None, lock)
2930 }
2931
2932 #[cfg(feature = "encryption")]
2933 pub(crate) fn open_encrypted_replica_recovery_durable(
2934 root: &crate::durable_file::DurableRoot,
2935 passphrase: &str,
2936 ) -> Result<Self> {
2937 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2938 let salt = read_encryption_salt(root)?;
2939 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2940 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
2941 }
2942
2943 fn open_replica_recovery_inner(
2944 root: PathBuf,
2945 kek: Option<Arc<crate::encryption::Kek>>,
2946 lock: ExclusiveDatabaseLease,
2947 ) -> Result<Self> {
2948 if !root.join(META_DIR).join("replica").is_file() {
2949 return Err(MongrelError::InvalidArgument(
2950 "recovery auth bypass requires a marked replica staging directory".into(),
2951 ));
2952 }
2953 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2954 let mut cat = catalog::read_durable(
2955 lock.durable_root.as_deref().ok_or_else(|| {
2956 MongrelError::Other("database root descriptor was not pinned".into())
2957 })?,
2958 meta_dek.as_ref(),
2959 )?
2960 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2961 let recovery_checkpoint = cat.clone();
2962 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2963 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2964 lock.durable_root.as_deref().ok_or_else(|| {
2965 MongrelError::Other("database root descriptor was not pinned".into())
2966 })?,
2967 wal_dek.as_ref(),
2968 )?;
2969 recover_ddl_from_records(
2970 &root,
2971 Some(lock.durable_root.as_deref().ok_or_else(|| {
2972 MongrelError::Other("database root descriptor was not pinned".into())
2973 })?),
2974 &mut cat,
2975 meta_dek.as_ref(),
2976 false,
2977 None,
2978 &recovery_records,
2979 )?;
2980 let principal = if cat.require_auth {
2981 cat.users
2982 .iter()
2983 .find(|user| user.is_admin)
2984 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
2985 .ok_or_else(|| {
2986 MongrelError::Schema(
2987 "authenticated replica catalog has no recoverable admin".into(),
2988 )
2989 })?
2990 .into()
2991 } else {
2992 None
2993 };
2994 Self::finish_open(
2995 root,
2996 cat,
2997 kek,
2998 meta_dek,
2999 true,
3000 Some(recovery_checkpoint),
3001 Some(recovery_records),
3002 principal,
3003 lock,
3004 CoreResourceConfig::default(),
3005 OpenModeGate::Normal,
3006 )
3007 }
3008
3009 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
3021 use fs2::FileExt;
3022 if timeout_ms == 0 {
3023 return f.try_lock_exclusive();
3024 }
3025 let deadline =
3027 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
3028 let mut next_sleep = std::time::Duration::from_millis(1);
3029 let mut recorded_wait = false;
3030 loop {
3031 match f.try_lock_exclusive() {
3032 Ok(()) => return Ok(()),
3033 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
3034 if !recorded_wait {
3035 DATABASE_OPEN_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
3036 recorded_wait = true;
3037 }
3038 let now = std::time::Instant::now();
3039 if now >= deadline {
3040 return Err(std::io::Error::new(
3041 std::io::ErrorKind::WouldBlock,
3042 format!("could not acquire database lock within {timeout_ms}ms"),
3043 ));
3044 }
3045 let remaining = deadline - now;
3046 let sleep = next_sleep.min(remaining);
3047 std::thread::sleep(sleep);
3048 next_sleep = next_sleep
3051 .saturating_mul(10)
3052 .min(std::time::Duration::from_millis(50));
3053 }
3054 Err(e) => return Err(e),
3055 }
3056 }
3057 }
3058
3059 #[allow(clippy::too_many_arguments)]
3060 fn finish_open(
3061 root: PathBuf,
3062 cat: Catalog,
3063 kek: Option<Arc<crate::encryption::Kek>>,
3064 meta_dek: Option<[u8; META_DEK_LEN]>,
3065 existing: bool,
3066 recovery_checkpoint: Option<Catalog>,
3067 recovery_records: Option<Vec<crate::wal::Record>>,
3068 principal: Option<crate::auth::Principal>,
3069 lock: ExclusiveDatabaseLease,
3070 resources: CoreResourceConfig,
3071 mode_gate: OpenModeGate,
3072 ) -> Result<Self> {
3073 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
3074 MongrelError::Other("database root descriptor was not pinned".into())
3075 })?);
3076 let storage_mode = if existing {
3081 crate::storage_mode::read(&durable_root)?
3082 } else {
3083 None
3084 };
3085 match (&mode_gate, &storage_mode) {
3086 (OpenModeGate::Create(_), _) => {}
3087 (OpenModeGate::Normal, mode) => crate::storage_mode::check_open(mode.as_ref(), false)?,
3088 (OpenModeGate::OfflineValidation, mode) => {
3089 crate::storage_mode::check_open(mode.as_ref(), true)?
3090 }
3091 (
3092 OpenModeGate::ClusterRuntime {
3093 cluster_id,
3094 node_id,
3095 database_id,
3096 },
3097 Some(actual),
3098 ) => {
3099 let expected = crate::storage_mode::StorageMode::ClusterReplica {
3100 cluster_id: *cluster_id,
3101 node_id: *node_id,
3102 database_id: *database_id,
3103 };
3104 if *actual != expected {
3105 return Err(
3106 crate::storage_mode::StorageModeError::IdentityMismatch(format!(
3107 "expected {expected:?}, marker holds {actual:?}"
3108 ))
3109 .into(),
3110 );
3111 }
3112 }
3113 (OpenModeGate::ClusterRuntime { .. }, None) => {
3114 return Err(crate::storage_mode::StorageModeError::IdentityMismatch(
3115 "expected a ClusterReplica marker; directory has none".into(),
3116 )
3117 .into());
3118 }
3119 }
3120 let read_only = match &mode_gate {
3121 OpenModeGate::OfflineValidation | OpenModeGate::ClusterRuntime { .. } => true,
3122 OpenModeGate::Create(mode) => mode.cluster_identity().is_some(),
3125 OpenModeGate::Normal => false,
3126 } || if existing {
3127 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
3128 Ok(_) => true,
3129 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
3130 Err(error) => return Err(error.into()),
3131 }
3132 } else {
3133 false
3134 };
3135 let recovered_catalog = cat;
3136 let mut cat = recovered_catalog.clone();
3137 let abandoned = if existing && !read_only {
3138 let abandoned = cat
3139 .tables
3140 .iter()
3141 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
3142 .map(|entry| entry.table_id)
3143 .collect::<Vec<_>>();
3144 for entry in &mut cat.tables {
3145 if abandoned.contains(&entry.table_id) {
3146 entry.state = TableState::Dropped {
3147 at_epoch: cat.db_epoch,
3148 };
3149 }
3150 }
3151 abandoned
3152 } else {
3153 Vec::new()
3154 };
3155 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3156 let recovery_records = match (existing, recovery_records) {
3157 (true, Some(records)) => records,
3158 (true, None) => {
3159 return Err(MongrelError::Other(
3160 "existing open has no validated WAL recovery plan".into(),
3161 ))
3162 }
3163 (false, _) => Vec::new(),
3164 };
3165 let (history_epochs, history_start) =
3166 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
3167 let open_generation = if existing {
3168 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
3169 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3170 })?;
3171 let recovered_table_ids = cat
3172 .tables
3173 .iter()
3174 .filter(|entry| {
3175 checkpoint
3176 .tables
3177 .iter()
3178 .all(|checkpoint| checkpoint.table_id != entry.table_id)
3179 })
3180 .map(|entry| entry.table_id)
3181 .collect::<HashSet<_>>();
3182 let reconciled_table_ids = cat
3183 .tables
3184 .iter()
3185 .filter(|entry| {
3186 checkpoint
3187 .tables
3188 .iter()
3189 .find(|checkpoint| checkpoint.table_id == entry.table_id)
3190 .is_some_and(|checkpoint| {
3191 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
3192 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
3193 })
3194 })
3195 .map(|entry| entry.table_id)
3196 .collect::<HashSet<_>>();
3197 validate_shared_wal_recovery_plan(
3198 &durable_root,
3199 &cat,
3200 &recovered_table_ids,
3201 &reconciled_table_ids,
3202 meta_dek.as_ref(),
3203 kek.clone(),
3204 &recovery_records,
3205 )?;
3206 let retained_generation = recovery_records
3207 .iter()
3208 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
3209 .map(|record| record.txn_id >> 32)
3210 .max()
3211 .unwrap_or(0);
3212 let head_generation =
3213 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
3214 let durable_floor = match head_generation {
3215 Some(head) if retained_generation > head => {
3216 return Err(MongrelError::CorruptWal {
3217 offset: retained_generation,
3218 reason: format!(
3219 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
3220 ),
3221 })
3222 }
3223 Some(head) => head,
3224 None => retained_generation,
3225 };
3226 let stored = catalog::read_generation(&durable_root)?;
3227 if stored.is_some_and(|generation| generation < durable_floor) {
3228 return Err(MongrelError::Other(format!(
3229 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
3230 )));
3231 }
3232 let bumped = stored
3233 .unwrap_or(durable_floor)
3234 .max(durable_floor)
3235 .checked_add(1)
3236 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
3237 if bumped > u32::MAX as u64 {
3238 return Err(MongrelError::Full(
3239 "open-generation namespace exhausted".into(),
3240 ));
3241 }
3242 bumped
3243 } else {
3244 0
3245 };
3246 let principal = if cat.require_auth {
3247 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3248 Some(
3249 Self::resolve_bound_principal_from_catalog(&cat, supplied)
3250 .ok_or(MongrelError::AuthRequired)?,
3251 )
3252 } else {
3253 principal
3254 };
3255 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
3256 if existing {
3257 for entry in &cat.tables {
3258 if !matches!(entry.state, TableState::Live) {
3259 continue;
3260 }
3261 match durable_root
3262 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
3263 {
3264 Ok(root) => {
3265 table_roots.insert(entry.table_id, Arc::new(root));
3266 }
3267 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3268 Err(error) => return Err(error.into()),
3269 }
3270 }
3271 }
3272
3273 if existing {
3277 let mut applied = recovery_checkpoint.ok_or_else(|| {
3278 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3279 })?;
3280 recover_ddl_from_records(
3281 &root,
3282 Some(&durable_root),
3283 &mut applied,
3284 meta_dek.as_ref(),
3285 true,
3286 Some(&table_roots),
3287 &recovery_records,
3288 )?;
3289 let catalog_value = |catalog: &Catalog| {
3290 serde_json::to_value(catalog)
3291 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
3292 };
3293 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
3294 return Err(MongrelError::CorruptWal {
3295 offset: 0,
3296 reason: "validated and applied DDL recovery plans differ".into(),
3297 });
3298 }
3299 if catalog_value(&cat)? != catalog_value(&applied)? {
3300 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3301 }
3302 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
3303 if !read_only {
3304 sweep_unreferenced_table_dirs(&root, &cat)?;
3305 }
3306 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
3307 Ok(()) => {}
3308 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3309 Err(error) => return Err(error.into()),
3310 }
3311 }
3312
3313 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
3314 let snapshots = Arc::new(SnapshotRegistry::new());
3315 snapshots.configure_history(history_epochs, history_start);
3316 let memory_governor = crate::memory::MemoryGovernor::new(
3321 crate::memory::GovernorConfig::new(resources.memory_budget_bytes),
3322 )
3323 .map_err(|error| MongrelError::InvalidArgument(format!("memory governor: {error}")))?;
3324 let spill_manager = crate::spill::SpillManager::open(
3328 &durable_root,
3329 crate::spill::SpillConfig::new(resources.temp_disk_budget_bytes),
3330 meta_dek,
3331 )?;
3332 let job_registry = Arc::new(crate::jobs::JobRegistry::open(&root, meta_dek.as_ref())?);
3336 let page_cache = Arc::new(crate::cache::Sharded::new(
3337 crate::cache::CACHE_SHARDS,
3338 || {
3339 crate::cache::PageCache::new(
3340 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3341 )
3342 .with_governor(
3343 memory_governor.clone(),
3344 crate::memory::MemoryClass::PageCache,
3345 )
3346 },
3347 ));
3348 memory_governor.register_reclaimable(&page_cache);
3349 let decoded_cache = Arc::new(crate::cache::Sharded::new(
3350 crate::cache::CACHE_SHARDS,
3351 || {
3352 crate::cache::DecodedPageCache::new(
3353 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3354 )
3355 .with_governor(
3356 memory_governor.clone(),
3357 crate::memory::MemoryClass::DecodedCache,
3358 )
3359 },
3360 ));
3361 memory_governor.register_reclaimable(&decoded_cache);
3362 let commit_lock = Arc::new(Mutex::new(()));
3363 let shared_wal = Arc::new(Mutex::new(if existing {
3364 crate::wal::SharedWal::open_durable_root_validated(
3365 Arc::clone(&durable_root),
3366 Epoch(cat.db_epoch),
3367 wal_dek.clone(),
3368 Some(&recovery_records),
3369 )?
3370 } else {
3371 crate::wal::SharedWal::create_with_durable_root(
3372 Arc::clone(&durable_root),
3373 Epoch(cat.db_epoch),
3374 wal_dek.clone(),
3375 )?
3376 }));
3377 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
3381 let lifecycle = Arc::new(crate::core::LifecycleController::new());
3386 let group = Arc::new(
3387 crate::txn::GroupCommit::new(shared_wal.lock().durable_seq())
3388 .with_lifecycle(Arc::clone(&lifecycle)),
3389 );
3390 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
3391 let txn_ids = Arc::new(Mutex::new(1u64));
3395 let _ = abandoned;
3396 let hlc = Arc::new(mongreldb_types::hlc::HlcClock::new(
3403 0,
3404 std::time::Duration::from_millis(500),
3405 ));
3406 let idempotency = crate::txn::IdempotencyLedger::open(Arc::clone(&durable_root), meta_dek)?;
3409 let commit_log = Arc::new(crate::commit_log::StandaloneCommitLog::new(
3410 Arc::clone(&shared_wal),
3411 Arc::clone(&group),
3412 Arc::clone(&epoch),
3413 Arc::clone(&commit_lock),
3414 Arc::clone(&txn_ids),
3415 root.clone(),
3416 wal_dek.clone(),
3417 Arc::clone(&hlc),
3418 ));
3419
3420 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
3425 let security_coordinator = security_coordinator(&root, cat.security_version);
3426 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
3427 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
3428 ));
3429
3430 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
3436 for entry in &cat.tables {
3437 if !matches!(entry.state, TableState::Live) {
3438 continue;
3439 }
3440 let table_root = match table_roots.remove(&entry.table_id) {
3441 Some(root) => root,
3442 None => Arc::new(
3443 durable_root
3444 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
3445 ),
3446 };
3447 let tdir = table_root.io_path()?;
3448 let ctx = SharedCtx {
3449 root_guard: Some(table_root),
3450 epoch: Arc::clone(&epoch),
3451 page_cache: Arc::clone(&page_cache),
3452 decoded_cache: Arc::clone(&decoded_cache),
3453 snapshots: Arc::clone(&snapshots),
3454 kek: kek.clone(),
3455 commit_lock: Arc::clone(&commit_lock),
3456 shared: Some(crate::engine::SharedWalCtx {
3457 wal: Arc::clone(&shared_wal),
3458 group: Arc::clone(&group),
3459 poisoned: Arc::clone(&poisoned),
3460 txn_ids: Arc::clone(&txn_ids),
3461 change_wake: change_wake.clone(),
3462 lifecycle: Arc::clone(&lifecycle),
3463 }),
3464 table_name: Some(entry.name.clone()),
3465 auth: auth_checker.clone(),
3466 read_only,
3467 };
3468 let t = Table::open_in(&tdir, ctx)?;
3469 tables.insert(entry.table_id, TableHandle::new(t));
3470 }
3471
3472 if existing {
3478 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
3479 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
3480 if read_only {
3481 crate::replication::reconcile_replica_epoch_durable(
3482 &durable_root,
3483 epoch.visible().0,
3484 )?;
3485 }
3486 sweep_pending_txn_dirs(&root, &cat);
3489 }
3490
3491 catalog::write_generation(&durable_root, open_generation)?;
3493 match &mode_gate {
3498 OpenModeGate::Create(mode) => crate::storage_mode::write(&durable_root, mode)?,
3499 _ if existing && storage_mode.is_none() => {
3500 crate::storage_mode::write(
3501 &durable_root,
3502 &crate::storage_mode::StorageMode::Standalone,
3503 )?;
3504 }
3505 _ => {}
3506 }
3507 shared_wal.lock().seal_open_generation(open_generation)?;
3508 crate::replication::replication_identity_durable(&durable_root)?;
3509 let next_txn_id = (open_generation << 32) | 1;
3510 *txn_ids.lock() = next_txn_id;
3512 let mut lock = lock;
3513 lock.mark_open()?;
3514
3515 lifecycle.mark_open();
3519 let core = DatabaseCore {
3520 root,
3521 durable_root,
3522 lifecycle,
3523 registry: std::sync::OnceLock::new(),
3524 read_only,
3525 catalog: RwLock::new(cat),
3526 security_coordinator,
3527 security_catalog_disk_reads: AtomicU64::new(0),
3528 rls_cache: Mutex::new(RlsCache::default()),
3529 epoch,
3530 snapshots,
3531 memory_governor,
3532 page_cache,
3533 decoded_cache,
3534 spill_manager,
3535 resource_groups: crate::resource::ResourceGroupRegistry::with_defaults(),
3536 embedding_providers: crate::embedding::EmbeddingProviderRegistry::new(),
3537 job_registry,
3538 commit_lock,
3539 shared_wal,
3540 next_txn_id: txn_ids,
3541 tables: RwLock::new(tables),
3542 kek,
3543 ddl_lock: Mutex::new(()),
3544 meta_dek,
3545 conflicts: crate::txn::ConflictIndex::new(),
3546 active_txns: crate::txn::ActiveTxns::new(),
3547 lock_manager: Arc::new(crate::locks::LockManager::new()),
3548 poisoned,
3549 group,
3550 commit_log,
3551 hlc,
3552 idempotency,
3553 commit_ts_ledger: Mutex::new(commit_ts_ledger_from_recovery(&recovery_records)),
3554 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
3555 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
3556 replication_barrier: parking_lot::RwLock::new(()),
3557 replication_wal_retention_segments: AtomicUsize::new(0),
3558 backup_pins: Arc::new(Mutex::new(HashMap::new())),
3559 spill_hook: Mutex::new(None),
3560 security_commit_hook: Mutex::new(None),
3561 catalog_commit_hook: Mutex::new(None),
3562 backup_hook: Mutex::new(None),
3563 fk_lock_hook: Mutex::new(None),
3564 replication_hook: Mutex::new(None),
3565 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
3566 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
3567 trigger_max_loop_iterations: AtomicU32::new(
3568 TriggerConfig::default().max_loop_iterations,
3569 ),
3570 notify: {
3571 let (tx, _rx) = tokio::sync::broadcast::channel(256);
3572 tx
3573 },
3574 change_wake,
3575 _lock: Mutex::new(Some(lock)),
3576 };
3577 Ok(Self {
3578 core: Arc::new(core),
3579 principal: RwLock::new(principal),
3580 auth_state,
3581 shared: false,
3582 })
3583 }
3584
3585 pub fn visible_epoch(&self) -> Epoch {
3587 self.epoch.visible()
3588 }
3589
3590 pub fn catalog_version(&self) -> u64 {
3592 self.catalog.read().catalog_version
3593 }
3594
3595 pub fn storage_mode(&self) -> Result<Option<crate::storage_mode::StorageMode>> {
3599 Ok(crate::storage_mode::read(&self.durable_root)?)
3600 }
3601
3602 pub fn memory_governor(&self) -> &crate::memory::MemoryGovernor {
3605 &self.memory_governor
3606 }
3607
3608 pub fn resource_groups(&self) -> &crate::resource::ResourceGroupRegistry {
3611 &self.resource_groups
3612 }
3613
3614 pub fn embedding_providers(&self) -> &crate::embedding::EmbeddingProviderRegistry {
3618 &self.embedding_providers
3619 }
3620
3621 pub fn lock_rows_for_update(
3625 &self,
3626 txn_id: u64,
3627 table_id: u64,
3628 row_ids: &[crate::rowid::RowId],
3629 control: Option<&crate::ExecutionControl>,
3630 ) -> Result<()> {
3631 for &row_id in row_ids {
3632 self.acquire_txn_lock(
3633 txn_id,
3634 crate::locks::LockKey::row(table_id, row_id),
3635 crate::locks::LockMode::Exclusive,
3636 control,
3637 )?;
3638 }
3639 Ok(())
3640 }
3641
3642 pub fn spill_manager(&self) -> &crate::spill::SpillManager {
3646 &self.spill_manager
3647 }
3648
3649 pub fn job_registry(&self) -> &Arc<crate::jobs::JobRegistry> {
3651 &self.job_registry
3652 }
3653
3654 pub fn version_pins_report(&self) -> Vec<TablePinsReport> {
3659 let names: HashMap<u64, String> = self
3660 .catalog
3661 .read()
3662 .tables
3663 .iter()
3664 .map(|entry| (entry.table_id, entry.name.clone()))
3665 .collect();
3666 let handles: Vec<_> = self
3667 .tables
3668 .read()
3669 .iter()
3670 .map(|(table_id, handle)| (*table_id, handle.clone()))
3671 .collect();
3672 handles
3673 .into_iter()
3674 .map(|(table_id, handle)| {
3675 let pins = handle.lock().version_pins_report();
3676 TablePinsReport {
3677 table_id,
3678 table: names.get(&table_id).cloned().unwrap_or_default(),
3679 pins,
3680 }
3681 })
3682 .collect()
3683 }
3684
3685 pub fn lock_manager(&self) -> &Arc<crate::locks::LockManager> {
3687 &self.lock_manager
3688 }
3689
3690 pub fn release_txn_locks(&self, txn_id: u64) {
3695 self.lock_manager.release_all(txn_id);
3696 }
3697
3698 fn txn_lock_request(
3701 txn_id: u64,
3702 mode: crate::locks::LockMode,
3703 control: Option<&crate::ExecutionControl>,
3704 ) -> crate::locks::LockRequest {
3705 let control = control
3706 .cloned()
3707 .unwrap_or_else(|| crate::ExecutionControl::new(None));
3708 crate::locks::LockRequest::new(txn_id, mode, control)
3709 }
3710
3711 pub(crate) fn acquire_txn_lock(
3715 &self,
3716 txn_id: u64,
3717 key: crate::locks::LockKey,
3718 mode: crate::locks::LockMode,
3719 control: Option<&crate::ExecutionControl>,
3720 ) -> Result<()> {
3721 self.lock_manager
3722 .acquire(key, Self::txn_lock_request(txn_id, mode, control))
3723 .map_err(MongrelError::from)
3724 }
3725
3726 fn acquire_schema_barrier_exclusive(&self) -> Result<TxnLockGuard<'_>> {
3731 let txn_id = self.alloc_txn_id()?;
3732 self.acquire_txn_lock(
3733 txn_id,
3734 crate::locks::LockKey::schema_barrier(),
3735 crate::locks::LockMode::Exclusive,
3736 None,
3737 )?;
3738 Ok(TxnLockGuard {
3739 locks: &self.lock_manager,
3740 txn_id,
3741 })
3742 }
3743
3744 pub fn commit_log(&self) -> Arc<crate::commit_log::StandaloneCommitLog> {
3749 Arc::clone(&self.commit_log)
3750 }
3751
3752 pub(crate) fn hlc_clock(&self) -> &mongreldb_types::hlc::HlcClock {
3757 &self.hlc
3758 }
3759
3760 pub fn catalog_snapshot(&self) -> Catalog {
3762 self.catalog.read().clone()
3763 }
3764
3765 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
3767 let catalog = self.catalog.read();
3768 match key {
3769 "user_version" => Ok(catalog.user_version),
3770 "application_id" => Ok(catalog.application_id),
3771 _ => Err(MongrelError::InvalidArgument(format!(
3772 "unsupported persistent SQL pragma {key:?}"
3773 ))),
3774 }
3775 }
3776
3777 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
3780 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
3781 }
3782
3783 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
3784 &self,
3785 key: &str,
3786 value: i64,
3787 mut before_commit: F,
3788 ) -> Result<Option<Epoch>>
3789 where
3790 F: FnMut() -> Result<()>,
3791 {
3792 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
3793 }
3794
3795 fn set_sql_pragma_i64_with_epoch_inner(
3796 &self,
3797 key: &str,
3798 value: i64,
3799 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
3800 ) -> Result<Option<Epoch>> {
3801 use crate::wal::DdlOp;
3802
3803 self.require(&crate::auth::Permission::Ddl)?;
3804 if self.read_only {
3805 return Err(MongrelError::ReadOnlyReplica);
3806 }
3807 if self.poisoned.load(Ordering::Relaxed) {
3808 return Err(MongrelError::Other(
3809 "database poisoned by fsync error".into(),
3810 ));
3811 }
3812 let _ddl = self.ddl_lock.lock();
3813 let _security_write = self.security_write()?;
3814 self.require(&crate::auth::Permission::Ddl)?;
3815 let mut next_catalog = self.catalog.read().clone();
3816 let target = match key {
3817 "user_version" => &mut next_catalog.user_version,
3818 "application_id" => &mut next_catalog.application_id,
3819 _ => {
3820 return Err(MongrelError::InvalidArgument(format!(
3821 "unsupported persistent SQL pragma {key:?}"
3822 )))
3823 }
3824 };
3825 if *target == Some(value) {
3826 return Ok(None);
3827 }
3828 *target = Some(value);
3829
3830 let _commit = self.commit_lock.lock();
3831 let epoch = self.epoch.bump_assigned();
3832 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3833 let txn_id = self.alloc_txn_id()?;
3834 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
3835 let commit_seq = {
3836 let mut wal = self.shared_wal.lock();
3837 if let Some(before_commit) = before_commit {
3838 before_commit()?;
3839 }
3840 let append: Result<u64> = (|| {
3841 wal.append(
3842 txn_id,
3843 WAL_TABLE_ID,
3844 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
3845 key: key.to_string(),
3846 value,
3847 }),
3848 )?;
3849 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
3850 wal.append_commit(txn_id, epoch, &[])
3851 })();
3852 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
3853 };
3854 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
3855 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
3856 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
3857 Ok(Some(epoch))
3858 }
3859
3860 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
3861 self.catalog
3862 .read()
3863 .materialized_views
3864 .iter()
3865 .find(|definition| definition.name == name)
3866 .cloned()
3867 }
3868
3869 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
3870 self.catalog.read().materialized_views.clone()
3871 }
3872
3873 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
3874 self.catalog.read().security.clone()
3875 }
3876
3877 pub fn security_active_for(&self, table: &str) -> bool {
3878 self.catalog.read().security.table_has_security(table)
3879 }
3880
3881 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
3882 if self.catalog.read().security_version == expected_version {
3883 return Ok(());
3884 }
3885 self.security_catalog_disk_reads
3886 .fetch_add(1, Ordering::Relaxed);
3887 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
3888 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
3889 let principal = self.principal.read().clone();
3890 let principal = if fresh.require_auth {
3891 principal
3892 .as_ref()
3893 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
3894 } else {
3895 principal
3896 };
3897 self.auth_state.set_require_auth(fresh.require_auth);
3898 *self.catalog.write() = fresh;
3899 *self.principal.write() = principal.clone();
3900 self.auth_state.set_principal(principal);
3901 Ok(())
3902 }
3903
3904 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
3905 let guard = self.security_coordinator.gate.write();
3906 let version = self.security_coordinator.version.load(Ordering::Acquire);
3907 self.refresh_security_catalog_if_stale(version)?;
3908 Ok(guard)
3909 }
3910
3911 pub(crate) fn admit_operation(&self) -> Result<crate::core::OperationGuard> {
3938 self.core.operation_guard()
3939 }
3940
3941 fn apply_catalog_command_to(
3947 &self,
3948 next_catalog: &mut Catalog,
3949 command: crate::catalog_cmds::CatalogCommand,
3950 ) -> Result<crate::catalog_cmds::CatalogDelta> {
3951 let record = crate::catalog_cmds::CatalogCommandRecord::next(next_catalog, command);
3952 next_catalog.apply_command(&record)
3953 }
3954
3955 pub fn apply_replicated_records(&self, records: &[crate::wal::Record]) -> Result<bool> {
3973 use crate::wal::Op;
3974 use std::sync::atomic::Ordering;
3975
3976 let _operation = self.admit_operation()?;
3977 if self.poisoned.load(Ordering::Relaxed) {
3978 return Err(MongrelError::Other(
3979 "database poisoned by fsync error".into(),
3980 ));
3981 }
3982 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
3985 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
3986 })?;
3987 if records.iter().any(|record| record.txn_id != txn_id) {
3988 return Err(MongrelError::InvalidArgument(
3989 "replicated transaction payload mixes transaction ids".into(),
3990 ));
3991 }
3992 let commits = records
3993 .iter()
3994 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
3995 .count();
3996 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
3997 return Err(MongrelError::InvalidArgument(
3998 "replicated transaction payload must end in exactly one commit marker".into(),
3999 ));
4000 }
4001 let commit_epoch = match records.last().map(|r| &r.op) {
4002 Some(Op::TxnCommit { epoch, .. }) => *epoch,
4003 _ => unreachable!("validated above"),
4004 };
4005 if let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) {
4014 if !added_runs.is_empty() {
4015 return Err(MongrelError::InvalidArgument(
4016 "replicated spilled-run commits are not appliable: the leader must translate \
4017 the commit through translate_records_for_replication before proposal"
4018 .into(),
4019 ));
4020 }
4021 }
4022 if commit_epoch <= self.epoch.visible().0 {
4026 return Ok(false);
4027 }
4028 {
4033 let mut wal = self.shared_wal.lock();
4034 for record in records {
4035 wal.append(record.txn_id, 0, record.op.clone())?;
4036 }
4037 if let Err(error) = wal.group_sync() {
4038 self.poisoned.store(true, Ordering::Relaxed);
4039 return Err(error);
4040 }
4041 }
4042 let replication_pins: Vec<crate::retention::PinGuard> = {
4045 let tables = self.tables.read();
4046 let floor = Epoch(self.epoch.visible().0);
4047 tables
4048 .values()
4049 .map(|handle| {
4050 let t = handle.lock();
4051 Arc::clone(t.pin_registry())
4052 .pin(crate::retention::PinSource::Replication, floor)
4053 })
4054 .collect()
4055 };
4056 let tables = self.tables.read().clone();
4057 let catalog = self.catalog.read().clone();
4058 recover_shared_wal(&self.durable_root, &tables, &catalog, &self.epoch, records)?;
4059 if let Some(Op::TxnCommit { epoch, .. }) = records.last().map(|r| &r.op) {
4064 let commit_ts = records
4065 .iter()
4066 .rev()
4067 .find_map(|record| match &record.op {
4068 Op::CommitTimestamp { unix_nanos } => {
4069 Some(mongreldb_types::hlc::HlcTimestamp {
4070 physical_micros: unix_nanos / 1_000,
4071 logical: 0,
4072 node_tiebreaker: 0,
4073 })
4074 }
4075 _ => None,
4076 })
4077 .unwrap_or_else(|| {
4078 self.hlc
4079 .now()
4080 .unwrap_or(mongreldb_types::hlc::HlcTimestamp {
4081 physical_micros: 0,
4082 logical: 0,
4083 node_tiebreaker: 0,
4084 })
4085 });
4086 self.record_commit_ts(Epoch(*epoch), commit_ts);
4087 }
4088 drop(replication_pins);
4089 Ok(true)
4090 }
4091
4092 pub fn validate_staged_txn_writes(&self, staged: &[Vec<u8>]) -> Result<()> {
4102 let tables = self.tables.read();
4103 for payload in staged {
4104 match StagedTxnWrite::decode(payload)? {
4105 StagedTxnWrite::Put { table_id, rows } => {
4106 let handle = tables.get(&table_id).ok_or_else(|| {
4107 MongrelError::InvalidArgument(format!(
4108 "staged write targets unmounted table {table_id}"
4109 ))
4110 })?;
4111 let rows: Vec<crate::memtable::Row> =
4112 bincode::deserialize(&rows).map_err(|error| {
4113 MongrelError::InvalidArgument(format!(
4114 "staged put payload for table {table_id} cannot decode: {error}"
4115 ))
4116 })?;
4117 let schema = handle.lock().schema().clone();
4118 for row in &rows {
4119 validate_recovered_row(&schema, row).map_err(|error| {
4120 MongrelError::InvalidArgument(format!(
4121 "staged row for table {table_id} is not appliable: {error}"
4122 ))
4123 })?;
4124 }
4125 }
4126 StagedTxnWrite::Delete { table_id, row_ids } => {
4127 if !tables.contains_key(&table_id) {
4128 return Err(MongrelError::InvalidArgument(format!(
4129 "staged delete targets unmounted table {table_id}"
4130 )));
4131 }
4132 if row_ids.contains(&u64::MAX) {
4133 return Err(MongrelError::InvalidArgument(format!(
4134 "staged delete for table {table_id} names an exhausted row id"
4135 )));
4136 }
4137 }
4138 }
4139 }
4140 Ok(())
4141 }
4142
4143 pub fn apply_staged_txn_writes(
4162 &self,
4163 txn_tag: u64,
4164 staged: &[Vec<u8>],
4165 commit_ts: mongreldb_types::hlc::HlcTimestamp,
4166 ) -> Result<bool> {
4167 use crate::wal::Op;
4168
4169 let mut writes = Vec::with_capacity(staged.len());
4171 for payload in staged {
4172 writes.push(StagedTxnWrite::decode(payload)?);
4173 }
4174 let generation = *self.next_txn_id.lock() >> 32;
4180 let txn_id = (generation << 32) | (txn_tag & 0x7FFF_FFFF) | 0x8000_0000;
4181 let mut records = Vec::with_capacity(writes.len() + 2);
4182 for write in writes {
4183 let op = match write {
4184 StagedTxnWrite::Put { table_id, rows } => Op::Put { table_id, rows },
4185 StagedTxnWrite::Delete { table_id, row_ids } => Op::Delete {
4186 table_id,
4187 row_ids: row_ids.into_iter().map(crate::RowId).collect(),
4188 },
4189 };
4190 records.push(crate::wal::Record::new(Epoch(0), txn_id, op));
4191 }
4192 let epoch = self.epoch.visible().0 + 1;
4193 let unix_nanos = commit_ts.physical_micros.saturating_mul(1_000);
4197 records.push(crate::wal::Record::new(
4198 Epoch(0),
4199 txn_id,
4200 Op::CommitTimestamp { unix_nanos },
4201 ));
4202 records.push(crate::wal::Record::new(
4203 Epoch(0),
4204 txn_id,
4205 Op::TxnCommit {
4206 epoch,
4207 added_runs: Vec::new(),
4208 },
4209 ));
4210 self.apply_replicated_records(&records)
4211 }
4212
4213 pub fn apply_replicated_catalog_command(
4222 &self,
4223 record: &crate::catalog_cmds::CatalogCommandRecord,
4224 ) -> Result<crate::catalog_cmds::CatalogDelta> {
4225 let _operation = self.admit_operation()?;
4226 let _g = self.ddl_lock.lock();
4227 let mut next_catalog = self.catalog.read().clone();
4228 let delta = next_catalog.apply_command(record)?;
4229 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
4230 return Ok(delta);
4231 }
4232 let referenced_epoch = match &delta {
4238 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => Some(entry.created_epoch),
4239 crate::catalog_cmds::CatalogDelta::TableDropped { at_epoch, .. }
4240 | crate::catalog_cmds::CatalogDelta::TableRenamed { at_epoch, .. } => Some(*at_epoch),
4241 _ => None,
4242 };
4243 if let Some(referenced) = referenced_epoch {
4244 self.epoch.advance_recovered(Epoch(referenced));
4245 next_catalog.db_epoch = next_catalog.db_epoch.max(referenced);
4246 }
4247 match &delta {
4248 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => {
4249 if !self.tables.read().contains_key(&entry.table_id) {
4254 self.mount_catalog_entry(entry)?;
4255 }
4256 }
4257 crate::catalog_cmds::CatalogDelta::TableDropped { table_id, .. } => {
4258 self.tables.write().remove(table_id);
4259 }
4260 _ => {}
4261 }
4262 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref())?;
4266 *self.catalog.write() = next_catalog;
4267 Ok(delta)
4268 }
4269
4270 fn mount_catalog_entry(&self, entry: &crate::catalog::CatalogEntry) -> Result<()> {
4275 let table_relative = Path::new(TABLES_DIR).join(entry.table_id.to_string());
4276 let table_root = Arc::new(
4277 self.durable_root
4278 .create_directory_all_pinned(&table_relative)?,
4279 );
4280 let tdir = table_root.io_path()?;
4281 let ctx = SharedCtx {
4282 root_guard: Some(table_root),
4283 epoch: Arc::clone(&self.epoch),
4284 page_cache: Arc::clone(&self.page_cache),
4285 decoded_cache: Arc::clone(&self.decoded_cache),
4286 snapshots: Arc::clone(&self.snapshots),
4287 kek: self.kek.clone(),
4288 commit_lock: Arc::clone(&self.commit_lock),
4289 shared: Some(crate::engine::SharedWalCtx {
4290 wal: Arc::clone(&self.shared_wal),
4291 group: Arc::clone(&self.group),
4292 poisoned: Arc::clone(&self.poisoned),
4293 txn_ids: Arc::clone(&self.next_txn_id),
4294 change_wake: self.change_wake.clone(),
4295 lifecycle: Arc::clone(&self.lifecycle),
4296 }),
4297 table_name: Some(entry.name.clone()),
4298 auth: self.table_auth_checker(),
4299 read_only: self.read_only,
4300 };
4301 let table = Table::create_in(&tdir, entry.schema.clone(), entry.table_id, ctx)?;
4302 self.tables
4303 .write()
4304 .insert(entry.table_id, TableHandle::new(table));
4305 Ok(())
4306 }
4307
4308 fn publish_catalog_candidate(
4309 &self,
4310 catalog: Catalog,
4311 epoch: Epoch,
4312 epoch_guard: &mut EpochGuard<'_>,
4313 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
4314 ) -> Result<()> {
4315 self.publish_catalog_candidate_with_prelude(
4316 catalog,
4317 epoch,
4318 epoch_guard,
4319 before_publish,
4320 Vec::new(),
4321 )
4322 }
4323
4324 fn publish_catalog_candidate_with_prelude(
4325 &self,
4326 catalog: Catalog,
4327 epoch: Epoch,
4328 epoch_guard: &mut EpochGuard<'_>,
4329 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
4330 prelude: Vec<(u64, crate::wal::Op)>,
4331 ) -> Result<()> {
4332 use crate::wal::DdlOp;
4333
4334 if self.read_only {
4335 return Err(MongrelError::ReadOnlyReplica);
4336 }
4337 if self.poisoned.load(Ordering::Relaxed) {
4338 return Err(MongrelError::Other(
4339 "database poisoned by fsync error".into(),
4340 ));
4341 }
4342 let _operation = self.admit_operation()?;
4344 if let Some(before_publish) = before_publish.as_mut() {
4345 (**before_publish)()?;
4346 }
4347 if catalog.db_epoch != epoch.0 {
4348 return Err(MongrelError::InvalidArgument(format!(
4349 "catalog epoch {} does not match commit epoch {}",
4350 catalog.db_epoch, epoch.0
4351 )));
4352 }
4353 {
4354 let current = self.catalog.read();
4355 validate_catalog_transition(¤t, &catalog)?;
4356 }
4357 validate_recovered_catalog(&catalog)?;
4358 let catalog_json = DdlOp::encode_catalog(&catalog)?;
4359 let txn_id = self.alloc_txn_id()?;
4360 let commit_seq = {
4361 let mut wal = self.shared_wal.lock();
4362 let append: Result<u64> = (|| {
4363 for (table_id, op) in prelude {
4364 wal.append(txn_id, table_id, op)?;
4365 }
4366 wal.append(
4367 txn_id,
4368 WAL_TABLE_ID,
4369 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
4370 )?;
4371 wal.append_commit(txn_id, epoch, &[])
4372 })();
4373 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4374 };
4375 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4376 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
4377 self.finish_durable_publish(epoch, epoch_guard, &receipt, checkpoint)
4378 }
4379
4380 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
4384 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
4385 let version = catalog.security_version;
4386 let principal = self.principal.read().clone();
4387 let principal = if catalog.require_auth {
4388 principal.as_ref().and_then(|principal| {
4389 Self::resolve_bound_principal_from_catalog(&catalog, principal)
4390 })
4391 } else {
4392 principal
4393 };
4394 *self.catalog.write() = catalog;
4395 self.security_coordinator
4396 .version
4397 .store(version, Ordering::Release);
4398 self.auth_state
4399 .set_require_auth(self.catalog.read().require_auth);
4400 *self.principal.write() = principal.clone();
4401 self.auth_state.set_principal(principal);
4402 checkpoint
4403 }
4404
4405 fn finish_durable_publish(
4406 &self,
4407 epoch: Epoch,
4408 epoch_guard: &mut EpochGuard<'_>,
4409 receipt: &mongreldb_log::CommitReceipt,
4410 post_step: Result<()>,
4411 ) -> Result<()> {
4412 if let Err(error) = self.publish_committed(receipt, epoch) {
4413 self.poisoned.store(true, Ordering::Relaxed);
4418 self.lifecycle.poison();
4419 return Err(MongrelError::DurableCommit {
4420 epoch: epoch.0,
4421 message: error.to_string(),
4422 });
4423 }
4424 epoch_guard.disarm();
4425 match post_step {
4426 Ok(()) => Ok(()),
4427 Err(error) => {
4428 self.poisoned.store(true, Ordering::Relaxed);
4429 self.lifecycle.poison();
4430 Err(MongrelError::DurableCommit {
4431 epoch: epoch.0,
4432 message: error.to_string(),
4433 })
4434 }
4435 }
4436 }
4437
4438 fn publish_committed(
4444 &self,
4445 receipt: &mongreldb_log::CommitReceipt,
4446 epoch: Epoch,
4447 ) -> Result<()> {
4448 debug_assert_eq!(
4449 receipt.log_position.index, epoch.0,
4450 "commit receipt position must match the published epoch"
4451 );
4452 mongreldb_fault::inject("commit.publish.before").map_err(crate::commit_log::fault_as_io)?;
4453 self.epoch.publish_in_order(epoch);
4454 mongreldb_fault::inject("commit.publish.after").map_err(crate::commit_log::fault_as_io)?;
4455 Ok(())
4456 }
4457
4458 fn await_durable_commit(
4467 &self,
4468 txn_id: u64,
4469 commit_seq: u64,
4470 epoch: Epoch,
4471 ) -> Result<mongreldb_log::CommitReceipt> {
4472 match self
4473 .commit_log
4474 .seal_transaction(txn_id, epoch, commit_seq, None)
4475 {
4476 Ok(receipt) => {
4477 self.record_commit_ts(epoch, receipt.commit_ts);
4478 Ok(receipt)
4479 }
4480 Err(error) => {
4481 self.poisoned.store(true, Ordering::Relaxed);
4482 self.lifecycle.poison();
4483 Err(MongrelError::CommitOutcomeUnknown {
4484 epoch: epoch.0,
4485 message: error.to_string(),
4486 })
4487 }
4488 }
4489 }
4490
4491 fn await_durable_commit_with_ts(
4496 &self,
4497 txn_id: u64,
4498 commit_seq: u64,
4499 epoch: Epoch,
4500 commit_ts: mongreldb_types::hlc::HlcTimestamp,
4501 ) -> Result<mongreldb_log::CommitReceipt> {
4502 match self
4503 .commit_log
4504 .seal_transaction(txn_id, epoch, commit_seq, Some(commit_ts))
4505 {
4506 Ok(receipt) => {
4507 self.record_commit_ts(epoch, receipt.commit_ts);
4508 Ok(receipt)
4509 }
4510 Err(error) => {
4511 self.poisoned.store(true, Ordering::Relaxed);
4512 self.lifecycle.poison();
4513 Err(MongrelError::CommitOutcomeUnknown {
4514 epoch: epoch.0,
4515 message: error.to_string(),
4516 })
4517 }
4518 }
4519 }
4520
4521 fn record_commit_ts(&self, epoch: Epoch, commit_ts: mongreldb_types::hlc::HlcTimestamp) {
4526 let mut ledger = self.commit_ts_ledger.lock();
4527 ledger.insert(epoch.0, commit_ts);
4528 while ledger.len() > COMMIT_TS_LEDGER_CAP {
4529 ledger.pop_first();
4530 }
4531 }
4532
4533 pub fn commit_ts_for_epoch(&self, epoch: Epoch) -> Option<mongreldb_types::hlc::HlcTimestamp> {
4547 self.commit_ts_ledger.lock().get(&epoch.0).copied()
4548 }
4549
4550 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
4551 self.poisoned.store(true, Ordering::Relaxed);
4552 self.lifecycle.poison();
4553 MongrelError::CommitOutcomeUnknown {
4554 epoch: epoch.0,
4555 message: error.to_string(),
4556 }
4557 }
4558
4559 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
4561 self.set_security_catalog_as_with_epoch(security, None)
4562 .map(|_| ())
4563 }
4564
4565 pub fn set_security_catalog_as(
4567 &self,
4568 security: crate::security::SecurityCatalog,
4569 principal: Option<&crate::auth::Principal>,
4570 ) -> Result<()> {
4571 self.set_security_catalog_as_with_epoch(security, principal)
4572 .map(|_| ())
4573 }
4574
4575 pub fn set_security_catalog_as_with_epoch(
4577 &self,
4578 security: crate::security::SecurityCatalog,
4579 principal: Option<&crate::auth::Principal>,
4580 ) -> Result<Epoch> {
4581 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
4582 }
4583
4584 pub fn set_security_catalog_as_with_epoch_controlled<F>(
4587 &self,
4588 security: crate::security::SecurityCatalog,
4589 principal: Option<&crate::auth::Principal>,
4590 mut before_commit: F,
4591 ) -> Result<Epoch>
4592 where
4593 F: FnMut() -> Result<()>,
4594 {
4595 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
4596 }
4597
4598 fn set_security_catalog_as_with_epoch_inner(
4599 &self,
4600 security: crate::security::SecurityCatalog,
4601 principal: Option<&crate::auth::Principal>,
4602 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
4603 ) -> Result<Epoch> {
4604 use crate::wal::DdlOp;
4605 use std::sync::atomic::Ordering;
4606
4607 let command = crate::catalog_cmds::CatalogCommand::SetSecurityCatalog {
4611 security: security.clone(),
4612 };
4613 self.require_for(
4614 principal,
4615 &crate::catalog_cmds::required_permission(&command),
4616 )?;
4617 if self.poisoned.load(Ordering::Relaxed) {
4618 return Err(MongrelError::Other(
4619 "database poisoned by fsync error".into(),
4620 ));
4621 }
4622 let _operation = self.admit_operation()?;
4625 let _ddl = self.ddl_lock.lock();
4626 let _security_write = self.security_write()?;
4629 self.require_for(
4630 principal,
4631 &crate::catalog_cmds::required_permission(&command),
4632 )?;
4633 let mut next_catalog = self.catalog.read().clone();
4634 validate_security_catalog(&next_catalog, &security)?;
4635 let payload = DdlOp::encode_security(&security)?;
4636 let _commit = self.commit_lock.lock();
4637 let epoch = self.epoch.bump_assigned();
4638 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4639 let txn_id = self.alloc_txn_id()?;
4640 self.apply_catalog_command_to(&mut next_catalog, command)?;
4641 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4642 let commit_seq = {
4643 let mut wal = self.shared_wal.lock();
4644 if let Some(before_commit) = before_commit {
4645 before_commit()?;
4646 }
4647 let append: Result<u64> = (|| {
4648 wal.append(
4649 txn_id,
4650 WAL_TABLE_ID,
4651 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
4652 security_json: payload,
4653 }),
4654 )?;
4655 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4656 wal.append_commit(txn_id, epoch, &[])
4657 })();
4658 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4659 };
4660 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4661 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4662 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
4663 Ok(epoch)
4664 }
4665
4666 pub fn require_for(
4667 &self,
4668 principal: Option<&crate::auth::Principal>,
4669 permission: &crate::auth::Permission,
4670 ) -> Result<()> {
4671 let Some(principal) = principal else {
4672 return self.require(permission);
4673 };
4674 let resolved;
4675 let principal = if self.auth_state.require_auth() || principal.user_id != 0 {
4676 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
4677 .ok_or(MongrelError::AuthRequired)?;
4678 &resolved
4679 } else {
4680 principal
4681 };
4682 #[cfg(test)]
4683 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
4684 if principal.has_permission(permission) {
4685 Ok(())
4686 } else {
4687 Err(MongrelError::PermissionDenied {
4688 required: permission.clone(),
4689 principal: principal.username.clone(),
4690 })
4691 }
4692 }
4693
4694 fn require_exact_principal_current(
4698 &self,
4699 principal: Option<&crate::auth::Principal>,
4700 permission: &crate::auth::Permission,
4701 ) -> Result<()> {
4702 let catalog = self.catalog.read();
4703 if !catalog.require_auth {
4704 return Ok(());
4705 }
4706 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
4707 let current = Self::resolve_bound_principal_from_catalog(&catalog, supplied)
4708 .ok_or(MongrelError::AuthRequired)?;
4709 if current.has_permission(permission) {
4710 Ok(())
4711 } else {
4712 Err(MongrelError::PermissionDenied {
4713 required: permission.clone(),
4714 principal: current.username,
4715 })
4716 }
4717 }
4718
4719 pub(crate) fn with_exact_principal_current<T, F>(
4720 &self,
4721 principal: Option<&crate::auth::Principal>,
4722 permission: &crate::auth::Permission,
4723 operation: F,
4724 ) -> Result<T>
4725 where
4726 F: FnOnce() -> Result<T>,
4727 {
4728 let _security = self.security_coordinator.gate.read();
4729 self.require_exact_principal_current(principal, permission)?;
4730 operation()
4731 }
4732
4733 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
4734 self.principal.read().clone()
4735 }
4736
4737 #[cfg(test)]
4738 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
4739 *self.principal.write() = principal.clone();
4740 self.auth_state.set_principal(principal);
4741 }
4742
4743 pub fn require_columns_for(
4744 &self,
4745 table: &str,
4746 operation: crate::auth::ColumnOperation,
4747 column_ids: &[u16],
4748 principal: Option<&crate::auth::Principal>,
4749 ) -> Result<()> {
4750 if principal.is_none() && !self.auth_state.require_auth() {
4751 return Ok(());
4752 }
4753 let cached = self.principal.read().clone();
4754 let principal = principal.or(cached.as_ref());
4755 let Some(principal) = principal else {
4756 let permission = match operation {
4757 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
4758 table: table.to_string(),
4759 },
4760 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
4761 table: table.to_string(),
4762 },
4763 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
4764 table: table.to_string(),
4765 },
4766 };
4767 return self.require(&permission);
4768 };
4769 let catalog = self.catalog.read();
4770 let resolved;
4771 let principal = if catalog.require_auth || principal.user_id != 0 {
4772 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
4773 .ok_or(MongrelError::AuthRequired)?;
4774 &resolved
4775 } else {
4776 principal
4777 };
4778 let schema = &catalog
4779 .live(table)
4780 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
4781 .schema;
4782 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
4783 }
4784
4785 fn require_columns_for_principal(
4786 table: &str,
4787 schema: &Schema,
4788 operation: crate::auth::ColumnOperation,
4789 column_ids: &[u16],
4790 principal: &crate::auth::Principal,
4791 ) -> Result<()> {
4792 #[cfg(test)]
4793 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
4794 match principal.column_access(table, operation) {
4795 crate::auth::ColumnAccess::All => Ok(()),
4796 crate::auth::ColumnAccess::Columns(allowed) => {
4797 let denied = column_ids.iter().find_map(|column_id| {
4798 schema
4799 .columns
4800 .iter()
4801 .find(|column| column.id == *column_id)
4802 .filter(|column| !allowed.contains(&column.name))
4803 });
4804 if denied.is_none() {
4805 Ok(())
4806 } else {
4807 Err(MongrelError::PermissionDenied {
4808 required: match operation {
4809 crate::auth::ColumnOperation::Select => {
4810 crate::auth::Permission::SelectColumns {
4811 table: table.to_string(),
4812 columns: denied
4813 .into_iter()
4814 .map(|column| column.name.clone())
4815 .collect(),
4816 }
4817 }
4818 crate::auth::ColumnOperation::Insert => {
4819 crate::auth::Permission::InsertColumns {
4820 table: table.to_string(),
4821 columns: denied
4822 .into_iter()
4823 .map(|column| column.name.clone())
4824 .collect(),
4825 }
4826 }
4827 crate::auth::ColumnOperation::Update => {
4828 crate::auth::Permission::UpdateColumns {
4829 table: table.to_string(),
4830 columns: denied
4831 .into_iter()
4832 .map(|column| column.name.clone())
4833 .collect(),
4834 }
4835 }
4836 },
4837 principal: principal.username.clone(),
4838 })
4839 }
4840 }
4841 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
4842 required: match operation {
4843 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
4844 table: table.to_string(),
4845 },
4846 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
4847 table: table.to_string(),
4848 },
4849 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
4850 table: table.to_string(),
4851 },
4852 },
4853 principal: principal.username.clone(),
4854 }),
4855 }
4856 }
4857
4858 pub fn select_column_ids_for(
4859 &self,
4860 table: &str,
4861 principal: Option<&crate::auth::Principal>,
4862 ) -> Result<Vec<u16>> {
4863 let catalog = self.catalog.read();
4864 let columns = catalog
4865 .live(table)
4866 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
4867 .schema
4868 .columns
4869 .iter()
4870 .map(|column| (column.id, column.name.clone()))
4871 .collect::<Vec<_>>();
4872 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
4873 drop(catalog);
4874 let Some(principal) = principal.as_ref() else {
4875 self.require(&crate::auth::Permission::Select {
4876 table: table.to_string(),
4877 })?;
4878 return Ok(columns.iter().map(|(id, _)| *id).collect());
4879 };
4880 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
4881 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
4882 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
4883 .iter()
4884 .filter(|(_, name)| allowed.contains(name))
4885 .map(|(id, _)| *id)
4886 .collect()),
4887 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
4888 required: crate::auth::Permission::Select {
4889 table: table.to_string(),
4890 },
4891 principal: principal.username.clone(),
4892 }),
4893 }
4894 }
4895
4896 pub fn secure_rows_for(
4897 &self,
4898 table: &str,
4899 rows: Vec<crate::memtable::Row>,
4900 principal: Option<&crate::auth::Principal>,
4901 ) -> Result<Vec<crate::memtable::Row>> {
4902 self.secure_rows_for_with_context(table, rows, principal, None)
4903 }
4904
4905 pub fn secure_rows_for_with_context(
4906 &self,
4907 table: &str,
4908 rows: Vec<crate::memtable::Row>,
4909 principal: Option<&crate::auth::Principal>,
4910 context: Option<&crate::query::AiExecutionContext>,
4911 ) -> Result<Vec<crate::memtable::Row>> {
4912 let (security, principal) = {
4913 let catalog = self.catalog.read();
4914 (
4915 catalog.security.clone(),
4916 self.principal_for_authorized_read(&catalog, principal, false)?,
4917 )
4918 };
4919 if !security.table_has_security(table) {
4920 return Ok(rows);
4921 }
4922 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
4923 let mut output = Vec::new();
4924 for mut row in rows {
4925 if let Some(context) = context {
4926 context.consume(1)?;
4927 }
4928 if security.row_allowed(
4929 table,
4930 crate::security::PolicyCommand::Select,
4931 &row,
4932 principal,
4933 false,
4934 ) {
4935 security.apply_masks(table, &mut row, principal);
4936 output.push(row);
4937 }
4938 }
4939 Ok(output)
4940 }
4941
4942 pub fn mask_search_hits_for(
4945 &self,
4946 table: &str,
4947 hits: &mut [crate::query::SearchHit],
4948 principal: Option<&crate::auth::Principal>,
4949 ) -> Result<()> {
4950 let (security, principal) = {
4951 let catalog = self.catalog.read();
4952 (
4953 catalog.security.clone(),
4954 self.principal_for_authorized_read(&catalog, principal, false)?,
4955 )
4956 };
4957 if !security.table_has_security(table) {
4958 return Ok(());
4959 }
4960 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
4961 for hit in hits {
4962 security.apply_masks_to_cells(table, &mut hit.cells, principal);
4963 }
4964 Ok(())
4965 }
4966
4967 pub fn mask_rows_for(
4969 &self,
4970 table: &str,
4971 rows: &mut [crate::memtable::Row],
4972 principal: Option<&crate::auth::Principal>,
4973 ) -> Result<()> {
4974 let (security, principal) = {
4975 let catalog = self.catalog.read();
4976 (
4977 catalog.security.clone(),
4978 self.principal_for_authorized_read(&catalog, principal, false)?,
4979 )
4980 };
4981 if !security.table_has_security(table) {
4982 return Ok(());
4983 }
4984 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
4985 for row in rows {
4986 security.apply_masks(table, row, principal);
4987 }
4988 Ok(())
4989 }
4990
4991 pub fn authorized_candidate_ids_for(
4993 &self,
4994 table: &str,
4995 principal: Option<&crate::auth::Principal>,
4996 ) -> Result<Option<std::collections::HashSet<RowId>>> {
4997 Ok(self
4998 .authorized_read_snapshot(table, principal)?
4999 .allowed_row_ids)
5000 }
5001
5002 fn allowed_row_ids_locked(
5003 &self,
5004 table_name: &str,
5005 table: &Table,
5006 table_snapshot: Snapshot,
5007 security_state: (&crate::security::SecurityCatalog, u64),
5008 principal: Option<&crate::auth::Principal>,
5009 context: Option<&crate::query::AiExecutionContext>,
5010 ) -> Result<Option<Arc<HashSet<RowId>>>> {
5011 let (security, security_version) = security_state;
5012 if !security.rls_enabled(table_name) {
5013 return Ok(None);
5014 }
5015 let authorization_started = std::time::Instant::now();
5016 let principal = principal.ok_or(MongrelError::AuthRequired)?;
5017 let mut roles = principal.roles.clone();
5018 roles.sort_unstable();
5019 let principal_key = format!(
5020 "{}:{}:{}:{}:{roles:?}",
5021 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
5022 );
5023 let cache_key = (
5024 table_name.to_string(),
5025 table.data_generation(),
5026 security_version,
5027 principal_key,
5028 );
5029 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
5030 crate::trace::QueryTrace::record(|trace| {
5031 trace.rls_cache_hit = true;
5032 trace.authorization_nanos = trace
5033 .authorization_nanos
5034 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5035 });
5036 return Ok(Some(allowed));
5037 }
5038 if let Some(context) = context {
5039 context.checkpoint()?;
5040 }
5041 let started = std::time::Instant::now();
5043 let rows = table.visible_rows(table_snapshot)?;
5044 let rows_evaluated = rows.len() as u64;
5045 let mut allowed = HashSet::new();
5046 for chunk in rows.chunks(256) {
5047 if let Some(context) = context {
5048 context.consume(chunk.len())?;
5049 }
5050 allowed.extend(chunk.iter().filter_map(|row| {
5051 security
5052 .row_allowed(
5053 table_name,
5054 crate::security::PolicyCommand::Select,
5055 row,
5056 principal,
5057 false,
5058 )
5059 .then_some(row.row_id)
5060 }));
5061 }
5062 let allowed = Arc::new(allowed);
5063 let mut cache = self.rls_cache.lock();
5064 cache.build_nanos = cache
5065 .build_nanos
5066 .saturating_add(started.elapsed().as_nanos() as u64);
5067 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
5068 cache.insert(cache_key, Arc::clone(&allowed));
5069 crate::trace::QueryTrace::record(|trace| {
5070 trace.rls_rows_evaluated = trace
5071 .rls_rows_evaluated
5072 .saturating_add(rows_evaluated as usize);
5073 trace.authorization_nanos = trace
5074 .authorization_nanos
5075 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5076 });
5077 Ok(Some(allowed))
5078 }
5079
5080 fn principal_for_authorized_read(
5081 &self,
5082 catalog: &Catalog,
5083 principal: Option<&crate::auth::Principal>,
5084 catalog_bound: bool,
5085 ) -> Result<Option<crate::auth::Principal>> {
5086 let principal = principal.cloned().or_else(|| self.principal.read().clone());
5087 let Some(principal) = principal else {
5088 return Ok(None);
5089 };
5090 if catalog.require_auth || catalog_bound || principal.user_id != 0 {
5091 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
5092 .map(Some)
5093 .ok_or(MongrelError::AuthRequired);
5094 }
5095 Ok(Some(principal))
5096 }
5097
5098 pub fn with_authorized_read<T, F>(
5102 &self,
5103 table_name: &str,
5104 principal: Option<&crate::auth::Principal>,
5105 catalog_bound: bool,
5106 read: F,
5107 ) -> Result<T>
5108 where
5109 F: FnMut(
5110 &mut Table,
5111 Snapshot,
5112 Option<&HashSet<RowId>>,
5113 Option<&crate::auth::Principal>,
5114 ) -> Result<T>,
5115 {
5116 self.with_authorized_read_context(
5117 table_name,
5118 principal,
5119 catalog_bound,
5120 None,
5121 None,
5122 None,
5123 read,
5124 )
5125 }
5126
5127 #[allow(clippy::too_many_arguments)]
5128 pub fn with_authorized_read_context<T, F>(
5129 &self,
5130 table_name: &str,
5131 principal: Option<&crate::auth::Principal>,
5132 catalog_bound: bool,
5133 authorization: Option<&ReadAuthorization>,
5134 context: Option<&crate::query::AiExecutionContext>,
5135 snapshot_override: Option<Snapshot>,
5136 read: F,
5137 ) -> Result<T>
5138 where
5139 F: FnMut(
5140 &mut Table,
5141 Snapshot,
5142 Option<&HashSet<RowId>>,
5143 Option<&crate::auth::Principal>,
5144 ) -> Result<T>,
5145 {
5146 self.with_authorized_read_context_stamped(
5147 table_name,
5148 principal,
5149 catalog_bound,
5150 authorization,
5151 context,
5152 snapshot_override,
5153 read,
5154 )
5155 .map(|(result, _)| result)
5156 }
5157
5158 #[allow(clippy::too_many_arguments)]
5159 pub fn with_authorized_read_context_stamped<T, F>(
5160 &self,
5161 table_name: &str,
5162 principal: Option<&crate::auth::Principal>,
5163 catalog_bound: bool,
5164 authorization: Option<&ReadAuthorization>,
5165 context: Option<&crate::query::AiExecutionContext>,
5166 snapshot_override: Option<Snapshot>,
5167 mut read: F,
5168 ) -> Result<(T, AuthorizedReadStamp)>
5169 where
5170 F: FnMut(
5171 &mut Table,
5172 Snapshot,
5173 Option<&HashSet<RowId>>,
5174 Option<&crate::auth::Principal>,
5175 ) -> Result<T>,
5176 {
5177 if principal.is_none() && self.principal.read().is_some() {
5178 self.refresh_principal()?;
5179 }
5180 const RETRIES: usize = 3;
5181 let handle = self.table(table_name)?;
5182 for attempt in 0..RETRIES {
5183 crate::trace::QueryTrace::record(|trace| {
5184 trace.authorization_retries = attempt;
5185 });
5186 let (security, security_version, effective_principal) = {
5187 let catalog = self.catalog.read();
5188 (
5189 catalog.security.clone(),
5190 catalog.security_version,
5191 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
5192 )
5193 };
5194 if let Some(authorization) = authorization {
5195 for permission in &authorization.permissions {
5196 self.require_for(effective_principal.as_ref(), permission)?;
5197 }
5198 self.require_columns_for(
5199 table_name,
5200 authorization.operation,
5201 &authorization.columns,
5202 effective_principal.as_ref(),
5203 )?;
5204 }
5205 let result = {
5206 let mut table = lock_table_with_context(&handle, context)?;
5207 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
5208 let allowed = self.allowed_row_ids_locked(
5209 table_name,
5210 &table,
5211 snapshot,
5212 (&security, security_version),
5213 effective_principal.as_ref(),
5214 context,
5215 )?;
5216 let stamp = AuthorizedReadStamp {
5217 table_id: table.table_id(),
5218 schema_id: table.schema().schema_id,
5219 data_generation: table.data_generation(),
5220 security_version,
5221 snapshot,
5222 };
5223 let result = read(
5224 &mut table,
5225 snapshot,
5226 allowed.as_deref(),
5227 effective_principal.as_ref(),
5228 )?;
5229 (result, stamp)
5230 };
5231 if let Some(context) = context {
5232 context.checkpoint()?;
5233 }
5234 if self.catalog.read().security_version == security_version {
5235 return Ok(result);
5236 }
5237 if attempt + 1 == RETRIES {
5238 return Err(MongrelError::Conflict(
5239 "security policy changed during scored read".into(),
5240 ));
5241 }
5242 }
5243 Err(MongrelError::Conflict(
5244 "authorization retry loop exhausted".into(),
5245 ))
5246 }
5247
5248 fn with_authorized_aggregate_table<T, F>(
5249 &self,
5250 table_name: &str,
5251 columns: &[u16],
5252 principal: Option<&crate::auth::Principal>,
5253 catalog_bound: bool,
5254 allow_table_security: bool,
5255 mut aggregate: F,
5256 ) -> Result<T>
5257 where
5258 F: FnMut(
5259 &mut Table,
5260 Option<&crate::security::CandidateAuthorization<'_>>,
5261 Option<&crate::auth::Principal>,
5262 u64,
5263 ) -> Result<T>,
5264 {
5265 if principal.is_none() && self.principal.read().is_some() {
5266 self.refresh_principal()?;
5267 }
5268 const RETRIES: usize = 3;
5269 let handle = self.table(table_name)?;
5270 for attempt in 0..RETRIES {
5271 let (security, security_version, effective_principal) = {
5272 let catalog = self.catalog.read();
5273 (
5274 catalog.security.clone(),
5275 catalog.security_version,
5276 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
5277 )
5278 };
5279 self.require_columns_for(
5280 table_name,
5281 crate::auth::ColumnOperation::Select,
5282 columns,
5283 effective_principal.as_ref(),
5284 )?;
5285 if !allow_table_security && security.table_has_security(table_name) {
5286 return Err(MongrelError::InvalidArgument(
5287 "incremental aggregate is unsupported while RLS or column masks are active"
5288 .into(),
5289 ));
5290 }
5291 let result = {
5292 let mut table = handle.lock();
5293 let authorization = if security.rls_enabled(table_name) {
5294 Some(crate::security::CandidateAuthorization {
5295 table: table_name,
5296 security: &security,
5297 principal: effective_principal
5298 .as_ref()
5299 .ok_or(MongrelError::AuthRequired)?,
5300 })
5301 } else {
5302 None
5303 };
5304 aggregate(
5305 &mut table,
5306 authorization.as_ref(),
5307 effective_principal.as_ref(),
5308 security_version,
5309 )?
5310 };
5311 if self.catalog.read().security_version == security_version {
5312 return Ok(result);
5313 }
5314 if attempt + 1 == RETRIES {
5315 return Err(MongrelError::Conflict(
5316 "security policy changed during aggregate read".into(),
5317 ));
5318 }
5319 }
5320 Err(MongrelError::Conflict(
5321 "aggregate authorization retry loop exhausted".into(),
5322 ))
5323 }
5324
5325 pub fn with_authorized_scored_read_context<T, F>(
5329 &self,
5330 table_name: &str,
5331 principal: Option<&crate::auth::Principal>,
5332 catalog_bound: bool,
5333 authorization: Option<&ReadAuthorization>,
5334 context: Option<&crate::query::AiExecutionContext>,
5335 mut read: F,
5336 ) -> Result<T>
5337 where
5338 F: FnMut(
5339 &mut Table,
5340 Snapshot,
5341 Option<&crate::security::CandidateAuthorization<'_>>,
5342 Option<&crate::auth::Principal>,
5343 ) -> Result<T>,
5344 {
5345 self.with_authorized_scored_read_context_at(
5346 table_name,
5347 principal,
5348 catalog_bound,
5349 authorization,
5350 context,
5351 None,
5352 |table, snapshot, authorization, principal| {
5353 let mut table = table.clone();
5354 read(&mut table, snapshot, authorization, principal)
5355 },
5356 )
5357 }
5358
5359 #[allow(clippy::too_many_arguments)]
5360 pub fn with_authorized_scored_read_context_at<T, F>(
5361 &self,
5362 table_name: &str,
5363 principal: Option<&crate::auth::Principal>,
5364 catalog_bound: bool,
5365 authorization: Option<&ReadAuthorization>,
5366 context: Option<&crate::query::AiExecutionContext>,
5367 snapshot_override: Option<Snapshot>,
5368 read: F,
5369 ) -> Result<T>
5370 where
5371 F: FnMut(
5372 &Table,
5373 Snapshot,
5374 Option<&crate::security::CandidateAuthorization<'_>>,
5375 Option<&crate::auth::Principal>,
5376 ) -> Result<T>,
5377 {
5378 self.with_authorized_scored_read_context_at_stamped(
5379 table_name,
5380 principal,
5381 catalog_bound,
5382 authorization,
5383 context,
5384 snapshot_override,
5385 read,
5386 )
5387 .map(|(result, _)| result)
5388 }
5389
5390 #[allow(clippy::too_many_arguments)]
5391 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
5392 &self,
5393 table_name: &str,
5394 principal: Option<&crate::auth::Principal>,
5395 catalog_bound: bool,
5396 authorization: Option<&ReadAuthorization>,
5397 context: Option<&crate::query::AiExecutionContext>,
5398 snapshot_override: Option<Snapshot>,
5399 mut read: F,
5400 ) -> Result<(T, AuthorizedReadStamp)>
5401 where
5402 F: FnMut(
5403 &Table,
5404 Snapshot,
5405 Option<&crate::security::CandidateAuthorization<'_>>,
5406 Option<&crate::auth::Principal>,
5407 ) -> Result<T>,
5408 {
5409 if principal.is_none() && self.principal.read().is_some() {
5410 self.refresh_principal()?;
5411 }
5412 const RETRIES: usize = 3;
5413 let handle = self.table(table_name)?;
5414 for attempt in 0..RETRIES {
5415 if let Some(context) = context {
5416 context.checkpoint()?;
5417 }
5418 crate::trace::QueryTrace::record(|trace| {
5419 trace.authorization_retries = attempt;
5420 });
5421 let (security, security_version, effective_principal) = {
5422 let catalog = self.catalog.read();
5423 (
5424 catalog.security.clone(),
5425 catalog.security_version,
5426 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
5427 )
5428 };
5429 if let Some(authorization) = authorization {
5430 for permission in &authorization.permissions {
5431 self.require_for(effective_principal.as_ref(), permission)?;
5432 }
5433 self.require_columns_for(
5434 table_name,
5435 authorization.operation,
5436 &authorization.columns,
5437 effective_principal.as_ref(),
5438 )?;
5439 }
5440 let result = {
5441 let (table, snapshot, _snapshot_guard, _run_pins) =
5442 self.scored_read_generation(&handle, context, snapshot_override)?;
5443 let candidate_authorization = if security.rls_enabled(table_name) {
5444 Some(crate::security::CandidateAuthorization {
5445 table: table_name,
5446 security: &security,
5447 principal: effective_principal
5448 .as_ref()
5449 .ok_or(MongrelError::AuthRequired)?,
5450 })
5451 } else {
5452 None
5453 };
5454 let stamp = AuthorizedReadStamp {
5455 table_id: table.table_id(),
5456 schema_id: table.schema().schema_id,
5457 data_generation: table.data_generation(),
5458 security_version,
5459 snapshot,
5460 };
5461 let result = read(
5462 table.as_ref(),
5463 snapshot,
5464 candidate_authorization.as_ref(),
5465 effective_principal.as_ref(),
5466 )?;
5467 (result, stamp)
5468 };
5469 if let Some(context) = context {
5470 context.checkpoint()?;
5471 }
5472 if self.catalog.read().security_version == security_version {
5473 return Ok(result);
5474 }
5475 if attempt + 1 == RETRIES {
5476 return Err(MongrelError::Conflict(
5477 "security policy changed during scored read".into(),
5478 ));
5479 }
5480 }
5481 Err(MongrelError::Conflict(
5482 "scored-read authorization retry loop exhausted".into(),
5483 ))
5484 }
5485
5486 fn scored_read_generation(
5487 &self,
5488 handle: &TableHandle,
5489 context: Option<&crate::query::AiExecutionContext>,
5490 snapshot_override: Option<Snapshot>,
5491 ) -> Result<(
5492 Arc<TableReadGeneration>,
5493 Snapshot,
5494 crate::retention::OwnedSnapshotGuard,
5495 RunPins,
5496 )> {
5497 let mut table = if let Some(context) = context {
5498 loop {
5499 context.checkpoint()?;
5500 let wait = context
5501 .remaining_duration()
5502 .unwrap_or(std::time::Duration::from_millis(5))
5503 .min(std::time::Duration::from_millis(5));
5504 if let Some(table) = handle.try_lock_for(wait) {
5505 break table;
5506 }
5507 }
5508 } else {
5509 handle.lock()
5510 };
5511 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
5512 self.snapshot_at_owned(snapshot.epoch)?
5513 } else {
5514 let snapshot = table.snapshot();
5515 let guard = self.snapshots.register_owned(snapshot.epoch);
5516 (snapshot, guard)
5517 };
5518 let table_id = table.table_id();
5519 let run_keys: Vec<_> = table
5520 .active_run_ids()
5521 .map(|run_id| (table_id, run_id))
5522 .collect();
5523 let generation = handle
5524 .generation_metrics
5525 .activate(table.clone_read_generation()?);
5526 let run_pins = self.pin_runs(&run_keys);
5527 Ok((generation, snapshot, snapshot_guard, run_pins))
5528 }
5529
5530 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
5531 let mut pins = self.backup_pins.lock();
5532 for run in runs {
5533 *pins.entry(*run).or_insert(0) += 1;
5534 }
5535 drop(pins);
5536 RunPins {
5537 pins: Arc::clone(&self.backup_pins),
5538 runs: runs.to_vec(),
5539 }
5540 }
5541
5542 pub fn query_for_current_principal(
5546 &self,
5547 table_name: &str,
5548 query: &crate::query::Query,
5549 projection: Option<&[u16]>,
5550 ) -> Result<Vec<crate::memtable::Row>> {
5551 let condition_columns = crate::query::condition_columns(&query.conditions);
5552 self.with_authorized_read(
5553 table_name,
5554 None,
5555 true,
5556 |table, snapshot, allowed, principal| {
5557 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5558 self.require_columns_for(
5559 table_name,
5560 crate::auth::ColumnOperation::Select,
5561 &condition_columns,
5562 principal,
5563 )?;
5564 if let Some(projection) = projection {
5565 self.require_columns_for(
5566 table_name,
5567 crate::auth::ColumnOperation::Select,
5568 projection,
5569 principal,
5570 )?;
5571 }
5572 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
5573 let projection =
5574 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
5575 for row in &mut rows {
5576 row.columns.retain(|column, _| {
5577 allowed_columns.contains(column)
5578 && projection
5579 .as_ref()
5580 .is_none_or(|projection| projection.contains(column))
5581 });
5582 }
5583 self.secure_rows_for(table_name, rows, principal)
5584 },
5585 )
5586 }
5587
5588 pub fn query_for_current_principal_controlled(
5592 &self,
5593 table_name: &str,
5594 query: &crate::query::Query,
5595 projection: Option<&[u16]>,
5596 control: &crate::ExecutionControl,
5597 ) -> Result<Vec<crate::memtable::Row>> {
5598 self.query_for_principal_controlled(table_name, query, projection, None, true, control)
5599 }
5600
5601 fn query_for_principal_controlled(
5602 &self,
5603 table_name: &str,
5604 query: &crate::query::Query,
5605 projection: Option<&[u16]>,
5606 principal: Option<&crate::auth::Principal>,
5607 catalog_bound: bool,
5608 control: &crate::ExecutionControl,
5609 ) -> Result<Vec<crate::memtable::Row>> {
5610 control.checkpoint()?;
5611 let context = crate::query::AiExecutionContext::with_control(
5612 control.clone(),
5613 usize::MAX,
5614 crate::query::MAX_FUSED_CANDIDATES,
5615 );
5616 let condition_columns = crate::query::condition_columns(&query.conditions);
5617 self.with_authorized_read_context(
5618 table_name,
5619 principal,
5620 catalog_bound,
5621 None,
5622 Some(&context),
5623 None,
5624 |table, snapshot, allowed, principal| {
5625 control.checkpoint()?;
5626 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5627 self.require_columns_for(
5628 table_name,
5629 crate::auth::ColumnOperation::Select,
5630 &condition_columns,
5631 principal,
5632 )?;
5633 if let Some(projection) = projection {
5634 self.require_columns_for(
5635 table_name,
5636 crate::auth::ColumnOperation::Select,
5637 projection,
5638 principal,
5639 )?;
5640 }
5641 let rows =
5642 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
5643 let projection =
5644 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
5645 let mut projected = Vec::with_capacity(rows.len());
5646 for (index, mut row) in rows.into_iter().enumerate() {
5647 if index & 255 == 0 {
5648 control.checkpoint()?;
5649 }
5650 row.columns.retain(|column, _| {
5651 allowed_columns.contains(column)
5652 && projection
5653 .as_ref()
5654 .is_none_or(|projection| projection.contains(column))
5655 });
5656 projected.push(row);
5657 }
5658 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
5659 },
5660 )
5661 }
5662
5663 pub fn approx_aggregate_for_current_principal(
5666 &self,
5667 table_name: &str,
5668 conditions: &[crate::query::Condition],
5669 column: Option<u16>,
5670 agg: crate::engine::ApproxAgg,
5671 z: f64,
5672 ) -> Result<Option<crate::engine::ApproxResult>> {
5673 if !z.is_finite() || z <= 0.0 {
5674 return Err(MongrelError::InvalidArgument(
5675 "z must be finite and > 0".into(),
5676 ));
5677 }
5678 let mut columns = crate::query::condition_columns(conditions);
5679 columns.extend(column);
5680 columns.sort_unstable();
5681 columns.dedup();
5682 self.with_authorized_aggregate_table(
5683 table_name,
5684 &columns,
5685 None,
5686 true,
5687 true,
5688 |table, authorization, _, _| {
5689 table.approx_aggregate_with_candidate_authorization(
5690 conditions,
5691 column,
5692 agg,
5693 z,
5694 authorization,
5695 )
5696 },
5697 )
5698 }
5699
5700 pub fn incremental_aggregate_for_current_principal(
5704 &self,
5705 table_name: &str,
5706 conditions: &[crate::query::Condition],
5707 column: Option<u16>,
5708 agg: crate::engine::NativeAgg,
5709 ) -> Result<crate::engine::IncrementalAggResult> {
5710 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
5711 }
5712
5713 pub fn incremental_aggregate_for_principal(
5717 &self,
5718 table_name: &str,
5719 conditions: &[crate::query::Condition],
5720 column: Option<u16>,
5721 agg: crate::engine::NativeAgg,
5722 principal: Option<&crate::auth::Principal>,
5723 catalog_bound: bool,
5724 ) -> Result<crate::engine::IncrementalAggResult> {
5725 let mut columns = crate::query::condition_columns(conditions);
5726 columns.extend(column);
5727 columns.sort_unstable();
5728 columns.dedup();
5729 self.with_authorized_aggregate_table(
5730 table_name,
5731 &columns,
5732 principal,
5733 catalog_bound,
5734 false,
5735 |table, _, principal, security_version| {
5736 let cache_key = incremental_aggregate_cache_key(
5737 table_name,
5738 conditions,
5739 column,
5740 agg,
5741 principal,
5742 security_version,
5743 );
5744 table.aggregate_incremental(cache_key, conditions, column, agg)
5745 },
5746 )
5747 }
5748
5749 pub fn get_for_current_principal(
5752 &self,
5753 table_name: &str,
5754 row_id: RowId,
5755 ) -> Result<Option<crate::memtable::Row>> {
5756 self.with_authorized_read(
5757 table_name,
5758 None,
5759 true,
5760 |table, snapshot, allowed, principal| {
5761 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5762 let Some(row) = table.get(row_id, snapshot) else {
5763 return Ok(None);
5764 };
5765 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
5766 return Ok(None);
5767 }
5768 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
5769 if let Some(row) = rows.first_mut() {
5770 row.columns
5771 .retain(|column, _| allowed_columns.contains(column));
5772 }
5773 Ok(rows.pop())
5774 },
5775 )
5776 }
5777
5778 pub fn ann_rerank_for_current_principal(
5781 &self,
5782 table_name: &str,
5783 request: &crate::query::AnnRerankRequest,
5784 ) -> Result<Vec<crate::query::AnnRerankHit>> {
5785 self.with_authorized_scored_read_context_at(
5786 table_name,
5787 None,
5788 true,
5789 Some(&ReadAuthorization {
5790 operation: crate::auth::ColumnOperation::Select,
5791 columns: vec![request.column_id],
5792 permissions: Vec::new(),
5793 }),
5794 None,
5795 None,
5796 |table, snapshot, authorization, principal| {
5797 self.require_columns_for(
5798 table_name,
5799 crate::auth::ColumnOperation::Select,
5800 &[request.column_id],
5801 principal,
5802 )?;
5803 table.ann_rerank_at_with_candidate_authorization_on_generation(
5804 request,
5805 snapshot,
5806 authorization,
5807 None,
5808 )
5809 },
5810 )
5811 }
5812
5813 pub fn search_for_current_principal(
5817 &self,
5818 table_name: &str,
5819 request: &crate::query::SearchRequest,
5820 ) -> Result<Vec<crate::query::SearchHit>> {
5821 let mut columns = crate::query::condition_columns(&request.must);
5822 for named in &request.retrievers {
5823 columns.push(named.retriever.column_id());
5824 }
5825 if let Some(crate::query::Rerank::ExactVector {
5826 embedding_column, ..
5827 }) = &request.rerank
5828 {
5829 columns.push(*embedding_column);
5830 }
5831 if let Some(proj) = &request.projection {
5832 columns.extend(proj);
5833 }
5834 columns.sort_unstable();
5835 columns.dedup();
5836 self.with_authorized_scored_read_context_at(
5837 table_name,
5838 None,
5839 true,
5840 Some(&ReadAuthorization {
5841 operation: crate::auth::ColumnOperation::Select,
5842 columns: columns.clone(),
5843 permissions: Vec::new(),
5844 }),
5845 None,
5846 None,
5847 |table, snapshot, authorization, principal| {
5848 self.require_columns_for(
5849 table_name,
5850 crate::auth::ColumnOperation::Select,
5851 &columns,
5852 principal,
5853 )?;
5854 let hits = table.search_at_with_candidate_authorization_on_generation(
5855 request,
5856 snapshot,
5857 authorization,
5858 None,
5859 )?;
5860 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5861 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
5862 for mut hit in hits {
5863 let row = crate::memtable::Row {
5864 row_id: hit.row_id,
5865 committed_epoch: crate::Epoch(0),
5866 columns: hit
5867 .cells
5868 .iter()
5869 .cloned()
5870 .collect::<std::collections::HashMap<u16, crate::Value>>(),
5871 deleted: false,
5872 };
5873 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
5874 if let Some(mut row) = rows.pop() {
5875 row.columns
5876 .retain(|column, _| allowed_columns.contains(column));
5877 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
5878 hit.cells.sort_by_key(|(column, _)| *column);
5879 secured.push(hit);
5880 }
5881 }
5882 Ok(secured)
5883 },
5884 )
5885 }
5886
5887 pub fn authorized_read_snapshot(
5890 &self,
5891 table: &str,
5892 principal: Option<&crate::auth::Principal>,
5893 ) -> Result<AuthorizedReadSnapshot> {
5894 let (security, security_version, effective_principal) = {
5895 let catalog = self.catalog.read();
5896 (
5897 catalog.security.clone(),
5898 catalog.security_version,
5899 self.principal_for_authorized_read(&catalog, principal, false)?,
5900 )
5901 };
5902 let handle = self.table(table)?;
5903 let (table_snapshot, data_generation, allowed_row_ids) = {
5904 let table_handle = handle.lock();
5905 let table_snapshot = table_handle.snapshot();
5906 let data_generation = table_handle.data_generation();
5907 let allowed = self.allowed_row_ids_locked(
5908 table,
5909 &table_handle,
5910 table_snapshot,
5911 (&security, security_version),
5912 effective_principal.as_ref(),
5913 None,
5914 )?;
5915 (
5916 table_snapshot,
5917 data_generation,
5918 allowed.map(|allowed| (*allowed).clone()),
5919 )
5920 };
5921 Ok(AuthorizedReadSnapshot {
5922 table: table.to_string(),
5923 table_snapshot,
5924 data_generation,
5925 security_version,
5926 allowed_row_ids,
5927 })
5928 }
5929
5930 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
5931 if self.catalog.read().security_version != snapshot.security_version {
5932 return false;
5933 }
5934 self.table(&snapshot.table)
5935 .ok()
5936 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
5937 }
5938
5939 pub fn rls_cache_stats(&self) -> RlsCacheStats {
5940 self.rls_cache.lock().stats()
5941 }
5942
5943 pub fn rows_for(
5945 &self,
5946 table: &str,
5947 principal: Option<&crate::auth::Principal>,
5948 ) -> Result<Vec<crate::memtable::Row>> {
5949 if principal.is_none() && self.principal.read().is_some() {
5950 self.refresh_principal()?;
5951 }
5952 let allowed = self.select_column_ids_for(table, principal)?;
5953 let handle = self.table(table)?;
5954 let rows = {
5955 let table = handle.lock();
5956 table.visible_rows(table.snapshot())?
5957 };
5958 let mut rows = self.secure_rows_for(table, rows, principal)?;
5959 for row in &mut rows {
5960 row.columns.retain(|column, _| allowed.contains(column));
5961 }
5962 Ok(rows)
5963 }
5964
5965 pub fn rows_at_epoch_for_current_principal(
5968 &self,
5969 table_name: &str,
5970 snapshot: Snapshot,
5971 ) -> Result<Vec<crate::memtable::Row>> {
5972 self.with_authorized_read_context(
5973 table_name,
5974 None,
5975 true,
5976 Some(&ReadAuthorization {
5977 operation: crate::auth::ColumnOperation::Select,
5978 columns: Vec::new(),
5979 permissions: Vec::new(),
5980 }),
5981 None,
5982 Some(snapshot),
5983 |table, snapshot, allowed, principal| {
5984 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5985 let mut rows = table.visible_rows(snapshot)?;
5986 if let Some(allowed) = allowed {
5987 rows.retain(|row| allowed.contains(&row.row_id));
5988 }
5989 rows = self.secure_rows_for(table_name, rows, principal)?;
5990 for row in &mut rows {
5991 row.columns
5992 .retain(|column, _| allowed_columns.contains(column));
5993 }
5994 Ok(rows)
5995 },
5996 )
5997 }
5998
5999 pub fn count_for(
6001 &self,
6002 table: &str,
6003 principal: Option<&crate::auth::Principal>,
6004 ) -> Result<u64> {
6005 if principal.is_none() && self.principal.read().is_some() {
6006 self.refresh_principal()?;
6007 }
6008 self.select_column_ids_for(table, principal)?;
6009 if self.security_active_for(table) {
6010 Ok(self.rows_for(table, principal)?.len() as u64)
6011 } else {
6012 Ok(self.table(table)?.lock().count())
6013 }
6014 }
6015
6016 pub fn put_for(
6018 &self,
6019 table: &str,
6020 mut cells: Vec<(u16, crate::memtable::Value)>,
6021 principal: Option<&crate::auth::Principal>,
6022 ) -> Result<RowId> {
6023 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
6024 self.require_columns_for(
6025 table,
6026 crate::auth::ColumnOperation::Insert,
6027 &columns,
6028 principal,
6029 )?;
6030 let handle = self.table(table)?;
6031 let mut table_handle = handle.lock();
6032 table_handle.fill_auto_inc(&mut cells)?;
6033 table_handle.apply_defaults(&mut cells)?;
6034 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
6035 row.columns.extend(cells.iter().cloned());
6036 self.check_row_policy_for(
6037 table,
6038 crate::security::PolicyCommand::Insert,
6039 &row,
6040 true,
6041 principal,
6042 )?;
6043 table_handle.put(cells)
6044 }
6045
6046 pub fn check_row_policy_for(
6047 &self,
6048 table: &str,
6049 command: crate::security::PolicyCommand,
6050 row: &crate::memtable::Row,
6051 check_new: bool,
6052 principal: Option<&crate::auth::Principal>,
6053 ) -> Result<()> {
6054 let security = self.catalog.read().security.clone();
6055 if !security.rls_enabled(table) {
6056 return Ok(());
6057 }
6058 let cached = self.principal.read().clone();
6059 let principal = principal
6060 .or(cached.as_ref())
6061 .ok_or(MongrelError::AuthRequired)?;
6062 if security.row_allowed(table, command, row, principal, check_new) {
6063 return Ok(());
6064 }
6065 let required = match command {
6066 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
6067 table: table.to_string(),
6068 },
6069 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
6070 table: table.to_string(),
6071 },
6072 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
6073 table: table.to_string(),
6074 },
6075 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
6076 crate::auth::Permission::Delete {
6077 table: table.to_string(),
6078 }
6079 }
6080 };
6081 Err(MongrelError::PermissionDenied {
6082 required,
6083 principal: principal.username.clone(),
6084 })
6085 }
6086
6087 pub fn set_materialized_view(
6090 &self,
6091 definition: crate::catalog::MaterializedViewEntry,
6092 ) -> Result<()> {
6093 self.set_materialized_view_with_epoch(definition)
6094 .map(|_| ())
6095 }
6096
6097 pub fn set_materialized_view_with_epoch(
6099 &self,
6100 definition: crate::catalog::MaterializedViewEntry,
6101 ) -> Result<Epoch> {
6102 use crate::wal::DdlOp;
6103 use std::sync::atomic::Ordering;
6104
6105 let command = crate::catalog_cmds::CatalogCommand::CreateMaterializedView {
6106 definition: definition.clone(),
6107 };
6108 self.require(&crate::catalog_cmds::required_permission(&command))?;
6109 if self.poisoned.load(Ordering::Relaxed) {
6110 return Err(MongrelError::Other(
6111 "database poisoned by fsync error".into(),
6112 ));
6113 }
6114 if definition.name.is_empty() || definition.query.trim().is_empty() {
6115 return Err(MongrelError::InvalidArgument(
6116 "materialized view name and query must not be empty".into(),
6117 ));
6118 }
6119 let _operation = self.admit_operation()?;
6122 let _ddl = self.ddl_lock.lock();
6123 let _security_write = self.security_write()?;
6124 self.require(&crate::catalog_cmds::required_permission(&command))?;
6125 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
6129 let table_id = self
6130 .catalog
6131 .read()
6132 .live(&definition.name)
6133 .ok_or_else(|| {
6134 MongrelError::NotFound(format!(
6135 "materialized view table {:?} not found",
6136 definition.name
6137 ))
6138 })?
6139 .table_id;
6140 let definition_json = DdlOp::encode_materialized_view(&definition)?;
6141 let _commit = self.commit_lock.lock();
6142 let epoch = self.epoch.bump_assigned();
6143 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6144 let txn_id = self.alloc_txn_id()?;
6145 let mut next_catalog = self.catalog.read().clone();
6146 self.apply_catalog_command_to(&mut next_catalog, command)?;
6147 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
6148 let commit_seq = {
6149 let mut wal = self.shared_wal.lock();
6150 let append: Result<u64> = (|| {
6151 wal.append(
6152 txn_id,
6153 table_id,
6154 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
6155 name: definition.name.clone(),
6156 definition_json,
6157 }),
6158 )?;
6159 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
6160 wal.append_commit(txn_id, epoch, &[])
6161 })();
6162 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
6163 };
6164 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
6165
6166 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
6167 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
6168 Ok(epoch)
6169 }
6170
6171 pub fn root(&self) -> &Path {
6173 self.durable_root.canonical_path()
6174 }
6175
6176 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
6179 Arc::clone(&self.durable_root)
6180 }
6181
6182 #[cfg(feature = "encryption")]
6186 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
6187 self.kek
6188 .as_deref()
6189 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
6190 }
6191
6192 #[cfg(not(feature = "encryption"))]
6193 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
6194 None
6195 }
6196
6197 pub fn is_read_only_replica(&self) -> bool {
6198 self.read_only
6199 }
6200
6201 pub fn ensure_consistent_read(&self) -> Result<()> {
6205 self.ensure_owner_process()?;
6206 if self.poisoned.load(Ordering::Relaxed) {
6207 return Err(MongrelError::Other(
6208 "database poisoned by post-commit failure; reopen required".into(),
6209 ));
6210 }
6211 Ok(())
6212 }
6213
6214 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
6215 self.replication_wal_retention_segments
6216 .store(segments, std::sync::atomic::Ordering::Relaxed);
6217 }
6218
6219 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
6224 let admin = crate::auth::Permission::Admin;
6225 self.require(&admin)?;
6226 let _operation = self.admit_operation()?;
6228 let operation_principal = self.principal_snapshot();
6229 let _barrier = self.replication_barrier.write();
6230 let _ddl = self.ddl_lock.lock();
6231 let _security = self.security_coordinator.gate.read();
6232 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6233 let mut handles: Vec<_> = self
6234 .tables
6235 .read()
6236 .iter()
6237 .map(|(id, handle)| (*id, handle.clone()))
6238 .collect();
6239 handles.sort_by_key(|(id, _)| *id);
6240 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
6241 let _commit = self.commit_lock.lock();
6242 let mut wal = self.shared_wal.lock();
6243 wal.group_sync()?;
6244 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6245 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
6246 let epoch = records
6247 .iter()
6248 .filter_map(|record| match &record.op {
6249 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
6250 _ => None,
6251 })
6252 .max()
6253 .unwrap_or(0)
6254 .max(self.epoch.committed().0);
6255 let files = crate::replication::capture_files(&self.root)?;
6256 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
6257 drop(wal);
6258 Ok(crate::replication::ReplicationSnapshot::new(
6259 source_id, epoch, files,
6260 ))
6261 }
6262
6263 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
6271 let control = crate::ExecutionControl::new(None);
6272 self.hot_backup_controlled(destination, &control, || true)
6273 }
6274
6275 pub(crate) fn hot_backup_to_durable_child(
6276 &self,
6277 parent: &crate::durable_file::DurableRoot,
6278 child: &Path,
6279 control: &crate::ExecutionControl,
6280 ) -> Result<crate::backup::BackupReport> {
6281 let mut components = child.components();
6282 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
6283 || components.next().is_some()
6284 {
6285 return Err(MongrelError::InvalidArgument(
6286 "durable backup child must be one relative path component".into(),
6287 ));
6288 }
6289 let destination_name = child.file_name().ok_or_else(|| {
6290 MongrelError::InvalidArgument("durable backup child has no filename".into())
6291 })?;
6292 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
6293 self.hot_backup_prepared(prepared, control, || true)
6294 }
6295
6296 #[doc(hidden)]
6299 pub fn hot_backup_controlled<F>(
6300 &self,
6301 destination: impl AsRef<Path>,
6302 control: &crate::ExecutionControl,
6303 before_publish: F,
6304 ) -> Result<crate::backup::BackupReport>
6305 where
6306 F: FnOnce() -> bool,
6307 {
6308 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
6309 self.hot_backup_prepared(prepared, control, before_publish)
6310 }
6311
6312 fn hot_backup_prepared<F>(
6313 &self,
6314 mut prepared: PreparedBackupDestination,
6315 control: &crate::ExecutionControl,
6316 before_publish: F,
6317 ) -> Result<crate::backup::BackupReport>
6318 where
6319 F: FnOnce() -> bool,
6320 {
6321 let admin = crate::auth::Permission::Admin;
6322 self.require(&admin)?;
6323 let _operation = self.admit_operation()?;
6326 let operation_principal = self.principal_snapshot();
6327 control.checkpoint()?;
6328 let destination = prepared.destination_path.clone();
6329 let mut before_publish = Some(before_publish);
6330
6331 let outcome = (|| {
6332 control.checkpoint()?;
6333 let barrier = self.replication_barrier.write();
6334 let ddl = self.ddl_lock.lock();
6335 let security = self.security_coordinator.gate.read();
6336 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6337 let mut handles: Vec<_> = self
6338 .tables
6339 .read()
6340 .iter()
6341 .map(|(id, handle)| (*id, handle.clone()))
6342 .collect();
6343 handles.sort_by_key(|(id, _)| *id);
6344 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
6345 let commit = self.commit_lock.lock();
6346 let mut wal = self.shared_wal.lock();
6347 wal.group_sync()?;
6348 let epoch = self.epoch.committed().0;
6349 let boundary_unix_nanos = current_unix_nanos();
6350
6351 let pin_nonce = std::time::SystemTime::now()
6352 .duration_since(std::time::UNIX_EPOCH)
6353 .unwrap_or_default()
6354 .as_nanos();
6355 let file_pin_root = self
6356 .root
6357 .join(META_DIR)
6358 .join("backup-pins")
6359 .join(format!("{}-{pin_nonce}", std::process::id()));
6360 std::fs::create_dir_all(&file_pin_root)?;
6361 let _file_pins = BackupFilePins {
6362 root: file_pin_root.clone(),
6363 };
6364 let mut run_files = Vec::new();
6365 for (index, (table_id, _)) in handles.iter().enumerate() {
6366 if index % 256 == 0 {
6367 control.checkpoint()?;
6368 }
6369 let table = &table_guards[index];
6370 for (run_index, run) in table.run_refs().iter().enumerate() {
6371 if run_index % 256 == 0 {
6372 control.checkpoint()?;
6373 }
6374 let source = table.run_path(run.run_id as u64);
6375 let relative = Path::new(TABLES_DIR)
6376 .join(table_id.to_string())
6377 .join(crate::engine::RUNS_DIR)
6378 .join(format!("r-{}.sr", run.run_id));
6379 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
6380 if std::fs::hard_link(&source, &pinned).is_err() {
6381 crate::backup::copy_file_synced(&source, &pinned)?;
6382 }
6383 run_files.push(((*table_id, run.run_id), pinned, relative));
6384 }
6385 }
6386 crate::durable_file::sync_directory(&file_pin_root)?;
6387 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
6388 {
6389 let mut pins = self.backup_pins.lock();
6390 for key in &run_keys {
6391 *pins.entry(*key).or_insert(0) += 1;
6392 }
6393 }
6394 let _run_pins = RunPins {
6395 pins: Arc::clone(&self.backup_pins),
6396 runs: run_keys,
6397 };
6398 let _registry_pins: Vec<crate::retention::PinGuard> = table_guards
6405 .iter()
6406 .map(|table| {
6407 table
6408 .pin_registry()
6409 .pin(crate::retention::PinSource::BackupPitr, Epoch(epoch))
6410 })
6411 .collect();
6412 let deferred: HashSet<_> = run_files
6413 .iter()
6414 .map(|(_, _, relative)| relative.clone())
6415 .collect();
6416 let mut copied = Vec::new();
6417 copy_backup_boundary(
6418 &self.root,
6419 prepared.stage.as_deref().ok_or_else(|| {
6420 MongrelError::Other("backup staging root was already released".into())
6421 })?,
6422 &deferred,
6423 &mut copied,
6424 Some(control),
6425 )?;
6426
6427 drop(wal);
6428 drop(commit);
6429 drop(table_guards);
6430 drop(security);
6431 drop(ddl);
6432 drop(barrier);
6433
6434 if let Some(hook) = self.backup_hook.lock().as_ref() {
6435 hook();
6436 }
6437 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
6438 if index % 256 == 0 {
6439 control.checkpoint()?;
6440 }
6441 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
6442 prepared
6443 .stage
6444 .as_deref()
6445 .ok_or_else(|| {
6446 MongrelError::Other("backup staging root was already released".into())
6447 })?
6448 .copy_new_from(&relative, &mut source)?;
6449 copied.push(relative);
6450 }
6451
6452 let manifest = crate::backup::BackupManifest::create_controlled_durable(
6453 prepared.stage.as_deref().ok_or_else(|| {
6454 MongrelError::Other("backup staging root was already released".into())
6455 })?,
6456 epoch,
6457 &copied,
6458 control,
6459 )?;
6460 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
6461 MongrelError::Other("backup staging root was already released".into())
6462 })?)?;
6463 control.checkpoint()?;
6464 let publish = before_publish.take().ok_or_else(|| {
6465 MongrelError::Other("backup publication callback already consumed".into())
6466 })?;
6467 if !publish() {
6468 return Err(MongrelError::Cancelled);
6469 }
6470 let final_security = self.security_coordinator.gate.read();
6471 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6472 drop(prepared.stage.take().ok_or_else(|| {
6476 MongrelError::Other("backup staging root was already released".into())
6477 })?);
6478 let published = std::cell::Cell::new(false);
6479 if let Err(error) = prepared.parent.rename_directory_new_with_after(
6480 Path::new(&prepared.stage_name),
6481 &prepared.parent,
6482 Path::new(&prepared.destination_name),
6483 || published.set(true),
6484 ) {
6485 if published.get() {
6486 return Err(MongrelError::CommitOutcomeUnknown {
6487 epoch,
6488 message: format!("backup publication was not durable: {error}"),
6489 });
6490 }
6491 return Err(error.into());
6492 }
6493 drop(final_security);
6494 Ok(crate::backup::BackupReport {
6495 destination,
6496 epoch,
6497 boundary_unix_nanos,
6498 files: manifest.files.len(),
6499 bytes: manifest.total_bytes(),
6500 })
6501 })();
6502
6503 if outcome.is_err() {
6504 drop(prepared.stage.take());
6505 let _ = prepared
6506 .parent
6507 .remove_directory_all(Path::new(&prepared.stage_name));
6508 }
6509 outcome
6510 }
6511
6512 pub fn replication_batch_since(
6515 &self,
6516 since_epoch: u64,
6517 ) -> Result<crate::replication::ReplicationBatch> {
6518 use crate::wal::Op;
6519
6520 let admin = crate::auth::Permission::Admin;
6521 self.require(&admin)?;
6522 let _operation = self.admit_operation()?;
6524 let operation_principal = self.principal_snapshot();
6525
6526 let mut wal = self.shared_wal.lock();
6527 wal.group_sync()?;
6528 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6529 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
6530 drop(wal);
6531
6532 let commits: HashMap<u64, u64> = records
6533 .iter()
6534 .filter_map(|record| match &record.op {
6535 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
6536 _ => None,
6537 })
6538 .collect();
6539 let earliest_epoch = commits.values().copied().min();
6540 let current_epoch = commits
6541 .values()
6542 .copied()
6543 .max()
6544 .unwrap_or(0)
6545 .max(self.epoch.committed().0);
6546 let selected: HashSet<u64> = commits
6547 .iter()
6548 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
6549 .collect();
6550 let retention_gap = since_epoch < current_epoch
6551 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
6552 let spilled = records.iter().any(|record| {
6553 selected.contains(&record.txn_id)
6554 && matches!(
6555 &record.op,
6556 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
6557 )
6558 });
6559 let records = records
6560 .into_iter()
6561 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
6562 .filter(|record| selected.contains(&record.txn_id))
6563 .collect();
6564 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
6565 let batch = crate::replication::ReplicationBatch::complete_for_source(
6566 source_id,
6567 since_epoch,
6568 current_epoch,
6569 earliest_epoch,
6570 retention_gap,
6571 spilled,
6572 records,
6573 )?;
6574 if let Some(hook) = self.replication_hook.lock().as_ref() {
6575 hook();
6576 }
6577 let _security = self.security_coordinator.gate.read();
6578 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6579 Ok(batch)
6580 }
6581
6582 pub fn append_replication_batch(
6586 &self,
6587 batch: &crate::replication::ReplicationBatch,
6588 ) -> Result<u64> {
6589 use crate::wal::Op;
6590
6591 if !self.read_only {
6592 return Err(MongrelError::InvalidArgument(
6593 "replication batches may only target a marked replica".into(),
6594 ));
6595 }
6596 let _operation = self.admit_operation()?;
6598 let current = crate::replication::replica_epoch(&self.root)?;
6599 if batch.is_source_bound() {
6600 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
6601 if batch.source_id != source_id {
6602 return Err(MongrelError::Conflict(
6603 "replication batch source does not match follower binding".into(),
6604 ));
6605 }
6606 }
6607 if batch.requires_snapshot {
6608 return Err(MongrelError::Conflict(
6609 "replication snapshot required for this batch".into(),
6610 ));
6611 }
6612 batch.validate_proof()?;
6613 if batch.from_epoch != current {
6614 if batch.from_epoch < current && batch.current_epoch == current {
6615 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6616 let _wal = self.shared_wal.lock();
6617 let existing: HashSet<(u64, u64)> =
6618 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
6619 .into_iter()
6620 .filter_map(|record| match record.op {
6621 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
6622 _ => None,
6623 })
6624 .collect();
6625 let already_applied = batch.records.iter().all(|record| match &record.op {
6626 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
6627 _ => true,
6628 });
6629 if already_applied {
6630 return Ok(current);
6631 }
6632 }
6633 return Err(MongrelError::Conflict(format!(
6634 "replication batch starts at epoch {}, follower is at epoch {current}",
6635 batch.from_epoch
6636 )));
6637 }
6638 if batch.current_epoch < current {
6639 return Err(MongrelError::InvalidArgument(format!(
6640 "replication batch current epoch {} precedes follower epoch {current}",
6641 batch.current_epoch
6642 )));
6643 }
6644 let records = &batch.records;
6645 let mut commits = HashMap::new();
6646 let mut commit_epochs = HashSet::new();
6647 let mut commit_timestamps = HashMap::new();
6648 for record in records {
6649 match &record.op {
6650 Op::TxnCommit { epoch, added_runs } => {
6651 if !added_runs.is_empty() {
6652 return Err(MongrelError::Conflict(
6653 "replication snapshot required for spilled-run transaction".into(),
6654 ));
6655 }
6656 if commits.insert(record.txn_id, *epoch).is_some() {
6657 return Err(MongrelError::InvalidArgument(format!(
6658 "duplicate commit for replication transaction {}",
6659 record.txn_id
6660 )));
6661 }
6662 if *epoch <= current || *epoch > batch.current_epoch {
6663 return Err(MongrelError::InvalidArgument(format!(
6664 "replication commit epoch {epoch} is outside ({current}, {}]",
6665 batch.current_epoch
6666 )));
6667 }
6668 if !commit_epochs.insert(*epoch) {
6669 return Err(MongrelError::InvalidArgument(format!(
6670 "duplicate replication commit epoch {epoch}"
6671 )));
6672 }
6673 }
6674 Op::CommitTimestamp { unix_nanos } => {
6675 commit_timestamps.insert(record.txn_id, *unix_nanos);
6676 }
6677 _ => {}
6678 }
6679 }
6680 for record in records {
6681 if record.txn_id == crate::wal::SYSTEM_TXN_ID
6682 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
6683 {
6684 return Err(MongrelError::InvalidArgument(
6685 "replication batch contains a non-committed record".into(),
6686 ));
6687 }
6688 if !commits.contains_key(&record.txn_id) {
6689 return Err(MongrelError::InvalidArgument(format!(
6690 "incomplete replication transaction {}",
6691 record.txn_id
6692 )));
6693 }
6694 }
6695 let target_epoch = commits
6696 .values()
6697 .copied()
6698 .filter(|epoch| *epoch > current)
6699 .max()
6700 .unwrap_or(current);
6701 if target_epoch != batch.current_epoch {
6702 return Err(MongrelError::InvalidArgument(format!(
6703 "replication batch ends at epoch {target_epoch}, expected {}",
6704 batch.current_epoch
6705 )));
6706 }
6707 let mut selected: HashSet<u64> = commits
6708 .iter()
6709 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
6710 .collect();
6711 if selected.is_empty() {
6712 return Ok(current);
6713 }
6714 let mut wal = self.shared_wal.lock();
6715 wal.group_sync()?;
6716 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6717 let existing: HashSet<(u64, u64)> =
6718 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
6719 .into_iter()
6720 .filter_map(|record| match record.op {
6721 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
6722 _ => None,
6723 })
6724 .collect();
6725 selected.retain(|txn_id| {
6726 commits
6727 .get(txn_id)
6728 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
6729 });
6730 for record in records {
6731 if !selected.contains(&record.txn_id) {
6732 continue;
6733 }
6734 match &record.op {
6735 Op::TxnCommit { epoch, added_runs } => {
6736 let timestamp = commit_timestamps
6737 .get(&record.txn_id)
6738 .copied()
6739 .unwrap_or_else(current_unix_nanos);
6740 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
6741 }
6742 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
6743 op => {
6744 wal.append(record.txn_id, 0, op.clone())?;
6745 }
6746 }
6747 }
6748 if !selected.is_empty() {
6749 wal.group_sync()?;
6750 }
6751 drop(wal);
6752
6753 let mut recovered_catalog = self.catalog.read().clone();
6757 if let Err(error) = recover_ddl_from_wal(
6758 &self.root,
6759 Some(&self.durable_root),
6760 &mut recovered_catalog,
6761 self.meta_dek.as_ref(),
6762 wal_dek.as_ref(),
6763 true,
6764 None,
6765 ) {
6766 return Err(MongrelError::DurableCommit {
6767 epoch: target_epoch,
6768 message: format!(
6769 "replication WAL is durable but catalog checkpoint failed: {error}"
6770 ),
6771 });
6772 }
6773 let _security = self.security_coordinator.gate.write();
6774 let old_security_version = self.catalog.read().security_version;
6775 let security_changed = old_security_version != recovered_catalog.security_version
6776 || self.catalog.read().require_auth != recovered_catalog.require_auth;
6777 let require_auth = recovered_catalog.require_auth;
6778 let principal = if security_changed {
6779 None
6780 } else {
6781 self.principal.read().as_ref().and_then(|principal| {
6782 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
6783 })
6784 };
6785 if require_auth {
6786 self.auth_state.set_require_auth(true);
6787 }
6788 self.auth_state.set_principal(principal.clone());
6789 *self.principal.write() = principal;
6790 let security_version = recovered_catalog.security_version;
6791 *self.catalog.write() = recovered_catalog;
6792 self.security_coordinator
6793 .version
6794 .store(security_version, Ordering::Release);
6795 if !require_auth {
6796 self.auth_state.set_require_auth(false);
6797 }
6798 if let Err(error) =
6799 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
6800 {
6801 return Err(MongrelError::DurableCommit {
6802 epoch: target_epoch,
6803 message: format!(
6804 "replication WAL and catalog are durable but follower watermark failed: {error}"
6805 ),
6806 });
6807 }
6808 Ok(target_epoch)
6809 }
6810
6811 pub fn table_id(&self, name: &str) -> Result<u64> {
6814 let cat = self.catalog.read();
6815 cat.live(name)
6816 .map(|e| e.table_id)
6817 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
6818 }
6819
6820 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
6825 let catalog = self.catalog.read();
6826 catalog
6827 .live(name)
6828 .map(|entry| (entry.table_id, entry.schema.schema_id))
6829 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
6830 }
6831
6832 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
6833 self.catalog
6834 .read()
6835 .building(name)
6836 .map(|entry| entry.table_id)
6837 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
6838 }
6839
6840 pub fn procedures(&self) -> Vec<StoredProcedure> {
6841 self.catalog
6842 .read()
6843 .procedures
6844 .iter()
6845 .map(|p| p.procedure.clone())
6846 .collect()
6847 }
6848
6849 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
6850 self.catalog
6851 .read()
6852 .procedures
6853 .iter()
6854 .find(|p| p.procedure.name == name)
6855 .map(|p| p.procedure.clone())
6856 }
6857
6858 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
6859 self.create_procedure_inner(procedure, None)
6860 }
6861
6862 pub fn create_procedure_controlled<F>(
6863 &self,
6864 procedure: StoredProcedure,
6865 mut before_publish: F,
6866 ) -> Result<StoredProcedure>
6867 where
6868 F: FnMut() -> Result<()>,
6869 {
6870 self.create_procedure_inner(procedure, Some(&mut before_publish))
6871 }
6872
6873 fn create_procedure_inner(
6874 &self,
6875 mut procedure: StoredProcedure,
6876 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6877 ) -> Result<StoredProcedure> {
6878 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
6879 procedure: procedure.clone(),
6880 };
6881 self.require(&crate::catalog_cmds::required_permission(&command))?;
6882 let _g = self.ddl_lock.lock();
6883 let _security_write = self.security_write()?;
6884 self.require(&crate::catalog_cmds::required_permission(&command))?;
6885 procedure.validate()?;
6886 self.validate_procedure_references(&procedure)?;
6887 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
6891 let commit_lock = Arc::clone(&self.commit_lock);
6892 let _c = commit_lock.lock();
6893 let epoch = self.epoch.bump_assigned();
6894 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6895 procedure.created_epoch = epoch.0;
6896 procedure.updated_epoch = epoch.0;
6897 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
6898 procedure: procedure.clone(),
6899 };
6900 let mut next_catalog = self.catalog.read().clone();
6901 self.apply_catalog_command_to(&mut next_catalog, command)?;
6902 next_catalog.db_epoch = epoch.0;
6903 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6904 Ok(procedure)
6905 }
6906
6907 pub fn create_or_replace_procedure(
6908 &self,
6909 procedure: StoredProcedure,
6910 ) -> Result<StoredProcedure> {
6911 self.create_or_replace_procedure_inner(procedure, None)
6912 }
6913
6914 pub fn create_or_replace_procedure_controlled<F>(
6915 &self,
6916 procedure: StoredProcedure,
6917 mut before_publish: F,
6918 ) -> Result<StoredProcedure>
6919 where
6920 F: FnMut() -> Result<()>,
6921 {
6922 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
6923 }
6924
6925 fn create_or_replace_procedure_inner(
6926 &self,
6927 procedure: StoredProcedure,
6928 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6929 ) -> Result<StoredProcedure> {
6930 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
6931 procedure: procedure.clone(),
6932 };
6933 self.require(&crate::catalog_cmds::required_permission(&command))?;
6934 let _g = self.ddl_lock.lock();
6935 let _security_write = self.security_write()?;
6936 self.require(&crate::catalog_cmds::required_permission(&command))?;
6937 procedure.validate()?;
6938 self.validate_procedure_references(&procedure)?;
6939 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
6943 let commit_lock = Arc::clone(&self.commit_lock);
6944 let _c = commit_lock.lock();
6945 let epoch = self.epoch.bump_assigned();
6946 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6947 let mut next_catalog = self.catalog.read().clone();
6948 let replaced = match next_catalog
6953 .procedures
6954 .iter()
6955 .position(|p| p.procedure.name == procedure.name)
6956 {
6957 Some(idx) => next_catalog.procedures[idx]
6958 .procedure
6959 .replaced(procedure.clone(), epoch.0)?,
6960 None => {
6961 let mut next = procedure;
6962 next.created_epoch = epoch.0;
6963 next.updated_epoch = epoch.0;
6964 next
6965 }
6966 };
6967 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
6968 procedure: replaced.clone(),
6969 };
6970 self.apply_catalog_command_to(&mut next_catalog, command)?;
6971 next_catalog.db_epoch = epoch.0;
6972 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6973 Ok(replaced)
6974 }
6975
6976 pub fn drop_procedure(&self, name: &str) -> Result<()> {
6977 self.drop_procedure_with_epoch(name).map(|_| ())
6978 }
6979
6980 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
6981 self.drop_procedure_with_epoch_inner(name, None)
6982 }
6983
6984 pub fn drop_procedure_with_epoch_controlled<F>(
6985 &self,
6986 name: &str,
6987 mut before_publish: F,
6988 ) -> Result<Epoch>
6989 where
6990 F: FnMut() -> Result<()>,
6991 {
6992 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
6993 }
6994
6995 fn drop_procedure_with_epoch_inner(
6996 &self,
6997 name: &str,
6998 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6999 ) -> Result<Epoch> {
7000 let command = crate::catalog_cmds::CatalogCommand::DropProcedure {
7001 name: name.to_string(),
7002 };
7003 self.require(&crate::catalog_cmds::required_permission(&command))?;
7004 let _g = self.ddl_lock.lock();
7005 let _security_write = self.security_write()?;
7006 self.require(&crate::catalog_cmds::required_permission(&command))?;
7007 let commit_lock = Arc::clone(&self.commit_lock);
7008 let _c = commit_lock.lock();
7009 let epoch = self.epoch.bump_assigned();
7010 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7011 let mut next_catalog = self.catalog.read().clone();
7012 self.apply_catalog_command_to(&mut next_catalog, command)?;
7013 next_catalog.db_epoch = epoch.0;
7014 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7015 Ok(epoch)
7016 }
7017
7018 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
7023 self.catalog.read().users.clone()
7024 }
7025
7026 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
7029 self.catalog
7030 .read()
7031 .users
7032 .iter()
7033 .find(|user| user.username == username)
7034 .map(|user| (user.id, user.created_epoch))
7035 }
7036
7037 pub fn security_version(&self) -> u64 {
7040 self.catalog.read().security_version
7041 }
7042
7043 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
7045 self.catalog.read().roles.clone()
7046 }
7047
7048 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
7050 self.require(&crate::auth::Permission::Admin)?;
7051 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
7052 self.create_user_with_password_hash(username, hash)
7053 }
7054
7055 pub fn create_user_with_password_hash(
7057 &self,
7058 username: &str,
7059 hash: String,
7060 ) -> Result<crate::auth::UserEntry> {
7061 self.create_user_with_password_hash_inner(username, hash, None)
7062 }
7063
7064 pub fn create_user_with_password_hash_controlled<F>(
7065 &self,
7066 username: &str,
7067 hash: String,
7068 mut before_publish: F,
7069 ) -> Result<crate::auth::UserEntry>
7070 where
7071 F: FnMut() -> Result<()>,
7072 {
7073 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
7074 }
7075
7076 fn create_user_with_password_hash_inner(
7077 &self,
7078 username: &str,
7079 hash: String,
7080 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7081 ) -> Result<crate::auth::UserEntry> {
7082 let command = crate::catalog_cmds::CatalogCommand::CreateUser {
7086 username: username.to_string(),
7087 password_hash: hash.clone(),
7088 is_admin: false,
7089 created_epoch: 0, };
7091 self.require(&crate::catalog_cmds::required_permission(&command))?;
7092 let _ddl = self.ddl_lock.lock();
7093 let _security_write = self.security_write()?;
7094 self.require(&crate::catalog_cmds::required_permission(&command))?;
7095 let _commit = self.commit_lock.lock();
7096 let epoch = self.epoch.bump_assigned();
7097 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7098 let command = crate::catalog_cmds::CatalogCommand::CreateUser {
7099 username: username.to_string(),
7100 password_hash: hash,
7101 is_admin: false,
7102 created_epoch: epoch.0,
7103 };
7104 let mut next_catalog = self.catalog.read().clone();
7105 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
7106 let crate::catalog_cmds::CatalogDelta::UserUpserted(entry) = delta else {
7107 return Err(MongrelError::Other(
7108 "CreateUser resolved to an unexpected catalog delta".into(),
7109 ));
7110 };
7111 next_catalog.db_epoch = epoch.0;
7112 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7113 Ok(entry)
7114 }
7115
7116 pub fn drop_user(&self, username: &str) -> Result<()> {
7118 self.drop_user_with_epoch(username).map(|_| ())
7119 }
7120
7121 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
7122 self.drop_user_with_epoch_inner(username, None)
7123 }
7124
7125 pub fn drop_user_with_epoch_controlled<F>(
7126 &self,
7127 username: &str,
7128 mut before_publish: F,
7129 ) -> Result<Epoch>
7130 where
7131 F: FnMut() -> Result<()>,
7132 {
7133 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
7134 }
7135
7136 fn drop_user_with_epoch_inner(
7137 &self,
7138 username: &str,
7139 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7140 ) -> Result<Epoch> {
7141 let command = crate::catalog_cmds::CatalogCommand::DropUser {
7142 username: username.to_string(),
7143 };
7144 self.require(&crate::catalog_cmds::required_permission(&command))?;
7145 let _ddl = self.ddl_lock.lock();
7146 let _security_write = self.security_write()?;
7147 self.require(&crate::catalog_cmds::required_permission(&command))?;
7148 let _commit = self.commit_lock.lock();
7149 let epoch = self.epoch.bump_assigned();
7150 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7151 let mut next_catalog = self.catalog.read().clone();
7152 self.apply_catalog_command_to(&mut next_catalog, command)?;
7153 next_catalog.db_epoch = epoch.0;
7154 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7155 Ok(epoch)
7156 }
7157
7158 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
7160 self.alter_user_password_with_epoch(username, new_password)
7161 .map(|_| ())
7162 }
7163
7164 pub fn alter_user_password_with_epoch(
7165 &self,
7166 username: &str,
7167 new_password: &str,
7168 ) -> Result<Epoch> {
7169 self.require(&crate::auth::Permission::Admin)?;
7170 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
7171 self.alter_user_password_hash_with_epoch(username, hash)
7172 }
7173
7174 pub fn alter_user_password_hash_with_epoch(
7175 &self,
7176 username: &str,
7177 hash: String,
7178 ) -> Result<Epoch> {
7179 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
7180 }
7181
7182 pub fn alter_user_password_hash_with_epoch_controlled<F>(
7183 &self,
7184 username: &str,
7185 hash: String,
7186 mut before_publish: F,
7187 ) -> Result<Epoch>
7188 where
7189 F: FnMut() -> Result<()>,
7190 {
7191 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
7192 }
7193
7194 fn alter_user_password_hash_with_epoch_inner(
7195 &self,
7196 username: &str,
7197 hash: String,
7198 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7199 ) -> Result<Epoch> {
7200 let command = crate::catalog_cmds::CatalogCommand::AlterUserPassword {
7201 username: username.to_string(),
7202 password_hash: hash,
7203 };
7204 self.require(&crate::catalog_cmds::required_permission(&command))?;
7205 let _ddl = self.ddl_lock.lock();
7206 let _security_write = self.security_write()?;
7207 self.require(&crate::catalog_cmds::required_permission(&command))?;
7208 let _commit = self.commit_lock.lock();
7209 let epoch = self.epoch.bump_assigned();
7210 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7211 let mut next_catalog = self.catalog.read().clone();
7212 self.apply_catalog_command_to(&mut next_catalog, command)?;
7213 next_catalog.db_epoch = epoch.0;
7214 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7215 Ok(epoch)
7216 }
7217
7218 pub fn verify_user(
7221 &self,
7222 username: &str,
7223 password: &str,
7224 ) -> Result<Option<crate::auth::UserEntry>> {
7225 let cat = self.catalog.read();
7226 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
7227 return Ok(None);
7228 };
7229 if user.password_hash.is_empty() {
7230 return Ok(None);
7231 }
7232 let ok = crate::auth::verify_password(password, &user.password_hash)
7233 .map_err(MongrelError::Other)?;
7234 if ok {
7235 Ok(Some(user.clone()))
7236 } else {
7237 Ok(None)
7238 }
7239 }
7240
7241 pub fn authenticate_principal(
7245 &self,
7246 username: &str,
7247 password: &str,
7248 ) -> Result<Option<crate::auth::Principal>> {
7249 self.authenticate_principal_inner(username, password, || {})
7250 }
7251
7252 fn authenticate_principal_inner<F>(
7253 &self,
7254 username: &str,
7255 password: &str,
7256 after_verify: F,
7257 ) -> Result<Option<crate::auth::Principal>>
7258 where
7259 F: FnOnce(),
7260 {
7261 let catalog = self.catalog.read();
7262 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
7263 return Ok(None);
7264 };
7265 if user.password_hash.is_empty()
7266 || !crate::auth::verify_password(password, &user.password_hash)
7267 .map_err(MongrelError::Other)?
7268 {
7269 return Ok(None);
7270 }
7271 after_verify();
7272 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
7273 }
7274
7275 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
7277 self.set_user_admin_with_epoch(username, is_admin)
7278 .map(|_| ())
7279 }
7280
7281 pub fn set_user_admin_with_epoch(
7282 &self,
7283 username: &str,
7284 is_admin: bool,
7285 ) -> Result<Option<Epoch>> {
7286 self.set_user_admin_with_epoch_inner(username, is_admin, None)
7287 }
7288
7289 pub fn set_user_admin_with_epoch_controlled<F>(
7290 &self,
7291 username: &str,
7292 is_admin: bool,
7293 mut before_publish: F,
7294 ) -> Result<Option<Epoch>>
7295 where
7296 F: FnMut() -> Result<()>,
7297 {
7298 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
7299 }
7300
7301 fn set_user_admin_with_epoch_inner(
7302 &self,
7303 username: &str,
7304 is_admin: bool,
7305 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7306 ) -> Result<Option<Epoch>> {
7307 let command = crate::catalog_cmds::CatalogCommand::SetUserAdmin {
7308 username: username.to_string(),
7309 is_admin,
7310 };
7311 self.require(&crate::catalog_cmds::required_permission(&command))?;
7312 let _ddl = self.ddl_lock.lock();
7313 let _security_write = self.security_write()?;
7314 self.require(&crate::catalog_cmds::required_permission(&command))?;
7315 let _commit = self.commit_lock.lock();
7316 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7320 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7321 return Ok(None);
7322 }
7323 let epoch = self.epoch.bump_assigned();
7324 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7325 let mut next_catalog = self.catalog.read().clone();
7326 self.apply_catalog_command_to(&mut next_catalog, command)?;
7327 next_catalog.db_epoch = epoch.0;
7328 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7329 Ok(Some(epoch))
7330 }
7331
7332 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
7334 self.create_role_inner(name, None)
7335 }
7336
7337 pub fn create_role_controlled<F>(
7338 &self,
7339 name: &str,
7340 mut before_publish: F,
7341 ) -> Result<crate::auth::RoleEntry>
7342 where
7343 F: FnMut() -> Result<()>,
7344 {
7345 self.create_role_inner(name, Some(&mut before_publish))
7346 }
7347
7348 fn create_role_inner(
7349 &self,
7350 name: &str,
7351 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7352 ) -> Result<crate::auth::RoleEntry> {
7353 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
7354 name: name.to_string(),
7355 created_epoch: 0, };
7357 self.require(&crate::catalog_cmds::required_permission(&command))?;
7358 let _ddl = self.ddl_lock.lock();
7359 let _security_write = self.security_write()?;
7360 self.require(&crate::catalog_cmds::required_permission(&command))?;
7361 let _commit = self.commit_lock.lock();
7362 let epoch = self.epoch.bump_assigned();
7363 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7364 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
7365 name: name.to_string(),
7366 created_epoch: epoch.0,
7367 };
7368 let mut next_catalog = self.catalog.read().clone();
7369 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
7370 let crate::catalog_cmds::CatalogDelta::RoleUpserted(entry) = delta else {
7371 return Err(MongrelError::Other(
7372 "CreateRole resolved to an unexpected catalog delta".into(),
7373 ));
7374 };
7375 next_catalog.db_epoch = epoch.0;
7376 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7377 Ok(entry)
7378 }
7379
7380 pub fn drop_role(&self, name: &str) -> Result<()> {
7382 self.drop_role_with_epoch(name).map(|_| ())
7383 }
7384
7385 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
7386 self.drop_role_with_epoch_inner(name, None)
7387 }
7388
7389 pub fn drop_role_with_epoch_controlled<F>(
7390 &self,
7391 name: &str,
7392 mut before_publish: F,
7393 ) -> Result<Epoch>
7394 where
7395 F: FnMut() -> Result<()>,
7396 {
7397 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
7398 }
7399
7400 fn drop_role_with_epoch_inner(
7401 &self,
7402 name: &str,
7403 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7404 ) -> Result<Epoch> {
7405 let command = crate::catalog_cmds::CatalogCommand::DropRole {
7406 name: name.to_string(),
7407 };
7408 self.require(&crate::catalog_cmds::required_permission(&command))?;
7409 let _ddl = self.ddl_lock.lock();
7410 let _security_write = self.security_write()?;
7411 self.require(&crate::catalog_cmds::required_permission(&command))?;
7412 let _commit = self.commit_lock.lock();
7413 let epoch = self.epoch.bump_assigned();
7414 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7415 let mut next_catalog = self.catalog.read().clone();
7416 self.apply_catalog_command_to(&mut next_catalog, command)?;
7417 next_catalog.db_epoch = epoch.0;
7418 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7419 Ok(epoch)
7420 }
7421
7422 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
7424 self.grant_role_with_epoch(username, role_name).map(|_| ())
7425 }
7426
7427 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
7428 self.grant_role_with_epoch_inner(username, role_name, None)
7429 }
7430
7431 pub fn grant_role_with_epoch_controlled<F>(
7432 &self,
7433 username: &str,
7434 role_name: &str,
7435 mut before_publish: F,
7436 ) -> Result<Option<Epoch>>
7437 where
7438 F: FnMut() -> Result<()>,
7439 {
7440 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
7441 }
7442
7443 fn grant_role_with_epoch_inner(
7444 &self,
7445 username: &str,
7446 role_name: &str,
7447 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7448 ) -> Result<Option<Epoch>> {
7449 let command = crate::catalog_cmds::CatalogCommand::GrantRole {
7450 username: username.to_string(),
7451 role: role_name.to_string(),
7452 };
7453 self.require(&crate::catalog_cmds::required_permission(&command))?;
7454 let _ddl = self.ddl_lock.lock();
7455 let _security_write = self.security_write()?;
7456 self.require(&crate::catalog_cmds::required_permission(&command))?;
7457 let _commit = self.commit_lock.lock();
7458 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7462 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7463 return Ok(None);
7464 }
7465 let epoch = self.epoch.bump_assigned();
7466 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7467 let mut next_catalog = self.catalog.read().clone();
7468 self.apply_catalog_command_to(&mut next_catalog, command)?;
7469 next_catalog.db_epoch = epoch.0;
7470 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7471 Ok(Some(epoch))
7472 }
7473
7474 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
7476 self.revoke_role_with_epoch(username, role_name).map(|_| ())
7477 }
7478
7479 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
7480 self.revoke_role_with_epoch_inner(username, role_name, None)
7481 }
7482
7483 pub fn revoke_role_with_epoch_controlled<F>(
7484 &self,
7485 username: &str,
7486 role_name: &str,
7487 mut before_publish: F,
7488 ) -> Result<Option<Epoch>>
7489 where
7490 F: FnMut() -> Result<()>,
7491 {
7492 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
7493 }
7494
7495 fn revoke_role_with_epoch_inner(
7496 &self,
7497 username: &str,
7498 role_name: &str,
7499 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7500 ) -> Result<Option<Epoch>> {
7501 let command = crate::catalog_cmds::CatalogCommand::RevokeRole {
7502 username: username.to_string(),
7503 role: role_name.to_string(),
7504 };
7505 self.require(&crate::catalog_cmds::required_permission(&command))?;
7506 let _ddl = self.ddl_lock.lock();
7507 let _security_write = self.security_write()?;
7508 self.require(&crate::catalog_cmds::required_permission(&command))?;
7509 let _commit = self.commit_lock.lock();
7510 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7514 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7515 return Ok(None);
7516 }
7517 let epoch = self.epoch.bump_assigned();
7518 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7519 let mut next_catalog = self.catalog.read().clone();
7520 self.apply_catalog_command_to(&mut next_catalog, command)?;
7521 next_catalog.db_epoch = epoch.0;
7522 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7523 Ok(Some(epoch))
7524 }
7525
7526 pub fn grant_permission(
7528 &self,
7529 role_name: &str,
7530 permission: crate::auth::Permission,
7531 ) -> Result<()> {
7532 self.grant_permission_with_epoch(role_name, permission)
7533 .map(|_| ())
7534 }
7535
7536 pub fn grant_permission_with_epoch(
7537 &self,
7538 role_name: &str,
7539 permission: crate::auth::Permission,
7540 ) -> Result<Option<Epoch>> {
7541 self.grant_permission_with_epoch_inner(role_name, permission, None)
7542 }
7543
7544 pub fn grant_permission_with_epoch_controlled<F>(
7545 &self,
7546 role_name: &str,
7547 permission: crate::auth::Permission,
7548 mut before_publish: F,
7549 ) -> Result<Option<Epoch>>
7550 where
7551 F: FnMut() -> Result<()>,
7552 {
7553 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
7554 }
7555
7556 fn grant_permission_with_epoch_inner(
7557 &self,
7558 role_name: &str,
7559 permission: crate::auth::Permission,
7560 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7561 ) -> Result<Option<Epoch>> {
7562 let command = crate::catalog_cmds::CatalogCommand::GrantPermission {
7563 role: role_name.to_string(),
7564 permission,
7565 };
7566 self.require(&crate::catalog_cmds::required_permission(&command))?;
7567 let _ddl = self.ddl_lock.lock();
7568 let _security_write = self.security_write()?;
7569 self.require(&crate::catalog_cmds::required_permission(&command))?;
7570 let _commit = self.commit_lock.lock();
7571 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7575 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7576 return Ok(None);
7577 }
7578 let epoch = self.epoch.bump_assigned();
7579 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7580 let mut next_catalog = self.catalog.read().clone();
7581 self.apply_catalog_command_to(&mut next_catalog, command)?;
7582 next_catalog.db_epoch = epoch.0;
7583 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7584 Ok(Some(epoch))
7585 }
7586
7587 pub fn revoke_permission(
7589 &self,
7590 role_name: &str,
7591 permission: crate::auth::Permission,
7592 ) -> Result<()> {
7593 self.revoke_permission_with_epoch(role_name, permission)
7594 .map(|_| ())
7595 }
7596
7597 pub fn revoke_permission_with_epoch(
7598 &self,
7599 role_name: &str,
7600 permission: crate::auth::Permission,
7601 ) -> Result<Option<Epoch>> {
7602 self.revoke_permission_with_epoch_inner(role_name, permission, None)
7603 }
7604
7605 pub fn revoke_permission_with_epoch_controlled<F>(
7606 &self,
7607 role_name: &str,
7608 permission: crate::auth::Permission,
7609 mut before_publish: F,
7610 ) -> Result<Option<Epoch>>
7611 where
7612 F: FnMut() -> Result<()>,
7613 {
7614 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
7615 }
7616
7617 fn revoke_permission_with_epoch_inner(
7618 &self,
7619 role_name: &str,
7620 permission: crate::auth::Permission,
7621 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7622 ) -> Result<Option<Epoch>> {
7623 let command = crate::catalog_cmds::CatalogCommand::RevokePermission {
7624 role: role_name.to_string(),
7625 permission,
7626 };
7627 self.require(&crate::catalog_cmds::required_permission(&command))?;
7628 let _ddl = self.ddl_lock.lock();
7629 let _security_write = self.security_write()?;
7630 self.require(&crate::catalog_cmds::required_permission(&command))?;
7631 let _commit = self.commit_lock.lock();
7632 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7636 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7637 return Ok(None);
7638 }
7639 let epoch = self.epoch.bump_assigned();
7640 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7641 let mut next_catalog = self.catalog.read().clone();
7642 self.apply_catalog_command_to(&mut next_catalog, command)?;
7643 next_catalog.db_epoch = epoch.0;
7644 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7645 Ok(Some(epoch))
7646 }
7647
7648 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
7651 let cat = self.catalog.read();
7652 Self::resolve_principal_from_catalog(&cat, username)
7653 }
7654
7655 pub fn resolve_current_principal(
7658 &self,
7659 principal: &crate::auth::Principal,
7660 ) -> Option<crate::auth::Principal> {
7661 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
7662 }
7663
7664 fn resolve_principal_from_catalog(
7669 cat: &Catalog,
7670 username: &str,
7671 ) -> Option<crate::auth::Principal> {
7672 let user = cat.users.iter().find(|u| u.username == username)?;
7673 Self::resolve_user_principal_from_catalog(cat, user)
7674 }
7675
7676 fn resolve_bound_principal_from_catalog(
7677 cat: &Catalog,
7678 principal: &crate::auth::Principal,
7679 ) -> Option<crate::auth::Principal> {
7680 let user = cat.users.iter().find(|user| {
7681 user.id == principal.user_id
7682 && user.created_epoch == principal.created_epoch
7683 && user.username == principal.username
7684 })?;
7685 Self::resolve_user_principal_from_catalog(cat, user)
7686 }
7687
7688 fn resolve_user_principal_from_catalog(
7689 cat: &Catalog,
7690 user: &crate::auth::UserEntry,
7691 ) -> Option<crate::auth::Principal> {
7692 let mut permissions = Vec::new();
7693 for role_name in &user.roles {
7694 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
7695 permissions.extend(role.permissions.iter().cloned());
7696 }
7697 }
7698 Some(crate::auth::Principal {
7699 user_id: user.id,
7700 created_epoch: user.created_epoch,
7701 username: user.username.clone(),
7702 is_admin: user.is_admin,
7703 roles: user.roles.clone(),
7704 permissions,
7705 })
7706 }
7707
7708 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
7710 match self.resolve_principal(username) {
7711 Some(p) => p.has_permission(permission),
7712 None => false,
7713 }
7714 }
7715
7716 pub fn require_auth_enabled(&self) -> bool {
7720 self.catalog.read().require_auth
7721 }
7722
7723 pub fn principal(&self) -> Option<crate::auth::Principal> {
7727 self.principal.read().clone()
7728 }
7729
7730 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
7736 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
7737 self.auth_state.clone(),
7738 )))
7739 }
7740
7741 pub fn refresh_principal(&self) -> Result<()> {
7755 let previous = match self.principal.read().clone() {
7756 Some(principal) => principal,
7757 None => return Ok(()),
7758 };
7759 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
7760 self.refresh_security_catalog_if_stale(observed_version)?;
7761 let cat = self.catalog.read();
7762 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
7763 Some(p) => {
7764 *self.principal.write() = Some(p.clone());
7765 self.auth_state.set_principal(Some(p));
7769 Ok(())
7770 }
7771 None => Err(MongrelError::InvalidCredentials {
7772 username: previous.username,
7773 }),
7774 }
7775 }
7776
7777 pub fn security_catalog_disk_read_count(&self) -> u64 {
7780 self.security_catalog_disk_reads.load(Ordering::Relaxed)
7781 }
7782
7783 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
7795 if self.shared {
7796 return Err(MongrelError::Conflict(
7802 "enable_auth requires an exclusive Database owner; shared cores reject \
7803 auth-mode transitions"
7804 .into(),
7805 ));
7806 }
7807 let password_hash =
7808 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
7809 let _ddl = self.ddl_lock.lock();
7810 let _security_write = self.security_write()?;
7811 let _commit = self.commit_lock.lock();
7812 let epoch = self.epoch.bump_assigned();
7813 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7814 let mut next_catalog = self.catalog.read().clone();
7815 if next_catalog.require_auth {
7816 return Err(MongrelError::InvalidArgument(
7817 "database already has require_auth enabled".into(),
7818 ));
7819 }
7820 if next_catalog
7821 .users
7822 .iter()
7823 .any(|u| u.username == admin_username)
7824 {
7825 return Err(MongrelError::InvalidArgument(format!(
7826 "user {admin_username:?} already exists"
7827 )));
7828 }
7829 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
7830 let id = next_catalog.next_user_id;
7831 next_catalog.next_user_id = id
7832 .checked_add(1)
7833 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
7834 next_catalog.users.push(crate::auth::UserEntry {
7835 id,
7836 username: admin_username.to_string(),
7837 password_hash,
7838 roles: Vec::new(),
7839 is_admin: true,
7840 created_epoch: epoch.0,
7841 });
7842 next_catalog.require_auth = true;
7843 advance_security_version(&mut next_catalog)?;
7844 next_catalog.db_epoch = epoch.0;
7845 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
7846 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
7850 let principal = crate::auth::Principal {
7851 user_id: id,
7852 created_epoch: epoch.0,
7853 username: admin_username.to_string(),
7854 is_admin: true,
7855 roles: Vec::new(),
7856 permissions: Vec::new(),
7857 };
7858 *self.principal.write() = Some(principal.clone());
7859 self.auth_state.set_principal(Some(principal));
7860 }
7861 publish
7862 }
7863
7864 pub fn disable_auth(&self) -> Result<()> {
7881 let _ddl = self.ddl_lock.lock();
7882 let _security_write = self.security_write()?;
7883 let _commit = self.commit_lock.lock();
7884 let epoch = self.epoch.bump_assigned();
7885 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7886 let mut next_catalog = self.catalog.read().clone();
7887 if !next_catalog.require_auth {
7888 return Err(MongrelError::InvalidArgument(
7889 "database does not have require_auth enabled".into(),
7890 ));
7891 }
7892 next_catalog.require_auth = false;
7893 advance_security_version(&mut next_catalog)?;
7894 next_catalog.db_epoch = epoch.0;
7895 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
7896 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
7898 *self.principal.write() = None;
7899 }
7900 publish
7901 }
7902
7903 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
7910 self.ensure_owner_process()?;
7911 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
7912 return Err(MongrelError::ReadOnlyReplica);
7913 }
7914 if self.principal.read().is_some() {
7915 self.refresh_principal().map_err(|error| match error {
7916 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
7917 error => error,
7918 })?;
7919 }
7920 if !self.catalog.read().require_auth {
7921 return Ok(());
7922 }
7923 let guard = self.principal.read();
7924 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
7925 if p.has_permission(perm) {
7926 Ok(())
7927 } else {
7928 Err(MongrelError::PermissionDenied {
7929 required: perm.clone(),
7930 principal: p.username.clone(),
7931 })
7932 }
7933 }
7934
7935 pub fn require_table(
7940 &self,
7941 table: &str,
7942 perm: crate::auth_state::RequiredPermission,
7943 ) -> Result<()> {
7944 self.require(&perm.into_permission(table))
7945 }
7946
7947 pub fn triggers(&self) -> Vec<StoredTrigger> {
7948 self.catalog
7949 .read()
7950 .triggers
7951 .iter()
7952 .map(|t| t.trigger.clone())
7953 .collect()
7954 }
7955
7956 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
7957 self.catalog
7958 .read()
7959 .triggers
7960 .iter()
7961 .find(|t| t.trigger.name == name)
7962 .map(|t| t.trigger.clone())
7963 }
7964
7965 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
7966 self.create_trigger_inner(trigger, None, None)
7967 }
7968
7969 pub fn create_trigger_controlled<F>(
7970 &self,
7971 trigger: StoredTrigger,
7972 mut before_publish: F,
7973 ) -> Result<StoredTrigger>
7974 where
7975 F: FnMut() -> Result<()>,
7976 {
7977 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
7978 }
7979
7980 pub fn create_trigger_as_controlled<F>(
7981 &self,
7982 trigger: StoredTrigger,
7983 principal: Option<&crate::auth::Principal>,
7984 mut before_publish: F,
7985 ) -> Result<StoredTrigger>
7986 where
7987 F: FnMut() -> Result<()>,
7988 {
7989 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
7990 }
7991
7992 fn create_trigger_inner(
7993 &self,
7994 mut trigger: StoredTrigger,
7995 principal: Option<&crate::auth::Principal>,
7996 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7997 ) -> Result<StoredTrigger> {
7998 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
7999 trigger: trigger.clone(),
8000 };
8001 self.require_for(
8002 principal,
8003 &crate::catalog_cmds::required_permission(&command),
8004 )?;
8005 let _g = self.ddl_lock.lock();
8006 let _security_write = self.security_write()?;
8007 self.require_for(
8008 principal,
8009 &crate::catalog_cmds::required_permission(&command),
8010 )?;
8011 trigger.validate()?;
8012 self.validate_trigger_references(&trigger)
8013 .map_err(trigger_validation_error)?;
8014 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8018 let commit_lock = Arc::clone(&self.commit_lock);
8019 let _c = commit_lock.lock();
8020 let epoch = self.epoch.bump_assigned();
8021 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8022 trigger.created_epoch = epoch.0;
8023 trigger.updated_epoch = epoch.0;
8024 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
8025 trigger: trigger.clone(),
8026 };
8027 let mut next_catalog = self.catalog.read().clone();
8028 self.apply_catalog_command_to(&mut next_catalog, command)?;
8029 next_catalog.db_epoch = epoch.0;
8030 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8031 Ok(trigger)
8032 }
8033
8034 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
8035 self.create_or_replace_trigger_inner(trigger, None, None)
8036 }
8037
8038 pub fn create_or_replace_trigger_controlled<F>(
8039 &self,
8040 trigger: StoredTrigger,
8041 mut before_publish: F,
8042 ) -> Result<StoredTrigger>
8043 where
8044 F: FnMut() -> Result<()>,
8045 {
8046 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
8047 }
8048
8049 pub fn create_or_replace_trigger_as_controlled<F>(
8050 &self,
8051 trigger: StoredTrigger,
8052 principal: Option<&crate::auth::Principal>,
8053 mut before_publish: F,
8054 ) -> Result<StoredTrigger>
8055 where
8056 F: FnMut() -> Result<()>,
8057 {
8058 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
8059 }
8060
8061 fn create_or_replace_trigger_inner(
8062 &self,
8063 trigger: StoredTrigger,
8064 principal: Option<&crate::auth::Principal>,
8065 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8066 ) -> Result<StoredTrigger> {
8067 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
8068 trigger: trigger.clone(),
8069 };
8070 self.require_for(
8071 principal,
8072 &crate::catalog_cmds::required_permission(&command),
8073 )?;
8074 let _g = self.ddl_lock.lock();
8075 let _security_write = self.security_write()?;
8076 self.require_for(
8077 principal,
8078 &crate::catalog_cmds::required_permission(&command),
8079 )?;
8080 trigger.validate()?;
8081 self.validate_trigger_references(&trigger)
8082 .map_err(trigger_validation_error)?;
8083 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8087 let commit_lock = Arc::clone(&self.commit_lock);
8088 let _c = commit_lock.lock();
8089 let epoch = self.epoch.bump_assigned();
8090 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8091 let mut next_catalog = self.catalog.read().clone();
8092 let replaced = match next_catalog
8097 .triggers
8098 .iter()
8099 .position(|t| t.trigger.name == trigger.name)
8100 {
8101 Some(idx) => next_catalog.triggers[idx]
8102 .trigger
8103 .replaced(trigger.clone(), epoch.0)?,
8104 None => {
8105 let mut next = trigger;
8106 next.created_epoch = epoch.0;
8107 next.updated_epoch = epoch.0;
8108 next
8109 }
8110 };
8111 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
8112 trigger: replaced.clone(),
8113 };
8114 self.apply_catalog_command_to(&mut next_catalog, command)?;
8115 next_catalog.db_epoch = epoch.0;
8116 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8117 Ok(replaced)
8118 }
8119
8120 pub fn drop_trigger(&self, name: &str) -> Result<()> {
8121 self.drop_trigger_with_epoch(name).map(|_| ())
8122 }
8123
8124 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
8126 self.drop_triggers_with_epoch(&[name.to_string()])
8127 }
8128
8129 pub fn drop_trigger_with_epoch_controlled<F>(
8130 &self,
8131 name: &str,
8132 before_publish: F,
8133 ) -> Result<Epoch>
8134 where
8135 F: FnMut() -> Result<()>,
8136 {
8137 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
8138 }
8139
8140 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
8142 self.drop_triggers_with_epoch_inner(names, None, None)
8143 }
8144
8145 pub fn drop_triggers_with_epoch_controlled<F>(
8146 &self,
8147 names: &[String],
8148 mut before_publish: F,
8149 ) -> Result<Epoch>
8150 where
8151 F: FnMut() -> Result<()>,
8152 {
8153 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
8154 }
8155
8156 pub fn drop_triggers_with_epoch_as_controlled<F>(
8157 &self,
8158 names: &[String],
8159 principal: Option<&crate::auth::Principal>,
8160 mut before_publish: F,
8161 ) -> Result<Epoch>
8162 where
8163 F: FnMut() -> Result<()>,
8164 {
8165 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
8166 }
8167
8168 fn drop_triggers_with_epoch_inner(
8169 &self,
8170 names: &[String],
8171 principal: Option<&crate::auth::Principal>,
8172 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8173 ) -> Result<Epoch> {
8174 if names.is_empty() {
8175 return Err(MongrelError::InvalidArgument(
8176 "at least one trigger name is required".into(),
8177 ));
8178 }
8179 let commands: Vec<crate::catalog_cmds::CatalogCommand> = names
8180 .iter()
8181 .map(|name| crate::catalog_cmds::CatalogCommand::DropTrigger { name: name.clone() })
8182 .collect();
8183 for command in &commands {
8184 self.require_for(
8185 principal,
8186 &crate::catalog_cmds::required_permission(command),
8187 )?;
8188 }
8189 let _g = self.ddl_lock.lock();
8190 let _security_write = self.security_write()?;
8191 for command in &commands {
8192 self.require_for(
8193 principal,
8194 &crate::catalog_cmds::required_permission(command),
8195 )?;
8196 }
8197 {
8202 let cat = self.catalog.read();
8203 for command in &commands {
8204 crate::catalog_cmds::apply(&cat, command)?;
8205 }
8206 }
8207 let commit_lock = Arc::clone(&self.commit_lock);
8208 let _c = commit_lock.lock();
8209 let epoch = self.epoch.bump_assigned();
8210 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8211 let mut next_catalog = self.catalog.read().clone();
8212 for command in commands {
8213 self.apply_catalog_command_to(&mut next_catalog, command)?;
8214 }
8215 next_catalog.db_epoch = epoch.0;
8216 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8217 Ok(epoch)
8218 }
8219
8220 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
8221 self.catalog.read().external_tables.clone()
8222 }
8223
8224 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
8225 self.catalog
8226 .read()
8227 .external_tables
8228 .iter()
8229 .find(|t| t.name == name)
8230 .cloned()
8231 }
8232
8233 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
8234 self.create_external_table_inner(entry, None)
8235 }
8236
8237 pub fn create_external_table_controlled<F>(
8238 &self,
8239 entry: ExternalTableEntry,
8240 mut before_publish: F,
8241 ) -> Result<ExternalTableEntry>
8242 where
8243 F: FnMut() -> Result<()>,
8244 {
8245 self.create_external_table_inner(entry, Some(&mut before_publish))
8246 }
8247
8248 fn create_external_table_inner(
8249 &self,
8250 mut entry: ExternalTableEntry,
8251 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8252 ) -> Result<ExternalTableEntry> {
8253 self.require(&crate::auth::Permission::Ddl)?;
8254 let _g = self.ddl_lock.lock();
8255 let _security_write = self.security_write()?;
8256 self.require(&crate::auth::Permission::Ddl)?;
8257 entry.validate()?;
8258 {
8259 let cat = self.catalog.read();
8260 if cat.live(&entry.name).is_some()
8261 || cat.external_tables.iter().any(|t| t.name == entry.name)
8262 {
8263 return Err(MongrelError::InvalidArgument(format!(
8264 "table {:?} already exists",
8265 entry.name
8266 )));
8267 }
8268 }
8269 let commit_lock = Arc::clone(&self.commit_lock);
8270 let _c = commit_lock.lock();
8271 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
8275 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
8276 let epoch = self.epoch.bump_assigned();
8277 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8278 entry.created_epoch = epoch.0;
8279 let mut next_catalog = self.catalog.read().clone();
8280 next_catalog.external_tables.push(entry.clone());
8281 next_catalog.db_epoch = epoch.0;
8282 self.publish_catalog_candidate_with_prelude(
8283 next_catalog,
8284 epoch,
8285 &mut _epoch_guard,
8286 before_publish,
8287 vec![(
8288 EXTERNAL_TABLE_ID,
8289 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
8290 name: entry.name.clone(),
8291 generation_epoch: epoch.0,
8292 }),
8293 )],
8294 )?;
8295 Ok(entry)
8296 }
8297
8298 pub fn drop_external_table(&self, name: &str) -> Result<()> {
8299 self.drop_external_table_with_epoch(name).map(|_| ())
8300 }
8301
8302 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
8304 self.drop_external_table_with_epoch_inner(name, None)
8305 }
8306
8307 pub fn drop_external_table_with_epoch_controlled<F>(
8308 &self,
8309 name: &str,
8310 mut before_publish: F,
8311 ) -> Result<Epoch>
8312 where
8313 F: FnMut() -> Result<()>,
8314 {
8315 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
8316 }
8317
8318 fn drop_external_table_with_epoch_inner(
8319 &self,
8320 name: &str,
8321 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8322 ) -> Result<Epoch> {
8323 self.require(&crate::auth::Permission::Ddl)?;
8324 let _g = self.ddl_lock.lock();
8325 let _security_write = self.security_write()?;
8326 self.require(&crate::auth::Permission::Ddl)?;
8327 let commit_lock = Arc::clone(&self.commit_lock);
8328 let _c = commit_lock.lock();
8329 let epoch = self.epoch.bump_assigned();
8330 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8331 let mut next_catalog = self.catalog.read().clone();
8332 let before = next_catalog.external_tables.len();
8333 next_catalog.external_tables.retain(|t| t.name != name);
8334 if next_catalog.external_tables.len() == before {
8335 return Err(MongrelError::NotFound(format!(
8336 "external table {name:?} not found"
8337 )));
8338 }
8339 next_catalog.db_epoch = epoch.0;
8340 self.publish_catalog_candidate_with_prelude(
8341 next_catalog,
8342 epoch,
8343 &mut _epoch_guard,
8344 before_publish,
8345 vec![(
8346 EXTERNAL_TABLE_ID,
8347 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
8348 name: name.to_string(),
8349 generation_epoch: epoch.0,
8350 }),
8351 )],
8352 )?;
8353 let state_dir = self.root.join(VTAB_DIR).join(name);
8354 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
8355 return Err(MongrelError::DurableCommit {
8356 epoch: epoch.0,
8357 message: format!(
8358 "external table was dropped but connector-state cleanup failed: {error}"
8359 ),
8360 });
8361 }
8362 Ok(epoch)
8363 }
8364
8365 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
8366 let txn_id = self.alloc_txn_id()?;
8367 let (principal, catalog_bound) = self.transaction_principal_snapshot();
8368 self.commit_transaction_with_external_states(
8369 txn_id,
8370 self.epoch.visible(),
8371 Vec::new(),
8372 vec![(name.to_string(), state.to_vec())],
8373 Vec::new(),
8374 principal,
8375 catalog_bound,
8376 None,
8377 crate::txn::TxnCommitContext::internal(),
8378 )
8379 .map(|(epoch, _)| epoch)
8380 }
8381
8382 pub fn trigger_config(&self) -> TriggerConfig {
8383 use std::sync::atomic::Ordering;
8384 TriggerConfig {
8385 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
8386 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
8387 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
8388 }
8389 }
8390
8391 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
8392 use std::sync::atomic::Ordering;
8393 if config.max_depth == 0 {
8394 return Err(MongrelError::InvalidArgument(
8395 "trigger max_depth must be greater than 0".into(),
8396 ));
8397 }
8398 self.trigger_recursive
8399 .store(config.recursive_triggers, Ordering::Relaxed);
8400 self.trigger_max_depth
8401 .store(config.max_depth, Ordering::Relaxed);
8402 self.trigger_max_loop_iterations
8403 .store(config.max_loop_iterations, Ordering::Relaxed);
8404 Ok(())
8405 }
8406
8407 pub fn set_recursive_triggers(&self, recursive: bool) {
8408 use std::sync::atomic::Ordering;
8409 self.trigger_recursive.store(recursive, Ordering::Relaxed);
8410 }
8411
8412 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
8416 self.notify.subscribe()
8417 }
8418
8419 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
8420 self.change_wake.subscribe()
8421 }
8422
8423 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
8428 let control = crate::ExecutionControl::new(None);
8429 self.change_events_since_controlled(last_event_id, &control)
8430 }
8431
8432 pub fn change_events_since_controlled(
8434 &self,
8435 last_event_id: Option<&str>,
8436 control: &crate::ExecutionControl,
8437 ) -> Result<CdcBatch> {
8438 use crate::wal::Op;
8439
8440 control.checkpoint()?;
8441 let resume = match last_event_id {
8442 Some(id) => {
8443 let (epoch, index) = id.split_once(':').ok_or_else(|| {
8444 MongrelError::InvalidArgument(format!(
8445 "invalid CDC event id {id:?}; expected <epoch>:<index>"
8446 ))
8447 })?;
8448 Some((
8449 epoch.parse::<u64>().map_err(|error| {
8450 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
8451 })?,
8452 index.parse::<u32>().map_err(|error| {
8453 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
8454 })?,
8455 ))
8456 }
8457 None => None,
8458 };
8459
8460 let mut wal = self.shared_wal.lock();
8461 wal.group_sync()?;
8462 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
8463 let records = crate::wal::SharedWal::replay_with_dek_controlled(
8464 &self.root,
8465 wal_dek.as_ref(),
8466 control,
8467 CDC_MAX_WAL_RECORDS,
8468 CDC_MAX_WAL_REPLAY_BYTES,
8469 )?;
8470 drop(wal);
8471 control.checkpoint()?;
8472
8473 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
8474 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
8475 for (index, record) in records.iter().enumerate() {
8476 if index % 256 == 0 {
8477 control.checkpoint()?;
8478 }
8479 if let Op::TxnCommit { epoch, added_runs } = &record.op {
8480 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
8481 }
8482 if let Op::SpilledRows { table_id, rows } = &record.op {
8483 spilled_payloads
8484 .entry((record.txn_id, *table_id))
8485 .or_default()
8486 .push(rows);
8487 }
8488 }
8489 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
8490 let current_epoch = self.epoch.committed().0;
8491 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
8492 let gap = resume.is_some_and(|(epoch, _)| {
8493 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
8494 });
8495 if gap {
8496 return Ok(CdcBatch {
8497 events: Vec::new(),
8498 current_epoch,
8499 earliest_epoch,
8500 gap: true,
8501 });
8502 }
8503
8504 let table_names: HashMap<u64, String> = self
8505 .catalog
8506 .read()
8507 .tables
8508 .iter()
8509 .map(|entry| (entry.table_id, entry.name.clone()))
8510 .collect();
8511 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
8512 let mut retained_bytes = 0_usize;
8513 for (index, record) in records.iter().enumerate() {
8514 if index % 256 == 0 {
8515 control.checkpoint()?;
8516 }
8517 if !commits.contains_key(&record.txn_id) {
8518 continue;
8519 }
8520 let Op::BeforeImage {
8521 table_id,
8522 row_id,
8523 row,
8524 } = &record.op
8525 else {
8526 continue;
8527 };
8528 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
8529 return Err(MongrelError::ResourceLimitExceeded {
8530 resource: "CDC before-image bytes",
8531 requested: row.len(),
8532 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
8533 });
8534 }
8535 let before: crate::memtable::Row = bincode::deserialize(row)?;
8536 if before_images.len() >= CDC_MAX_ROWS {
8537 return Err(MongrelError::ResourceLimitExceeded {
8538 resource: "CDC before-image rows",
8539 requested: before_images.len().saturating_add(1),
8540 limit: CDC_MAX_ROWS,
8541 });
8542 }
8543 charge_cdc_bytes(
8544 &mut retained_bytes,
8545 cdc_row_storage_bytes(&before),
8546 "CDC retained bytes",
8547 )?;
8548 before_images.insert((record.txn_id, *table_id, row_id.0), before);
8549 }
8550 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
8551 let mut events = Vec::new();
8552 let mut decoded_rows = before_images.len();
8553 for (record_index, record) in records.iter().enumerate() {
8554 if record_index % 256 == 0 {
8555 control.checkpoint()?;
8556 }
8557 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
8558 continue;
8559 };
8560 let event = match &record.op {
8561 Op::Put { table_id, rows } => {
8562 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
8563 return Err(MongrelError::ResourceLimitExceeded {
8564 resource: "CDC inline row bytes",
8565 requested: rows.len(),
8566 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
8567 });
8568 }
8569 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
8570 decoded_rows = decoded_rows.saturating_add(rows.len());
8571 if decoded_rows > CDC_MAX_ROWS {
8572 return Err(MongrelError::ResourceLimitExceeded {
8573 resource: "CDC decoded rows",
8574 requested: decoded_rows,
8575 limit: CDC_MAX_ROWS,
8576 });
8577 }
8578 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
8579 let mut peak_bytes = retained_bytes;
8580 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
8581 let data = serde_json::to_value(rows)
8582 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
8583 Some((*table_id, "put", data, event_bytes))
8584 }
8585 Op::Delete { table_id, row_ids } => {
8586 let before = row_ids
8587 .iter()
8588 .filter_map(|row_id| {
8589 before_images
8590 .get(&(record.txn_id, *table_id, row_id.0))
8591 .cloned()
8592 })
8593 .collect::<Vec<_>>();
8594 let event_bytes = cdc_rows_json_bytes(&before)
8595 .saturating_add(
8596 row_ids
8597 .len()
8598 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
8599 )
8600 .saturating_add(512);
8601 let mut peak_bytes = retained_bytes;
8602 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
8603 Some((
8604 *table_id,
8605 "delete",
8606 serde_json::json!({
8607 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
8608 "before": before,
8609 }),
8610 event_bytes,
8611 ))
8612 }
8613 Op::TruncateTable { table_id } => {
8614 Some((*table_id, "truncate", serde_json::Value::Null, 512))
8615 }
8616 _ => None,
8617 };
8618 if let Some((table_id, op, data, event_bytes)) = event {
8619 let index = operation_indices.entry(record.txn_id).or_insert(0);
8620 let event_position = (*commit_epoch, *index);
8621 *index = index.saturating_add(1);
8622 if resume.is_some_and(|position| event_position <= position) {
8623 continue;
8624 }
8625 if events.len() >= CDC_MAX_EVENTS {
8626 return Err(MongrelError::ResourceLimitExceeded {
8627 resource: "CDC events",
8628 requested: events.len().saturating_add(1),
8629 limit: CDC_MAX_EVENTS,
8630 });
8631 }
8632 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
8633 events.push(ChangeEvent {
8634 id: Some(format!("{}:{}", event_position.0, event_position.1)),
8635 channel: "changes".into(),
8636 table_id: Some(table_id),
8637 table: table_names.get(&table_id).cloned().unwrap_or_default(),
8638 op: op.into(),
8639 epoch: *commit_epoch,
8640 txn_id: Some(record.txn_id),
8641 message: None,
8642 data: Some(data),
8643 });
8644 }
8645 if let Op::TxnCommit { added_runs, .. } = &record.op {
8646 for run in added_runs {
8647 control.checkpoint()?;
8648 let index = operation_indices.entry(record.txn_id).or_insert(0);
8649 let event_position = (*commit_epoch, *index);
8650 *index = index.saturating_add(1);
8651 if resume.is_some_and(|position| event_position <= position) {
8652 continue;
8653 }
8654 let mut rows = if let Some(payloads) =
8655 spilled_payloads.get(&(record.txn_id, run.table_id))
8656 {
8657 let mut rows = Vec::new();
8658 for payload in payloads {
8659 control.checkpoint()?;
8660 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
8661 return Err(MongrelError::ResourceLimitExceeded {
8662 resource: "CDC spilled row bytes",
8663 requested: payload.len(),
8664 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
8665 });
8666 }
8667 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
8668 if decoded_rows
8669 .saturating_add(rows.len())
8670 .saturating_add(chunk.len())
8671 > CDC_MAX_ROWS
8672 {
8673 return Err(MongrelError::ResourceLimitExceeded {
8674 resource: "CDC decoded rows",
8675 requested: decoded_rows
8676 .saturating_add(rows.len())
8677 .saturating_add(chunk.len()),
8678 limit: CDC_MAX_ROWS,
8679 });
8680 }
8681 rows.extend(chunk);
8682 }
8683 rows
8684 } else {
8685 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
8686 return Ok(CdcBatch {
8687 events: Vec::new(),
8688 current_epoch,
8689 earliest_epoch,
8690 gap: true,
8691 });
8692 };
8693 let table = handle.lock();
8694 let mut reader = match table.open_reader(run.run_id) {
8695 Ok(reader) => reader,
8696 Err(_) => {
8697 return Ok(CdcBatch {
8698 events: Vec::new(),
8699 current_epoch,
8700 earliest_epoch,
8701 gap: true,
8702 })
8703 }
8704 };
8705 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
8706 let rows = reader.all_rows_controlled(control, remaining)?;
8707 drop(reader);
8708 drop(table);
8709 rows
8710 };
8711 for row in &mut rows {
8712 row.committed_epoch = Epoch(*commit_epoch);
8713 }
8714 decoded_rows = decoded_rows.saturating_add(rows.len());
8715 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
8716 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
8717 if events.len() >= CDC_MAX_EVENTS {
8718 return Err(MongrelError::ResourceLimitExceeded {
8719 resource: "CDC events",
8720 requested: events.len().saturating_add(1),
8721 limit: CDC_MAX_EVENTS,
8722 });
8723 }
8724 events.push(ChangeEvent {
8725 id: Some(format!("{}:{}", event_position.0, event_position.1)),
8726 channel: "changes".into(),
8727 table_id: Some(run.table_id),
8728 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
8729 op: "put_run".into(),
8730 epoch: *commit_epoch,
8731 txn_id: Some(record.txn_id),
8732 message: None,
8733 data: Some(serde_json::json!({
8734 "run_id": run.run_id.to_string(),
8735 "row_count": run.row_count,
8736 "min_row_id": run.min_row_id,
8737 "max_row_id": run.max_row_id,
8738 "rows": rows,
8739 })),
8740 });
8741 }
8742 }
8743 }
8744 control.checkpoint()?;
8745 Ok(CdcBatch {
8746 events,
8747 current_epoch,
8748 earliest_epoch,
8749 gap: false,
8750 })
8751 }
8752
8753 pub fn notify(&self, channel: &str, message: Option<String>) {
8756 let _ = self.notify.send(ChangeEvent {
8757 id: None,
8758 channel: channel.to_string(),
8759 table_id: None,
8760 table: String::new(),
8761 op: "notify".into(),
8762 epoch: self.epoch.visible().0,
8763 txn_id: None,
8764 message,
8765 data: None,
8766 });
8767 }
8768
8769 pub fn call_procedure(
8770 &self,
8771 name: &str,
8772 args: HashMap<String, crate::Value>,
8773 ) -> Result<ProcedureCallResult> {
8774 self.call_procedure_as(name, args, None)
8775 }
8776
8777 pub fn call_procedure_as(
8778 &self,
8779 name: &str,
8780 args: HashMap<String, crate::Value>,
8781 principal: Option<&crate::auth::Principal>,
8782 ) -> Result<ProcedureCallResult> {
8783 let control = crate::ExecutionControl::new(None);
8784 self.call_procedure_as_controlled(name, args, principal, &control, || true)
8785 }
8786
8787 #[doc(hidden)]
8790 pub fn call_procedure_as_bound(
8791 &self,
8792 expected: &StoredProcedure,
8793 args: HashMap<String, crate::Value>,
8794 principal: Option<&crate::auth::Principal>,
8795 ) -> Result<ProcedureCallResult> {
8796 self.require_for(principal, &crate::auth::Permission::All)?;
8797 let procedure = self.procedure(&expected.name).ok_or_else(|| {
8798 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
8799 })?;
8800 if &procedure != expected {
8801 return Err(MongrelError::Conflict(format!(
8802 "procedure {:?} changed after request authorization",
8803 expected.name
8804 )));
8805 }
8806 let control = crate::ExecutionControl::new(None);
8807 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
8808 }
8809
8810 #[doc(hidden)]
8815 pub fn call_procedure_as_controlled<F>(
8816 &self,
8817 name: &str,
8818 args: HashMap<String, crate::Value>,
8819 principal: Option<&crate::auth::Principal>,
8820 control: &crate::ExecutionControl,
8821 before_commit: F,
8822 ) -> Result<ProcedureCallResult>
8823 where
8824 F: FnOnce() -> bool,
8825 {
8826 self.require_for(principal, &crate::auth::Permission::All)?;
8830 let procedure = self
8831 .procedure(name)
8832 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
8833 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
8834 }
8835
8836 fn execute_procedure_as_controlled<F>(
8837 &self,
8838 procedure: StoredProcedure,
8839 args: HashMap<String, crate::Value>,
8840 principal: Option<&crate::auth::Principal>,
8841 control: &crate::ExecutionControl,
8842 before_commit: F,
8843 ) -> Result<ProcedureCallResult>
8844 where
8845 F: FnOnce() -> bool,
8846 {
8847 let args = bind_procedure_args(&procedure, args)?;
8848 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
8849 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
8850 if has_writes {
8851 let mut tx = self.begin_as(principal.cloned());
8852 let run = (|| {
8853 for (step_index, step) in procedure.body.steps.iter().enumerate() {
8854 if step_index % 256 == 0 {
8855 control.checkpoint()?;
8856 }
8857 let output = self.execute_procedure_step(
8858 step,
8859 &args,
8860 &outputs,
8861 Some(&mut tx),
8862 principal,
8863 Some(control),
8864 )?;
8865 outputs.insert(step.id().to_string(), output);
8866 }
8867 control.checkpoint()?;
8868 eval_return_output(&procedure.body.return_value, &args, &outputs)
8869 })();
8870 match run {
8871 Ok(output) => {
8872 control.checkpoint()?;
8873 if !before_commit() {
8874 tx.rollback();
8875 return Err(MongrelError::Cancelled);
8876 }
8877 let epoch = tx.commit()?.0;
8878 Ok(ProcedureCallResult {
8879 epoch: Some(epoch),
8880 output,
8881 })
8882 }
8883 Err(e) => {
8884 tx.rollback();
8885 Err(e)
8886 }
8887 }
8888 } else {
8889 for (step_index, step) in procedure.body.steps.iter().enumerate() {
8890 if step_index % 256 == 0 {
8891 control.checkpoint()?;
8892 }
8893 let output = self.execute_procedure_step(
8894 step,
8895 &args,
8896 &outputs,
8897 None,
8898 principal,
8899 Some(control),
8900 )?;
8901 outputs.insert(step.id().to_string(), output);
8902 }
8903 control.checkpoint()?;
8904 Ok(ProcedureCallResult {
8905 epoch: None,
8906 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
8907 })
8908 }
8909 }
8910
8911 fn execute_procedure_step(
8912 &self,
8913 step: &ProcedureStep,
8914 args: &HashMap<String, crate::Value>,
8915 outputs: &HashMap<String, ProcedureCallOutput>,
8916 tx: Option<&mut crate::txn::Transaction<'_>>,
8917 principal: Option<&crate::auth::Principal>,
8918 control: Option<&crate::ExecutionControl>,
8919 ) -> Result<ProcedureCallOutput> {
8920 if let Some(control) = control {
8921 control.checkpoint()?;
8922 }
8923 match step {
8924 ProcedureStep::NativeQuery {
8925 table,
8926 conditions,
8927 projection,
8928 limit,
8929 ..
8930 } => {
8931 let mut q = crate::Query::new();
8932 for condition in conditions {
8933 q = q.and(eval_condition(condition, args, outputs)?);
8934 }
8935 let fallback_control = crate::ExecutionControl::new(None);
8936 let query_control = control.unwrap_or(&fallback_control);
8937 let mut rows = self.query_for_principal_controlled(
8938 table,
8939 &q,
8940 projection.as_deref(),
8941 principal,
8942 false,
8943 query_control,
8944 )?;
8945 if let Some(limit) = limit {
8946 rows.truncate(*limit);
8947 }
8948 let mut output = Vec::with_capacity(rows.len());
8949 for (row_index, row) in rows.into_iter().enumerate() {
8950 if row_index % 256 == 0 {
8951 if let Some(control) = control {
8952 control.checkpoint()?;
8953 }
8954 }
8955 output.push(ProcedureCallRow {
8956 row_id: Some(row.row_id),
8957 columns: row.columns,
8958 });
8959 }
8960 Ok(ProcedureCallOutput::Rows(output))
8961 }
8962 ProcedureStep::Put {
8963 table,
8964 cells,
8965 returning,
8966 ..
8967 } => {
8968 let tx = tx.ok_or_else(|| {
8969 MongrelError::InvalidArgument(
8970 "write procedure step requires a transaction".into(),
8971 )
8972 })?;
8973 let cells = eval_cells(cells, args, outputs)?;
8974 if *returning {
8975 let out = tx.put_returning(table, cells)?;
8976 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
8977 row_id: None,
8978 columns: out.row.columns.into_iter().collect(),
8979 }))
8980 } else {
8981 tx.put(table, cells)?;
8982 Ok(ProcedureCallOutput::Null)
8983 }
8984 }
8985 ProcedureStep::Upsert {
8986 table,
8987 cells,
8988 update_cells,
8989 returning,
8990 ..
8991 } => {
8992 let tx = tx.ok_or_else(|| {
8993 MongrelError::InvalidArgument(
8994 "write procedure step requires a transaction".into(),
8995 )
8996 })?;
8997 let cells = eval_cells(cells, args, outputs)?;
8998 let action = match update_cells {
8999 Some(update_cells) => {
9000 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
9001 }
9002 None => crate::UpsertAction::DoNothing,
9003 };
9004 let out = tx.upsert(table, cells, action)?;
9005 if *returning {
9006 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
9007 row_id: None,
9008 columns: out.row.columns.into_iter().collect(),
9009 }))
9010 } else {
9011 Ok(ProcedureCallOutput::Null)
9012 }
9013 }
9014 ProcedureStep::DeleteByPk { table, pk, .. } => {
9015 let tx = tx.ok_or_else(|| {
9016 MongrelError::InvalidArgument(
9017 "write procedure step requires a transaction".into(),
9018 )
9019 })?;
9020 let pk = eval_value(pk, args, outputs)?;
9021 let handle = self.table(table)?;
9022 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
9023 MongrelError::NotFound("procedure delete_by_pk target not found".into())
9024 })?;
9025 tx.delete(table, row_id)?;
9026 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
9027 }
9028 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
9029 "DeleteRows procedure step is not supported by the core executor yet".into(),
9030 )),
9031 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
9032 "SqlQuery procedure step must be executed by mongreldb-query".into(),
9033 )),
9034 }
9035 }
9036
9037 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
9038 let cat = self.catalog.read();
9039 for step in &procedure.body.steps {
9040 let Some(table_name) = step.table() else {
9041 continue;
9042 };
9043 let schema = &cat
9044 .live(table_name)
9045 .ok_or_else(|| {
9046 MongrelError::InvalidArgument(format!(
9047 "procedure {:?} references unknown table {table_name:?}",
9048 procedure.name
9049 ))
9050 })?
9051 .schema;
9052 match step {
9053 ProcedureStep::NativeQuery {
9054 conditions,
9055 projection,
9056 ..
9057 } => {
9058 for condition in conditions {
9059 validate_condition_columns(condition, schema)?;
9060 }
9061 if let Some(projection) = projection {
9062 for id in projection {
9063 validate_column_id(*id, schema)?;
9064 }
9065 }
9066 }
9067 ProcedureStep::Put { cells, .. } => {
9068 for cell in cells {
9069 validate_column_id(cell.column_id, schema)?;
9070 }
9071 }
9072 ProcedureStep::Upsert {
9073 cells,
9074 update_cells,
9075 ..
9076 } => {
9077 for cell in cells {
9078 validate_column_id(cell.column_id, schema)?;
9079 }
9080 if let Some(update_cells) = update_cells {
9081 for cell in update_cells {
9082 validate_column_id(cell.column_id, schema)?;
9083 }
9084 }
9085 }
9086 ProcedureStep::DeleteByPk { .. } => {
9087 if schema.primary_key().is_none() {
9088 return Err(MongrelError::InvalidArgument(format!(
9089 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
9090 procedure.name
9091 )));
9092 }
9093 }
9094 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
9095 }
9096 }
9097 Ok(())
9098 }
9099
9100 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
9101 let cat = self.catalog.read();
9102 let target_schema = match &trigger.target {
9103 TriggerTarget::Table(target_name) => cat
9104 .live(target_name)
9105 .ok_or_else(|| {
9106 MongrelError::InvalidArgument(format!(
9107 "trigger {:?} references unknown target table {target_name:?}",
9108 trigger.name
9109 ))
9110 })?
9111 .schema
9112 .clone(),
9113 TriggerTarget::View(_) => Schema {
9114 columns: trigger.target_columns.clone(),
9115 ..Schema::default()
9116 },
9117 };
9118 for col in &trigger.update_of {
9119 if target_schema.column(col).is_none() {
9120 return Err(MongrelError::InvalidArgument(format!(
9121 "trigger {:?} UPDATE OF references unknown column {col:?}",
9122 trigger.name
9123 )));
9124 }
9125 }
9126 if let Some(expr) = &trigger.when {
9127 validate_trigger_expr(expr, &target_schema, trigger.event)?;
9128 }
9129 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
9130 for step in &trigger.program.steps {
9131 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
9132 {
9133 return Err(MongrelError::InvalidArgument(
9134 "SetNew trigger steps are only valid in BEFORE triggers".into(),
9135 ));
9136 }
9137 validate_trigger_step(
9138 step,
9139 &cat,
9140 &target_schema,
9141 trigger.event,
9142 &mut select_schemas,
9143 )?;
9144 }
9145 Ok(())
9146 }
9147
9148 pub fn begin(&self) -> crate::txn::Transaction<'_> {
9150 self.begin_with_isolation(crate::txn::IsolationLevel::default())
9151 }
9152
9153 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
9154 let principal = self.principal.read().clone();
9155 let catalog_bound = principal.as_ref().is_some_and(|principal| {
9156 let catalog = self.catalog.read();
9157 catalog.require_auth || principal.user_id != 0
9158 });
9159 (principal, catalog_bound)
9160 }
9161
9162 pub fn begin_as(
9163 &self,
9164 principal: Option<crate::auth::Principal>,
9165 ) -> crate::txn::Transaction<'_> {
9166 let catalog_bound = principal.as_ref().is_some_and(|principal| {
9167 let catalog = self.catalog.read();
9168 catalog.require_auth || principal.user_id != 0
9169 });
9170 let txn_id = self.alloc_txn_id();
9171 let read = Snapshot::at(self.epoch.visible());
9172 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
9173 .with_principal(principal, catalog_bound)
9174 }
9175
9176 pub fn begin_with_isolation(
9178 &self,
9179 level: crate::txn::IsolationLevel,
9180 ) -> crate::txn::Transaction<'_> {
9181 let txn_id = self.alloc_txn_id();
9182 let read = Snapshot::at(self.epoch.visible());
9185 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9186 crate::txn::Transaction::new(self, txn_id, read, level)
9187 .with_principal(principal, catalog_bound)
9188 }
9189
9190 pub fn begin_with_external_trigger_bridge<'a>(
9193 &'a self,
9194 bridge: &'a dyn ExternalTriggerBridge,
9195 ) -> crate::txn::Transaction<'a> {
9196 let txn_id = self.alloc_txn_id();
9197 let read = Snapshot::at(self.epoch.visible());
9198 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9199 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
9200 .with_external_trigger_bridge(bridge)
9201 .with_principal(principal, catalog_bound)
9202 }
9203
9204 pub fn begin_with_external_trigger_bridge_as<'a>(
9205 &'a self,
9206 bridge: &'a dyn ExternalTriggerBridge,
9207 principal: Option<crate::auth::Principal>,
9208 ) -> crate::txn::Transaction<'a> {
9209 let catalog_bound = principal.as_ref().is_some_and(|principal| {
9210 let catalog = self.catalog.read();
9211 catalog.require_auth || principal.user_id != 0
9212 });
9213 let txn_id = self.alloc_txn_id();
9214 let read = Snapshot::at(self.epoch.visible());
9215 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
9216 .with_external_trigger_bridge(bridge)
9217 .with_principal(principal, catalog_bound)
9218 }
9219
9220 pub fn transaction<T>(
9222 &self,
9223 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9224 ) -> Result<T> {
9225 let mut tx = self.begin();
9226 match f(&mut tx) {
9227 Ok(out) => {
9228 tx.commit()?;
9229 Ok(out)
9230 }
9231 Err(e) => {
9232 tx.rollback();
9233 Err(e)
9234 }
9235 }
9236 }
9237
9238 pub fn transaction_with_row_ids<T>(
9239 &self,
9240 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9241 ) -> Result<(T, Vec<RowId>)> {
9242 let mut tx = self.begin();
9243 match f(&mut tx) {
9244 Ok(output) => {
9245 let (_, row_ids) = tx.commit_with_row_ids()?;
9246 Ok((output, row_ids))
9247 }
9248 Err(error) => {
9249 tx.rollback();
9250 Err(error)
9251 }
9252 }
9253 }
9254
9255 pub fn transaction_for_current_principal<T>(
9256 &self,
9257 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9258 ) -> Result<T> {
9259 if self.principal.read().is_some() {
9260 self.refresh_principal()?;
9261 }
9262 let mut transaction = self.begin_as(self.principal.read().clone());
9263 match f(&mut transaction) {
9264 Ok(output) => {
9265 transaction.commit()?;
9266 Ok(output)
9267 }
9268 Err(error) => {
9269 transaction.rollback();
9270 Err(error)
9271 }
9272 }
9273 }
9274
9275 pub fn transaction_for_current_principal_with_epoch<T>(
9276 &self,
9277 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9278 ) -> Result<(Epoch, T)> {
9279 if self.principal.read().is_some() {
9280 self.refresh_principal()?;
9281 }
9282 let mut transaction = self.begin_as(self.principal.read().clone());
9283 match f(&mut transaction) {
9284 Ok(output) => {
9285 let epoch = transaction.commit()?;
9286 Ok((epoch, output))
9287 }
9288 Err(error) => {
9289 transaction.rollback();
9290 Err(error)
9291 }
9292 }
9293 }
9294
9295 pub fn transaction_with_row_ids_for_current_principal<T>(
9296 &self,
9297 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9298 ) -> Result<(T, Vec<RowId>)> {
9299 if self.principal.read().is_some() {
9300 self.refresh_principal()?;
9301 }
9302 let mut transaction = self.begin_as(self.principal.read().clone());
9303 match f(&mut transaction) {
9304 Ok(output) => {
9305 let (_, row_ids) = transaction.commit_with_row_ids()?;
9306 Ok((output, row_ids))
9307 }
9308 Err(error) => {
9309 transaction.rollback();
9310 Err(error)
9311 }
9312 }
9313 }
9314
9315 pub fn transaction_with_external_trigger_bridge<'a, T>(
9318 &'a self,
9319 bridge: &'a dyn ExternalTriggerBridge,
9320 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9321 ) -> Result<T> {
9322 let mut tx = self.begin_with_external_trigger_bridge(bridge);
9323 match f(&mut tx) {
9324 Ok(out) => {
9325 tx.commit()?;
9326 Ok(out)
9327 }
9328 Err(e) => {
9329 tx.rollback();
9330 Err(e)
9331 }
9332 }
9333 }
9334
9335 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
9336 &'a self,
9337 bridge: &'a dyn ExternalTriggerBridge,
9338 principal: Option<crate::auth::Principal>,
9339 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9340 ) -> Result<T> {
9341 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
9342 match f(&mut tx) {
9343 Ok(output) => {
9344 tx.commit()?;
9345 Ok(output)
9346 }
9347 Err(error) => {
9348 tx.rollback();
9349 Err(error)
9350 }
9351 }
9352 }
9353
9354 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
9357 self.active_txns.register(epoch)
9358 }
9359
9360 fn fill_auto_increment_for_staging(
9361 &self,
9362 txn_id: u64,
9363 staging: &mut [(u64, crate::txn::Staged)],
9364 control: Option<&crate::ExecutionControl>,
9365 ) -> Result<()> {
9366 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9367 for (index, (table_id, staged)) in staging.iter().enumerate() {
9368 commit_prepare_checkpoint(control, index)?;
9369 if matches!(staged, crate::txn::Staged::Put(_)) {
9370 puts_by_table.entry(*table_id).or_default().push(index);
9371 }
9372 }
9373
9374 {
9381 let mut barrier_tables: Vec<u64> = puts_by_table
9382 .iter()
9383 .filter(|(table_id, indexes)| {
9384 indexes.iter().any(|index| {
9385 matches!(
9386 &staging[*index].1,
9387 crate::txn::Staged::Put(cells)
9388 if self.table_auto_inc_would_allocate(**table_id, cells)
9389 )
9390 })
9391 })
9392 .map(|(table_id, _)| *table_id)
9393 .collect();
9394 barrier_tables.sort_unstable();
9395 for table_id in barrier_tables {
9396 self.acquire_txn_lock(
9397 txn_id,
9398 crate::locks::LockKey::sequence_barrier(&format!("auto_inc:{table_id}")),
9399 crate::locks::LockMode::Exclusive,
9400 control,
9401 )?;
9402 }
9403 }
9404
9405 let tables = self.tables.read();
9406 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
9407 commit_prepare_checkpoint(control, table_index)?;
9408 if let Some(handle) = tables.get(&table_id) {
9409 #[cfg(test)]
9410 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9411 let mut t = handle.lock();
9412 for (fill_index, index) in indexes.into_iter().enumerate() {
9413 commit_prepare_checkpoint(control, fill_index)?;
9414 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
9415 t.fill_auto_inc(cells)?;
9416 }
9417 }
9418 }
9419 }
9420 Ok(())
9421 }
9422
9423 fn expand_table_triggers(
9424 &self,
9425 txn_id: u64,
9426 staging: &mut Vec<(u64, crate::txn::Staged)>,
9427 read_epoch: Epoch,
9428 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9429 external_states: &mut Vec<(String, Vec<u8>)>,
9430 control: Option<&crate::ExecutionControl>,
9431 ) -> Result<()> {
9432 commit_prepare_checkpoint(control, 0)?;
9433 let mut external_writes = Vec::new();
9434 let config = self.trigger_config();
9435 if config.recursive_triggers {
9436 let chunk = std::mem::take(staging);
9437 let stacks = vec![Vec::new(); chunk.len()];
9438 *staging = self.expand_trigger_chunk(
9439 txn_id,
9440 chunk,
9441 stacks,
9442 read_epoch,
9443 0,
9444 config.max_depth,
9445 &mut external_writes,
9446 &config,
9447 control,
9448 )?;
9449 self.apply_external_trigger_writes(
9450 external_writes,
9451 external_trigger_bridge,
9452 external_states,
9453 staging,
9454 control,
9455 )?;
9456 return Ok(());
9457 }
9458
9459 let mut expansion =
9460 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
9461 if !expansion.before.is_empty() {
9462 let mut final_staging = expansion.before;
9463 final_staging.extend(filter_ignored_staging(
9464 std::mem::take(staging),
9465 &expansion.ignored_indices,
9466 ));
9467 *staging = final_staging;
9468 } else if !expansion.ignored_indices.is_empty() {
9469 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
9470 }
9471 staging.append(&mut expansion.after);
9472 external_writes.append(&mut expansion.before_external);
9473 external_writes.append(&mut expansion.after_external);
9474 self.apply_external_trigger_writes(
9475 external_writes,
9476 external_trigger_bridge,
9477 external_states,
9478 staging,
9479 control,
9480 )?;
9481 Ok(())
9482 }
9483
9484 #[allow(clippy::too_many_arguments)]
9485 fn expand_trigger_chunk(
9486 &self,
9487 txn_id: u64,
9488 mut chunk: Vec<(u64, crate::txn::Staged)>,
9489 stacks: Vec<Vec<String>>,
9490 read_epoch: Epoch,
9491 depth: u32,
9492 max_depth: u32,
9493 external_writes: &mut Vec<ExternalTriggerWrite>,
9494 config: &TriggerConfig,
9495 control: Option<&crate::ExecutionControl>,
9496 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
9497 if chunk.is_empty() {
9498 return Ok(Vec::new());
9499 }
9500 commit_prepare_checkpoint(control, 0)?;
9501 self.fill_auto_increment_for_staging(txn_id, &mut chunk, control)?;
9502 let expansion = self.expand_table_triggers_once(
9503 &mut chunk,
9504 read_epoch,
9505 Some(&stacks),
9506 config,
9507 control,
9508 )?;
9509 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
9510 let stack = expansion
9511 .before_stacks
9512 .first()
9513 .or_else(|| expansion.after_stacks.first())
9514 .cloned()
9515 .unwrap_or_default();
9516 return Err(MongrelError::TriggerValidation(format!(
9517 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
9518 Self::format_trigger_stack(&stack)
9519 )));
9520 }
9521
9522 let mut out = Vec::new();
9523 external_writes.extend(expansion.before_external);
9524 out.extend(self.expand_trigger_chunk(
9525 txn_id,
9526 expansion.before,
9527 expansion.before_stacks,
9528 read_epoch,
9529 depth + 1,
9530 max_depth,
9531 external_writes,
9532 config,
9533 control,
9534 )?);
9535 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
9536 external_writes.extend(expansion.after_external);
9537 out.extend(self.expand_trigger_chunk(
9538 txn_id,
9539 expansion.after,
9540 expansion.after_stacks,
9541 read_epoch,
9542 depth + 1,
9543 max_depth,
9544 external_writes,
9545 config,
9546 control,
9547 )?);
9548 Ok(out)
9549 }
9550
9551 fn apply_external_trigger_writes(
9552 &self,
9553 writes: Vec<ExternalTriggerWrite>,
9554 bridge: Option<&dyn ExternalTriggerBridge>,
9555 external_states: &mut Vec<(String, Vec<u8>)>,
9556 staging: &mut Vec<(u64, crate::txn::Staged)>,
9557 control: Option<&crate::ExecutionControl>,
9558 ) -> Result<()> {
9559 if writes.is_empty() {
9560 return Ok(());
9561 }
9562 let bridge = bridge.ok_or_else(|| {
9563 MongrelError::TriggerValidation(
9564 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
9565 )
9566 })?;
9567 for (write_index, write) in writes.into_iter().enumerate() {
9568 commit_prepare_checkpoint(control, write_index)?;
9569 let table = write.table().to_string();
9570 let entry = self.external_table(&table).ok_or_else(|| {
9571 MongrelError::NotFound(format!("external table {table:?} not found"))
9572 })?;
9573 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
9574 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
9575 external_states.push((table, result.state));
9576 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
9577 commit_prepare_checkpoint(control, base_index)?;
9578 match base_write {
9579 ExternalTriggerBaseWrite::Put { table, cells } => {
9580 let table_id = self.table_id(&table)?;
9581 staging.push((table_id, crate::txn::Staged::Put(cells)));
9582 }
9583 ExternalTriggerBaseWrite::Delete { table, row_id } => {
9584 let table_id = self.table_id(&table)?;
9585 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
9586 }
9587 }
9588 }
9589 }
9590 dedup_external_states_in_place(external_states);
9591 Ok(())
9592 }
9593
9594 fn expand_table_triggers_once(
9595 &self,
9596 staging: &mut Vec<(u64, crate::txn::Staged)>,
9597 read_epoch: Epoch,
9598 trigger_stacks: Option<&[Vec<String>]>,
9599 config: &TriggerConfig,
9600 control: Option<&crate::ExecutionControl>,
9601 ) -> Result<TriggerExpansion> {
9602 commit_prepare_checkpoint(control, 0)?;
9603 let triggers: Vec<StoredTrigger> = self
9604 .catalog
9605 .read()
9606 .triggers
9607 .iter()
9608 .filter(|entry| {
9609 entry.trigger.enabled
9610 && matches!(
9611 entry.trigger.timing,
9612 TriggerTiming::Before | TriggerTiming::After
9613 )
9614 && matches!(entry.trigger.target, TriggerTarget::Table(_))
9615 })
9616 .map(|entry| entry.trigger.clone())
9617 .collect();
9618 if triggers.is_empty() || staging.is_empty() {
9619 return Ok(TriggerExpansion::default());
9620 }
9621
9622 let before_triggers = triggers
9623 .iter()
9624 .filter(|trigger| trigger.timing == TriggerTiming::Before)
9625 .cloned()
9626 .collect::<Vec<_>>();
9627 let after_triggers = triggers
9628 .iter()
9629 .filter(|trigger| trigger.timing == TriggerTiming::After)
9630 .cloned()
9631 .collect::<Vec<_>>();
9632
9633 let mut before_added = Vec::new();
9634 let mut before_stacks = Vec::new();
9635 let mut before_external = Vec::new();
9636 let mut ignored_indices = std::collections::BTreeSet::new();
9637 if !before_triggers.is_empty() {
9638 let before_events =
9639 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
9640 let mut out = TriggerProgramOutput {
9641 added: &mut before_added,
9642 added_stacks: &mut before_stacks,
9643 added_external: &mut before_external,
9644 ignored_indices: &mut ignored_indices,
9645 };
9646 self.execute_triggers_for_events(
9647 &before_triggers,
9648 &before_events,
9649 Some(staging),
9650 &mut out,
9651 config,
9652 read_epoch,
9653 control,
9654 )?;
9655 }
9656
9657 let after_events = if after_triggers.is_empty() {
9658 Vec::new()
9659 } else {
9660 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
9661 .into_iter()
9662 .filter(|event| {
9663 !event
9664 .op_indices
9665 .iter()
9666 .any(|idx| ignored_indices.contains(idx))
9667 })
9668 .collect()
9669 };
9670
9671 let mut after_added = Vec::new();
9672 let mut after_stacks = Vec::new();
9673 let mut after_external = Vec::new();
9674 let mut out = TriggerProgramOutput {
9675 added: &mut after_added,
9676 added_stacks: &mut after_stacks,
9677 added_external: &mut after_external,
9678 ignored_indices: &mut ignored_indices,
9679 };
9680 self.execute_triggers_for_events(
9681 &after_triggers,
9682 &after_events,
9683 None,
9684 &mut out,
9685 config,
9686 read_epoch,
9687 control,
9688 )?;
9689 Ok(TriggerExpansion {
9690 before: before_added,
9691 before_stacks,
9692 before_external,
9693 after: after_added,
9694 after_stacks,
9695 after_external,
9696 ignored_indices,
9697 })
9698 }
9699
9700 #[allow(clippy::too_many_arguments)]
9701 fn execute_triggers_for_events(
9702 &self,
9703 triggers: &[StoredTrigger],
9704 events: &[WriteEvent],
9705 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
9706 out: &mut TriggerProgramOutput<'_>,
9707 config: &TriggerConfig,
9708 read_epoch: Epoch,
9709 control: Option<&crate::ExecutionControl>,
9710 ) -> Result<()> {
9711 let mut checkpoint_index = 0_usize;
9712 for event in events {
9713 for trigger in triggers {
9714 commit_prepare_checkpoint(control, checkpoint_index)?;
9715 checkpoint_index += 1;
9716 if event
9717 .op_indices
9718 .iter()
9719 .any(|idx| out.ignored_indices.contains(idx))
9720 {
9721 break;
9722 }
9723 let matches = {
9724 let cat = self.catalog.read();
9725 trigger_matches_event(trigger, event, &cat)?
9726 };
9727 if !matches {
9728 continue;
9729 }
9730 if let Some(when) = &trigger.when {
9731 if !eval_trigger_expr(when, event)? {
9732 continue;
9733 }
9734 }
9735 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
9736 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
9737 return Err(MongrelError::TriggerValidation(format!(
9738 "trigger recursion cycle detected; trigger stack: {}",
9739 Self::format_trigger_stack(&trigger_stack)
9740 )));
9741 }
9742 let outcome = match staging.as_mut() {
9743 Some(staging) => self.execute_trigger_program(
9744 trigger,
9745 event,
9746 Some(&mut **staging),
9747 out,
9748 &trigger_stack,
9749 config,
9750 read_epoch,
9751 control,
9752 )?,
9753 None => self.execute_trigger_program(
9754 trigger,
9755 event,
9756 None,
9757 out,
9758 &trigger_stack,
9759 config,
9760 read_epoch,
9761 control,
9762 )?,
9763 };
9764 if outcome == TriggerProgramOutcome::Ignore {
9765 out.ignored_indices.extend(event.op_indices.iter().copied());
9766 break;
9767 }
9768 }
9769 }
9770 Ok(())
9771 }
9772
9773 fn trigger_events_for_staging(
9774 &self,
9775 staging: &[(u64, crate::txn::Staged)],
9776 read_epoch: Epoch,
9777 trigger_stacks: Option<&[Vec<String>]>,
9778 control: Option<&crate::ExecutionControl>,
9779 ) -> Result<Vec<WriteEvent>> {
9780 use crate::txn::Staged;
9781 use std::collections::{HashMap, VecDeque};
9782
9783 let snapshot = Snapshot::at(read_epoch);
9784 let cat = self.catalog.read();
9785 let mut table_names = HashMap::new();
9786 let mut table_schemas = HashMap::new();
9787 for entry in cat
9788 .tables
9789 .iter()
9790 .filter(|entry| matches!(entry.state, TableState::Live))
9791 {
9792 table_names.insert(entry.table_id, entry.name.clone());
9793 table_schemas.insert(entry.table_id, entry.schema.clone());
9794 }
9795 drop(cat);
9796
9797 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
9798 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
9799 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
9800
9801 for (idx, (table_id, staged)) in staging.iter().enumerate() {
9802 commit_prepare_checkpoint(control, idx)?;
9803 let Some(schema) = table_schemas.get(table_id) else {
9804 continue;
9805 };
9806 let Some(pk) = schema.primary_key() else {
9807 continue;
9808 };
9809 match staged {
9810 Staged::Delete(row_id) => {
9811 let handle = self.table_by_id(*table_id)?;
9812 let Some(row) = handle.lock().get(*row_id, snapshot) else {
9813 continue;
9814 };
9815 let Some(pk_value) = row.columns.get(&pk.id) else {
9816 continue;
9817 };
9818 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
9819 delete_by_key
9820 .entry((*table_id, pk_value.encode_key()))
9821 .or_default()
9822 .push_back(idx);
9823 }
9824 Staged::Put(cells) => {
9825 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
9826 put_by_key
9827 .entry((*table_id, value.encode_key()))
9828 .or_default()
9829 .push_back(idx);
9830 }
9831 }
9832 Staged::Update { row_id, .. } => {
9833 let handle = self.table_by_id(*table_id)?;
9834 let row = handle.lock().get(*row_id, snapshot);
9835 if let Some(row) = row {
9836 old_rows.insert(idx, TriggerRowImage::from_row(row));
9837 }
9838 }
9839 Staged::Truncate => {}
9840 }
9841 }
9842
9843 let mut paired_delete = std::collections::HashSet::new();
9844 let mut paired_put = std::collections::HashSet::new();
9845 let mut events = Vec::new();
9846
9847 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
9848 commit_prepare_checkpoint(control, pair_index)?;
9849 let Some(puts) = put_by_key.get_mut(key) else {
9850 continue;
9851 };
9852 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
9853 paired_delete.insert(delete_idx);
9854 paired_put.insert(put_idx);
9855 let (table_id, _) = &staging[put_idx];
9856 let Some(table_name) = table_names.get(table_id).cloned() else {
9857 continue;
9858 };
9859 let old = old_rows.get(&delete_idx).cloned();
9860 let new = match &staging[put_idx].1 {
9861 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
9862 _ => None,
9863 };
9864 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
9865 events.push(WriteEvent {
9866 table: table_name,
9867 kind: TriggerEvent::Update,
9868 old,
9869 new,
9870 changed_columns,
9871 op_indices: vec![delete_idx, put_idx],
9872 put_idx: Some(put_idx),
9873 trigger_stack: Self::trigger_stack_for_indices(
9874 trigger_stacks,
9875 &[delete_idx, put_idx],
9876 ),
9877 });
9878 }
9879 }
9880
9881 for (idx, (table_id, staged)) in staging.iter().enumerate() {
9882 commit_prepare_checkpoint(control, idx)?;
9883 let Some(table_name) = table_names.get(table_id).cloned() else {
9884 continue;
9885 };
9886 match staged {
9887 Staged::Put(cells) if !paired_put.contains(&idx) => {
9888 let new = Some(TriggerRowImage::from_cells(cells));
9889 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
9890 events.push(WriteEvent {
9891 table: table_name,
9892 kind: TriggerEvent::Insert,
9893 old: None,
9894 new,
9895 changed_columns,
9896 op_indices: vec![idx],
9897 put_idx: Some(idx),
9898 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
9899 });
9900 }
9901 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
9902 let old = match old_rows.get(&idx).cloned() {
9903 Some(old) => Some(old),
9904 None => {
9905 let handle = self.table_by_id(*table_id)?;
9906 let row = handle.lock().get(*row_id, snapshot);
9907 row.map(TriggerRowImage::from_row)
9908 }
9909 };
9910 let Some(old) = old else {
9911 continue;
9912 };
9913 let changed_columns = old.columns.keys().copied().collect();
9914 events.push(WriteEvent {
9915 table: table_name,
9916 kind: TriggerEvent::Delete,
9917 old: Some(old),
9918 new: None,
9919 changed_columns,
9920 op_indices: vec![idx],
9921 put_idx: None,
9922 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
9923 });
9924 }
9925 Staged::Update { new_row: cells, .. } => {
9926 let old = old_rows.get(&idx).cloned();
9927 let new = Some(TriggerRowImage::from_cells(cells));
9928 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
9929 events.push(WriteEvent {
9930 table: table_name,
9931 kind: TriggerEvent::Update,
9932 old,
9933 new,
9934 changed_columns,
9935 op_indices: vec![idx],
9936 put_idx: Some(idx),
9937 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
9938 });
9939 }
9940 Staged::Truncate => {}
9941 _ => {}
9942 }
9943 }
9944
9945 Ok(events)
9946 }
9947
9948 #[allow(clippy::too_many_arguments)]
9949 fn execute_trigger_program(
9950 &self,
9951 trigger: &StoredTrigger,
9952 event: &WriteEvent,
9953 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
9954 out: &mut TriggerProgramOutput<'_>,
9955 trigger_stack: &[String],
9956 config: &TriggerConfig,
9957 read_epoch: Epoch,
9958 control: Option<&crate::ExecutionControl>,
9959 ) -> Result<TriggerProgramOutcome> {
9960 let mut event = event.clone();
9961 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
9962 self.execute_trigger_steps(
9963 trigger,
9964 &trigger.program.steps,
9965 &mut event,
9966 staging,
9967 out,
9968 trigger_stack,
9969 config,
9970 &mut select_results,
9971 0,
9972 None,
9973 read_epoch,
9974 control,
9975 )
9976 }
9977
9978 #[allow(clippy::too_many_arguments)]
9979 fn execute_trigger_steps(
9980 &self,
9981 trigger: &StoredTrigger,
9982 steps: &[TriggerStep],
9983 event: &mut WriteEvent,
9984 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
9985 out: &mut TriggerProgramOutput<'_>,
9986 trigger_stack: &[String],
9987 config: &TriggerConfig,
9988 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
9989 depth: u32,
9990 selected: Option<&TriggerRowImage>,
9991 read_epoch: Epoch,
9992 control: Option<&crate::ExecutionControl>,
9993 ) -> Result<TriggerProgramOutcome> {
9994 let _ = depth;
9995 for (step_index, step) in steps.iter().enumerate() {
9996 commit_prepare_checkpoint(control, step_index)?;
9997 match step {
9998 TriggerStep::SetNew { cells } => {
9999 if trigger.timing != TriggerTiming::Before {
10000 return Err(MongrelError::InvalidArgument(
10001 "SetNew trigger step is only valid in BEFORE triggers".into(),
10002 ));
10003 }
10004 let put_idx = event.put_idx.ok_or_else(|| {
10005 MongrelError::InvalidArgument(
10006 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
10007 )
10008 })?;
10009 let staging = staging.as_deref_mut().ok_or_else(|| {
10010 MongrelError::InvalidArgument(
10011 "SetNew trigger step requires mutable trigger staging".into(),
10012 )
10013 })?;
10014 let mut update_changed_columns = None;
10015 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
10016 Some(crate::txn::Staged::Put(cells)) => cells,
10017 Some(crate::txn::Staged::Update {
10018 new_row,
10019 changed_columns,
10020 ..
10021 }) => {
10022 update_changed_columns = Some(changed_columns);
10023 new_row
10024 }
10025 _ => {
10026 return Err(MongrelError::InvalidArgument(
10027 "SetNew trigger step target row is not mutable".into(),
10028 ))
10029 }
10030 };
10031 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
10032 row_cells.retain(|(id, _)| *id != column_id);
10033 row_cells.push((column_id, value.clone()));
10034 if let Some(changed_columns) = &mut update_changed_columns {
10035 changed_columns.push(column_id);
10036 }
10037 if let Some(new) = &mut event.new {
10038 new.columns.insert(column_id, value);
10039 }
10040 }
10041 row_cells.sort_by_key(|(id, _)| *id);
10042 if let Some(changed_columns) = update_changed_columns {
10043 changed_columns.sort_unstable();
10044 changed_columns.dedup();
10045 }
10046 }
10047 TriggerStep::Insert { table, cells } => {
10048 let cells = eval_trigger_cells(cells, event, selected)?;
10049 if let Ok(table_id) = self.table_id(table) {
10050 out.added.push((table_id, crate::txn::Staged::Put(cells)));
10051 out.added_stacks.push(trigger_stack.to_vec());
10052 } else if self.external_table(table).is_some() {
10053 out.added_external.push(ExternalTriggerWrite::Insert {
10054 table: table.clone(),
10055 cells,
10056 });
10057 } else {
10058 return Err(MongrelError::NotFound(format!(
10059 "trigger {:?} insert target {table:?} not found",
10060 trigger.name
10061 )));
10062 }
10063 }
10064 TriggerStep::UpdateByPk { table, pk, cells } => {
10065 let pk = eval_trigger_value(pk, event, selected)?;
10066 let cells = eval_trigger_cells(cells, event, selected)?;
10067 if self.external_table(table).is_some() {
10068 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
10069 table: table.clone(),
10070 pk,
10071 cells,
10072 });
10073 } else {
10074 let row_id = self
10075 .table(table)?
10076 .lock()
10077 .lookup_pk(&pk.encode_key())
10078 .ok_or_else(|| {
10079 MongrelError::NotFound(format!(
10080 "trigger {:?} update target not found",
10081 trigger.name
10082 ))
10083 })?;
10084 let handle = self.table(table)?;
10085 let snapshot = Snapshot::at(self.epoch.visible());
10086 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
10087 MongrelError::NotFound(format!(
10088 "trigger {:?} update target not visible",
10089 trigger.name
10090 ))
10091 })?;
10092 let mut changed_columns = cells
10093 .iter()
10094 .map(|(column_id, _)| *column_id)
10095 .collect::<Vec<_>>();
10096 changed_columns.sort_unstable();
10097 changed_columns.dedup();
10098 let mut merged = old.columns;
10099 for (column_id, value) in cells {
10100 merged.insert(column_id, value);
10101 }
10102 out.added.push((
10103 self.table_id(table)?,
10104 crate::txn::Staged::Update {
10105 row_id,
10106 new_row: merged.into_iter().collect(),
10107 changed_columns,
10108 },
10109 ));
10110 out.added_stacks.push(trigger_stack.to_vec());
10111 }
10112 }
10113 TriggerStep::DeleteByPk { table, pk } => {
10114 let pk = eval_trigger_value(pk, event, selected)?;
10115 if self.external_table(table).is_some() {
10116 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
10117 table: table.clone(),
10118 pk,
10119 });
10120 } else {
10121 let row_id = self
10122 .table(table)?
10123 .lock()
10124 .lookup_pk(&pk.encode_key())
10125 .ok_or_else(|| {
10126 MongrelError::NotFound(format!(
10127 "trigger {:?} delete target not found",
10128 trigger.name
10129 ))
10130 })?;
10131 out.added
10132 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
10133 out.added_stacks.push(trigger_stack.to_vec());
10134 }
10135 }
10136 TriggerStep::Select {
10137 id,
10138 table,
10139 conditions,
10140 } => {
10141 let schema = self.table(table)?.lock().schema().clone();
10142 let snapshot = Snapshot::at(read_epoch);
10143 let handle = self.table(table)?;
10144 let rows = match control {
10145 Some(control) => {
10146 handle.lock().visible_rows_controlled(snapshot, control)?
10147 }
10148 None => handle.lock().visible_rows(snapshot)?,
10149 };
10150 let mut matched = Vec::new();
10151 for (row_index, row) in rows.into_iter().enumerate() {
10152 commit_prepare_checkpoint(control, row_index)?;
10153 let image = TriggerRowImage::from_row(row);
10154 let passes = conditions
10155 .iter()
10156 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
10157 .collect::<Result<Vec<_>>>()?
10158 .into_iter()
10159 .all(|b| b);
10160 if passes {
10161 matched.push(image);
10162 }
10163 }
10164 if let Some(pk) = schema.primary_key() {
10165 matched.sort_by(|a, b| {
10166 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
10167 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
10168 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
10169 });
10170 }
10171 select_results.insert(id.clone(), matched);
10172 }
10173 TriggerStep::Foreach { id, steps } => {
10174 let rows = select_results.get(id).ok_or_else(|| {
10175 MongrelError::InvalidArgument(format!(
10176 "trigger {:?} foreach references unknown select id {id:?}",
10177 trigger.name
10178 ))
10179 })?;
10180 if rows.len() > config.max_loop_iterations as usize {
10181 return Err(MongrelError::InvalidArgument(format!(
10182 "trigger {:?} foreach exceeded max_loop_iterations ({})",
10183 trigger.name, config.max_loop_iterations
10184 )));
10185 }
10186 for (row_index, row) in rows.clone().into_iter().enumerate() {
10187 commit_prepare_checkpoint(control, row_index)?;
10188 let result = self.execute_trigger_steps(
10189 trigger,
10190 steps,
10191 event,
10192 staging.as_deref_mut(),
10193 out,
10194 trigger_stack,
10195 config,
10196 select_results,
10197 depth + 1,
10198 Some(&row),
10199 read_epoch,
10200 control,
10201 )?;
10202 if result == TriggerProgramOutcome::Ignore {
10203 return Ok(TriggerProgramOutcome::Ignore);
10204 }
10205 }
10206 }
10207 TriggerStep::DeleteWhere { table, conditions } => {
10208 let schema = self.table(table)?.lock().schema().clone();
10209 let snapshot = Snapshot::at(read_epoch);
10210 let handle = self.table(table)?;
10211 let rows = match control {
10212 Some(control) => {
10213 handle.lock().visible_rows_controlled(snapshot, control)?
10214 }
10215 None => handle.lock().visible_rows(snapshot)?,
10216 };
10217 let table_id = self.table_id(table)?;
10218 let mut to_delete = Vec::new();
10219 for (row_index, row) in rows.into_iter().enumerate() {
10220 commit_prepare_checkpoint(control, row_index)?;
10221 let image = TriggerRowImage::from_row(row.clone());
10222 let passes = conditions
10223 .iter()
10224 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
10225 .collect::<Result<Vec<_>>>()?
10226 .into_iter()
10227 .all(|b| b);
10228 if passes {
10229 to_delete.push((table_id, row.row_id));
10230 }
10231 }
10232 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
10233 commit_prepare_checkpoint(control, row_index)?;
10234 out.added
10235 .push((table_id, crate::txn::Staged::Delete(row_id)));
10236 out.added_stacks.push(trigger_stack.to_vec());
10237 }
10238 }
10239 TriggerStep::UpdateWhere {
10240 table,
10241 conditions,
10242 cells,
10243 } => {
10244 let schema = self.table(table)?.lock().schema().clone();
10245 let snapshot = Snapshot::at(read_epoch);
10246 let handle = self.table(table)?;
10247 let rows = match control {
10248 Some(control) => {
10249 handle.lock().visible_rows_controlled(snapshot, control)?
10250 }
10251 None => handle.lock().visible_rows(snapshot)?,
10252 };
10253 let table_id = self.table_id(table)?;
10254 let mut changed_columns =
10255 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
10256 changed_columns.sort_unstable();
10257 changed_columns.dedup();
10258 let mut to_update = Vec::new();
10259 for (row_index, row) in rows.into_iter().enumerate() {
10260 commit_prepare_checkpoint(control, row_index)?;
10261 let image = TriggerRowImage::from_row(row.clone());
10262 let passes = conditions
10263 .iter()
10264 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
10265 .collect::<Result<Vec<_>>>()?
10266 .into_iter()
10267 .all(|b| b);
10268 if passes {
10269 let new_cells = cells
10270 .iter()
10271 .map(|cell| {
10272 Ok((
10273 cell.column_id,
10274 eval_trigger_value(&cell.value, event, Some(&image))?,
10275 ))
10276 })
10277 .collect::<Result<Vec<_>>>()?;
10278 let mut merged = row.columns.clone();
10279 for (column_id, value) in new_cells {
10280 merged.insert(column_id, value);
10281 }
10282 to_update.push((table_id, row.row_id, merged));
10283 }
10284 }
10285 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
10286 {
10287 commit_prepare_checkpoint(control, row_index)?;
10288 out.added.push((
10289 table_id,
10290 crate::txn::Staged::Update {
10291 row_id,
10292 new_row: merged.into_iter().collect(),
10293 changed_columns: changed_columns.clone(),
10294 },
10295 ));
10296 out.added_stacks.push(trigger_stack.to_vec());
10297 }
10298 }
10299 TriggerStep::Raise { action, message } => match action {
10300 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
10301 TriggerRaiseAction::Abort
10302 | TriggerRaiseAction::Fail
10303 | TriggerRaiseAction::Rollback => {
10304 let message = eval_trigger_value(message, event, selected)?;
10305 return Err(MongrelError::TriggerValidation(format!(
10306 "trigger {:?} raised: {}; trigger stack: {}",
10307 trigger.name,
10308 trigger_message(message),
10309 Self::format_trigger_stack(trigger_stack)
10310 )));
10311 }
10312 },
10313 }
10314 }
10315 Ok(TriggerProgramOutcome::Continue)
10316 }
10317
10318 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
10319 let Some(stacks) = stacks else {
10320 return Vec::new();
10321 };
10322 let mut out = Vec::new();
10323 for idx in indices {
10324 let Some(stack) = stacks.get(*idx) else {
10325 continue;
10326 };
10327 for name in stack {
10328 if !out.iter().any(|existing| existing == name) {
10329 out.push(name.clone());
10330 }
10331 }
10332 }
10333 out
10334 }
10335
10336 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
10337 let mut out = stack.to_vec();
10338 out.push(trigger_name.to_string());
10339 out
10340 }
10341
10342 fn format_trigger_stack(stack: &[String]) -> String {
10343 if stack.is_empty() {
10344 "<root>".into()
10345 } else {
10346 stack.join(" -> ")
10347 }
10348 }
10349
10350 fn acquire_unique_key_claims(
10371 &self,
10372 txn_id: u64,
10373 staging: &[(u64, crate::txn::Staged)],
10374 control: Option<&crate::ExecutionControl>,
10375 ) -> Result<()> {
10376 let catalog = self.catalog.read();
10377 let has_uniques = staging.iter().any(|(table_id, staged)| {
10378 matches!(
10379 staged,
10380 crate::txn::Staged::Put(_) | crate::txn::Staged::Update { .. }
10381 ) && catalog.tables.iter().any(|entry| {
10382 entry.table_id == *table_id
10383 && (entry.schema.primary_key().is_some()
10384 || !entry.schema.constraints.uniques.is_empty())
10385 })
10386 });
10387 if !has_uniques {
10388 return Ok(());
10389 }
10390 let mut claims: Vec<(u64, Vec<u8>)> = Vec::new();
10391 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
10392 commit_prepare_checkpoint(control, staged_index)?;
10393 let cells = match staged {
10394 crate::txn::Staged::Put(cells) => cells,
10395 crate::txn::Staged::Update { new_row, .. } => new_row,
10396 _ => continue,
10397 };
10398 let Some(entry) = catalog
10399 .tables
10400 .iter()
10401 .find(|entry| entry.table_id == *table_id)
10402 else {
10403 continue;
10404 };
10405 for column in &entry.schema.columns {
10406 if !column
10407 .flags
10408 .contains(crate::schema::ColumnFlags::PRIMARY_KEY)
10409 {
10410 continue;
10411 }
10412 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == column.id) {
10413 let mut key = b"pk:".to_vec();
10414 key.extend_from_slice(&value.encode_key());
10415 claims.push((*table_id, key));
10416 }
10417 }
10418 let cells_map: HashMap<u16, Value> = cells.iter().cloned().collect();
10423 for uc in &entry.schema.constraints.uniques {
10424 if let Some(composite) =
10425 crate::constraint::encode_composite_key(&uc.columns, &cells_map)
10426 {
10427 let mut key = format!("uq{}:", uc.id).into_bytes();
10428 key.extend_from_slice(&composite);
10429 claims.push((*table_id, key));
10430 }
10431 }
10432 }
10433 claims.sort();
10434 claims.dedup();
10435 for (table_id, key) in claims {
10436 self.acquire_txn_lock(
10437 txn_id,
10438 crate::locks::LockKey::key(table_id, key),
10439 crate::locks::LockMode::Exclusive,
10440 control,
10441 )?;
10442 }
10443 Ok(())
10444 }
10445
10446 fn acquire_fk_lock(
10450 &self,
10451 txn_id: u64,
10452 table_id: u64,
10453 key: &[u8],
10454 mode: crate::locks::LockMode,
10455 control: Option<&crate::ExecutionControl>,
10456 ) -> Result<()> {
10457 let mut namespaced = b"fk:".to_vec();
10458 namespaced.extend_from_slice(key);
10459 self.acquire_txn_lock(
10460 txn_id,
10461 crate::locks::LockKey::key(table_id, namespaced),
10462 mode,
10463 control,
10464 )?;
10465 let hook = self.fk_lock_hook.lock().clone();
10468 if let Some(hook) = hook {
10469 hook();
10470 }
10471 Ok(())
10472 }
10473
10474 fn validate_constraints(
10475 &self,
10476 txn_id: u64,
10477 staging: &mut Vec<(u64, crate::txn::Staged)>,
10478 read_epoch: Epoch,
10479 control: Option<&crate::ExecutionControl>,
10480 ) -> Result<()> {
10481 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
10482 use crate::memtable::Row;
10483 use crate::txn::Staged;
10484 use std::collections::HashSet;
10485
10486 commit_prepare_checkpoint(control, 0)?;
10487 let snapshot = Snapshot::at(read_epoch);
10488 let cat = self.catalog.read();
10489
10490 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
10492 .tables
10493 .iter()
10494 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
10495 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
10496 .collect();
10497
10498 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
10500 if !any_constraints {
10501 return Ok(());
10502 }
10503
10504 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
10506 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
10507 if let Some(r) = rows_cache.get(&table_id) {
10508 return Ok(r.clone());
10509 }
10510 let handle = self.table_by_id(table_id)?;
10511 let rows = match control {
10512 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
10513 None => handle.lock().visible_rows(snapshot)?,
10514 };
10515 rows_cache.insert(table_id, rows.clone());
10516 Ok(rows)
10517 };
10518
10519 let mut processed_updates = HashSet::new();
10524 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
10525 let mut update_pass = 0_usize;
10526 loop {
10527 commit_prepare_checkpoint(control, update_pass)?;
10528 update_pass += 1;
10529 let updates: Vec<PendingUpdate> = staging
10530 .iter()
10531 .enumerate()
10532 .filter_map(|(index, (table_id, op))| match op {
10533 Staged::Update {
10534 row_id,
10535 new_row: cells,
10536 ..
10537 } if !processed_updates.contains(&index) => {
10538 Some((index, *table_id, *row_id, cells.clone()))
10539 }
10540 _ => None,
10541 })
10542 .collect();
10543 if updates.is_empty() {
10544 break;
10545 }
10546 let mut new_ops = Vec::new();
10547 for (update_index, (index, table_id, row_id, new_cells)) in
10548 updates.into_iter().enumerate()
10549 {
10550 commit_prepare_checkpoint(control, update_index)?;
10551 processed_updates.insert(index);
10552 let Some(tname) = live
10553 .iter()
10554 .find(|(id, _, _)| *id == table_id)
10555 .map(|(_, name, _)| *name)
10556 else {
10557 continue;
10558 };
10559 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
10560 continue;
10561 };
10562 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
10563 for (child_id, _child_name, child_schema) in &live {
10564 for fk in &child_schema.constraints.foreign_keys {
10565 if fk.ref_table != tname {
10566 continue;
10567 }
10568 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
10569 else {
10570 continue;
10571 };
10572 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
10573 == Some(old_key.as_slice())
10574 {
10575 continue;
10576 }
10577 if fk.on_update == FkAction::Restrict {
10578 continue;
10579 }
10580 self.acquire_fk_lock(
10585 txn_id,
10586 table_id,
10587 &old_key,
10588 crate::locks::LockMode::Exclusive,
10589 control,
10590 )?;
10591 let child_rows = load_rows(*child_id)?;
10592 for (child_index, child) in child_rows.into_iter().enumerate() {
10593 commit_prepare_checkpoint(control, child_index)?;
10594 if encode_composite_key(&fk.columns, &child.columns).as_deref()
10595 != Some(old_key.as_slice())
10596 {
10597 continue;
10598 }
10599 if staging.iter().any(|(id, op)| {
10600 *id == *child_id
10601 && matches!(op, Staged::Delete(id) if *id == child.row_id)
10602 }) {
10603 continue;
10604 }
10605 let mut cells: Vec<(u16, Value)> = child
10606 .columns
10607 .iter()
10608 .map(|(column_id, value)| (*column_id, value.clone()))
10609 .collect();
10610 for (child_column, parent_column) in
10611 fk.columns.iter().zip(&fk.ref_columns)
10612 {
10613 cells.retain(|(column_id, _)| column_id != child_column);
10614 let value = match fk.on_update {
10615 FkAction::Cascade => {
10616 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
10617 }
10618 FkAction::SetNull => Value::Null,
10619 FkAction::Restrict => {
10620 return Err(MongrelError::Other(
10621 "restricted foreign-key update reached cascade preparation"
10622 .into(),
10623 ));
10624 }
10625 };
10626 cells.push((*child_column, value));
10627 }
10628 cells.sort_by_key(|(column_id, _)| *column_id);
10629 if let Some(existing_index) = staging.iter().position(|(id, op)| {
10630 *id == *child_id
10631 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
10632 }) {
10633 if let Staged::Update {
10634 new_row: existing,
10635 changed_columns,
10636 ..
10637 } = &mut staging[existing_index].1 {
10638 changed_columns.extend(fk.columns.iter().copied());
10639 changed_columns.sort_unstable();
10640 changed_columns.dedup();
10641 if *existing != cells {
10642 *existing = cells;
10643 processed_updates.remove(&existing_index);
10644 }
10645 }
10646 } else {
10647 new_ops.push((
10648 *child_id,
10649 Staged::Update {
10650 row_id: child.row_id,
10651 new_row: cells,
10652 changed_columns: fk.columns.clone(),
10653 },
10654 ));
10655 }
10656 }
10657 }
10658 }
10659 }
10660 staging.extend(new_ops);
10661 }
10662
10663 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
10668 let mut cascade_pass = 0_usize;
10669 loop {
10670 commit_prepare_checkpoint(control, cascade_pass)?;
10671 cascade_pass += 1;
10672 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
10673 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
10674 .iter()
10675 .filter_map(|(t, op)| match op {
10676 Staged::Delete(rid) => Some((*t, *rid)),
10677 _ => None,
10678 })
10679 .collect();
10680 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
10681 commit_prepare_checkpoint(control, delete_index)?;
10682 if !cascaded.insert((table_id, rid.0)) {
10683 continue;
10684 }
10685 let Some(tname) = live
10686 .iter()
10687 .find(|(t, _, _)| *t == table_id)
10688 .map(|(_, n, _)| *n)
10689 else {
10690 continue;
10691 };
10692 let parent_handle = self.table_by_id(table_id)?;
10693 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
10694 continue;
10695 };
10696 for (child_id, _child_name, child_schema) in &live {
10697 for fk in &child_schema.constraints.foreign_keys {
10698 if fk.ref_table != tname {
10699 continue;
10700 }
10701 let Some(parent_key) =
10702 encode_composite_key(&fk.ref_columns, &parent_row.columns)
10703 else {
10704 continue;
10705 };
10706 let key_preserved = staging.iter().any(|(t, op)| {
10715 if *t != table_id {
10716 return false;
10717 }
10718 let Staged::Put(cells) = op else {
10719 return false;
10720 };
10721 let map: HashMap<u16, crate::memtable::Value> =
10722 cells.iter().cloned().collect();
10723 encode_composite_key(&fk.ref_columns, &map).as_deref()
10724 == Some(parent_key.as_slice())
10725 });
10726 if key_preserved {
10727 continue;
10728 }
10729 self.acquire_fk_lock(
10736 txn_id,
10737 table_id,
10738 &parent_key,
10739 crate::locks::LockMode::Exclusive,
10740 control,
10741 )?;
10742 match fk.on_delete {
10743 FkAction::Restrict => continue,
10744 FkAction::Cascade => {
10745 let child_rows = load_rows(*child_id)?;
10746 for (child_index, cr) in child_rows.iter().enumerate() {
10747 commit_prepare_checkpoint(control, child_index)?;
10748 if !cascaded.contains(&(*child_id, cr.row_id.0))
10749 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
10750 == Some(parent_key.as_slice())
10751 {
10752 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
10753 }
10754 }
10755 }
10756 FkAction::SetNull => {
10757 let child_rows = load_rows(*child_id)?;
10758 for (child_index, cr) in child_rows.iter().enumerate() {
10759 commit_prepare_checkpoint(control, child_index)?;
10760 if !cascaded.contains(&(*child_id, cr.row_id.0))
10761 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
10762 == Some(parent_key.as_slice())
10763 {
10764 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
10767 .columns
10768 .iter()
10769 .map(|(k, v)| (*k, v.clone()))
10770 .collect();
10771 for cid in &fk.columns {
10772 cells.retain(|(k, _)| k != cid);
10773 cells.push((*cid, crate::memtable::Value::Null));
10774 }
10775 new_ops.push((
10776 *child_id,
10777 Staged::Update {
10778 row_id: cr.row_id,
10779 new_row: cells,
10780 changed_columns: fk.columns.clone(),
10781 },
10782 ));
10783 }
10784 }
10785 }
10786 }
10787 }
10788 }
10789 }
10790 if new_ops.is_empty() {
10791 break;
10792 }
10793 staging.extend(new_ops);
10794 }
10795
10796 let staged_deletes: HashSet<(u64, u64)> = staging
10800 .iter()
10801 .filter_map(|(t, op)| match op {
10802 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
10803 _ => None,
10804 })
10805 .collect();
10806
10807 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
10809
10810 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
10812 commit_prepare_checkpoint(control, operation_index)?;
10813 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
10814 else {
10815 continue;
10816 };
10817 let cells_map: HashMap<u16, crate::memtable::Value>;
10818 match op {
10819 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
10820 cells_map = cells.iter().cloned().collect();
10821
10822 if !schema.constraints.checks.is_empty() {
10824 validate_checks(&schema.constraints.checks, &cells_map)?;
10825 }
10826
10827 for uc in &schema.constraints.uniques {
10829 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
10830 continue; };
10832 let marker = (*table_id, uc.id, key.clone());
10833 if !seen_unique.insert(marker) {
10834 return Err(MongrelError::Conflict(format!(
10835 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
10836 uc.name
10837 )));
10838 }
10839 let rows = load_rows(*table_id)?;
10840 for (row_index, r) in rows.iter().enumerate() {
10841 commit_prepare_checkpoint(control, row_index)?;
10842 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
10845 continue;
10846 }
10847 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
10848 if theirs == key {
10849 return Err(MongrelError::Conflict(format!(
10850 "UNIQUE constraint '{}' on table '{tname}' violated",
10851 uc.name
10852 )));
10853 }
10854 }
10855 }
10856 }
10857
10858 for fk in &schema.constraints.foreign_keys {
10860 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
10861 continue; };
10863 let Some(parent_id) = cat
10864 .tables
10865 .iter()
10866 .find(|t| t.name == fk.ref_table)
10867 .map(|t| t.table_id)
10868 else {
10869 return Err(MongrelError::InvalidArgument(format!(
10870 "FOREIGN KEY '{}' references unknown table '{}'",
10871 fk.name, fk.ref_table
10872 )));
10873 };
10874 self.acquire_fk_lock(
10879 txn_id,
10880 parent_id,
10881 &child_key,
10882 crate::locks::LockMode::Shared,
10883 control,
10884 )?;
10885 let parent_rows = load_rows(parent_id)?;
10886 let mut found = false;
10887 for (row_index, r) in parent_rows.iter().enumerate() {
10888 commit_prepare_checkpoint(control, row_index)?;
10889 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
10890 continue;
10891 }
10892 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
10893 if pkey == child_key {
10894 found = true;
10895 break;
10896 }
10897 }
10898 }
10899 if !found {
10906 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
10907 commit_prepare_checkpoint(control, staged_index)?;
10908 if *st_table != parent_id {
10909 continue;
10910 }
10911 if let Staged::Put(pcells)
10912 | Staged::Update {
10913 new_row: pcells, ..
10914 } = st_op
10915 {
10916 let pmap: HashMap<u16, crate::memtable::Value> =
10917 pcells.iter().cloned().collect();
10918 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
10919 {
10920 if pkey == child_key {
10921 found = true;
10922 break;
10923 }
10924 }
10925 }
10926 }
10927 }
10928 if !found {
10929 return Err(MongrelError::Conflict(format!(
10930 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
10931 fk.name, fk.ref_table
10932 )));
10933 }
10934 }
10935
10936 if let Staged::Update { row_id, .. } = op {
10941 let parent_handle = self.table_by_id(*table_id)?;
10942 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
10943 continue;
10944 };
10945 for (child_id, child_name, child_schema) in &live {
10946 for fk in &child_schema.constraints.foreign_keys {
10947 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
10948 continue;
10949 }
10950 let Some(old_key) =
10951 encode_composite_key(&fk.ref_columns, &old_parent.columns)
10952 else {
10953 continue;
10954 };
10955 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
10956 == Some(old_key.as_slice())
10957 {
10958 continue;
10959 }
10960 self.acquire_fk_lock(
10965 txn_id,
10966 *table_id,
10967 &old_key,
10968 crate::locks::LockMode::Exclusive,
10969 control,
10970 )?;
10971 for (child_index, child) in
10972 load_rows(*child_id)?.into_iter().enumerate()
10973 {
10974 commit_prepare_checkpoint(control, child_index)?;
10975 if encode_composite_key(&fk.columns, &child.columns).as_deref()
10976 != Some(old_key.as_slice())
10977 {
10978 continue;
10979 }
10980 let replacement = staging.iter().find_map(|(id, op)| {
10981 if *id != *child_id {
10982 return None;
10983 }
10984 match op {
10985 Staged::Delete(id) if *id == child.row_id => Some(None),
10986 Staged::Update {
10987 row_id,
10988 new_row: cells,
10989 ..
10990 } if *row_id == child.row_id => {
10991 let map: HashMap<u16, Value> =
10992 cells.iter().cloned().collect();
10993 Some(encode_composite_key(&fk.columns, &map))
10994 }
10995 _ => None,
10996 }
10997 });
10998 if replacement.is_some_and(|key| {
10999 key.as_deref() != Some(old_key.as_slice())
11000 }) {
11001 continue;
11002 }
11003 return Err(MongrelError::Conflict(format!(
11004 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
11005 fk.name
11006 )));
11007 }
11008 }
11009 }
11010 }
11011 }
11012 Staged::Delete(rid) => {
11013 let parent_handle = self.table_by_id(*table_id)?;
11017 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
11018 continue;
11019 };
11020 for (child_id, child_name, child_schema) in &live {
11021 for fk in &child_schema.constraints.foreign_keys {
11022 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
11023 continue;
11024 }
11025 let Some(parent_key) =
11026 encode_composite_key(&fk.ref_columns, &parent_row.columns)
11027 else {
11028 continue;
11029 };
11030 let child_rows = load_rows(*child_id)?;
11031 for (row_index, r) in child_rows.iter().enumerate() {
11032 commit_prepare_checkpoint(control, row_index)?;
11033 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
11036 continue;
11037 }
11038 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
11039 if ck == parent_key {
11040 return Err(MongrelError::Conflict(format!(
11041 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
11042 fk.name
11043 )));
11044 }
11045 }
11046 }
11047 }
11048 }
11049 }
11050 Staged::Truncate => {
11051 for (child_id, child_name, child_schema) in &live {
11055 for fk in &child_schema.constraints.foreign_keys {
11056 if fk.ref_table != tname {
11057 continue;
11058 }
11059 let child_rows = load_rows(*child_id)?;
11060 if child_rows
11061 .iter()
11062 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
11063 {
11064 return Err(MongrelError::Conflict(format!(
11065 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
11066 fk.name
11067 )));
11068 }
11069 }
11070 }
11071 }
11072 }
11073 }
11074 Ok(())
11075 }
11076
11077 fn validate_write_permissions(
11078 &self,
11079 staging: &[(u64, crate::txn::Staged)],
11080 principal: Option<&crate::auth::Principal>,
11081 control: Option<&crate::ExecutionControl>,
11082 ) -> Result<()> {
11083 commit_prepare_checkpoint(control, 0)?;
11084 if principal.is_none() && !self.auth_state.require_auth() {
11085 return Ok(());
11086 }
11087 let principal = principal.ok_or(MongrelError::AuthRequired)?;
11088 let needs = summarize_write_permissions(staging);
11089 let catalog = self.catalog.read();
11090
11091 if needs.values().any(|need| need.truncate) {
11092 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
11093 }
11094 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
11095 commit_prepare_checkpoint(control, need_index)?;
11096 let entry = catalog
11097 .tables
11098 .iter()
11099 .find(|entry| {
11100 entry.table_id == table_id
11101 && matches!(entry.state, TableState::Live | TableState::Building { .. })
11102 })
11103 .ok_or_else(|| {
11104 MongrelError::NotFound(format!(
11105 "live table {table_id} not found during write validation"
11106 ))
11107 })?;
11108 if matches!(entry.state, TableState::Building { .. }) {
11109 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
11110 continue;
11111 }
11112 if need.insert {
11113 Self::require_columns_for_principal(
11114 &entry.name,
11115 &entry.schema,
11116 crate::auth::ColumnOperation::Insert,
11117 &need.insert_columns,
11118 principal,
11119 )?;
11120 }
11121 if need.update {
11122 Self::require_columns_for_principal(
11123 &entry.name,
11124 &entry.schema,
11125 crate::auth::ColumnOperation::Update,
11126 &need.update_columns,
11127 principal,
11128 )?;
11129 }
11130 if need.delete {
11131 self.require_for(
11132 Some(principal),
11133 &crate::auth::Permission::Delete {
11134 table: entry.name.clone(),
11135 },
11136 )?;
11137 }
11138 }
11139 Ok(())
11140 }
11141
11142 fn validate_security_writes(
11143 &self,
11144 staging: &[(u64, crate::txn::Staged)],
11145 read_epoch: Epoch,
11146 explicit_principal: Option<&crate::auth::Principal>,
11147 control: Option<&crate::ExecutionControl>,
11148 ) -> Result<()> {
11149 commit_prepare_checkpoint(control, 0)?;
11150 use crate::security::PolicyCommand;
11151 use crate::txn::Staged;
11152
11153 let catalog = self.catalog.read();
11154 if catalog.security.rls_tables.is_empty() {
11155 return Ok(());
11156 }
11157 let security = catalog.security.clone();
11158 let table_names = catalog
11159 .tables
11160 .iter()
11161 .filter(|entry| matches!(entry.state, TableState::Live))
11162 .map(|entry| (entry.table_id, entry.name.clone()))
11163 .collect::<HashMap<_, _>>();
11164 drop(catalog);
11165 if !staging.iter().any(|(table_id, _)| {
11166 table_names
11167 .get(table_id)
11168 .is_some_and(|table| security.rls_enabled(table))
11169 }) {
11170 return Ok(());
11171 }
11172 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
11173
11174 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
11175 commit_prepare_checkpoint(control, operation_index)?;
11176 let Some(table) = table_names.get(table_id) else {
11177 continue;
11178 };
11179 if !security.rls_enabled(table) || principal.is_admin {
11180 continue;
11181 }
11182 let denied = |command| MongrelError::PermissionDenied {
11183 required: match command {
11184 PolicyCommand::Insert => crate::auth::Permission::Insert {
11185 table: table.clone(),
11186 },
11187 PolicyCommand::Update => crate::auth::Permission::Update {
11188 table: table.clone(),
11189 },
11190 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
11191 crate::auth::Permission::Delete {
11192 table: table.clone(),
11193 }
11194 }
11195 },
11196 principal: principal.username.clone(),
11197 };
11198 match operation {
11199 Staged::Put(cells) => {
11200 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
11201 row.columns.extend(cells.iter().cloned());
11202 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
11203 return Err(denied(PolicyCommand::Insert));
11204 }
11205 }
11206 Staged::Update {
11207 row_id,
11208 new_row: cells,
11209 ..
11210 } => {
11211 let old = self
11212 .table_by_id(*table_id)?
11213 .lock()
11214 .get(*row_id, Snapshot::at(read_epoch))
11215 .ok_or_else(|| {
11216 MongrelError::NotFound(format!("row {} not found", row_id.0))
11217 })?;
11218 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
11219 return Err(denied(PolicyCommand::Update));
11220 }
11221 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
11222 new.columns.extend(cells.iter().cloned());
11223 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
11224 return Err(denied(PolicyCommand::Update));
11225 }
11226 }
11227 Staged::Delete(row_id) => {
11228 let old = self
11229 .table_by_id(*table_id)?
11230 .lock()
11231 .get(*row_id, Snapshot::at(read_epoch))
11232 .ok_or_else(|| {
11233 MongrelError::NotFound(format!("row {} not found", row_id.0))
11234 })?;
11235 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
11236 return Err(denied(PolicyCommand::Delete));
11237 }
11238 }
11239 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
11240 }
11241 }
11242 Ok(())
11243 }
11244
11245 #[allow(clippy::too_many_arguments)]
11252 pub(crate) fn commit_transaction_with_external_states(
11253 &self,
11254 txn_id: u64,
11255 read_epoch: Epoch,
11256 staging: Vec<(u64, crate::txn::Staged)>,
11257 external_states: Vec<(String, Vec<u8>)>,
11258 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
11259 security_principal: Option<crate::auth::Principal>,
11260 principal_catalog_bound: bool,
11261 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
11262 context: crate::txn::TxnCommitContext,
11263 ) -> Result<(Epoch, Vec<RowId>)> {
11264 self.commit_transaction_with_external_states_inner(
11265 txn_id,
11266 read_epoch,
11267 staging,
11268 external_states,
11269 materialized_view_updates,
11270 security_principal,
11271 principal_catalog_bound,
11272 external_trigger_bridge,
11273 context,
11274 None,
11275 None,
11276 )
11277 }
11278
11279 #[allow(clippy::too_many_arguments)]
11280 pub(crate) fn commit_transaction_with_external_states_controlled(
11281 &self,
11282 txn_id: u64,
11283 read_epoch: Epoch,
11284 staging: Vec<(u64, crate::txn::Staged)>,
11285 external_states: Vec<(String, Vec<u8>)>,
11286 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
11287 security_principal: Option<crate::auth::Principal>,
11288 principal_catalog_bound: bool,
11289 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
11290 context: crate::txn::TxnCommitContext,
11291 control: &crate::ExecutionControl,
11292 before_commit: &mut dyn FnMut() -> Result<()>,
11293 ) -> Result<(Epoch, Vec<RowId>)> {
11294 self.commit_transaction_with_external_states_inner(
11295 txn_id,
11296 read_epoch,
11297 staging,
11298 external_states,
11299 materialized_view_updates,
11300 security_principal,
11301 principal_catalog_bound,
11302 external_trigger_bridge,
11303 context,
11304 Some(control),
11305 Some(before_commit),
11306 )
11307 }
11308
11309 #[allow(clippy::too_many_arguments)]
11310 fn commit_transaction_with_external_states_inner(
11311 &self,
11312 txn_id: u64,
11313 read_epoch: Epoch,
11314 mut staging: Vec<(u64, crate::txn::Staged)>,
11315 external_states: Vec<(String, Vec<u8>)>,
11316 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
11317 mut security_principal: Option<crate::auth::Principal>,
11318 principal_catalog_bound: bool,
11319 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
11320 context: crate::txn::TxnCommitContext,
11321 control: Option<&crate::ExecutionControl>,
11322 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11323 ) -> Result<(Epoch, Vec<RowId>)> {
11324 use crate::memtable::Row;
11325 use crate::txn::{Staged, StagedOp, WriteKey};
11326 use crate::wal::Op;
11327 use std::collections::hash_map::DefaultHasher;
11328 use std::hash::{Hash, Hasher};
11329 use std::sync::atomic::Ordering;
11330
11331 if txn_id == crate::wal::SYSTEM_TXN_ID {
11332 return Err(MongrelError::Full(
11333 "per-open transaction id namespace exhausted; reopen the database".into(),
11334 ));
11335 }
11336 if self.read_only {
11337 return Err(MongrelError::ReadOnlyReplica);
11338 }
11339 commit_prepare_checkpoint(control, 0)?;
11340 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
11341 self.refresh_security_catalog_if_stale(observed_security_version)?;
11342 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
11343 if self.auth_state.require_auth() && security_principal.is_none() {
11344 return Err(MongrelError::AuthRequired);
11345 }
11346 {
11347 let catalog = self.catalog.read();
11348 if catalog.require_auth
11349 || principal_catalog_bound
11350 || security_principal
11351 .as_ref()
11352 .is_some_and(|principal| principal.user_id != 0)
11353 {
11354 let principal = security_principal
11355 .as_ref()
11356 .ok_or(MongrelError::AuthRequired)?;
11357 security_principal =
11358 Self::resolve_bound_principal_from_catalog(&catalog, principal);
11359 if security_principal.is_none() {
11360 return Err(MongrelError::AuthRequired);
11361 }
11362 }
11363 }
11364 let _replication_guard = self.replication_barrier.read();
11365 if self.poisoned.load(Ordering::Relaxed) {
11366 return Err(MongrelError::Other(
11367 "database poisoned by fsync error".into(),
11368 ));
11369 }
11370 let _operation = self.admit_operation()?;
11374
11375 if context.state.is_some() {
11384 self.acquire_txn_lock(
11385 txn_id,
11386 crate::locks::LockKey::schema_barrier(),
11387 crate::locks::LockMode::Shared,
11388 control,
11389 )?;
11390 }
11391 let _txn_lock_guard = TxnLockGuard {
11392 locks: &self.lock_manager,
11393 txn_id,
11394 };
11395
11396 let idempotency_request = match &context.idempotency {
11402 Some(request) => match self.idempotency.check_and_reserve(request)? {
11403 crate::txn::IdempotencyCheck::Replay(receipt) => {
11404 let epoch = Epoch(receipt.log_position.index);
11405 if let Some(state) = &context.state {
11406 state.committed(receipt);
11407 }
11408 return Ok((epoch, Vec::new()));
11409 }
11410 crate::txn::IdempotencyCheck::Reserved => Some(request.clone()),
11411 },
11412 None => None,
11413 };
11414 let mut idempotency_guard = idempotency_request.as_ref().map(|request| {
11416 crate::txn::IdempotencyReservationGuard::new(&self.idempotency, request.clone())
11417 });
11418 let mut external_states = dedup_external_states(external_states);
11419 if !external_states.is_empty() {
11420 let cat = self.catalog.read();
11421 for (name, _) in &external_states {
11422 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
11423 return Err(MongrelError::NotFound(format!(
11424 "external table {name:?} not found"
11425 )));
11426 }
11427 }
11428 }
11429 let prepared_materialized_views = {
11430 let mut deduplicated = HashMap::new();
11431 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
11432 {
11433 commit_prepare_checkpoint(control, definition_index)?;
11434 if definition.name.is_empty() || definition.query.trim().is_empty() {
11435 return Err(MongrelError::InvalidArgument(
11436 "materialized view name and query must not be empty".into(),
11437 ));
11438 }
11439 deduplicated.insert(definition.name.clone(), definition);
11440 }
11441 let catalog = self.catalog.read();
11442 let mut prepared = Vec::with_capacity(deduplicated.len());
11443 for (definition_index, definition) in deduplicated.into_values().enumerate() {
11444 commit_prepare_checkpoint(control, definition_index)?;
11445 let table_id = catalog
11446 .live(&definition.name)
11447 .ok_or_else(|| {
11448 MongrelError::NotFound(format!(
11449 "materialized view table {:?} not found",
11450 definition.name
11451 ))
11452 })?
11453 .table_id;
11454 prepared.push((table_id, definition));
11455 }
11456 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
11457 prepared
11458 };
11459
11460 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
11463 self.expand_table_triggers(
11464 txn_id,
11465 &mut staging,
11466 read_epoch,
11467 external_trigger_bridge,
11468 &mut external_states,
11469 control,
11470 )?;
11471 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
11472 external_states = dedup_external_states(external_states);
11473 let expected_external_generations = {
11474 let catalog = self.catalog.read();
11475 let mut generations = HashMap::with_capacity(external_states.len());
11476 for (name, _) in &external_states {
11477 let entry = catalog
11478 .external_tables
11479 .iter()
11480 .find(|entry| entry.name == *name)
11481 .ok_or_else(|| {
11482 MongrelError::NotFound(format!("external table {name:?} not found"))
11483 })?;
11484 generations.insert(name.clone(), entry.created_epoch);
11485 }
11486 generations
11487 };
11488
11489 self.acquire_unique_key_claims(txn_id, &staging, control)?;
11495 self.validate_constraints(txn_id, &mut staging, read_epoch, control)?;
11500 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
11501 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
11502 let mut normalized = Vec::with_capacity(staging.len() * 2);
11503 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
11504 commit_prepare_checkpoint(control, staged_index)?;
11505 match op {
11506 crate::txn::Staged::Update {
11507 row_id,
11508 new_row: cells,
11509 ..
11510 } => {
11511 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
11512 normalized.push((table_id, crate::txn::Staged::Put(cells)));
11513 }
11514 op => normalized.push((table_id, op)),
11515 }
11516 }
11517 staging = normalized;
11518 let has_changes = !staging.is_empty()
11519 || !external_states.is_empty()
11520 || !prepared_materialized_views.is_empty();
11521 let truncated_tables: HashSet<u64> = staging
11522 .iter()
11523 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
11524 .collect();
11525
11526 let write_keys = {
11527 let cat = self.catalog.read();
11528 let mut keys: Vec<WriteKey> = Vec::new();
11529 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11530 commit_prepare_checkpoint(control, staged_index)?;
11531 match staged {
11532 Staged::Put(cells) => {
11533 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
11534 for col in &entry.schema.columns {
11535 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
11536 if let Some((_, val)) =
11537 cells.iter().find(|(id, _)| *id == col.id)
11538 {
11539 let mut h = DefaultHasher::new();
11540 val.encode_key().hash(&mut h);
11541 keys.push(WriteKey::Unique {
11542 table_id: *table_id,
11543 index_id: 0,
11544 key_hash: h.finish(),
11545 });
11546 }
11547 }
11548 }
11549 for uc in &entry.schema.constraints.uniques {
11556 if let Some(key_bytes) = crate::constraint::encode_composite_key(
11557 &uc.columns,
11558 &cells.iter().cloned().collect(),
11559 ) {
11560 let mut h = DefaultHasher::new();
11561 key_bytes.hash(&mut h);
11562 keys.push(WriteKey::Unique {
11563 table_id: *table_id,
11564 index_id: uc.id | 0x8000,
11565 key_hash: h.finish(),
11566 });
11567 }
11568 }
11569 }
11570 }
11571 Staged::Delete(rid) => keys.push(WriteKey::Row {
11572 table_id: *table_id,
11573 row_id: rid.0,
11574 }),
11575 Staged::Truncate => keys.push(WriteKey::Table {
11576 table_id: *table_id,
11577 }),
11578 Staged::Update { .. } => {
11579 return Err(MongrelError::Other(
11580 "transaction contains an unnormalized update during preparation".into(),
11581 ));
11582 }
11583 }
11584 }
11585 for (external_index, (name, _)) in external_states.iter().enumerate() {
11586 commit_prepare_checkpoint(control, external_index)?;
11587 let mut h = DefaultHasher::new();
11588 name.hash(&mut h);
11589 keys.push(WriteKey::Unique {
11590 table_id: EXTERNAL_TABLE_ID,
11591 index_id: 0,
11592 key_hash: h.finish(),
11593 });
11594 }
11595 keys
11596 };
11597
11598 let min_active = self.active_txns.min_read_epoch();
11600 if min_active < u64::MAX {
11601 self.conflicts.prune_below(Epoch(min_active));
11602 }
11603
11604 let ssi_keys = match context.isolation.canonical() {
11611 crate::txn::IsolationLevel::Serializable => {
11612 crate::txn::ssi_validation_keys(&context.read_set, &context.predicate_set)
11613 }
11614 _ => Vec::new(),
11615 };
11616
11617 if self.conflicts.conflicts(&write_keys, read_epoch) {
11621 return Err(MongrelError::Conflict(
11622 "write-write conflict (pre-validate, first-committer-wins)".into(),
11623 ));
11624 }
11625 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
11626 return Err(MongrelError::SerializationFailure {
11627 message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
11628 .into(),
11629 });
11630 }
11631 let pre_validate_version = self.conflicts.version();
11632
11633 let mut spilled: Vec<SpilledRun> = Vec::new();
11637 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
11638 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
11642 {
11643 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
11644 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
11645 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11646 commit_prepare_checkpoint(control, staged_index)?;
11647 if let Staged::Put(cells) = staged {
11648 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
11649 bytes.saturating_add(value.estimated_bytes())
11650 });
11651 let table_bytes = table_bytes.entry(*table_id).or_default();
11652 *table_bytes = table_bytes.saturating_add(bytes);
11653 put_indexes.entry(*table_id).or_default().push(staged_index);
11654 }
11655 }
11656 let tables = self.tables.read();
11657 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
11658 commit_prepare_checkpoint(control, table_index)?;
11659 if bytes
11660 <= self
11661 .spill_threshold
11662 .load(std::sync::atomic::Ordering::Relaxed)
11663 {
11664 continue;
11665 }
11666 let Some(handle) = tables.get(&table_id) else {
11667 continue;
11668 };
11669 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
11670 let mut t = handle.lock();
11671 let tdir = t.table_dir().to_path_buf();
11672 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
11673 std::fs::create_dir_all(&txn_dir)?;
11674 let run_id = t.alloc_run_id()? as u128;
11675 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
11676 let final_path = t.run_path(run_id as u64);
11677
11678 let mut rows: Vec<Row> = Vec::new();
11679 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
11680 commit_prepare_checkpoint(control, put_index)?;
11681 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
11682 return Err(MongrelError::Other(
11683 "transaction put index no longer references a put".into(),
11684 ));
11685 };
11686 t.validate_cells_not_null(cells)?;
11687 let row_id = t.alloc_row_id()?;
11688 let mut row = Row::new(row_id, Epoch(0));
11689 row.columns.extend(std::mem::take(cells));
11690 rows.push(row);
11691 }
11692 let schema = t.schema_ref().clone();
11693 let kek = t.kek_ref().cloned();
11694 let specs = t.indexable_column_specs();
11695 drop(t);
11696
11697 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
11698 .uniform_epoch(true);
11699 if let Some(ref kek) = kek {
11700 writer = writer.with_encryption(kek.as_ref(), specs);
11701 }
11702 commit_prepare_checkpoint(control, 0)?;
11703 let header = writer.write(&pending_path, &rows)?;
11704 commit_prepare_checkpoint(control, 0)?;
11705 let row_count = header.row_count;
11706 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
11707 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
11708
11709 spilled.push(SpilledRun {
11710 table_id,
11711 run_id,
11712 pending_path,
11713 final_path,
11714 rows,
11715 row_count,
11716 min_rid,
11717 max_rid,
11718 content_hash: header.content_hash,
11719 });
11720 spilled_tables.insert(table_id);
11721 }
11722 }
11723
11724 if spill_guard.is_some() {
11726 if let Some(hook) = self.spill_hook.lock().as_ref() {
11727 hook();
11728 }
11729 }
11730
11731 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
11742 .take(staging.len())
11743 .collect();
11744 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
11745 .take(staging.len())
11746 .collect();
11747 {
11748 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
11749 for (index, (table_id, staged)) in staging.iter().enumerate() {
11750 commit_prepare_checkpoint(control, index)?;
11751 if matches!(staged, Staged::Delete(_))
11752 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
11753 {
11754 indexes_by_table.entry(*table_id).or_default().push(index);
11755 }
11756 }
11757 let tables = self.tables.read();
11758 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
11759 commit_prepare_checkpoint(control, table_index)?;
11760 let handle = tables.get(&table_id).ok_or_else(|| {
11761 MongrelError::NotFound(format!("table {table_id} not mounted"))
11762 })?;
11763 #[cfg(test)]
11764 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
11765 let mut t = handle.lock();
11766 for (prepare_index, index) in indexes.into_iter().enumerate() {
11767 commit_prepare_checkpoint(control, prepare_index)?;
11768 match &staging[index].1 {
11769 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
11770 t.validate_cells_not_null(cells)?;
11771 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
11772 for (column, value) in cells {
11773 row.columns.insert(*column, value.clone());
11774 }
11775 prebuilt[index] = Some(row);
11776 }
11777 Staged::Delete(row_id) => {
11778 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
11779 }
11780 Staged::Put(_) | Staged::Truncate => {}
11781 Staged::Update { .. } => {
11782 return Err(MongrelError::Other(
11783 "transaction contains an unnormalized update during row preparation"
11784 .into(),
11785 ));
11786 }
11787 }
11788 }
11789 }
11790 }
11791
11792 let prepared_table_handles = {
11796 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
11797 let put_table_ids: HashSet<u64> = staging
11798 .iter()
11799 .filter_map(|(table_id, staged)| {
11800 matches!(staged, Staged::Put(_)).then_some(*table_id)
11801 })
11802 .collect();
11803 let tables = self.tables.read();
11804 let mut handles = HashMap::with_capacity(table_ids.len());
11805 for (table_index, table_id) in table_ids.into_iter().enumerate() {
11806 commit_prepare_checkpoint(control, table_index)?;
11807 let handle = tables.get(&table_id).ok_or_else(|| {
11808 MongrelError::NotFound(format!("table {table_id} not mounted"))
11809 })?;
11810 if put_table_ids.contains(&table_id) {
11811 match control {
11812 Some(control) => {
11813 handle.lock().prepare_durable_publish_controlled(control)?
11814 }
11815 None => handle.lock().prepare_durable_publish()?,
11816 }
11817 }
11818 handles.insert(table_id, handle.clone());
11819 }
11820 handles
11821 };
11822
11823 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
11827
11828 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
11829 .iter()
11830 .map(|run| {
11831 (
11832 run.table_id,
11833 run.rows.iter().map(|row| row.row_id).collect(),
11834 )
11835 })
11836 .collect();
11837 let committed_row_ids = staging
11838 .iter()
11839 .enumerate()
11840 .filter_map(|(index, (table_id, staged))| {
11841 if !matches!(staged, Staged::Put(_)) {
11842 return None;
11843 }
11844 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
11845 spilled_row_ids
11846 .get_mut(table_id)
11847 .and_then(VecDeque::pop_front)
11848 })
11849 })
11850 .collect();
11851
11852 let mut prepared_external = Vec::with_capacity(external_states.len());
11853 for (external_index, (name, state)) in external_states.iter().enumerate() {
11854 commit_prepare_checkpoint(control, external_index)?;
11855 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
11856 prepared_external.push((name.clone(), state.clone(), pending));
11857 }
11858
11859 if context.state.is_some() {
11869 self.lock_manager
11870 .release(txn_id, &crate::locks::LockKey::schema_barrier());
11871 }
11872 let added_runs: Vec<crate::wal::AddedRun> = spilled
11873 .iter()
11874 .map(|s| crate::wal::AddedRun {
11875 table_id: s.table_id,
11876 run_id: s.run_id,
11877 row_count: s.row_count,
11878 level: 0,
11879 min_row_id: s.min_rid,
11880 max_row_id: s.max_rid,
11881 content_hash: s.content_hash,
11882 })
11883 .collect();
11884 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
11885 hook();
11886 }
11887 let security_guard = self.security_coordinator.gate.read();
11891 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
11892 return Err(MongrelError::Conflict(
11893 "security policy changed during write".into(),
11894 ));
11895 }
11896 if spill_guard.is_some() {
11897 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
11898 hook();
11899 }
11900 }
11901 let commit_guard = self.commit_lock.lock();
11902 let catalog_generation_result = (|| {
11903 {
11904 let catalog = self.catalog.read();
11905 for table_id in prepared_table_handles.keys() {
11906 let is_current = catalog.tables.iter().any(|entry| {
11907 entry.table_id == *table_id
11908 && matches!(entry.state, TableState::Live | TableState::Building { .. })
11909 });
11910 if !is_current {
11911 return Err(MongrelError::Conflict(format!(
11912 "table {table_id} changed during transaction preparation"
11913 )));
11914 }
11915 }
11916 for (name, created_epoch) in &expected_external_generations {
11917 let current = catalog
11918 .external_tables
11919 .iter()
11920 .find(|entry| entry.name == *name)
11921 .map(|entry| entry.created_epoch);
11922 if current != Some(*created_epoch) {
11923 return Err(MongrelError::Conflict(format!(
11924 "external table {name:?} changed during transaction preparation"
11925 )));
11926 }
11927 }
11928 for (table_id, definition) in &prepared_materialized_views {
11929 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
11930 if current != Some(*table_id) {
11931 return Err(MongrelError::Conflict(format!(
11932 "materialized view {:?} changed during transaction preparation",
11933 definition.name
11934 )));
11935 }
11936 }
11937 if trigger_catalog_binding(&catalog) != trigger_binding {
11938 return Err(MongrelError::Conflict(
11939 "trigger or referenced table generation changed during transaction preparation"
11940 .into(),
11941 ));
11942 }
11943 }
11944 let tables = self.tables.read();
11945 for (table_id, prepared) in &prepared_table_handles {
11946 if !tables
11947 .get(table_id)
11948 .is_some_and(|current| current.ptr_eq(prepared))
11949 {
11950 return Err(MongrelError::Conflict(format!(
11951 "table {table_id} mount changed during transaction preparation"
11952 )));
11953 }
11954 }
11955 Ok(())
11956 })();
11957 if let Err(error) = catalog_generation_result {
11958 drop(commit_guard);
11959 for (_, _, pending) in &prepared_external {
11960 let _ = std::fs::remove_file(pending);
11961 }
11962 return Err(error);
11963 }
11964 let new_epoch = self.epoch.assigned().next();
11968 let mut spilled_wal_bytes = 0;
11969 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
11970 let spill_prepare = (|| {
11971 for run in &mut spilled {
11972 for row in &mut run.rows {
11973 row.committed_epoch = new_epoch;
11974 }
11975 for rows in encode_spilled_row_chunks(
11976 &run.rows,
11977 &mut spilled_wal_bytes,
11978 SPILLED_WAL_TOTAL_MAX_BYTES,
11979 control,
11980 )? {
11981 spilled_wal_records.push((
11982 run.table_id,
11983 Op::SpilledRows {
11984 table_id: run.table_id,
11985 rows,
11986 },
11987 ));
11988 }
11989 }
11990 Result::<()>::Ok(())
11991 })();
11992 if let Err(error) = spill_prepare {
11993 for (_, _, pending) in &prepared_external {
11994 let _ = std::fs::remove_file(pending);
11995 }
11996 return Err(error);
11997 }
11998 let (
11999 new_epoch,
12000 mut _epoch_guard,
12001 applies,
12002 committed_materialized_views,
12003 commit_seq,
12004 commit_ts,
12005 ) = {
12006 let mut wal = self.shared_wal.lock();
12007
12008 if self.conflicts.version() != pre_validate_version {
12013 if self.conflicts.conflicts(&write_keys, read_epoch) {
12014 drop(wal);
12017 for (_, _, pending) in &prepared_external {
12018 let _ = std::fs::remove_file(pending);
12019 }
12020 return Err(MongrelError::Conflict(
12021 "write-write conflict (sequencer delta re-check)".into(),
12022 ));
12023 }
12024 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
12025 drop(wal);
12029 for (_, _, pending) in &prepared_external {
12030 let _ = std::fs::remove_file(pending);
12031 }
12032 return Err(MongrelError::SerializationFailure {
12033 message:
12034 "a concurrent commit invalidated this transaction's reads (sequencer re-check)"
12035 .into(),
12036 });
12037 }
12038 }
12039
12040 if let Some(control) = control {
12041 if let Err(error) = control.checkpoint() {
12042 drop(wal);
12043 for (_, _, pending) in &prepared_external {
12044 let _ = std::fs::remove_file(pending);
12045 }
12046 return Err(error);
12047 }
12048 }
12049 let mut applies = Vec::<TableApplyBatch>::new();
12050 let mut apply_indexes = HashMap::<u64, usize>::new();
12051 let mut committed_materialized_views = Vec::new();
12052 let mut wal_records = spilled_wal_records;
12053
12054 let mut index = 0;
12055 while index < staging.len() {
12056 let table_id = staging[index].0;
12057 let handle = prepared_table_handles
12058 .get(&table_id)
12059 .cloned()
12060 .ok_or_else(|| {
12061 MongrelError::NotFound(format!("table {table_id} not prepared"))
12062 })?;
12063 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
12064 let index = applies.len();
12065 applies.push(TableApplyBatch {
12066 table_id,
12067 handle,
12068 ops: Vec::new(),
12069 });
12070 index
12071 });
12072
12073 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
12076 {
12077 index += 1;
12078 continue;
12079 }
12080
12081 match &staging[index].1 {
12082 Staged::Put(_) => {
12083 let mut rows = Vec::new();
12084 while index < staging.len()
12085 && staging[index].0 == table_id
12086 && matches!(&staging[index].1, Staged::Put(_))
12087 {
12088 let mut row = prebuilt[index].take().ok_or_else(|| {
12089 MongrelError::Other(
12090 "transaction prepare lost a prebuilt put row".into(),
12091 )
12092 })?;
12093 row.committed_epoch = new_epoch;
12094 rows.push(row);
12095 index += 1;
12096 }
12097 let payload = bincode::serialize(&rows)
12098 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
12099 wal_records.push((
12100 table_id,
12101 Op::Put {
12102 table_id,
12103 rows: payload,
12104 },
12105 ));
12106 applies[batch_index].ops.push(StagedOp::Put(rows));
12107 }
12108 Staged::Delete(_) => {
12109 let mut row_ids = Vec::new();
12110 while index < staging.len()
12111 && staging[index].0 == table_id
12112 && matches!(&staging[index].1, Staged::Delete(_))
12113 {
12114 let Staged::Delete(row_id) = &staging[index].1 else {
12115 return Err(MongrelError::Other(
12116 "transaction delete batch changed during WAL preparation"
12117 .into(),
12118 ));
12119 };
12120 if let Some(before) = &delete_images[index] {
12121 wal_records.push((
12122 table_id,
12123 Op::BeforeImage {
12124 table_id,
12125 row_id: *row_id,
12126 row: bincode::serialize(before).map_err(|error| {
12127 MongrelError::Other(format!(
12128 "before-image serialize: {error}"
12129 ))
12130 })?,
12131 },
12132 ));
12133 }
12134 row_ids.push(*row_id);
12135 index += 1;
12136 }
12137 wal_records.push((
12138 table_id,
12139 Op::Delete {
12140 table_id,
12141 row_ids: row_ids.clone(),
12142 },
12143 ));
12144 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
12145 }
12146 Staged::Truncate => {
12147 wal_records.push((table_id, Op::TruncateTable { table_id }));
12148 applies[batch_index].ops.push(StagedOp::Truncate);
12149 index += 1;
12150 }
12151 Staged::Update { .. } => {
12152 return Err(MongrelError::Other(
12153 "transaction contains an unnormalized update at the sequencer".into(),
12154 ));
12155 }
12156 }
12157 }
12158
12159 for (name, state, _) in &prepared_external {
12160 wal_records.push((
12161 EXTERNAL_TABLE_ID,
12162 Op::ExternalTableState {
12163 name: name.clone(),
12164 state: state.clone(),
12165 },
12166 ));
12167 }
12168
12169 for (table_id, definition) in &prepared_materialized_views {
12170 let mut definition = definition.clone();
12171 definition.last_refresh_epoch = new_epoch.0;
12172 wal_records.push((
12173 *table_id,
12174 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
12175 name: definition.name.clone(),
12176 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
12177 }),
12178 ));
12179 committed_materialized_views.push(definition);
12180 }
12181 if !committed_materialized_views.is_empty() {
12182 let mut next_catalog = self.catalog.read().clone();
12183 for definition in &committed_materialized_views {
12184 if let Some(existing) = next_catalog
12185 .materialized_views
12186 .iter_mut()
12187 .find(|existing| existing.name == definition.name)
12188 {
12189 *existing = definition.clone();
12190 } else {
12191 next_catalog.materialized_views.push(definition.clone());
12192 }
12193 }
12194 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
12195 wal_records.push((
12196 WAL_TABLE_ID,
12197 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
12198 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
12199 }),
12200 ));
12201 }
12202
12203 if let Some(control) = control {
12204 if let Err(error) = control.checkpoint() {
12205 drop(wal);
12206 for (_, _, pending) in &prepared_external {
12207 let _ = std::fs::remove_file(pending);
12208 }
12209 return Err(error);
12210 }
12211 }
12212 if let Some(before_commit) = before_commit.as_mut() {
12213 if let Err(error) = before_commit() {
12214 drop(wal);
12215 for (_, _, pending) in &prepared_external {
12216 let _ = std::fs::remove_file(pending);
12217 }
12218 return Err(error);
12219 }
12220 }
12221
12222 let assigned_epoch = self.epoch.bump_assigned();
12223 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
12224 if assigned_epoch != new_epoch {
12225 for (_, _, pending) in &prepared_external {
12226 let _ = std::fs::remove_file(pending);
12227 }
12228 return Err(MongrelError::Conflict(
12229 "commit epoch changed while sequencer lock was held".into(),
12230 ));
12231 }
12232
12233 let commit_ts = match context.read_ts {
12241 Some(read_ts) => self.hlc.commit_timestamp([read_ts]),
12242 None => self.hlc.now().map_err(|skew| {
12243 MongrelError::Other(format!(
12244 "clock skew rejected commit timestamp allocation: {skew}"
12245 ))
12246 })?,
12247 };
12248 if let Some(state) = &context.state {
12249 state.enter_commit_critical();
12250 }
12251
12252 prepared_run_links.disarm();
12256
12257 let commit_seq = self
12261 .commit_log
12262 .append_transaction(
12263 &mut wal,
12264 txn_id,
12265 new_epoch,
12266 commit_ts,
12267 wal_records,
12268 &added_runs,
12269 )
12270 .map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
12271
12272 self.conflicts.record(&write_keys, new_epoch);
12277 (
12278 new_epoch,
12279 _epoch_guard,
12280 applies,
12281 committed_materialized_views,
12282 commit_seq,
12283 commit_ts,
12284 )
12285 };
12286 drop(commit_guard);
12287
12288 let receipt =
12290 self.await_durable_commit_with_ts(txn_id, commit_seq, new_epoch, commit_ts)?;
12291 drop(security_guard);
12292
12293 if let Some(request) = &idempotency_request {
12297 if let Err(error) =
12298 self.idempotency
12299 .complete(request, txn_id, new_epoch, receipt.commit_ts)
12300 {
12301 return Err(MongrelError::DurableCommit {
12302 epoch: new_epoch.0,
12303 message: format!("idempotency record was not durable: {error}"),
12304 });
12305 }
12306 if let Some(guard) = idempotency_guard.as_mut() {
12307 guard.disarm();
12308 }
12309 }
12310
12311 let publish_result: Result<()> = {
12313 let mut first_error = None;
12314 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
12315 for run in &spilled {
12316 spilled_by_table.entry(run.table_id).or_default().push(run);
12317 }
12318 let mut modified_tables = Vec::with_capacity(applies.len());
12319 for batch in applies {
12322 #[cfg(test)]
12323 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
12324 let mut t = batch.handle.lock();
12325 for op in batch.ops {
12326 match op {
12327 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
12328 StagedOp::Delete(row_ids) => {
12329 for row_id in row_ids {
12330 t.apply_delete(row_id, new_epoch);
12331 }
12332 }
12333 StagedOp::Truncate => t.apply_truncate(new_epoch),
12334 }
12335 }
12336 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
12337 for run in runs {
12338 t.link_run(crate::manifest::RunRef {
12339 run_id: run.run_id,
12340 level: 0,
12341 epoch_created: new_epoch.0,
12342 row_count: run.row_count,
12343 });
12344 t.apply_run_metadata_prepared(&run.rows)?;
12345 if truncated_tables.contains(&batch.table_id) {
12346 t.set_flushed_epoch(new_epoch);
12351 }
12352 }
12353 }
12354 t.invalidate_pending_cache();
12355 drop(t);
12356 modified_tables.push(batch.handle);
12357 }
12358
12359 for handle in modified_tables {
12363 #[cfg(test)]
12364 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
12365 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
12366 first_error.get_or_insert(error);
12367 }
12368 }
12369 for (name, _, pending) in &prepared_external {
12370 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
12371 first_error.get_or_insert(error);
12372 }
12373 }
12374 if !committed_materialized_views.is_empty() {
12375 let mut next_catalog = self.catalog.read().clone();
12376 for definition in committed_materialized_views {
12377 if let Some(existing) = next_catalog
12378 .materialized_views
12379 .iter_mut()
12380 .find(|existing| existing.name == definition.name)
12381 {
12382 *existing = definition;
12383 } else {
12384 next_catalog.materialized_views.push(definition);
12385 }
12386 }
12387 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
12388 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
12389 first_error.get_or_insert(error);
12390 }
12391 }
12392 match first_error {
12393 Some(error) => Err(error),
12394 None => Ok(()),
12395 }
12396 };
12397
12398 if has_changes {
12399 let _ = self.change_wake.send(());
12400 }
12401 self.finish_durable_publish(new_epoch, &mut _epoch_guard, &receipt, publish_result)?;
12402 if let Some(state) = &context.state {
12406 state.committed(receipt.clone());
12407 }
12408 Ok((new_epoch, committed_row_ids))
12409 }
12410
12411 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
12414 let e = self.epoch.visible();
12415 let g = self.snapshots.register(e);
12416 (Snapshot::at(e), g)
12417 }
12418
12419 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
12422 let e = self.epoch.visible();
12423 let g = self.snapshots.register_owned(e);
12424 (Snapshot::at(e), g)
12425 }
12426
12427 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
12432 let _guard = self.ddl_lock.lock();
12433 let current = self.epoch.visible();
12434 let (old_epochs, old_start) = self.snapshots.history_config();
12435 let earliest_already_guaranteed = if old_epochs == 0 {
12436 current
12437 } else {
12438 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
12439 };
12440 let start = if epochs == 0 {
12441 current
12442 } else {
12443 earliest_already_guaranteed
12444 };
12445 let published = std::cell::Cell::new(false);
12446 let result = write_history_retention(&self.root, epochs, start, || {
12447 self.snapshots.configure_history(epochs, start);
12448 published.set(true);
12449 });
12450 match result {
12451 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
12452 epoch: current.0,
12453 message: format!("history-retention publication was not durable: {error}"),
12454 }),
12455 result => result,
12456 }
12457 }
12458
12459 pub fn history_retention_epochs(&self) -> u64 {
12460 self.snapshots.history_config().0
12461 }
12462
12463 pub fn earliest_retained_epoch(&self) -> Epoch {
12464 let current = self.epoch.visible();
12465 self.snapshots.history_floor(current).unwrap_or(current)
12466 }
12467
12468 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
12471 let current = self.epoch.visible();
12472 if epoch > current {
12473 return Err(MongrelError::InvalidArgument(format!(
12474 "epoch {} is in the future; current epoch is {}",
12475 epoch.0, current.0
12476 )));
12477 }
12478 let earliest = self.earliest_retained_epoch();
12479 if epoch < earliest {
12480 return Err(MongrelError::InvalidArgument(format!(
12481 "epoch {} is no longer retained; earliest available epoch is {}",
12482 epoch.0, earliest.0
12483 )));
12484 }
12485 let guard = self.snapshots.register_owned(epoch);
12486 Ok((Snapshot::at(epoch), guard))
12487 }
12488
12489 pub fn table_names(&self) -> Vec<String> {
12491 self.catalog
12492 .read()
12493 .tables
12494 .iter()
12495 .filter(|t| matches!(t.state, TableState::Live))
12496 .map(|t| t.name.clone())
12497 .collect()
12498 }
12499
12500 pub fn close(&self) -> Result<()> {
12506 for name in self.table_names() {
12507 if let Ok(handle) = self.table(&name) {
12508 if let Err(e) = handle.lock().close() {
12509 eprintln!("[close] flush failed for {name}: {e}");
12510 }
12511 }
12512 }
12513 Ok(())
12514 }
12515
12516 pub fn compact(&self) -> Result<(usize, usize)> {
12523 self.require(&crate::auth::Permission::Ddl)?;
12524 let _operation = self.admit_operation()?;
12526 let mut compacted = 0;
12527 let mut skipped = 0;
12528 for name in self.table_names() {
12529 let Ok(handle) = self.table(&name) else {
12530 continue;
12531 };
12532 {
12533 let mut t = handle.lock();
12534 let before = t.run_count();
12535 if before < 2 && !t.should_compact() {
12536 skipped += 1;
12537 continue;
12538 }
12539 match t.compact() {
12540 Ok(()) => {
12541 let after = t.run_count();
12542 compacted += 1;
12543 eprintln!("[compact] {name}: {before} -> {after} runs");
12544 }
12545 Err(e) => {
12546 eprintln!("[compact] {name}: compaction failed: {e}");
12547 skipped += 1;
12548 }
12549 }
12550 }
12551 }
12552 Ok((compacted, skipped))
12553 }
12554
12555 pub fn compact_table(&self, name: &str) -> Result<bool> {
12558 self.require(&crate::auth::Permission::Ddl)?;
12559 let handle = self.table(name)?;
12560 let mut t = handle.lock();
12561 let before = t.run_count();
12562 if before < 2 {
12563 return Ok(false);
12564 }
12565 t.compact()?;
12566 Ok(t.run_count() < before)
12567 }
12568
12569 pub fn table(&self, name: &str) -> Result<TableHandle> {
12571 self.ensure_owner_process()?;
12572 let cat = self.catalog.read();
12573 let entry = cat
12574 .live(name)
12575 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
12576 let id = entry.table_id;
12577 drop(cat);
12578 self.tables
12579 .read()
12580 .get(&id)
12581 .cloned()
12582 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
12583 }
12584
12585 pub fn has_ttl_tables(&self) -> bool {
12588 self.tables
12589 .read()
12590 .values()
12591 .any(|table| table.lock().ttl().is_some())
12592 }
12593
12594 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
12597 self.tables
12598 .read()
12599 .get(&id)
12600 .cloned()
12601 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
12602 }
12603
12604 pub(crate) fn table_auto_inc_would_allocate(
12610 &self,
12611 table_id: u64,
12612 cells: &[(u16, Value)],
12613 ) -> bool {
12614 let catalog = self.catalog.read();
12615 let Some(entry) = catalog
12616 .tables
12617 .iter()
12618 .find(|entry| entry.table_id == table_id)
12619 else {
12620 return false;
12621 };
12622 let Some(column) = entry.schema.auto_increment_column() else {
12623 return false;
12624 };
12625 matches!(
12626 cells.iter().find(|(id, _)| *id == column.id),
12627 None | Some((_, Value::Null))
12628 )
12629 }
12630
12631 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
12637 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
12638 return Err(MongrelError::InvalidArgument(format!(
12639 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
12640 )));
12641 }
12642 self.create_table_with_state(name, schema, TableState::Live)
12643 }
12644
12645 #[doc(hidden)]
12647 pub fn create_building_table(
12648 &self,
12649 build_name: &str,
12650 intended_name: &str,
12651 query_id: &str,
12652 schema: Schema,
12653 ) -> Result<u64> {
12654 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12655 || intended_name.is_empty()
12656 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12657 || query_id.is_empty()
12658 {
12659 return Err(MongrelError::InvalidArgument(
12660 "invalid CTAS building-table identity".into(),
12661 ));
12662 }
12663 self.create_table_with_state(
12664 build_name,
12665 schema,
12666 TableState::Building {
12667 intended_name: intended_name.to_string(),
12668 query_id: query_id.to_string(),
12669 created_at_unix_nanos: current_unix_nanos(),
12670 replaces_table_id: None,
12671 },
12672 )
12673 }
12674
12675 #[doc(hidden)]
12678 pub fn create_rebuilding_table(
12679 &self,
12680 build_name: &str,
12681 intended_name: &str,
12682 query_id: &str,
12683 schema: Schema,
12684 ) -> Result<u64> {
12685 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12686 || intended_name.is_empty()
12687 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12688 || query_id.is_empty()
12689 {
12690 return Err(MongrelError::InvalidArgument(
12691 "invalid rebuilding-table identity".into(),
12692 ));
12693 }
12694 let replaces_table_id = self
12695 .catalog
12696 .read()
12697 .live(intended_name)
12698 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
12699 .table_id;
12700 self.create_table_with_state(
12701 build_name,
12702 schema,
12703 TableState::Building {
12704 intended_name: intended_name.to_string(),
12705 query_id: query_id.to_string(),
12706 created_at_unix_nanos: current_unix_nanos(),
12707 replaces_table_id: Some(replaces_table_id),
12708 },
12709 )
12710 }
12711
12712 fn create_table_with_state(
12713 &self,
12714 name: &str,
12715 schema: Schema,
12716 state: TableState,
12717 ) -> Result<u64> {
12718 use crate::wal::DdlOp;
12719 use std::sync::atomic::Ordering;
12720
12721 self.require(&crate::auth::Permission::Ddl)?;
12722 if self.poisoned.load(Ordering::Relaxed) {
12723 return Err(MongrelError::Other(
12724 "database poisoned by fsync error".into(),
12725 ));
12726 }
12727 let _operation = self.admit_operation()?;
12729 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
12730 let _g = self.ddl_lock.lock();
12731 let _security_write = self.security_write()?;
12732 self.require(&crate::auth::Permission::Ddl)?;
12733 {
12734 let cat = self.catalog.read();
12735 match &state {
12736 TableState::Live => {
12737 if cat.live(name).is_some() || cat.building_for(name).is_some() {
12738 return Err(MongrelError::InvalidArgument(format!(
12739 "table {name:?} already exists or is being built"
12740 )));
12741 }
12742 }
12743 TableState::Building {
12744 intended_name,
12745 replaces_table_id,
12746 ..
12747 } => {
12748 let target_matches = match replaces_table_id {
12749 Some(table_id) => cat
12750 .live(intended_name)
12751 .is_some_and(|entry| entry.table_id == *table_id),
12752 None => cat.live(intended_name).is_none(),
12753 };
12754 if !target_matches || cat.building_for(intended_name).is_some() {
12755 return Err(MongrelError::InvalidArgument(format!(
12756 "table {intended_name:?} changed or is already being built"
12757 )));
12758 }
12759 if cat.building(name).is_some() {
12760 return Err(MongrelError::InvalidArgument(format!(
12761 "building table {name:?} already exists"
12762 )));
12763 }
12764 }
12765 TableState::Dropped { .. } => {
12766 return Err(MongrelError::InvalidArgument(
12767 "cannot create a dropped table".into(),
12768 ));
12769 }
12770 }
12771 }
12772
12773 let commit_lock = Arc::clone(&self.commit_lock);
12778 let _c = commit_lock.lock();
12779 let live_create = matches!(state, TableState::Live);
12780 let legacy_table_id = if live_create {
12781 None
12782 } else {
12783 let mut cat = self.catalog.write();
12784 let id = cat.next_table_id;
12785 cat.next_table_id = id
12786 .checked_add(1)
12787 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
12788 Some(id)
12789 };
12790 let epoch = self.epoch.bump_assigned();
12791 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
12792 let txn_id = self.alloc_txn_id()?;
12793
12794 let mut schema = schema;
12795 if let Some(table_id) = legacy_table_id {
12796 schema.schema_id = table_id;
12800 schema.validate_auto_increment()?;
12808 schema.validate_defaults()?;
12809 schema.validate_ai()?;
12810 for index in &schema.indexes {
12811 index.validate_options()?;
12812 }
12813 for constraint in &schema.constraints.checks {
12814 constraint.expr.validate()?;
12815 }
12816 }
12817
12818 let mut next_catalog = self.catalog.read().clone();
12819 let (table_id, schema) = match legacy_table_id {
12820 Some(table_id) => {
12821 next_catalog.tables.push(CatalogEntry {
12822 table_id,
12823 name: name.to_string(),
12824 schema: schema.clone(),
12825 state: state.clone(),
12826 created_epoch: epoch.0,
12827 });
12828 (table_id, schema)
12829 }
12830 None => {
12831 let command = crate::catalog_cmds::CatalogCommand::CreateTable {
12835 name: name.to_string(),
12836 schema,
12837 created_epoch: epoch.0,
12838 };
12839 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
12840 let crate::catalog_cmds::CatalogDelta::TableCreated { entry } = delta else {
12841 return Err(MongrelError::Other(
12842 "CreateTable resolved to an unexpected catalog delta".into(),
12843 ));
12844 };
12845 (entry.table_id, entry.schema)
12846 }
12847 };
12848 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
12849
12850 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
12853 let canonical_tdir = self.root.join(&table_relative);
12854 let table_root = Arc::new(
12855 self.durable_root
12856 .create_directory_all_pinned(&table_relative)?,
12857 );
12858 let tdir = table_root.io_path()?;
12859 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
12860 let ctx = SharedCtx {
12861 root_guard: Some(table_root),
12862 epoch: Arc::clone(&self.epoch),
12863 page_cache: Arc::clone(&self.page_cache),
12864 decoded_cache: Arc::clone(&self.decoded_cache),
12865 snapshots: Arc::clone(&self.snapshots),
12866 kek: self.kek.clone(),
12867 commit_lock: Arc::clone(&self.commit_lock),
12868 shared: Some(crate::engine::SharedWalCtx {
12869 wal: Arc::clone(&self.shared_wal),
12870 group: Arc::clone(&self.group),
12871 poisoned: Arc::clone(&self.poisoned),
12872 txn_ids: Arc::clone(&self.next_txn_id),
12873 change_wake: self.change_wake.clone(),
12874 lifecycle: Arc::clone(&self.lifecycle),
12875 }),
12876 table_name: Some(name.to_string()),
12877 auth: self.table_auth_checker(),
12878 read_only: self.read_only,
12879 };
12880 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
12881
12882 let schema_json = DdlOp::encode_schema(&schema)?;
12885 let ddl = match &state {
12886 TableState::Live => DdlOp::CreateTable {
12887 table_id,
12888 name: name.to_string(),
12889 schema_json,
12890 },
12891 TableState::Building {
12892 intended_name,
12893 query_id,
12894 created_at_unix_nanos,
12895 replaces_table_id,
12896 } => match replaces_table_id {
12897 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
12898 table_id,
12899 build_name: name.to_string(),
12900 intended_name: intended_name.clone(),
12901 query_id: query_id.clone(),
12902 created_at_unix_nanos: *created_at_unix_nanos,
12903 replaces_table_id: *replaces_table_id,
12904 schema_json,
12905 },
12906 None => DdlOp::CreateBuildingTable {
12907 table_id,
12908 build_name: name.to_string(),
12909 intended_name: intended_name.clone(),
12910 query_id: query_id.clone(),
12911 created_at_unix_nanos: *created_at_unix_nanos,
12912 schema_json,
12913 },
12914 },
12915 TableState::Dropped { .. } => {
12916 return Err(MongrelError::InvalidArgument(
12917 "cannot create a table in dropped state".into(),
12918 ));
12919 }
12920 };
12921 let commit_seq = {
12922 let mut wal = self.shared_wal.lock();
12923 let append: Result<u64> = (|| {
12924 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
12925 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12926 wal.append_commit(txn_id, epoch, &[])
12927 })();
12928 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
12929 };
12930 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
12931 pending_table_dir.disarm();
12932
12933 self.tables
12936 .write()
12937 .insert(table_id, TableHandle::new(table));
12938 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12939 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
12940 Ok(table_id)
12941 }
12942
12943 pub fn drop_table(&self, name: &str) -> Result<()> {
12945 self.drop_table_with_epoch(name).map(|_| ())
12946 }
12947
12948 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
12950 self.drop_table_with_state(name, false, None)
12951 }
12952
12953 pub fn drop_table_with_epoch_controlled<F>(
12954 &self,
12955 name: &str,
12956 mut before_commit: F,
12957 ) -> Result<Epoch>
12958 where
12959 F: FnMut() -> Result<()>,
12960 {
12961 self.drop_table_with_state(name, false, Some(&mut before_commit))
12962 }
12963
12964 #[doc(hidden)]
12966 pub fn discard_building_table(&self, name: &str) -> Result<()> {
12967 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
12968 return Err(MongrelError::InvalidArgument(
12969 "not a CTAS building table".into(),
12970 ));
12971 }
12972 self.drop_table_with_state(name, true, None).map(|_| ())
12973 }
12974
12975 fn drop_table_with_state(
12976 &self,
12977 name: &str,
12978 building: bool,
12979 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
12980 ) -> Result<Epoch> {
12981 use crate::wal::DdlOp;
12982 use std::sync::atomic::Ordering;
12983
12984 self.require(&crate::auth::Permission::Ddl)?;
12985 if self.poisoned.load(Ordering::Relaxed) {
12986 return Err(MongrelError::Other(
12987 "database poisoned by fsync error".into(),
12988 ));
12989 }
12990 let _operation = self.admit_operation()?;
12992 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
12993 let _g = self.ddl_lock.lock();
12994 let _security_write = self.security_write()?;
12995 self.require(&crate::auth::Permission::Ddl)?;
12996 let table_id = {
12997 let cat = self.catalog.read();
12998 if building {
12999 cat.building(name)
13000 } else {
13001 cat.live(name)
13002 }
13003 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
13004 .table_id
13005 };
13006
13007 let commit_lock = Arc::clone(&self.commit_lock);
13008 let _c = commit_lock.lock();
13009 let epoch = self.epoch.bump_assigned();
13010 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13011 let txn_id = self.alloc_txn_id()?;
13012 let mut next_catalog = self.catalog.read().clone();
13013 if building {
13014 let entry = next_catalog
13017 .tables
13018 .iter_mut()
13019 .find(|t| t.table_id == table_id)
13020 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13021 entry.state = TableState::Dropped { at_epoch: epoch.0 };
13022 next_catalog.triggers.retain(|trigger| {
13023 !matches!(
13024 &trigger.trigger.target,
13025 TriggerTarget::Table(target) if target == name
13026 )
13027 });
13028 next_catalog
13029 .materialized_views
13030 .retain(|definition| definition.name != name);
13031 next_catalog
13032 .security
13033 .rls_tables
13034 .retain(|table| table != name);
13035 next_catalog
13036 .security
13037 .policies
13038 .retain(|policy| policy.table != name);
13039 next_catalog
13040 .security
13041 .masks
13042 .retain(|mask| mask.table != name);
13043 for role in &mut next_catalog.roles {
13044 role.permissions
13045 .retain(|permission| permission_table(permission) != Some(name));
13046 }
13047 advance_security_version(&mut next_catalog)?;
13048 } else {
13049 let command = crate::catalog_cmds::CatalogCommand::DropTable {
13053 name: name.to_string(),
13054 at_epoch: epoch.0,
13055 };
13056 self.apply_catalog_command_to(&mut next_catalog, command)?;
13057 }
13058 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13059 let commit_seq = {
13060 let mut wal = self.shared_wal.lock();
13061 if let Some(before_commit) = before_commit {
13062 before_commit()?;
13063 }
13064 let append: Result<u64> = (|| {
13065 wal.append(
13066 txn_id,
13067 table_id,
13068 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
13069 )?;
13070 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13071 wal.append_commit(txn_id, epoch, &[])
13072 })();
13073 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13074 };
13075 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13076
13077 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
13078 self.tables.write().remove(&table_id);
13079 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
13080 Ok(epoch)
13081 }
13082
13083 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
13092 self.rename_table_with_epoch(name, new_name).map(|_| ())
13093 }
13094
13095 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
13097 self.rename_table_with_epoch_inner(name, new_name, None)
13098 }
13099
13100 pub fn rename_table_with_epoch_controlled<F>(
13101 &self,
13102 name: &str,
13103 new_name: &str,
13104 mut before_commit: F,
13105 ) -> Result<Epoch>
13106 where
13107 F: FnMut() -> Result<()>,
13108 {
13109 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
13110 }
13111
13112 fn rename_table_with_epoch_inner(
13113 &self,
13114 name: &str,
13115 new_name: &str,
13116 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13117 ) -> Result<Epoch> {
13118 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13119 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13120 {
13121 return Err(MongrelError::InvalidArgument(
13122 "the CTAS building-table namespace is reserved".into(),
13123 ));
13124 }
13125 self.rename_table_with_state(name, new_name, false, None, before_commit)
13126 }
13127
13128 #[doc(hidden)]
13130 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
13131 self.publish_building_table_inner(build_name, new_name, None)
13132 }
13133
13134 #[doc(hidden)]
13135 pub fn publish_building_table_controlled<F>(
13136 &self,
13137 build_name: &str,
13138 new_name: &str,
13139 mut before_commit: F,
13140 ) -> Result<Epoch>
13141 where
13142 F: FnMut() -> Result<()>,
13143 {
13144 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
13145 }
13146
13147 fn publish_building_table_inner(
13148 &self,
13149 build_name: &str,
13150 new_name: &str,
13151 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13152 ) -> Result<Epoch> {
13153 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13154 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13155 {
13156 return Err(MongrelError::InvalidArgument(
13157 "invalid CTAS publish identity".into(),
13158 ));
13159 }
13160 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
13161 }
13162
13163 #[doc(hidden)]
13165 pub fn publish_materialized_building_table(
13166 &self,
13167 build_name: &str,
13168 new_name: &str,
13169 definition: crate::catalog::MaterializedViewEntry,
13170 ) -> Result<Epoch> {
13171 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
13172 }
13173
13174 #[doc(hidden)]
13175 pub fn publish_materialized_building_table_controlled<F>(
13176 &self,
13177 build_name: &str,
13178 new_name: &str,
13179 definition: crate::catalog::MaterializedViewEntry,
13180 mut before_commit: F,
13181 ) -> Result<Epoch>
13182 where
13183 F: FnMut() -> Result<()>,
13184 {
13185 self.publish_materialized_building_table_inner(
13186 build_name,
13187 new_name,
13188 definition,
13189 Some(&mut before_commit),
13190 )
13191 }
13192
13193 fn publish_materialized_building_table_inner(
13194 &self,
13195 build_name: &str,
13196 new_name: &str,
13197 definition: crate::catalog::MaterializedViewEntry,
13198 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13199 ) -> Result<Epoch> {
13200 if definition.name != new_name || definition.query.trim().is_empty() {
13201 return Err(MongrelError::InvalidArgument(
13202 "invalid materialized-view publication".into(),
13203 ));
13204 }
13205 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
13206 }
13207
13208 #[doc(hidden)]
13210 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
13211 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
13212 }
13213
13214 #[doc(hidden)]
13215 pub fn publish_rebuilding_table_controlled<F>(
13216 &self,
13217 build_name: &str,
13218 new_name: &str,
13219 mut before_commit: F,
13220 ) -> Result<Epoch>
13221 where
13222 F: FnMut() -> Result<()>,
13223 {
13224 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
13225 }
13226
13227 #[doc(hidden)]
13229 pub fn publish_materialized_rebuilding_table_controlled<F>(
13230 &self,
13231 build_name: &str,
13232 new_name: &str,
13233 definition: crate::catalog::MaterializedViewEntry,
13234 mut before_commit: F,
13235 ) -> Result<Epoch>
13236 where
13237 F: FnMut() -> Result<()>,
13238 {
13239 self.publish_rebuilding_table_inner(
13240 build_name,
13241 new_name,
13242 Some(definition),
13243 Some(&mut before_commit),
13244 )
13245 }
13246
13247 fn publish_rebuilding_table_inner(
13248 &self,
13249 build_name: &str,
13250 new_name: &str,
13251 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
13252 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13253 ) -> Result<Epoch> {
13254 use crate::wal::DdlOp;
13255
13256 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13257 || new_name.is_empty()
13258 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13259 {
13260 return Err(MongrelError::InvalidArgument(
13261 "invalid rebuilding-table publish identity".into(),
13262 ));
13263 }
13264 if materialized_view.as_ref().is_some_and(|definition| {
13265 definition.name != new_name || definition.query.trim().is_empty()
13266 }) {
13267 return Err(MongrelError::InvalidArgument(
13268 "invalid materialized-view replacement".into(),
13269 ));
13270 }
13271 self.require(&crate::auth::Permission::Ddl)?;
13272 if self.poisoned.load(Ordering::Relaxed) {
13273 return Err(MongrelError::Other(
13274 "database poisoned by fsync error".into(),
13275 ));
13276 }
13277 let _operation = self.admit_operation()?;
13279 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13280 let _ddl = self.ddl_lock.lock();
13281 let _security_write = self.security_write()?;
13282 let (table_id, replaced_table_id) = {
13283 let catalog = self.catalog.read();
13284 let build = catalog.building(build_name).ok_or_else(|| {
13285 MongrelError::NotFound(format!("building table {build_name:?} not found"))
13286 })?;
13287 let replaced_table_id = match &build.state {
13288 TableState::Building {
13289 intended_name,
13290 replaces_table_id: Some(replaced_table_id),
13291 ..
13292 } if intended_name == new_name => *replaced_table_id,
13293 _ => {
13294 return Err(MongrelError::InvalidArgument(format!(
13295 "building table {build_name:?} is not a replacement for {new_name:?}"
13296 )))
13297 }
13298 };
13299 if catalog
13300 .live(new_name)
13301 .is_none_or(|entry| entry.table_id != replaced_table_id)
13302 {
13303 return Err(MongrelError::Conflict(format!(
13304 "table {new_name:?} changed while its replacement was built"
13305 )));
13306 }
13307 (build.table_id, replaced_table_id)
13308 };
13309
13310 let _commit = self.commit_lock.lock();
13311 let epoch = self.epoch.assigned().next();
13312 let txn_id = self.alloc_txn_id()?;
13313 let mut next_catalog = self.catalog.read().clone();
13314 apply_rebuilding_publish(
13315 &mut next_catalog,
13316 table_id,
13317 replaced_table_id,
13318 new_name,
13319 epoch.0,
13320 )?;
13321 if let Some(definition) = materialized_view.as_mut() {
13322 definition.last_refresh_epoch = epoch.0;
13323 }
13324 let materialized_view_json = materialized_view
13325 .as_ref()
13326 .map(DdlOp::encode_materialized_view)
13327 .transpose()?;
13328 if let Some(definition) = materialized_view {
13329 if let Some(existing) = next_catalog
13330 .materialized_views
13331 .iter_mut()
13332 .find(|existing| existing.name == definition.name)
13333 {
13334 *existing = definition;
13335 } else {
13336 next_catalog.materialized_views.push(definition);
13337 }
13338 }
13339 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13340 if let Some(before_commit) = before_commit {
13341 before_commit()?;
13342 }
13343 let assigned_epoch = self.epoch.bump_assigned();
13344 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
13345 if assigned_epoch != epoch {
13346 return Err(MongrelError::Conflict(
13347 "commit epoch changed while sequencer lock was held".into(),
13348 ));
13349 }
13350 let commit_seq = {
13351 let mut wal = self.shared_wal.lock();
13352 let append: Result<u64> = (|| {
13353 wal.append(
13354 txn_id,
13355 table_id,
13356 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
13357 table_id,
13358 replaced_table_id,
13359 new_name: new_name.to_string(),
13360 }),
13361 )?;
13362 if let Some(definition_json) = materialized_view_json {
13363 wal.append(
13364 txn_id,
13365 table_id,
13366 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
13367 name: new_name.to_string(),
13368 definition_json,
13369 }),
13370 )?;
13371 }
13372 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13373 wal.append_commit(txn_id, epoch, &[])
13374 })();
13375 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13376 };
13377 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13378
13379 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
13380 self.tables.write().remove(&replaced_table_id);
13381 if let Some(table) = self.tables.read().get(&table_id) {
13382 table.lock().set_catalog_name(new_name.to_string());
13383 }
13384 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
13385 Ok(epoch)
13386 }
13387
13388 fn rename_table_with_state(
13389 &self,
13390 name: &str,
13391 new_name: &str,
13392 building: bool,
13393 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
13394 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13395 ) -> Result<Epoch> {
13396 use crate::wal::DdlOp;
13397 use std::sync::atomic::Ordering;
13398
13399 self.require(&crate::auth::Permission::Ddl)?;
13400 if self.poisoned.load(Ordering::Relaxed) {
13401 return Err(MongrelError::Other(
13402 "database poisoned by fsync error".into(),
13403 ));
13404 }
13405
13406 if name == new_name {
13409 return Ok(self.visible_epoch());
13410 }
13411 if new_name.is_empty() {
13412 return Err(MongrelError::InvalidArgument(
13413 "rename_table: new name must not be empty".into(),
13414 ));
13415 }
13416 let _operation = self.admit_operation()?;
13418 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13419 let _g = self.ddl_lock.lock();
13420 let _security_write = self.security_write()?;
13421 self.require(&crate::auth::Permission::Ddl)?;
13422 let table_id = {
13423 let cat = self.catalog.read();
13424 let src = if building {
13425 cat.building(name)
13426 } else {
13427 cat.live(name)
13428 }
13429 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13430 if building
13431 && !matches!(
13432 &src.state,
13433 TableState::Building { intended_name, .. } if intended_name == new_name
13434 )
13435 {
13436 return Err(MongrelError::InvalidArgument(format!(
13437 "building table {name:?} is not reserved for {new_name:?}"
13438 )));
13439 }
13440 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
13444 return Err(MongrelError::InvalidArgument(format!(
13445 "rename_table: a table named {new_name:?} already exists"
13446 )));
13447 }
13448 src.table_id
13449 };
13450
13451 let commit_lock = Arc::clone(&self.commit_lock);
13452 let _c = commit_lock.lock();
13453 let epoch = self.epoch.bump_assigned();
13454 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13455 let txn_id = self.alloc_txn_id()?;
13456 if let Some(definition) = materialized_view.as_mut() {
13457 definition.last_refresh_epoch = epoch.0;
13458 }
13459 let materialized_view_json = materialized_view
13460 .as_ref()
13461 .map(DdlOp::encode_materialized_view)
13462 .transpose()?;
13463 let mut next_catalog = self.catalog.read().clone();
13464 if building {
13465 let entry = next_catalog
13468 .tables
13469 .iter_mut()
13470 .find(|t| t.table_id == table_id)
13471 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13472 entry.name = new_name.to_string();
13473 entry.state = TableState::Live;
13474 for trigger in &mut next_catalog.triggers {
13475 if matches!(
13476 &trigger.trigger.target,
13477 TriggerTarget::Table(target) if target == name
13478 ) {
13479 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
13480 }
13481 }
13482 if let Some(definition) = next_catalog
13483 .materialized_views
13484 .iter_mut()
13485 .find(|definition| definition.name == name)
13486 {
13487 definition.name = new_name.to_string();
13488 }
13489 if let Some(definition) = materialized_view.take() {
13490 next_catalog.materialized_views.push(definition);
13491 }
13492 for table in &mut next_catalog.security.rls_tables {
13493 if table == name {
13494 *table = new_name.to_string();
13495 }
13496 }
13497 for policy in &mut next_catalog.security.policies {
13498 if policy.table == name {
13499 policy.table = new_name.to_string();
13500 }
13501 }
13502 for mask in &mut next_catalog.security.masks {
13503 if mask.table == name {
13504 mask.table = new_name.to_string();
13505 }
13506 }
13507 for role in &mut next_catalog.roles {
13508 for permission in &mut role.permissions {
13509 rename_permission_table(permission, name, new_name);
13510 }
13511 }
13512 advance_security_version(&mut next_catalog)?;
13513 } else {
13514 let command = crate::catalog_cmds::CatalogCommand::RenameTable {
13518 name: name.to_string(),
13519 new_name: new_name.to_string(),
13520 at_epoch: epoch.0,
13521 };
13522 self.apply_catalog_command_to(&mut next_catalog, command)?;
13523 }
13524 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13525 let ddl = if building {
13526 DdlOp::PublishBuildingTable {
13527 table_id,
13528 new_name: new_name.to_string(),
13529 }
13530 } else {
13531 DdlOp::RenameTable {
13532 table_id,
13533 new_name: new_name.to_string(),
13534 }
13535 };
13536 let commit_seq = {
13537 let mut wal = self.shared_wal.lock();
13538 if let Some(before_commit) = before_commit {
13539 before_commit()?;
13540 }
13541 let append: Result<u64> = (|| {
13542 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
13543 if let Some(definition_json) = materialized_view_json {
13544 wal.append(
13545 txn_id,
13546 table_id,
13547 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
13548 name: new_name.to_string(),
13549 definition_json,
13550 }),
13551 )?;
13552 }
13553 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13554 wal.append_commit(txn_id, epoch, &[])
13555 })();
13556 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13557 };
13558 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13559
13560 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
13561 if let Some(table) = self.tables.read().get(&table_id) {
13564 table.lock().set_catalog_name(new_name.to_string());
13565 }
13566 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
13567 Ok(epoch)
13568 }
13569
13570 pub fn alter_column(
13571 &self,
13572 table_name: &str,
13573 column_name: &str,
13574 change: AlterColumn,
13575 ) -> Result<ColumnDef> {
13576 self.alter_column_with_epoch(table_name, column_name, change)
13577 .map(|(column, _)| column)
13578 }
13579
13580 pub fn alter_column_with_epoch(
13581 &self,
13582 table_name: &str,
13583 column_name: &str,
13584 change: AlterColumn,
13585 ) -> Result<(ColumnDef, Option<Epoch>)> {
13586 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
13587 }
13588
13589 pub fn alter_column_with_epoch_controlled<B, A>(
13594 &self,
13595 table_name: &str,
13596 column_name: &str,
13597 change: AlterColumn,
13598 control: &crate::ExecutionControl,
13599 mut before_commit: B,
13600 mut after_commit: A,
13601 ) -> Result<(ColumnDef, Option<Epoch>)>
13602 where
13603 B: FnMut() -> Result<()>,
13604 A: FnMut(Option<Epoch>) -> Result<()>,
13605 {
13606 self.alter_column_with_epoch_inner(
13607 table_name,
13608 column_name,
13609 change,
13610 Some(control),
13611 Some(&mut before_commit),
13612 Some(&mut after_commit),
13613 )
13614 }
13615
13616 #[allow(clippy::too_many_arguments)]
13617 fn alter_column_with_epoch_inner(
13618 &self,
13619 table_name: &str,
13620 column_name: &str,
13621 change: AlterColumn,
13622 control: Option<&crate::ExecutionControl>,
13623 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13624 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
13625 ) -> Result<(ColumnDef, Option<Epoch>)> {
13626 use crate::wal::DdlOp;
13627 use std::sync::atomic::Ordering;
13628
13629 self.require(&crate::auth::Permission::Ddl)?;
13630 commit_prepare_checkpoint(control, 0)?;
13631 if self.poisoned.load(Ordering::Relaxed) {
13632 return Err(MongrelError::Other(
13633 "database poisoned by fsync error".into(),
13634 ));
13635 }
13636 let _operation = self.admit_operation()?;
13638 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13639 let _g = self.ddl_lock.lock();
13640 let table_id = {
13641 let cat = self.catalog.read();
13642 cat.live(table_name)
13643 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
13644 .table_id
13645 };
13646 let handle =
13647 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
13648 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
13649 })?;
13650
13651 let backfill = {
13657 let table = handle.lock();
13658 let old = table
13659 .schema()
13660 .column(column_name)
13661 .cloned()
13662 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
13663 let next_flags = change.flags.unwrap_or(old.flags);
13664 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
13665 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
13666 && old.default_value.is_some()
13667 {
13668 let snapshot = Snapshot::at(self.epoch.visible());
13669 let mut updates = Vec::new();
13670 let rows = match control {
13671 Some(control) => table.visible_rows_controlled(snapshot, control)?,
13672 None => table.visible_rows(snapshot)?,
13673 };
13674 for (row_index, row) in rows.into_iter().enumerate() {
13675 commit_prepare_checkpoint(control, row_index)?;
13676 if row
13677 .columns
13678 .get(&old.id)
13679 .is_some_and(|value| !matches!(value, Value::Null))
13680 {
13681 continue;
13682 }
13683 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
13684 table.apply_defaults(&mut cells)?;
13685 updates.push((
13686 table_id,
13687 crate::txn::Staged::Update {
13688 row_id: row.row_id,
13689 new_row: cells,
13690 changed_columns: vec![old.id],
13691 },
13692 ));
13693 }
13694 updates
13695 } else {
13696 Vec::new()
13697 }
13698 };
13699 let durable_epoch = std::cell::Cell::new(None);
13700 let backfill_epoch = if backfill.is_empty() {
13701 None
13702 } else {
13703 let (principal, catalog_bound) = self.transaction_principal_snapshot();
13704 let txn_id = self.alloc_txn_id()?;
13705 let mut entered_fence = false;
13706 let commit_result = match (control, before_commit.as_deref_mut()) {
13707 (Some(control), Some(before_commit)) => self
13708 .commit_transaction_with_external_states_controlled(
13709 txn_id,
13710 self.epoch.visible(),
13711 backfill,
13712 Vec::new(),
13713 Vec::new(),
13714 principal.clone(),
13715 catalog_bound,
13716 None,
13717 crate::txn::TxnCommitContext::internal(),
13718 control,
13719 &mut || {
13720 before_commit()?;
13721 entered_fence = true;
13722 Ok(())
13723 },
13724 )
13725 .map(|(epoch, _)| epoch),
13726 _ => self
13727 .commit_transaction_with_external_states(
13728 txn_id,
13729 self.epoch.visible(),
13730 backfill,
13731 Vec::new(),
13732 Vec::new(),
13733 principal,
13734 catalog_bound,
13735 None,
13736 crate::txn::TxnCommitContext::internal(),
13737 )
13738 .map(|(epoch, _)| epoch),
13739 };
13740 let commit_result = if entered_fence {
13741 finish_controlled_commit_attempt(commit_result, &mut after_commit)
13742 } else {
13743 commit_result
13744 };
13745 match &commit_result {
13746 Ok(epoch) => durable_epoch.set(Some(*epoch)),
13747 Err(MongrelError::DurableCommit { epoch, .. }) => {
13748 durable_epoch.set(Some(Epoch(*epoch)));
13749 }
13750 Err(_) => {}
13751 }
13752 Some(commit_result?)
13753 };
13754 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
13755 let _security_write = self.security_write()?;
13756 self.require(&crate::auth::Permission::Ddl)?;
13757 if self
13758 .catalog
13759 .read()
13760 .live(table_name)
13761 .is_none_or(|entry| entry.table_id != table_id)
13762 {
13763 return Err(MongrelError::Conflict(format!(
13764 "table {table_name:?} changed during ALTER"
13765 )));
13766 }
13767 let mut table = handle.lock();
13768 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
13769 let renamed_column = (column.name != column_name).then(|| column.name.clone());
13770 let Some(prepared_schema) = prepared_schema else {
13771 return Ok((column, backfill_epoch));
13772 };
13773 let command = crate::catalog_cmds::CatalogCommand::AlterColumn {
13778 table: table_name.to_string(),
13779 column: column.clone(),
13780 };
13781 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
13782
13783 let commit_lock = Arc::clone(&self.commit_lock);
13784 let _c = commit_lock.lock();
13785 let epoch = self.epoch.bump_assigned();
13786 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13787 let txn_id = self.alloc_txn_id()?;
13788 let column_json = DdlOp::encode_column(&column)?;
13789 let mut next_catalog = self.catalog.read().clone();
13790 let catalog_entry_index = next_catalog
13791 .tables
13792 .iter()
13793 .position(|entry| entry.table_id == table_id)
13794 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
13795 if let Some(new_column_name) = &renamed_column {
13796 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
13797 commit_prepare_checkpoint(control, trigger_index)?;
13798 if matches!(
13799 &trigger.trigger.target,
13800 TriggerTarget::Table(target) if target == table_name
13801 ) {
13802 trigger.trigger = trigger.trigger.renamed_update_column(
13803 column_name,
13804 new_column_name.clone(),
13805 epoch.0,
13806 )?;
13807 }
13808 }
13809 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
13810 commit_prepare_checkpoint(control, role_index)?;
13811 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
13812 commit_prepare_checkpoint(control, permission_index)?;
13813 rename_permission_column(
13814 permission,
13815 table_name,
13816 column_name,
13817 new_column_name,
13818 );
13819 }
13820 }
13821 advance_security_version(&mut next_catalog)?;
13822 }
13823 self.apply_catalog_command_to(&mut next_catalog, command)?;
13829 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
13830 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13831 commit_prepare_checkpoint(control, 0)?;
13832 let mut entered_fence = false;
13833 if let Some(before_commit) = before_commit.as_deref_mut() {
13834 before_commit()?;
13835 entered_fence = true;
13836 }
13837 let commit_result: Result<Epoch> = (|| {
13838 let commit_seq = {
13839 let mut wal = self.shared_wal.lock();
13840 let append: Result<u64> = (|| {
13841 wal.append(
13842 txn_id,
13843 table_id,
13844 crate::wal::Op::Ddl(DdlOp::AlterTable {
13845 table_id,
13846 column_json,
13847 }),
13848 )?;
13849 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13850 wal.append_commit(txn_id, epoch, &[])
13851 })();
13852 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13853 };
13854 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13855 durable_epoch.set(Some(epoch));
13856
13857 table.apply_altered_schema_prepared(prepared_schema);
13858 let schema = table.schema().clone();
13859 let table_checkpoint = table.checkpoint_altered_schema();
13860 drop(table);
13861 next_catalog.tables[catalog_entry_index].schema = schema;
13862 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13863 let catalog_result =
13864 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
13865 let security_version = next_catalog.security_version;
13866 *self.catalog.write() = next_catalog;
13867 if renamed_column.is_some() {
13868 self.security_coordinator
13869 .version
13870 .store(security_version, Ordering::Release);
13871 }
13872 self.publish_committed(&receipt, epoch)?;
13873 _epoch_guard.disarm();
13874 if let Err(error) = table_checkpoint.and(catalog_result) {
13875 self.poisoned.store(true, Ordering::Relaxed);
13876 self.lifecycle.poison();
13877 return Err(MongrelError::DurableCommit {
13878 epoch: epoch.0,
13879 message: error.to_string(),
13880 });
13881 }
13882 Ok(epoch)
13883 })();
13884 let commit_result = if entered_fence {
13885 finish_controlled_commit_attempt(commit_result, &mut after_commit)
13886 } else {
13887 commit_result
13888 };
13889 let epoch = commit_result?;
13890 Ok((column, Some(epoch)))
13891 })();
13892 result.map_err(|error| match (durable_epoch.get(), error) {
13893 (_, error @ MongrelError::DurableCommit { .. }) => error,
13894 (Some(epoch), error) => MongrelError::DurableCommit {
13895 epoch: epoch.0,
13896 message: error.to_string(),
13897 },
13898 (None, error) => error,
13899 })
13900 }
13901
13902 pub fn set_table_ttl(
13905 &self,
13906 table_name: &str,
13907 column_name: &str,
13908 duration_nanos: u64,
13909 ) -> Result<crate::manifest::TtlPolicy> {
13910 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
13911 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
13912 }
13913
13914 #[doc(hidden)]
13916 pub fn set_building_table_ttl(
13917 &self,
13918 table_name: &str,
13919 column_name: &str,
13920 duration_nanos: u64,
13921 ) -> Result<crate::manifest::TtlPolicy> {
13922 let policy = self.replace_table_ttl_with_state(
13923 table_name,
13924 Some((column_name, duration_nanos)),
13925 true,
13926 )?;
13927 policy
13928 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
13929 }
13930
13931 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
13932 self.replace_table_ttl(table_name, None)?;
13933 Ok(())
13934 }
13935
13936 fn replace_table_ttl(
13937 &self,
13938 table_name: &str,
13939 requested: Option<(&str, u64)>,
13940 ) -> Result<Option<crate::manifest::TtlPolicy>> {
13941 self.replace_table_ttl_with_state(table_name, requested, false)
13942 }
13943
13944 fn replace_table_ttl_with_state(
13945 &self,
13946 table_name: &str,
13947 requested: Option<(&str, u64)>,
13948 building: bool,
13949 ) -> Result<Option<crate::manifest::TtlPolicy>> {
13950 use crate::wal::DdlOp;
13951 use std::sync::atomic::Ordering;
13952
13953 self.require(&crate::auth::Permission::Ddl)?;
13954 if self.poisoned.load(Ordering::Relaxed) {
13955 return Err(MongrelError::Other(
13956 "database poisoned by fsync error".into(),
13957 ));
13958 }
13959
13960 let _g = self.ddl_lock.lock();
13961 let _security_write = self.security_write()?;
13962 self.require(&crate::auth::Permission::Ddl)?;
13963 let table_id = {
13964 let cat = self.catalog.read();
13965 if building {
13966 cat.building(table_name)
13967 } else {
13968 cat.live(table_name)
13969 }
13970 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
13971 .table_id
13972 };
13973 let handle =
13974 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
13975 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
13976 })?;
13977 let mut table = handle.lock();
13978 let policy = match requested {
13979 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
13980 None => None,
13981 };
13982 if table.ttl() == policy {
13983 return Ok(policy);
13984 }
13985
13986 let commit_lock = Arc::clone(&self.commit_lock);
13987 let _c = commit_lock.lock();
13988 let epoch = self.epoch.bump_assigned();
13989 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13990 let txn_id = self.alloc_txn_id()?;
13991 let policy_json = DdlOp::encode_ttl(policy)?;
13992 let mut next_catalog = self.catalog.read().clone();
13993 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13994 let commit_seq = {
13995 let mut wal = self.shared_wal.lock();
13996 let append: Result<u64> = (|| {
13997 wal.append(
13998 txn_id,
13999 table_id,
14000 crate::wal::Op::Ddl(DdlOp::SetTtl {
14001 table_id,
14002 policy_json,
14003 }),
14004 )?;
14005 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14006 wal.append_commit(txn_id, epoch, &[])
14007 })();
14008 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14009 };
14010 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14011
14012 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
14013 drop(table);
14014 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
14015 publish_error.get_or_insert(error);
14016 }
14017 self.finish_durable_publish(
14018 epoch,
14019 &mut _epoch_guard,
14020 &receipt,
14021 publish_error.map_or(Ok(()), Err),
14022 )?;
14023 Ok(policy)
14024 }
14025
14026 pub fn gc(&self) -> Result<usize> {
14032 let control = crate::ExecutionControl::new(None);
14033 self.gc_controlled(&control, || true)
14034 }
14035
14036 #[doc(hidden)]
14039 pub fn gc_controlled<F>(
14040 &self,
14041 control: &crate::ExecutionControl,
14042 before_publish: F,
14043 ) -> Result<usize>
14044 where
14045 F: FnOnce() -> bool,
14046 {
14047 self.gc_controlled_with_receipt(control, before_publish)
14048 .map(|(reclaimed, _)| reclaimed)
14049 }
14050
14051 #[doc(hidden)]
14054 pub fn gc_controlled_with_receipt<F>(
14055 &self,
14056 control: &crate::ExecutionControl,
14057 before_publish: F,
14058 ) -> Result<(usize, Option<MaintenanceReceipt>)>
14059 where
14060 F: FnOnce() -> bool,
14061 {
14062 enum Candidate {
14063 Directory(PathBuf),
14064 File(PathBuf),
14065 }
14066
14067 self.require(&crate::auth::Permission::Ddl)?;
14068 let _operation = self.admit_operation()?;
14070 let _ddl = self.ddl_lock.lock();
14071 self.require(&crate::auth::Permission::Ddl)?;
14072 control.checkpoint()?;
14073 let maintenance_epoch = self.epoch.visible();
14074 let min_active = self.snapshots.min_active(maintenance_epoch).0;
14075 let mut candidates = Vec::new();
14076
14077 let cat = self.catalog.read();
14079 for (entry_index, entry) in cat.tables.iter().enumerate() {
14080 if entry_index % 256 == 0 {
14081 control.checkpoint()?;
14082 }
14083 if let TableState::Dropped { at_epoch } = entry.state {
14084 if at_epoch <= min_active {
14085 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
14086 if tdir.exists() {
14087 candidates.push(Candidate::Directory(tdir));
14088 }
14089 }
14090 }
14091 }
14092 drop(cat);
14093
14094 let cat = self.catalog.read();
14099 for (entry_index, entry) in cat.tables.iter().enumerate() {
14100 if entry_index % 256 == 0 {
14101 control.checkpoint()?;
14102 }
14103 if !matches!(entry.state, TableState::Live) {
14104 continue;
14105 }
14106 let txn_dir = self
14107 .root
14108 .join(TABLES_DIR)
14109 .join(entry.table_id.to_string())
14110 .join("_txn");
14111 if !txn_dir.exists() {
14112 continue;
14113 }
14114 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
14115 if sub_index % 256 == 0 {
14116 control.checkpoint()?;
14117 }
14118 let sub = sub?;
14119 let name = sub.file_name();
14120 let Some(name) = name.to_str() else { continue };
14121 let is_active = name
14123 .parse::<u64>()
14124 .map(|id| self.active_spills.is_active(id))
14125 .unwrap_or(false);
14126 if is_active {
14127 continue;
14128 }
14129 candidates.push(Candidate::Directory(sub.path()));
14130 }
14131 }
14132 drop(cat);
14133
14134 let external_names = {
14135 let cat = self.catalog.read();
14136 cat.external_tables
14137 .iter()
14138 .map(|entry| entry.name.clone())
14139 .collect::<std::collections::HashSet<_>>()
14140 };
14141 let vtab_dir = self.root.join(VTAB_DIR);
14142 if vtab_dir.exists() {
14143 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
14144 if entry_index % 256 == 0 {
14145 control.checkpoint()?;
14146 }
14147 let entry = entry?;
14148 let name = entry.file_name();
14149 let Some(name) = name.to_str() else { continue };
14150 if external_names.contains(name) {
14151 continue;
14152 }
14153 let path = entry.path();
14154 if path.is_dir() {
14155 candidates.push(Candidate::Directory(path));
14156 } else {
14157 candidates.push(Candidate::File(path));
14158 }
14159 }
14160 }
14161
14162 let tables = self
14166 .tables
14167 .read()
14168 .iter()
14169 .map(|(table_id, handle)| (*table_id, handle.clone()))
14170 .collect::<Vec<_>>();
14171 let mut retiring = Vec::new();
14172 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
14173 if table_index % 256 == 0 {
14174 control.checkpoint()?;
14175 }
14176 let backup_pinned: HashSet<u128> = self
14177 .backup_pins
14178 .lock()
14179 .keys()
14180 .filter_map(|(pinned_table, run_id)| {
14181 (*pinned_table == *table_id).then_some(*run_id)
14182 })
14183 .collect();
14184 if handle
14185 .lock()
14186 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
14187 {
14188 retiring.push((handle.clone(), backup_pinned));
14189 }
14190 }
14191
14192 let all_durable = self.active_spills.is_idle()
14200 && tables.iter().all(|(_, handle)| {
14201 let g = handle.lock();
14202 g.memtable_len() == 0 && g.mutable_run_len() == 0
14203 });
14204 let retain = self
14205 .replication_wal_retention_segments
14206 .load(std::sync::atomic::Ordering::Relaxed);
14207 let reap_wal = all_durable
14208 && self
14209 .shared_wal
14210 .lock()
14211 .has_gc_segments_retain_recent(retain)?;
14212
14213 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
14214 return Ok((0, None));
14215 }
14216 control.checkpoint()?;
14217 if !before_publish() {
14218 return Err(MongrelError::Cancelled);
14219 }
14220
14221 let mut reclaimed = 0;
14222 for candidate in candidates {
14223 match candidate {
14224 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
14225 Candidate::File(path) => std::fs::remove_file(path)?,
14226 }
14227 reclaimed += 1;
14228 }
14229 for (handle, backup_pinned) in retiring {
14230 reclaimed += handle
14231 .lock()
14232 .reap_retiring(Epoch(min_active), &backup_pinned)?;
14233 }
14234 if reap_wal {
14235 reclaimed += self
14236 .shared_wal
14237 .lock()
14238 .gc_segments_retain_recent(u64::MAX, retain)?;
14239 }
14240
14241 Ok((
14242 reclaimed,
14243 Some(MaintenanceReceipt {
14244 epoch: maintenance_epoch,
14245 }),
14246 ))
14247 }
14248
14249 pub fn checkpoint(&self) -> Result<()> {
14267 self.checkpoint_controlled(|| Ok(()))
14268 }
14269
14270 #[doc(hidden)]
14273 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
14274 where
14275 F: FnOnce() -> Result<()>,
14276 {
14277 self.require(&crate::auth::Permission::Ddl)?;
14278 let _operation = self.admit_operation()?;
14280 let _replication = self.replication_barrier.write();
14284 let _ddl = self.ddl_lock.lock();
14285 let _security = self.security_coordinator.gate.read();
14286 self.require(&crate::auth::Permission::Ddl)?;
14287
14288 let mut handles = self
14289 .tables
14290 .read()
14291 .iter()
14292 .map(|(table_id, handle)| (*table_id, handle.clone()))
14293 .collect::<Vec<_>>();
14294 handles.sort_by_key(|(table_id, _)| *table_id);
14295 let mut tables = handles
14296 .iter()
14297 .map(|(table_id, handle)| (*table_id, handle.lock()))
14298 .collect::<Vec<_>>();
14299
14300 for (_, table) in &mut tables {
14302 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
14303 {
14304 table.force_flush()?;
14305 }
14306 }
14307
14308 for (_, table) in &mut tables {
14311 if table.run_count() >= 2 || table.should_compact() {
14312 table.compact()?;
14313 }
14314 }
14315
14316 before_wal_reset()?;
14317
14318 let maintenance_epoch = self.epoch.visible();
14320 let min_active = self.snapshots.min_active(maintenance_epoch);
14321 for (table_id, table) in &mut tables {
14322 let backup_pinned: HashSet<u128> = self
14323 .backup_pins
14324 .lock()
14325 .keys()
14326 .filter_map(|(pinned_table, run_id)| {
14327 (*pinned_table == *table_id).then_some(*run_id)
14328 })
14329 .collect();
14330 table.reap_retiring(min_active, &backup_pinned)?;
14331 }
14332
14333 self.shared_wal.lock().reset_after_checkpoint()?;
14336
14337 let catalog_snapshot = self.catalog.read().clone();
14339 for entry in &catalog_snapshot.tables {
14340 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
14341 crate::durable_file::remove_directory_all(
14342 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
14343 )?;
14344 }
14345 if !matches!(entry.state, TableState::Live) {
14346 continue;
14347 }
14348 let transaction_dir = self
14349 .root
14350 .join(TABLES_DIR)
14351 .join(entry.table_id.to_string())
14352 .join("_txn");
14353 if transaction_dir.is_dir() {
14354 for child in std::fs::read_dir(&transaction_dir)? {
14355 let child = child?;
14356 let active = child
14357 .file_name()
14358 .to_str()
14359 .and_then(|name| name.parse::<u64>().ok())
14360 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
14361 if !active {
14362 crate::durable_file::remove_directory_all(&child.path())?;
14363 }
14364 }
14365 }
14366 }
14367 let external_names = catalog_snapshot
14368 .external_tables
14369 .iter()
14370 .map(|entry| entry.name.as_str())
14371 .collect::<HashSet<_>>();
14372 let external_root = self.root.join(VTAB_DIR);
14373 if external_root.is_dir() {
14374 for entry in std::fs::read_dir(&external_root)? {
14375 let entry = entry?;
14376 let name = entry.file_name();
14377 if name
14378 .to_str()
14379 .is_some_and(|name| external_names.contains(name))
14380 {
14381 continue;
14382 }
14383 if entry.file_type()?.is_dir() {
14384 crate::durable_file::remove_directory_all(&entry.path())?;
14385 } else {
14386 std::fs::remove_file(entry.path())?;
14387 crate::durable_file::sync_directory(&external_root)?;
14388 }
14389 }
14390 }
14391
14392 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
14395 let visible = self.epoch.visible();
14396 for (_, table) in &tables {
14397 table.persist_manifest(visible)?;
14398 }
14399
14400 Ok(())
14401 }
14402 fn alloc_txn_id(&self) -> Result<u64> {
14403 self.ensure_owner_process()?;
14404 crate::txn::allocate_txn_id(&self.next_txn_id)
14405 }
14406
14407 pub fn allocate_lock_txn_id(&self) -> Result<u64> {
14411 self.alloc_txn_id()
14412 }
14413
14414 pub fn set_spill_threshold(&self, bytes: u64) {
14418 self.spill_threshold
14419 .store(bytes, std::sync::atomic::Ordering::Relaxed);
14420 }
14421
14422 #[doc(hidden)]
14426 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14427 *self.spill_hook.lock() = Some(Box::new(f));
14428 }
14429
14430 #[doc(hidden)]
14433 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14434 *self.security_commit_hook.lock() = Some(Box::new(f));
14435 }
14436
14437 #[doc(hidden)]
14440 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14441 *self.catalog_commit_hook.lock() = Some(Box::new(f));
14442 }
14443
14444 #[doc(hidden)]
14447 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14448 *self.backup_hook.lock() = Some(Box::new(f));
14449 }
14450
14451 #[doc(hidden)]
14455 pub fn __set_fk_lock_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14456 *self.fk_lock_hook.lock() = Some(Arc::new(f));
14457 }
14458
14459 #[doc(hidden)]
14461 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14462 *self.replication_hook.lock() = Some(Box::new(f));
14463 }
14464
14465 #[doc(hidden)]
14469 pub fn __wal_group_sync_count(&self) -> u64 {
14470 self.shared_wal.lock().group_sync_count()
14471 }
14472
14473 #[doc(hidden)]
14476 pub fn __poison(&self) {
14477 self.poisoned
14478 .store(true, std::sync::atomic::Ordering::Relaxed);
14479 }
14480
14481 pub fn check(&self) -> Vec<CheckIssue> {
14494 match self.check_inner(None) {
14495 Ok(issues) => issues,
14496 Err(error) => vec![CheckIssue {
14497 table_id: WAL_TABLE_ID,
14498 table_name: "shared WAL".into(),
14499 severity: "error".into(),
14500 description: error.to_string(),
14501 }],
14502 }
14503 }
14504
14505 #[doc(hidden)]
14507 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
14508 self.check_inner(Some(control))
14509 }
14510
14511 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
14512 let mut issues = Vec::new();
14513 let cat = self.catalog.read();
14514 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
14515 for (table_index, entry) in cat.tables.iter().enumerate() {
14516 if table_index % 256 == 0 {
14517 if let Some(control) = control {
14518 control.checkpoint()?;
14519 }
14520 }
14521 if !matches!(entry.state, TableState::Live) {
14522 continue;
14523 }
14524 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
14525 let mut err = |sev: &str, desc: String| {
14526 issues.push(CheckIssue {
14527 table_id: entry.table_id,
14528 table_name: entry.name.clone(),
14529 severity: sev.into(),
14530 description: desc,
14531 });
14532 };
14533 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
14534 Ok(m) => m,
14535 Err(e) => {
14536 err("error", format!("manifest read failed: {e}"));
14537 continue;
14538 }
14539 };
14540 if m.flushed_epoch > m.current_epoch {
14541 err(
14542 "error",
14543 format!(
14544 "flushed_epoch {} exceeds current_epoch {} (impossible)",
14545 m.flushed_epoch, m.current_epoch
14546 ),
14547 );
14548 }
14549
14550 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
14551 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
14552 for (run_index, rr) in m.runs.iter().enumerate() {
14553 if run_index % 256 == 0 {
14554 if let Some(control) = control {
14555 control.checkpoint()?;
14556 }
14557 }
14558 referenced.insert(rr.run_id);
14559 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
14560 if !run_path.exists() {
14561 err("error", format!("missing run file: r-{}.sr", rr.run_id));
14562 continue;
14563 }
14564 match crate::sorted_run::RunReader::open(
14565 &run_path,
14566 entry.schema.clone(),
14567 self.kek.clone(),
14568 ) {
14569 Ok(reader) => {
14570 if reader.row_count() as u64 != rr.row_count {
14571 err(
14572 "error",
14573 format!(
14574 "run r-{} row count mismatch: manifest {} vs run {}",
14575 rr.run_id,
14576 rr.row_count,
14577 reader.row_count()
14578 ),
14579 );
14580 }
14581 }
14582 Err(e) => {
14583 err(
14584 "error",
14585 format!("run r-{} integrity check failed: {e}", rr.run_id),
14586 );
14587 }
14588 }
14589 }
14590
14591 for r in &m.retiring {
14595 referenced.insert(r.run_id);
14596 }
14597
14598 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
14600 for (entry_index, ent) in rd.flatten().enumerate() {
14601 if entry_index % 256 == 0 {
14602 if let Some(control) = control {
14603 control.checkpoint()?;
14604 }
14605 }
14606 let p = ent.path();
14607 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
14608 continue;
14609 }
14610 let run_id = p
14611 .file_stem()
14612 .and_then(|s| s.to_str())
14613 .and_then(|s| s.strip_prefix("r-"))
14614 .and_then(|s| s.parse::<u128>().ok());
14615 if let Some(id) = run_id {
14616 if !referenced.contains(&id) {
14617 err(
14618 "warning",
14619 format!("orphan run file r-{id}.sr not referenced by the manifest"),
14620 );
14621 }
14622 }
14623 }
14624 }
14625 }
14626
14627 let external_names = cat
14628 .external_tables
14629 .iter()
14630 .map(|entry| entry.name.clone())
14631 .collect::<std::collections::HashSet<_>>();
14632 let vtab_dir = self.root.join(VTAB_DIR);
14633 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
14634 for (entry_index, entry) in entries.flatten().enumerate() {
14635 if entry_index % 256 == 0 {
14636 if let Some(control) = control {
14637 control.checkpoint()?;
14638 }
14639 }
14640 let name = entry.file_name();
14641 let Some(name) = name.to_str() else { continue };
14642 if !external_names.contains(name) {
14643 issues.push(CheckIssue {
14644 table_id: EXTERNAL_TABLE_ID,
14645 table_name: name.to_string(),
14646 severity: "warning".into(),
14647 description: format!(
14648 "orphan external table state entry {:?} not referenced by the catalog",
14649 entry.path()
14650 ),
14651 });
14652 }
14653 }
14654 }
14655
14656 if let Some(control) = control {
14663 control.checkpoint()?;
14664 }
14665 for (seg, msg) in self.shared_wal.lock().verify_segments() {
14666 issues.push(CheckIssue {
14667 table_id: WAL_TABLE_ID,
14668 table_name: "<wal>".into(),
14669 severity: "error".into(),
14670 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
14671 });
14672 }
14673 Ok(issues)
14674 }
14675
14676 pub fn doctor(&self) -> Result<Vec<u64>> {
14680 let control = crate::ExecutionControl::new(None);
14681 self.doctor_controlled(&control, || true)
14682 }
14683
14684 #[doc(hidden)]
14688 pub fn doctor_controlled<F>(
14689 &self,
14690 control: &crate::ExecutionControl,
14691 before_publish: F,
14692 ) -> Result<Vec<u64>>
14693 where
14694 F: FnOnce() -> bool,
14695 {
14696 self.doctor_controlled_with_receipt(control, before_publish)
14697 .map(|(quarantined, _)| quarantined)
14698 }
14699
14700 #[doc(hidden)]
14703 pub fn doctor_controlled_with_receipt<F>(
14704 &self,
14705 control: &crate::ExecutionControl,
14706 before_publish: F,
14707 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
14708 where
14709 F: FnOnce() -> bool,
14710 {
14711 let _ddl = self.ddl_lock.lock();
14714 let _security_write = self.security_write()?;
14715 let issues = self.check_inner(Some(control))?;
14716 let bad_tables: std::collections::HashSet<u64> = issues
14721 .iter()
14722 .filter(|i| {
14723 i.severity == "error"
14724 && i.table_id != WAL_TABLE_ID
14725 && i.table_id != EXTERNAL_TABLE_ID
14726 })
14727 .map(|i| i.table_id)
14728 .collect();
14729 if bad_tables.is_empty() {
14730 return Ok((Vec::new(), None));
14731 }
14732 let _commit = self.commit_lock.lock();
14733 control.checkpoint()?;
14734 if !before_publish() {
14735 return Err(MongrelError::Cancelled);
14736 }
14737 let maintenance_epoch = self.epoch.bump_assigned();
14738 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
14739
14740 let qdir = self.root.join("_quarantine");
14741 crate::durable_file::create_directory(&qdir)?;
14742 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
14743 bad_tables.sort_unstable();
14744
14745 let mut handles = self
14749 .tables
14750 .read()
14751 .iter()
14752 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
14753 .map(|(table_id, handle)| (*table_id, handle.clone()))
14754 .collect::<Vec<_>>();
14755 handles.sort_by_key(|(table_id, _)| *table_id);
14756 let mut table_guards = handles
14757 .iter()
14758 .map(|(table_id, handle)| (*table_id, handle.lock()))
14759 .collect::<Vec<_>>();
14760
14761 let mut next_catalog = self.catalog.read().clone();
14762 for table_id in &bad_tables {
14763 if let Some(entry) = next_catalog
14764 .tables
14765 .iter_mut()
14766 .find(|entry| entry.table_id == *table_id)
14767 {
14768 entry.state = TableState::Dropped {
14769 at_epoch: maintenance_epoch.0,
14770 };
14771 }
14772 }
14773 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
14774
14775 let txn_id = self.alloc_txn_id()?;
14776 let commit_seq = {
14777 let mut wal = self.shared_wal.lock();
14778 let append: Result<u64> = (|| {
14779 for table_id in &bad_tables {
14780 wal.append(
14781 txn_id,
14782 *table_id,
14783 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
14784 table_id: *table_id,
14785 }),
14786 )?;
14787 }
14788 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14789 wal.append_commit(txn_id, maintenance_epoch, &[])
14790 })();
14791 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
14792 };
14793 let receipt = self.await_durable_commit(txn_id, commit_seq, maintenance_epoch)?;
14794 for (_, table) in &mut table_guards {
14795 table.mark_unavailable_after_quarantine();
14796 }
14797 {
14798 let mut live_tables = self.tables.write();
14799 for table_id in &bad_tables {
14800 live_tables.remove(table_id);
14801 }
14802 }
14803 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14804 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, &receipt, checkpoint)?;
14805
14806 for table_id in &bad_tables {
14810 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
14811 if source.exists() {
14812 let destination = qdir.join(table_id.to_string());
14813 if let Err(error) = crate::durable_file::rename(&source, &destination) {
14814 return Err(MongrelError::DurableCommit {
14815 epoch: maintenance_epoch.0,
14816 message: format!(
14817 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
14818 ),
14819 });
14820 }
14821 }
14822 }
14823 Ok((
14824 bad_tables,
14825 Some(MaintenanceReceipt {
14826 epoch: maintenance_epoch,
14827 }),
14828 ))
14829 }
14830
14831 #[allow(dead_code)]
14833 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
14834 self.kek.as_ref()
14835 }
14836
14837 #[allow(dead_code)]
14839 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
14840 &self.epoch
14841 }
14842
14843 #[allow(dead_code)]
14845 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
14846 &self.snapshots
14847 }
14848}
14849
14850fn external_state_dir(root: &Path, name: &str) -> PathBuf {
14851 root.join(VTAB_DIR).join(name)
14852}
14853
14854fn append_catalog_snapshot(
14855 wal: &mut crate::wal::SharedWal,
14856 txn_id: u64,
14857 catalog: &Catalog,
14858) -> Result<()> {
14859 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
14860 wal.append(
14861 txn_id,
14862 WAL_TABLE_ID,
14863 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
14864 )?;
14865 Ok(())
14866}
14867
14868fn filter_ignored_staging(
14869 staging: Vec<(u64, crate::txn::Staged)>,
14870 ignored_indices: &std::collections::BTreeSet<usize>,
14871) -> Vec<(u64, crate::txn::Staged)> {
14872 if ignored_indices.is_empty() {
14873 return staging;
14874 }
14875 staging
14876 .into_iter()
14877 .enumerate()
14878 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
14879 .collect()
14880}
14881
14882fn external_state_file(root: &Path, name: &str) -> PathBuf {
14883 external_state_dir(root, name).join("state.json")
14884}
14885
14886fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
14887 let path = external_state_file(root, name);
14888 match std::fs::read(path) {
14889 Ok(bytes) => Ok(bytes),
14890 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
14891 Err(e) => Err(e.into()),
14892 }
14893}
14894
14895fn current_external_state_bytes(
14896 root: &Path,
14897 external_states: &[(String, Vec<u8>)],
14898 name: &str,
14899) -> Result<Vec<u8>> {
14900 for (table, state) in external_states.iter().rev() {
14901 if table == name {
14902 return Ok(state.clone());
14903 }
14904 }
14905 read_external_state_file(root, name)
14906}
14907
14908fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
14909 let mut out = external_states;
14910 dedup_external_states_in_place(&mut out);
14911 out
14912}
14913
14914fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
14915 let mut seen = std::collections::HashSet::new();
14916 let mut out = Vec::with_capacity(external_states.len());
14917 for (name, state) in std::mem::take(external_states).into_iter().rev() {
14918 if seen.insert(name.clone()) {
14919 out.push((name, state));
14920 }
14921 }
14922 out.reverse();
14923 *external_states = out;
14924}
14925
14926fn prepare_external_state_file(
14927 root: &Path,
14928 name: &str,
14929 state: &[u8],
14930 txn_id: u64,
14931) -> Result<PathBuf> {
14932 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
14933 let dir = external_state_dir(root, name);
14934 crate::durable_file::create_directory(&dir)?;
14935 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
14936 {
14937 let mut file = std::fs::OpenOptions::new()
14938 .create_new(true)
14939 .write(true)
14940 .open(&pending)?;
14941 file.write_all(state)?;
14942 file.sync_all()?;
14943 }
14944 Ok(pending)
14945}
14946
14947fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
14948 let path = external_state_file(root, name);
14949 crate::durable_file::replace(pending, &path)?;
14950 Ok(())
14951}
14952
14953fn write_external_state_file(
14954 durable: &crate::durable_file::DurableRoot,
14955 name: &str,
14956 state: &[u8],
14957) -> Result<()> {
14958 let directory = Path::new(VTAB_DIR).join(name);
14959 durable.create_directory_all(&directory)?;
14960 durable.write_atomic(directory.join("state.json"), state)?;
14961 Ok(())
14962}
14963
14964fn validate_recovered_data_table(
14965 catalog: &Catalog,
14966 tables: &HashMap<u64, TableHandle>,
14967 table_id: u64,
14968 commit_epoch: u64,
14969 offset: u64,
14970) -> Result<bool> {
14971 let entry = catalog
14972 .tables
14973 .iter()
14974 .find(|entry| entry.table_id == table_id)
14975 .ok_or_else(|| MongrelError::CorruptWal {
14976 offset,
14977 reason: format!("committed record references unknown table {table_id}"),
14978 })?;
14979 if commit_epoch < entry.created_epoch {
14980 return Err(MongrelError::CorruptWal {
14981 offset,
14982 reason: format!(
14983 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
14984 entry.created_epoch
14985 ),
14986 });
14987 }
14988 match entry.state {
14989 TableState::Dropped { at_epoch } => {
14990 let abandoned_build_boundary =
14995 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
14996 if commit_epoch >= at_epoch && !abandoned_build_boundary {
14997 Err(MongrelError::CorruptWal {
14998 offset,
14999 reason: format!(
15000 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
15001 ),
15002 })
15003 } else {
15004 Ok(false)
15005 }
15006 }
15007 TableState::Live | TableState::Building { .. } => {
15008 if tables.contains_key(&table_id) {
15009 Ok(true)
15010 } else {
15011 Err(MongrelError::CorruptWal {
15012 offset,
15013 reason: format!("live table {table_id} has no mounted recovery handle"),
15014 })
15015 }
15016 }
15017 }
15018}
15019
15020type RecoveryTableStage = (
15021 Vec<crate::memtable::Row>,
15022 Vec<(crate::rowid::RowId, Epoch)>,
15023 Option<Epoch>,
15024 Epoch,
15025);
15026
15027#[derive(Clone)]
15028struct RecoveryValidationTable {
15029 schema: Schema,
15030 flushed_epoch: u64,
15031}
15032
15033fn validate_shared_wal_recovery_plan(
15034 durable_root: &crate::durable_file::DurableRoot,
15035 catalog: &Catalog,
15036 recovered_table_ids: &HashSet<u64>,
15037 reconciled_table_ids: &HashSet<u64>,
15038 meta_dek: Option<&[u8; META_DEK_LEN]>,
15039 kek: Option<Arc<crate::encryption::Kek>>,
15040 records: &[crate::wal::Record],
15041) -> Result<()> {
15042 use crate::wal::{DdlOp, Op};
15043
15044 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
15045 for entry in &catalog.tables {
15046 if !matches!(entry.state, TableState::Live) {
15047 continue;
15048 }
15049 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15050 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
15051 Ok(manifest) => Some(manifest),
15052 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
15053 Err(error) => return Err(error),
15054 };
15055 let flushed_epoch = if let Some(manifest) = manifest {
15056 if manifest.table_id != entry.table_id {
15057 return Err(MongrelError::Conflict(format!(
15058 "catalog table {} storage identity mismatch",
15059 entry.table_id
15060 )));
15061 }
15062 if (manifest.schema_id != entry.schema.schema_id
15063 && !reconciled_table_ids.contains(&entry.table_id))
15064 || manifest.flushed_epoch > manifest.current_epoch
15065 || manifest.global_idx_epoch > manifest.current_epoch
15066 || manifest.next_row_id == u64::MAX
15067 || manifest.auto_inc_next < 0
15068 || manifest.auto_inc_next == i64::MAX
15069 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
15070 {
15071 return Err(MongrelError::InvalidArgument(format!(
15072 "table {} manifest counters or schema identity are invalid",
15073 entry.table_id
15074 )));
15075 }
15076 #[cfg(feature = "encryption")]
15077 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
15078 #[cfg(not(feature = "encryption"))]
15079 let idx_dek: Option<zeroize::Zeroizing<[u8; 32]>> = None;
15080 crate::global_idx::read_durable_for(
15081 durable_root,
15082 &relative_dir,
15083 entry.table_id,
15084 &entry.schema,
15085 idx_dek.as_deref(),
15086 )?;
15087 let mut run_ids = HashSet::new();
15088 let mut maximum_row_id = None::<u64>;
15089 for run in &manifest.runs {
15090 if run.run_id >= u64::MAX as u128
15091 || run.epoch_created > manifest.current_epoch
15092 || !run_ids.insert(run.run_id)
15093 {
15094 return Err(MongrelError::InvalidArgument(format!(
15095 "table {} manifest contains an invalid or duplicate run id",
15096 entry.table_id
15097 )));
15098 }
15099 let relative = relative_dir
15100 .join(crate::engine::RUNS_DIR)
15101 .join(format!("r-{}.sr", run.run_id as u64));
15102 let file = durable_root.open_regular(&relative)?;
15103 let mut reader = crate::sorted_run::RunReader::open_file(
15104 file,
15105 entry.schema.clone(),
15106 kek.clone(),
15107 )?;
15108 let header = reader.header();
15109 if header.run_id != run.run_id
15110 || header.level != run.level
15111 || header.row_count != run.row_count
15112 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
15113 || header.is_uniform_epoch() && header.epoch_created != 0
15114 || header.schema_id > entry.schema.schema_id
15115 {
15116 return Err(MongrelError::InvalidArgument(format!(
15117 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
15118 entry.table_id,
15119 run.run_id,
15120 header.run_id,
15121 header.level,
15122 header.row_count,
15123 header.epoch_created,
15124 header.schema_id,
15125 run.run_id,
15126 run.level,
15127 run.row_count,
15128 run.epoch_created,
15129 entry.schema.schema_id,
15130 )));
15131 }
15132 if header.row_count != 0 {
15133 maximum_row_id = Some(
15134 maximum_row_id
15135 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
15136 );
15137 }
15138 reader.validate_all_pages()?;
15139 }
15140 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
15141 return Err(MongrelError::InvalidArgument(format!(
15142 "table {} next_row_id does not advance beyond persisted rows",
15143 entry.table_id
15144 )));
15145 }
15146 for run in &manifest.retiring {
15147 if run.run_id >= u64::MAX as u128
15148 || run.retire_epoch > manifest.current_epoch
15149 || !run_ids.insert(run.run_id)
15150 {
15151 return Err(MongrelError::InvalidArgument(format!(
15152 "table {} manifest contains an invalid or aliased retired run",
15153 entry.table_id
15154 )));
15155 }
15156 }
15157 manifest.flushed_epoch
15158 } else {
15159 if !recovered_table_ids.contains(&entry.table_id) {
15160 return Err(MongrelError::NotFound(format!(
15161 "live table {} manifest is missing",
15162 entry.table_id
15163 )));
15164 }
15165 0
15166 };
15167 tables.insert(
15168 entry.table_id,
15169 RecoveryValidationTable {
15170 schema: entry.schema.clone(),
15171 flushed_epoch,
15172 },
15173 );
15174 }
15175
15176 let committed = records
15177 .iter()
15178 .filter_map(|record| match record.op {
15179 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
15180 _ => None,
15181 })
15182 .collect::<HashMap<_, _>>();
15183 let mut run_ids = HashSet::new();
15184 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
15185 for record in records {
15186 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
15187 continue;
15188 };
15189 match &record.op {
15190 Op::Put { table_id, rows } => {
15191 let table = validate_recovery_data_table_plan(
15192 catalog,
15193 &tables,
15194 *table_id,
15195 commit_epoch,
15196 record.seq.0,
15197 )?;
15198 let decoded: Vec<crate::memtable::Row> =
15199 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
15200 offset: record.seq.0,
15201 reason: format!(
15202 "committed Put payload for transaction {} could not be decoded: {error}",
15203 record.txn_id
15204 ),
15205 })?;
15206 if let Some(table) = table {
15207 for row in &decoded {
15208 if !recovered_row_ids
15209 .entry(*table_id)
15210 .or_default()
15211 .insert(row.row_id.0)
15212 {
15213 return Err(MongrelError::CorruptWal {
15214 offset: record.seq.0,
15215 reason: format!(
15216 "committed WAL repeats recovered row id {} for table {table_id}",
15217 row.row_id.0
15218 ),
15219 });
15220 }
15221 validate_recovered_row(&table.schema, row)?;
15222 }
15223 }
15224 }
15225 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
15226 validate_recovery_data_table_plan(
15227 catalog,
15228 &tables,
15229 *table_id,
15230 commit_epoch,
15231 record.seq.0,
15232 )?;
15233 }
15234 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
15235 Op::Ddl(DdlOp::ResetExternalTableState {
15236 name,
15237 generation_epoch,
15238 }) => {
15239 if *generation_epoch != commit_epoch {
15240 return Err(MongrelError::CorruptWal {
15241 offset: record.seq.0,
15242 reason: format!(
15243 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
15244 ),
15245 });
15246 }
15247 validate_recovered_external_name(name)?;
15248 }
15249 Op::TxnCommit { added_runs, .. } => {
15250 for added in added_runs {
15251 let Some(table) = validate_recovery_data_table_plan(
15252 catalog,
15253 &tables,
15254 added.table_id,
15255 commit_epoch,
15256 record.seq.0,
15257 )?
15258 else {
15259 continue;
15260 };
15261 if added.run_id >= u64::MAX as u128
15262 || !run_ids.insert((added.table_id, added.run_id))
15263 {
15264 return Err(MongrelError::CorruptWal {
15265 offset: record.seq.0,
15266 reason: format!(
15267 "duplicate or invalid recovered run {} for table {}",
15268 added.run_id, added.table_id
15269 ),
15270 });
15271 }
15272 if commit_epoch <= table.flushed_epoch {
15273 continue;
15274 }
15275 validate_planned_spilled_run(
15276 durable_root,
15277 record.txn_id,
15278 commit_epoch,
15279 added,
15280 &table.schema,
15281 kek.clone(),
15282 )?;
15283 }
15284 }
15285 _ => {}
15286 }
15287 }
15288 Ok(())
15289}
15290
15291fn validate_recovery_data_table_plan<'a>(
15292 catalog: &Catalog,
15293 tables: &'a HashMap<u64, RecoveryValidationTable>,
15294 table_id: u64,
15295 commit_epoch: u64,
15296 offset: u64,
15297) -> Result<Option<&'a RecoveryValidationTable>> {
15298 let entry = catalog
15299 .tables
15300 .iter()
15301 .find(|entry| entry.table_id == table_id)
15302 .ok_or_else(|| MongrelError::CorruptWal {
15303 offset,
15304 reason: format!("committed record references unknown table {table_id}"),
15305 })?;
15306 if commit_epoch < entry.created_epoch {
15307 return Err(MongrelError::CorruptWal {
15308 offset,
15309 reason: format!(
15310 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
15311 entry.created_epoch
15312 ),
15313 });
15314 }
15315 match entry.state {
15316 TableState::Dropped { at_epoch } => {
15317 let abandoned =
15318 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
15319 if commit_epoch >= at_epoch && !abandoned {
15320 return Err(MongrelError::CorruptWal {
15321 offset,
15322 reason: format!(
15323 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
15324 ),
15325 });
15326 }
15327 Ok(None)
15328 }
15329 TableState::Live => {
15330 tables
15331 .get(&table_id)
15332 .map(Some)
15333 .ok_or_else(|| MongrelError::CorruptWal {
15334 offset,
15335 reason: format!("live table {table_id} has no recovery plan"),
15336 })
15337 }
15338 TableState::Building { .. } => Err(MongrelError::CorruptWal {
15339 offset,
15340 reason: format!("building table {table_id} was not normalized before recovery"),
15341 }),
15342 }
15343}
15344
15345fn validate_planned_spilled_run(
15346 root: &crate::durable_file::DurableRoot,
15347 txn_id: u64,
15348 commit_epoch: u64,
15349 added: &crate::wal::AddedRun,
15350 schema: &Schema,
15351 kek: Option<Arc<crate::encryption::Kek>>,
15352) -> Result<()> {
15353 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
15354 let destination = table
15355 .join(crate::engine::RUNS_DIR)
15356 .join(format!("r-{}.sr", added.run_id as u64));
15357 let pending = table
15358 .join("_txn")
15359 .join(txn_id.to_string())
15360 .join(format!("r-{}.sr", added.run_id as u64));
15361 let file = match root.open_regular(&destination) {
15362 Ok(file) => file,
15363 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
15364 root.open_regular(&pending).map_err(|pending_error| {
15365 if pending_error.kind() == std::io::ErrorKind::NotFound {
15366 MongrelError::CorruptWal {
15367 offset: commit_epoch,
15368 reason: format!(
15369 "committed spilled run {} for transaction {txn_id} is missing",
15370 added.run_id
15371 ),
15372 }
15373 } else {
15374 pending_error.into()
15375 }
15376 })?
15377 }
15378 Err(error) => return Err(error.into()),
15379 };
15380 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
15381 let header = reader.header();
15382 if header.run_id != added.run_id
15383 || header.content_hash != added.content_hash
15384 || header.row_count != added.row_count
15385 || header.level != added.level
15386 || header.min_row_id != added.min_row_id
15387 || header.max_row_id != added.max_row_id
15388 || header.schema_id != schema.schema_id
15389 || !header.is_uniform_epoch()
15390 || header.epoch_created != 0
15391 {
15392 return Err(MongrelError::CorruptWal {
15393 offset: commit_epoch,
15394 reason: format!(
15395 "committed spilled run {} metadata differs from WAL",
15396 added.run_id
15397 ),
15398 });
15399 }
15400 reader.validate_all_pages()?;
15401 Ok(())
15402}
15403
15404#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15424pub enum StagedTxnWrite {
15425 Put {
15430 table_id: u64,
15432 rows: Vec<u8>,
15434 },
15435 Delete {
15437 table_id: u64,
15439 row_ids: Vec<u64>,
15441 },
15442}
15443
15444impl StagedTxnWrite {
15445 pub fn encode(&self) -> Result<Vec<u8>> {
15447 Ok(bincode::serialize(self)?)
15448 }
15449
15450 pub fn decode(bytes: &[u8]) -> Result<Self> {
15452 Ok(bincode::deserialize(bytes)?)
15453 }
15454}
15455
15456pub fn translate_records_for_replication(
15493 records: &[crate::wal::Record],
15494) -> Result<Vec<crate::wal::Record>> {
15495 use crate::wal::Op;
15496
15497 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
15500 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
15501 })?;
15502 if records.iter().any(|record| record.txn_id != txn_id) {
15503 return Err(MongrelError::InvalidArgument(
15504 "replicated transaction payload mixes transaction ids".into(),
15505 ));
15506 }
15507 let commits = records
15508 .iter()
15509 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
15510 .count();
15511 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
15512 return Err(MongrelError::InvalidArgument(
15513 "replicated transaction payload must end in exactly one commit marker".into(),
15514 ));
15515 }
15516
15517 let mut spilled_rows: HashMap<u64, Vec<crate::memtable::Row>> = HashMap::new();
15521 for record in records {
15522 if let Op::SpilledRows { table_id, rows } = &record.op {
15523 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(rows).map_err(|error| {
15524 MongrelError::InvalidArgument(format!(
15525 "spilled row payload for table {table_id} cannot decode for replication: \
15526 {error}"
15527 ))
15528 })?;
15529 spilled_rows.entry(*table_id).or_default().extend(chunk);
15530 }
15531 }
15532
15533 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
15537 unreachable!("one trailing commit marker validated above");
15538 };
15539 for run in added_runs {
15540 let rows = spilled_rows.get(&run.table_id).ok_or_else(|| {
15541 MongrelError::InvalidArgument(format!(
15542 "commit links spilled run {} for table {} but carries no logical row records \
15543 for it",
15544 run.run_id, run.table_id
15545 ))
15546 })?;
15547 let covered = rows
15548 .iter()
15549 .filter(|row| row.row_id.0 >= run.min_row_id && row.row_id.0 <= run.max_row_id)
15550 .count() as u64;
15551 if covered != run.row_count {
15552 return Err(MongrelError::InvalidArgument(format!(
15553 "commit links spilled run {} for table {} ({} rows in [{}, {}]) but the logical \
15554 row records cover {} rows",
15555 run.run_id, run.table_id, run.row_count, run.min_row_id, run.max_row_id, covered
15556 )));
15557 }
15558 }
15559
15560 let translated = records
15563 .iter()
15564 .map(|record| {
15565 let op = match &record.op {
15566 Op::SpilledRows { table_id, rows } => Op::Put {
15567 table_id: *table_id,
15568 rows: rows.clone(),
15569 },
15570 Op::TxnCommit { epoch, .. } => Op::TxnCommit {
15571 epoch: *epoch,
15572 added_runs: Vec::new(),
15573 },
15574 op => op.clone(),
15575 };
15576 crate::wal::Record::new(record.seq, record.txn_id, op)
15577 })
15578 .collect();
15579 Ok(translated)
15580}
15581
15582fn recover_shared_wal(
15583 durable_root: &crate::durable_file::DurableRoot,
15584 tables: &HashMap<u64, TableHandle>,
15585 catalog: &Catalog,
15586 epoch: &EpochAuthority,
15587 records: &[crate::wal::Record],
15588) -> Result<()> {
15589 use crate::memtable::Row;
15590 use crate::wal::{DdlOp, Op};
15591
15592 let mut committed: HashMap<u64, u64> = HashMap::new();
15594 let mut spilled_to_link: Vec<(
15595 u64, u64, Vec<crate::wal::AddedRun>,
15598 )> = Vec::new();
15599 for r in records {
15600 if let Op::TxnCommit {
15601 epoch: ce,
15602 ref added_runs,
15603 } = r.op
15604 {
15605 committed.insert(r.txn_id, ce);
15606 if !added_runs.is_empty() {
15607 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
15608 }
15609 }
15610 }
15611 for record in records {
15612 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
15613 continue;
15614 };
15615 match &record.op {
15616 Op::Put { table_id, .. }
15617 | Op::Delete { table_id, .. }
15618 | Op::TruncateTable { table_id } => {
15619 validate_recovered_data_table(
15620 catalog,
15621 tables,
15622 *table_id,
15623 commit_epoch,
15624 record.seq.0,
15625 )?;
15626 }
15627 Op::TxnCommit { added_runs, .. } => {
15628 for run in added_runs {
15629 validate_recovered_data_table(
15630 catalog,
15631 tables,
15632 run.table_id,
15633 commit_epoch,
15634 record.seq.0,
15635 )?;
15636 }
15637 }
15638 _ => {}
15639 }
15640 }
15641 let truncated_transactions: HashSet<(u64, u64)> = records
15642 .iter()
15643 .filter_map(|record| {
15644 committed.get(&record.txn_id)?;
15645 match record.op {
15646 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
15647 _ => None,
15648 }
15649 })
15650 .collect();
15651
15652 enum ExternalRecoveryAction {
15654 Write { name: String, state: Vec<u8> },
15655 Reset { name: String },
15656 }
15657 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
15658 let mut external_actions = Vec::new();
15659 let mut max_epoch = epoch.visible().0;
15660 for r in records.iter().cloned() {
15661 let Some(&ce) = committed.get(&r.txn_id) else {
15662 continue; };
15664 let commit_epoch = Epoch(ce);
15665 max_epoch = max_epoch.max(ce);
15666 match r.op {
15667 Op::Put { table_id, rows } => {
15668 let skip = tables
15670 .get(&table_id)
15671 .map(|h| h.lock().flushed_epoch() >= ce)
15672 .unwrap_or(true);
15673 if skip {
15674 continue;
15675 }
15676 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
15677 MongrelError::CorruptWal {
15678 offset: r.seq.0,
15679 reason: format!(
15680 "committed Put payload for transaction {} could not be decoded: {error}",
15681 r.txn_id
15682 ),
15683 }
15684 })?;
15685 let rows: Vec<Row> = rows
15688 .into_iter()
15689 .map(|mut row| {
15690 row.committed_epoch = commit_epoch;
15691 row
15692 })
15693 .collect();
15694 let entry = stage
15695 .entry(table_id)
15696 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
15697 entry.0.extend(rows);
15698 entry.3 = commit_epoch;
15699 }
15700 Op::Delete { table_id, row_ids } => {
15701 let skip = tables
15702 .get(&table_id)
15703 .map(|h| h.lock().flushed_epoch() >= ce)
15704 .unwrap_or(true);
15705 if skip {
15706 continue;
15707 }
15708 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
15709 let entry = stage
15710 .entry(table_id)
15711 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
15712 entry.1.extend(dels);
15713 entry.3 = commit_epoch;
15714 }
15715 Op::TruncateTable { table_id } => {
15716 let skip = tables
15717 .get(&table_id)
15718 .map(|h| h.lock().flushed_epoch() >= ce)
15719 .unwrap_or(true);
15720 if skip {
15721 continue;
15722 }
15723 stage.insert(
15724 table_id,
15725 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
15726 );
15727 }
15728 Op::ExternalTableState { name, state } => {
15729 let current_generation = catalog
15730 .external_tables
15731 .iter()
15732 .find(|entry| entry.name == name)
15733 .map(|entry| entry.created_epoch);
15734 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
15735 validate_recovered_external_name(&name)?;
15736 external_actions.push(ExternalRecoveryAction::Write { name, state });
15737 }
15738 }
15739 Op::Ddl(DdlOp::ResetExternalTableState {
15740 name,
15741 generation_epoch,
15742 }) => {
15743 if generation_epoch != ce {
15744 return Err(MongrelError::CorruptWal {
15745 offset: r.seq.0,
15746 reason: format!(
15747 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
15748 ),
15749 });
15750 }
15751 validate_recovered_external_name(&name)?;
15752 external_actions.push(ExternalRecoveryAction::Reset { name });
15753 }
15754 Op::Flush { .. }
15755 | Op::TxnCommit { .. }
15756 | Op::TxnAbort
15757 | Op::Ddl(_)
15758 | Op::BeforeImage { .. }
15759 | Op::CommitTimestamp { .. }
15760 | Op::SpilledRows { .. } => {}
15761 }
15762 }
15763 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
15764 added_runs.retain(|added| {
15765 tables
15766 .get(&added.table_id)
15767 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
15768 });
15769 }
15770 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
15771 validate_recovery_table_stages(tables, &stage)?;
15772 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
15773
15774 for action in external_actions {
15778 match action {
15779 ExternalRecoveryAction::Write { name, state } => {
15780 write_external_state_file(durable_root, &name, &state)?;
15781 }
15782 ExternalRecoveryAction::Reset { name } => {
15783 durable_root.create_directory_all(VTAB_DIR)?;
15784 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
15785 }
15786 }
15787 }
15788 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
15789 let Some(handle) = tables.get(&table_id) else {
15790 continue;
15791 };
15792 let mut t = handle.lock();
15793 if let Some(epoch) = truncate_epoch {
15794 t.apply_truncate(epoch);
15795 }
15796 t.recover_apply(rows, deletes)?;
15797 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
15801 t.live_count = rows.len() as u64;
15802 t.persist_manifest(table_epoch.max(epoch.visible()))?;
15806 }
15807
15808 for (txn_id, ce, added_runs) in &spilled_to_link {
15812 for ar in added_runs {
15813 let Some(handle) = tables.get(&ar.table_id) else {
15814 continue;
15815 };
15816 let mut t = handle.lock();
15817 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
15818 let destination = table_dir
15819 .join(crate::engine::RUNS_DIR)
15820 .join(format!("r-{}.sr", ar.run_id));
15821 match durable_root.open_regular(&destination) {
15822 Ok(_) => {}
15823 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
15824 let pending = table_dir
15825 .join("_txn")
15826 .join(txn_id.to_string())
15827 .join(format!("r-{}.sr", ar.run_id));
15828 durable_root.rename_file_new(&pending, &destination)?;
15829 }
15830 Err(error) => return Err(error.into()),
15831 }
15832 let linked = t.recover_spilled_run(crate::manifest::RunRef {
15838 run_id: ar.run_id,
15839 level: ar.level,
15840 epoch_created: *ce,
15841 row_count: ar.row_count,
15842 });
15843 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
15844 if replaced {
15845 t.set_flushed_epoch(Epoch(*ce));
15846 }
15847 if linked || replaced {
15848 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
15849 }
15850 }
15851 }
15852
15853 epoch.advance_recovered(Epoch(max_epoch));
15854 Ok(())
15855}
15856
15857fn reconcile_recovered_table_metadata(
15858 tables: &HashMap<u64, TableHandle>,
15859 epoch: Epoch,
15860) -> Result<()> {
15861 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
15862 table_ids.sort_unstable();
15863 let mut plans = Vec::with_capacity(table_ids.len());
15864 for table_id in &table_ids {
15865 let handle = tables.get(table_id).ok_or_else(|| {
15866 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
15867 })?;
15868 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
15869 }
15870 for (table_id, plan) in plans {
15873 let handle = tables.get(&table_id).ok_or_else(|| {
15874 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
15875 })?;
15876 handle.lock().apply_recovered_metadata(plan, epoch)?;
15877 }
15878 Ok(())
15879}
15880
15881fn validate_recovered_external_name(name: &str) -> Result<()> {
15882 if name.is_empty()
15883 || !name.chars().all(|character| {
15884 character.is_ascii_alphanumeric() || character == '_' || character == '-'
15885 })
15886 {
15887 return Err(MongrelError::CorruptWal {
15888 offset: 0,
15889 reason: format!("unsafe recovered external-table name {name:?}"),
15890 });
15891 }
15892 Ok(())
15893}
15894
15895fn validate_recovery_table_stages(
15896 tables: &HashMap<u64, TableHandle>,
15897 stages: &HashMap<u64, RecoveryTableStage>,
15898) -> Result<()> {
15899 for (table_id, (rows, _, _, _)) in stages {
15900 let handle = tables
15901 .get(table_id)
15902 .ok_or_else(|| MongrelError::CorruptWal {
15903 offset: *table_id,
15904 reason: format!("recovery stage references unmounted table {table_id}"),
15905 })?;
15906 let table = handle.lock();
15907 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
15910 for row in rows {
15911 validate_recovered_row(table.schema(), row)?;
15912 }
15913 }
15914 Ok(())
15915}
15916
15917fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
15918 if row.deleted || row.row_id.0 == u64::MAX {
15919 return Err(MongrelError::CorruptWal {
15920 offset: row.row_id.0,
15921 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
15922 });
15923 }
15924 let cells = row
15925 .columns
15926 .iter()
15927 .map(|(column, value)| (*column, value.clone()))
15928 .collect::<Vec<_>>();
15929 schema
15930 .validate_persisted_values(&cells)
15931 .map_err(|error| MongrelError::CorruptWal {
15932 offset: row.row_id.0,
15933 reason: format!("recovered row violates table schema: {error}"),
15934 })?;
15935 if schema.auto_increment_column().is_some_and(|column| {
15936 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
15937 }) {
15938 return Err(MongrelError::CorruptWal {
15939 offset: row.row_id.0,
15940 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
15941 });
15942 }
15943 Ok(())
15944}
15945
15946fn validate_recovery_spilled_runs(
15947 root: &crate::durable_file::DurableRoot,
15948 tables: &HashMap<u64, TableHandle>,
15949 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
15950) -> Result<()> {
15951 let mut identities = HashSet::new();
15952 for (txn_id, commit_epoch, added_runs) in spilled {
15953 for added in added_runs {
15954 if added.run_id >= u64::MAX as u128 {
15955 return Err(MongrelError::CorruptWal {
15956 offset: *commit_epoch,
15957 reason: format!(
15958 "recovered run id {} exceeds the on-disk namespace",
15959 added.run_id
15960 ),
15961 });
15962 }
15963 let Some(handle) = tables.get(&added.table_id) else {
15964 continue;
15965 };
15966 if !identities.insert((added.table_id, added.run_id)) {
15967 return Err(MongrelError::CorruptWal {
15968 offset: *commit_epoch,
15969 reason: format!(
15970 "duplicate recovered run {} for table {}",
15971 added.run_id, added.table_id
15972 ),
15973 });
15974 }
15975 let table = handle.lock();
15976 validate_planned_spilled_run(
15977 root,
15978 *txn_id,
15979 *commit_epoch,
15980 added,
15981 table.schema(),
15982 table.kek(),
15983 )?;
15984 }
15985 }
15986 Ok(())
15987}
15988
15989fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
15990 match condition {
15991 ProcedureCondition::Pk { .. } => {
15992 if schema.primary_key().is_none() {
15993 return Err(MongrelError::InvalidArgument(
15994 "procedure condition Pk references a table without a primary key".into(),
15995 ));
15996 }
15997 }
15998 ProcedureCondition::BitmapEq { column_id, .. }
15999 | ProcedureCondition::BitmapIn { column_id, .. }
16000 | ProcedureCondition::Range { column_id, .. }
16001 | ProcedureCondition::RangeF64 { column_id, .. }
16002 | ProcedureCondition::IsNull { column_id }
16003 | ProcedureCondition::IsNotNull { column_id }
16004 | ProcedureCondition::FmContains { column_id, .. } => {
16005 validate_column_id(*column_id, schema)?;
16006 }
16007 }
16008 Ok(())
16009}
16010
16011fn bind_procedure_args(
16012 procedure: &StoredProcedure,
16013 mut args: HashMap<String, crate::Value>,
16014) -> Result<HashMap<String, crate::Value>> {
16015 let mut out = HashMap::new();
16016 for param in &procedure.params {
16017 let value = match args.remove(¶m.name) {
16018 Some(value) => value,
16019 None => param.default.clone().ok_or_else(|| {
16020 MongrelError::InvalidArgument(format!(
16021 "missing required procedure parameter {:?}",
16022 param.name
16023 ))
16024 })?,
16025 };
16026 if !param.nullable && matches!(value, crate::Value::Null) {
16027 return Err(MongrelError::InvalidArgument(format!(
16028 "procedure parameter {:?} must not be NULL",
16029 param.name
16030 )));
16031 }
16032 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
16033 return Err(MongrelError::InvalidArgument(format!(
16034 "procedure parameter {:?} has wrong type",
16035 param.name
16036 )));
16037 }
16038 out.insert(param.name.clone(), value);
16039 }
16040 if let Some(extra) = args.keys().next() {
16041 return Err(MongrelError::InvalidArgument(format!(
16042 "unknown procedure parameter {extra:?}"
16043 )));
16044 }
16045 Ok(out)
16046}
16047
16048fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
16049 matches!(
16050 (value, ty),
16051 (crate::Value::Bool(_), crate::TypeId::Bool)
16052 | (crate::Value::Int64(_), crate::TypeId::Int8)
16053 | (crate::Value::Int64(_), crate::TypeId::Int16)
16054 | (crate::Value::Int64(_), crate::TypeId::Int32)
16055 | (crate::Value::Int64(_), crate::TypeId::Int64)
16056 | (crate::Value::Int64(_), crate::TypeId::UInt8)
16057 | (crate::Value::Int64(_), crate::TypeId::UInt16)
16058 | (crate::Value::Int64(_), crate::TypeId::UInt32)
16059 | (crate::Value::Int64(_), crate::TypeId::UInt64)
16060 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
16061 | (crate::Value::Int64(_), crate::TypeId::Date32)
16062 | (crate::Value::Float64(_), crate::TypeId::Float32)
16063 | (crate::Value::Float64(_), crate::TypeId::Float64)
16064 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
16065 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
16066 )
16067}
16068
16069fn eval_cells(
16070 cells: &[crate::procedure::ProcedureCell],
16071 args: &HashMap<String, crate::Value>,
16072 outputs: &HashMap<String, ProcedureCallOutput>,
16073) -> Result<Vec<(u16, crate::Value)>> {
16074 cells
16075 .iter()
16076 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
16077 .collect()
16078}
16079
16080fn eval_condition(
16081 condition: &ProcedureCondition,
16082 args: &HashMap<String, crate::Value>,
16083 outputs: &HashMap<String, ProcedureCallOutput>,
16084) -> Result<crate::Condition> {
16085 Ok(match condition {
16086 ProcedureCondition::Pk { value } => {
16087 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
16088 }
16089 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
16090 column_id: *column_id,
16091 value: eval_value(value, args, outputs)?.encode_key(),
16092 },
16093 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
16094 column_id: *column_id,
16095 values: values
16096 .iter()
16097 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
16098 .collect::<Result<Vec<_>>>()?,
16099 },
16100 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
16101 column_id: *column_id,
16102 lo: expect_i64(eval_value(lo, args, outputs)?)?,
16103 hi: expect_i64(eval_value(hi, args, outputs)?)?,
16104 },
16105 ProcedureCondition::RangeF64 {
16106 column_id,
16107 lo,
16108 lo_inclusive,
16109 hi,
16110 hi_inclusive,
16111 } => crate::Condition::RangeF64 {
16112 column_id: *column_id,
16113 lo: expect_f64(eval_value(lo, args, outputs)?)?,
16114 lo_inclusive: *lo_inclusive,
16115 hi: expect_f64(eval_value(hi, args, outputs)?)?,
16116 hi_inclusive: *hi_inclusive,
16117 },
16118 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
16119 column_id: *column_id,
16120 },
16121 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
16122 column_id: *column_id,
16123 },
16124 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
16125 column_id: *column_id,
16126 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
16127 },
16128 })
16129}
16130
16131fn eval_value(
16132 value: &ProcedureValue,
16133 args: &HashMap<String, crate::Value>,
16134 outputs: &HashMap<String, ProcedureCallOutput>,
16135) -> Result<crate::Value> {
16136 match value {
16137 ProcedureValue::Literal(value) => Ok(value.clone()),
16138 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
16139 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
16140 }),
16141 ProcedureValue::StepScalar(id) => match outputs.get(id) {
16142 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
16143 _ => Err(MongrelError::InvalidArgument(format!(
16144 "procedure step {id:?} did not return a scalar"
16145 ))),
16146 },
16147 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
16148 Err(MongrelError::InvalidArgument(
16149 "row-valued procedure reference cannot be used as a scalar".into(),
16150 ))
16151 }
16152 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
16153 "structured procedure value cannot be used as a scalar cell".into(),
16154 )),
16155 }
16156}
16157
16158fn eval_return_output(
16159 value: &ProcedureValue,
16160 args: &HashMap<String, crate::Value>,
16161 outputs: &HashMap<String, ProcedureCallOutput>,
16162) -> Result<ProcedureCallOutput> {
16163 match value {
16164 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
16165 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
16166 args.get(name).cloned().ok_or_else(|| {
16167 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
16168 })?,
16169 )),
16170 ProcedureValue::StepRows(id)
16171 | ProcedureValue::StepRow(id)
16172 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
16173 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
16174 }),
16175 ProcedureValue::Object(fields) => {
16176 let mut out = Vec::with_capacity(fields.len());
16177 for (name, value) in fields {
16178 out.push((name.clone(), eval_return_output(value, args, outputs)?));
16179 }
16180 Ok(ProcedureCallOutput::Object(out))
16181 }
16182 ProcedureValue::Array(values) => {
16183 let mut out = Vec::with_capacity(values.len());
16184 for value in values {
16185 out.push(eval_return_output(value, args, outputs)?);
16186 }
16187 Ok(ProcedureCallOutput::Array(out))
16188 }
16189 }
16190}
16191
16192fn expect_i64(value: crate::Value) -> Result<i64> {
16193 match value {
16194 crate::Value::Int64(value) => Ok(value),
16195 _ => Err(MongrelError::InvalidArgument(
16196 "procedure value must be Int64".into(),
16197 )),
16198 }
16199}
16200
16201fn expect_f64(value: crate::Value) -> Result<f64> {
16202 match value {
16203 crate::Value::Float64(value) => Ok(value),
16204 _ => Err(MongrelError::InvalidArgument(
16205 "procedure value must be Float64".into(),
16206 )),
16207 }
16208}
16209
16210fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
16211 match value {
16212 crate::Value::Bytes(value) => Ok(value),
16213 _ => Err(MongrelError::InvalidArgument(
16214 "procedure value must be Bytes".into(),
16215 )),
16216 }
16217}
16218
16219fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
16220 if schema.columns.iter().any(|c| c.id == column_id) {
16221 Ok(())
16222 } else {
16223 Err(MongrelError::InvalidArgument(format!(
16224 "unknown column id {column_id}"
16225 )))
16226 }
16227}
16228
16229fn trigger_matches_event(
16230 trigger: &StoredTrigger,
16231 event: &WriteEvent,
16232 cat: &Catalog,
16233) -> Result<bool> {
16234 if trigger.event != event.kind {
16235 return Ok(false);
16236 }
16237 let TriggerTarget::Table(target) = &trigger.target else {
16238 return Ok(false);
16239 };
16240 if target != &event.table {
16241 return Ok(false);
16242 }
16243 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
16244 let schema = &cat
16245 .live(target)
16246 .ok_or_else(|| {
16247 MongrelError::InvalidArgument(format!(
16248 "trigger {:?} references unknown table {target:?}",
16249 trigger.name
16250 ))
16251 })?
16252 .schema;
16253 let mut watched = Vec::with_capacity(trigger.update_of.len());
16254 for name in &trigger.update_of {
16255 let col = schema.column(name).ok_or_else(|| {
16256 MongrelError::InvalidArgument(format!(
16257 "trigger {:?} references unknown UPDATE OF column {name:?}",
16258 trigger.name
16259 ))
16260 })?;
16261 watched.push(col.id);
16262 }
16263 if !event
16264 .changed_columns
16265 .iter()
16266 .any(|column_id| watched.contains(column_id))
16267 {
16268 return Ok(false);
16269 }
16270 }
16271 Ok(true)
16272}
16273
16274fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
16275 let mut ids = std::collections::BTreeSet::new();
16276 if let Some(old) = old {
16277 ids.extend(old.columns.keys().copied());
16278 }
16279 if let Some(new) = new {
16280 ids.extend(new.columns.keys().copied());
16281 }
16282 ids.into_iter()
16283 .filter(|id| {
16284 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
16285 })
16286 .collect()
16287}
16288
16289fn eval_trigger_cells(
16290 cells: &[crate::trigger::TriggerCell],
16291 event: &WriteEvent,
16292 selected: Option<&TriggerRowImage>,
16293) -> Result<Vec<(u16, Value)>> {
16294 cells
16295 .iter()
16296 .map(|cell| {
16297 Ok((
16298 cell.column_id,
16299 eval_trigger_value(&cell.value, event, selected)?,
16300 ))
16301 })
16302 .collect()
16303}
16304
16305fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
16306 match expr {
16307 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
16308 Value::Bool(value) => Ok(value),
16309 Value::Null => Ok(false),
16310 other => Err(MongrelError::InvalidArgument(format!(
16311 "trigger WHEN value must be boolean, got {other:?}"
16312 ))),
16313 },
16314 TriggerExpr::Eq { left, right } => Ok(values_equal(
16315 &eval_trigger_value(left, event, None)?,
16316 &eval_trigger_value(right, event, None)?,
16317 )),
16318 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
16319 &eval_trigger_value(left, event, None)?,
16320 &eval_trigger_value(right, event, None)?,
16321 )),
16322 TriggerExpr::Lt { left, right } => match value_order(
16323 &eval_trigger_value(left, event, None)?,
16324 &eval_trigger_value(right, event, None)?,
16325 ) {
16326 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
16327 None => Ok(false),
16328 },
16329 TriggerExpr::Lte { left, right } => match value_order(
16330 &eval_trigger_value(left, event, None)?,
16331 &eval_trigger_value(right, event, None)?,
16332 ) {
16333 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
16334 None => Ok(false),
16335 },
16336 TriggerExpr::Gt { left, right } => match value_order(
16337 &eval_trigger_value(left, event, None)?,
16338 &eval_trigger_value(right, event, None)?,
16339 ) {
16340 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
16341 None => Ok(false),
16342 },
16343 TriggerExpr::Gte { left, right } => match value_order(
16344 &eval_trigger_value(left, event, None)?,
16345 &eval_trigger_value(right, event, None)?,
16346 ) {
16347 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
16348 None => Ok(false),
16349 },
16350 TriggerExpr::IsNull(value) => Ok(matches!(
16351 eval_trigger_value(value, event, None)?,
16352 Value::Null
16353 )),
16354 TriggerExpr::IsNotNull(value) => Ok(!matches!(
16355 eval_trigger_value(value, event, None)?,
16356 Value::Null
16357 )),
16358 TriggerExpr::And { left, right } => {
16359 if !eval_trigger_expr(left, event)? {
16360 Ok(false)
16361 } else {
16362 Ok(eval_trigger_expr(right, event)?)
16363 }
16364 }
16365 TriggerExpr::Or { left, right } => {
16366 if eval_trigger_expr(left, event)? {
16367 Ok(true)
16368 } else {
16369 Ok(eval_trigger_expr(right, event)?)
16370 }
16371 }
16372 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
16373 }
16374}
16375
16376fn eval_trigger_condition(
16377 condition: &TriggerCondition,
16378 event: &WriteEvent,
16379 selected: &TriggerRowImage,
16380 schema: &Schema,
16381) -> Result<bool> {
16382 match condition {
16383 TriggerCondition::Pk { value } => {
16384 let pk = schema.primary_key().ok_or_else(|| {
16385 MongrelError::InvalidArgument(
16386 "trigger condition Pk references a table without a primary key".into(),
16387 )
16388 })?;
16389 let lhs = eval_trigger_value(value, event, Some(selected))?;
16390 Ok(values_equal(
16391 &lhs,
16392 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
16393 ))
16394 }
16395 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
16396 selected.columns.get(column_id).unwrap_or(&Value::Null),
16397 &eval_trigger_value(value, event, Some(selected))?,
16398 )),
16399 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
16400 selected.columns.get(column_id).unwrap_or(&Value::Null),
16401 &eval_trigger_value(value, event, Some(selected))?,
16402 )),
16403 TriggerCondition::Lt { column_id, value } => match value_order(
16404 selected.columns.get(column_id).unwrap_or(&Value::Null),
16405 &eval_trigger_value(value, event, Some(selected))?,
16406 ) {
16407 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
16408 None => Ok(false),
16409 },
16410 TriggerCondition::Lte { column_id, value } => match value_order(
16411 selected.columns.get(column_id).unwrap_or(&Value::Null),
16412 &eval_trigger_value(value, event, Some(selected))?,
16413 ) {
16414 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
16415 None => Ok(false),
16416 },
16417 TriggerCondition::Gt { column_id, value } => match value_order(
16418 selected.columns.get(column_id).unwrap_or(&Value::Null),
16419 &eval_trigger_value(value, event, Some(selected))?,
16420 ) {
16421 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
16422 None => Ok(false),
16423 },
16424 TriggerCondition::Gte { column_id, value } => match value_order(
16425 selected.columns.get(column_id).unwrap_or(&Value::Null),
16426 &eval_trigger_value(value, event, Some(selected))?,
16427 ) {
16428 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
16429 None => Ok(false),
16430 },
16431 TriggerCondition::IsNull { column_id } => Ok(matches!(
16432 selected.columns.get(column_id),
16433 None | Some(Value::Null)
16434 )),
16435 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
16436 selected.columns.get(column_id),
16437 None | Some(Value::Null)
16438 )),
16439 TriggerCondition::And { left, right } => {
16440 if !eval_trigger_condition(left, event, selected, schema)? {
16441 Ok(false)
16442 } else {
16443 Ok(eval_trigger_condition(right, event, selected, schema)?)
16444 }
16445 }
16446 TriggerCondition::Or { left, right } => {
16447 if eval_trigger_condition(left, event, selected, schema)? {
16448 Ok(true)
16449 } else {
16450 Ok(eval_trigger_condition(right, event, selected, schema)?)
16451 }
16452 }
16453 TriggerCondition::Not(condition) => {
16454 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
16455 }
16456 }
16457}
16458
16459fn eval_trigger_value(
16460 value: &TriggerValue,
16461 event: &WriteEvent,
16462 selected: Option<&TriggerRowImage>,
16463) -> Result<Value> {
16464 match value {
16465 TriggerValue::Literal(value) => Ok(value.clone()),
16466 TriggerValue::NewColumn(column_id) => event
16467 .new
16468 .as_ref()
16469 .and_then(|row| row.columns.get(column_id))
16470 .cloned()
16471 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
16472 TriggerValue::OldColumn(column_id) => event
16473 .old
16474 .as_ref()
16475 .and_then(|row| row.columns.get(column_id))
16476 .cloned()
16477 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
16478 TriggerValue::SelectedColumn(column_id) => selected
16479 .and_then(|row| row.columns.get(column_id))
16480 .cloned()
16481 .ok_or_else(|| {
16482 MongrelError::InvalidArgument("SELECTED column is not available".into())
16483 }),
16484 }
16485}
16486
16487fn values_equal(left: &Value, right: &Value) -> bool {
16488 match (left, right) {
16489 (Value::Null, Value::Null) => true,
16490 (Value::Bool(a), Value::Bool(b)) => a == b,
16491 (Value::Int64(a), Value::Int64(b)) => a == b,
16492 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
16493 (Value::Bytes(a), Value::Bytes(b)) => a == b,
16494 (Value::Embedding(a), Value::Embedding(b)) => {
16495 a.len() == b.len()
16496 && a.iter()
16497 .zip(b.iter())
16498 .all(|(a, b)| a.to_bits() == b.to_bits())
16499 }
16500 _ => false,
16501 }
16502}
16503
16504fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
16505 match (left, right) {
16506 (Value::Null, _) | (_, Value::Null) => None,
16507 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
16508 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
16509 (Value::Int64(a), Value::Float64(b)) => {
16512 let af = *a as f64;
16513 Some(af.total_cmp(b))
16514 }
16515 (Value::Float64(a), Value::Int64(b)) => {
16518 let bf = *b as f64;
16519 Some(a.total_cmp(&bf))
16520 }
16521 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
16522 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
16523 (Value::Embedding(_), Value::Embedding(_)) => None,
16524 _ => None,
16525 }
16526}
16527
16528fn trigger_message(value: Value) -> String {
16529 match value {
16530 Value::Null => "NULL".into(),
16531 Value::Bool(value) => value.to_string(),
16532 Value::Int64(value) => value.to_string(),
16533 Value::Float64(value) => value.to_string(),
16534 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
16535 Value::Embedding(value) => format!("{value:?}"),
16536 Value::Decimal(value) => value.to_string(),
16537 Value::Interval {
16538 months,
16539 days,
16540 nanos,
16541 } => format!("{months}m {days}d {nanos}ns"),
16542 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
16543 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
16544 }
16545}
16546
16547fn validate_trigger_step<'a>(
16548 step: &TriggerStep,
16549 cat: &'a Catalog,
16550 target_schema: &Schema,
16551 event: TriggerEvent,
16552 select_schemas: &mut HashMap<String, &'a Schema>,
16553) -> Result<()> {
16554 match step {
16555 TriggerStep::SetNew { cells } => {
16556 if event == TriggerEvent::Delete {
16557 return Err(MongrelError::InvalidArgument(
16558 "SetNew trigger step is not valid for DELETE triggers".into(),
16559 ));
16560 }
16561 for cell in cells {
16562 validate_column_id(cell.column_id, target_schema)?;
16563 validate_trigger_value(&cell.value, target_schema, event)?;
16564 }
16565 }
16566 TriggerStep::Insert { table, cells } => {
16567 let schema = trigger_write_schema(cat, table, "insert")?;
16568 for cell in cells {
16569 validate_column_id(cell.column_id, schema)?;
16570 validate_trigger_value(&cell.value, target_schema, event)?;
16571 }
16572 }
16573 TriggerStep::UpdateByPk { table, pk, cells } => {
16574 let schema = trigger_write_schema(cat, table, "update")?;
16575 if schema.primary_key().is_none() {
16576 return Err(MongrelError::InvalidArgument(format!(
16577 "trigger update_by_pk references table {table:?} without a primary key"
16578 )));
16579 }
16580 validate_trigger_value(pk, target_schema, event)?;
16581 for cell in cells {
16582 validate_column_id(cell.column_id, schema)?;
16583 validate_trigger_value(&cell.value, target_schema, event)?;
16584 }
16585 }
16586 TriggerStep::DeleteByPk { table, pk } => {
16587 let schema = trigger_write_schema(cat, table, "delete")?;
16588 if schema.primary_key().is_none() {
16589 return Err(MongrelError::InvalidArgument(format!(
16590 "trigger delete_by_pk references table {table:?} without a primary key"
16591 )));
16592 }
16593 validate_trigger_value(pk, target_schema, event)?;
16594 }
16595 TriggerStep::Select {
16596 id,
16597 table,
16598 conditions,
16599 } => {
16600 let schema = trigger_read_schema(cat, table)?;
16601 for condition in conditions {
16602 validate_trigger_condition(condition, schema, target_schema, event)?;
16603 }
16604 if select_schemas.contains_key(id) {
16605 return Err(MongrelError::InvalidArgument(format!(
16606 "duplicate select id {id:?} in trigger program"
16607 )));
16608 }
16609 select_schemas.insert(id.clone(), schema);
16610 }
16611 TriggerStep::Foreach { id, steps } => {
16612 if !select_schemas.contains_key(id) {
16613 return Err(MongrelError::InvalidArgument(format!(
16614 "foreach references unknown select id {id:?}"
16615 )));
16616 }
16617 let mut inner_select_schemas = select_schemas.clone();
16618 for step in steps {
16619 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
16620 }
16621 }
16622 TriggerStep::DeleteWhere { table, conditions } => {
16623 let schema = trigger_write_schema(cat, table, "delete")?;
16624 for condition in conditions {
16625 validate_trigger_condition(condition, schema, target_schema, event)?;
16626 }
16627 }
16628 TriggerStep::UpdateWhere {
16629 table,
16630 conditions,
16631 cells,
16632 } => {
16633 let schema = trigger_write_schema(cat, table, "update")?;
16634 for condition in conditions {
16635 validate_trigger_condition(condition, schema, target_schema, event)?;
16636 }
16637 for cell in cells {
16638 validate_column_id(cell.column_id, schema)?;
16639 validate_trigger_value(&cell.value, target_schema, event)?;
16640 }
16641 }
16642 TriggerStep::Raise { message, .. } => {
16643 validate_trigger_value(message, target_schema, event)?
16644 }
16645 }
16646 Ok(())
16647}
16648
16649fn trigger_validation_error(error: MongrelError) -> MongrelError {
16650 match error {
16651 MongrelError::TriggerValidation(_) => error,
16652 MongrelError::InvalidArgument(message)
16653 | MongrelError::Conflict(message)
16654 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
16655 error => error,
16656 }
16657}
16658
16659fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
16660 if let Some(entry) = cat.live(table) {
16661 return Ok(&entry.schema);
16662 }
16663 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
16664 let allowed = match op {
16665 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
16666 "update" | "delete" => entry.capabilities.writable,
16667 _ => false,
16668 };
16669 if !allowed {
16670 return Err(MongrelError::InvalidArgument(format!(
16671 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
16672 entry.module
16673 )));
16674 }
16675 if !entry.capabilities.transaction_safe {
16676 return Err(MongrelError::InvalidArgument(format!(
16677 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
16678 entry.module
16679 )));
16680 }
16681 return Ok(&entry.declared_schema);
16682 }
16683 Err(MongrelError::InvalidArgument(format!(
16684 "trigger references unknown table {table:?}"
16685 )))
16686}
16687
16688fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
16689 if let Some(entry) = cat.live(table) {
16690 return Ok(&entry.schema);
16691 }
16692 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
16693 if entry.capabilities.trigger_safe {
16694 return Ok(&entry.declared_schema);
16695 }
16696 return Err(MongrelError::InvalidArgument(format!(
16697 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
16698 entry.module
16699 )));
16700 }
16701 Err(MongrelError::InvalidArgument(format!(
16702 "trigger references unknown table {table:?}"
16703 )))
16704}
16705
16706fn validate_trigger_condition(
16707 condition: &TriggerCondition,
16708 schema: &Schema,
16709 target_schema: &Schema,
16710 event: TriggerEvent,
16711) -> Result<()> {
16712 match condition {
16713 TriggerCondition::Pk { value } => {
16714 if schema.primary_key().is_none() {
16715 return Err(MongrelError::InvalidArgument(
16716 "trigger condition Pk references a table without a primary key".into(),
16717 ));
16718 }
16719 validate_trigger_value(value, target_schema, event)
16720 }
16721 TriggerCondition::Eq { column_id, value }
16722 | TriggerCondition::NotEq { column_id, value }
16723 | TriggerCondition::Lt { column_id, value }
16724 | TriggerCondition::Lte { column_id, value }
16725 | TriggerCondition::Gt { column_id, value }
16726 | TriggerCondition::Gte { column_id, value } => {
16727 validate_column_id(*column_id, schema)?;
16728 validate_trigger_value(value, target_schema, event)
16729 }
16730 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
16731 validate_column_id(*column_id, schema)
16732 }
16733 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
16734 validate_trigger_condition(left, schema, target_schema, event)?;
16735 validate_trigger_condition(right, schema, target_schema, event)
16736 }
16737 TriggerCondition::Not(condition) => {
16738 validate_trigger_condition(condition, schema, target_schema, event)
16739 }
16740 }
16741}
16742
16743fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
16744 match expr {
16745 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
16746 validate_trigger_value(value, schema, event)
16747 }
16748 TriggerExpr::Eq { left, right }
16749 | TriggerExpr::NotEq { left, right }
16750 | TriggerExpr::Lt { left, right }
16751 | TriggerExpr::Lte { left, right }
16752 | TriggerExpr::Gt { left, right }
16753 | TriggerExpr::Gte { left, right } => {
16754 validate_trigger_value(left, schema, event)?;
16755 validate_trigger_value(right, schema, event)
16756 }
16757 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
16758 validate_trigger_expr(left, schema, event)?;
16759 validate_trigger_expr(right, schema, event)
16760 }
16761 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
16762 }
16763}
16764
16765fn validate_trigger_value(
16766 value: &TriggerValue,
16767 schema: &Schema,
16768 event: TriggerEvent,
16769) -> Result<()> {
16770 match value {
16771 TriggerValue::Literal(_) => Ok(()),
16772 TriggerValue::NewColumn(id) => {
16773 if event == TriggerEvent::Delete {
16774 return Err(MongrelError::InvalidArgument(
16775 "DELETE triggers cannot reference NEW".into(),
16776 ));
16777 }
16778 validate_column_id(*id, schema)
16779 }
16780 TriggerValue::OldColumn(id) => {
16781 if event == TriggerEvent::Insert {
16782 return Err(MongrelError::InvalidArgument(
16783 "INSERT triggers cannot reference OLD".into(),
16784 ));
16785 }
16786 validate_column_id(*id, schema)
16787 }
16788 TriggerValue::SelectedColumn(_) => Ok(()),
16792 }
16793}
16794
16795const COMMIT_TS_LEDGER_CAP: usize = 10_000;
16800
16801fn commit_ts_ledger_from_recovery(
16809 records: &[crate::wal::Record],
16810) -> std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp> {
16811 use crate::wal::Op;
16812 let mut commits = HashMap::new();
16813 let mut timestamps = HashMap::new();
16814 for record in records {
16815 match &record.op {
16816 Op::TxnCommit { epoch, .. } => {
16817 commits.insert(record.txn_id, *epoch);
16818 }
16819 Op::CommitTimestamp { unix_nanos } => {
16820 timestamps.insert(record.txn_id, *unix_nanos);
16821 }
16822 _ => {}
16823 }
16824 }
16825 let mut ledger = std::collections::BTreeMap::new();
16826 for (txn_id, epoch) in commits {
16827 let Some(unix_nanos) = timestamps.get(&txn_id) else {
16828 continue;
16829 };
16830 ledger.insert(
16831 epoch,
16832 mongreldb_types::hlc::HlcTimestamp {
16833 physical_micros: unix_nanos / 1_000,
16834 logical: 0,
16835 node_tiebreaker: 0,
16836 },
16837 );
16838 }
16839 while ledger.len() > COMMIT_TS_LEDGER_CAP {
16840 ledger.pop_first();
16841 }
16842 ledger
16843}
16844
16845fn recover_ddl_from_wal(
16851 root: &Path,
16852 durable_root: Option<&crate::durable_file::DurableRoot>,
16853 target_catalog: &mut Catalog,
16854 meta_dek: Option<&[u8; META_DEK_LEN]>,
16855 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
16856 apply: bool,
16857 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
16858) -> Result<()> {
16859 use crate::wal::SharedWal;
16860 let records = match durable_root {
16861 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
16862 None => SharedWal::replay_with_dek(root, wal_dek)?,
16863 };
16864 recover_ddl_from_records(
16865 root,
16866 durable_root,
16867 target_catalog,
16868 meta_dek,
16869 apply,
16870 table_roots,
16871 &records,
16872 )
16873}
16874
16875fn recover_ddl_from_records(
16876 root: &Path,
16877 durable_root: Option<&crate::durable_file::DurableRoot>,
16878 target_catalog: &mut Catalog,
16879 meta_dek: Option<&[u8; META_DEK_LEN]>,
16880 apply: bool,
16881 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
16882 records: &[crate::wal::Record],
16883) -> Result<()> {
16884 use crate::wal::{DdlOp, Op};
16885
16886 let original_catalog = target_catalog.clone();
16887 let mut recovered_catalog = original_catalog.clone();
16888 let cat = &mut recovered_catalog;
16889 let mut created_table_ids = HashSet::<u64>::new();
16890 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
16891
16892 let mut committed: HashMap<u64, u64> = HashMap::new();
16893 for r in records {
16894 if let Op::TxnCommit { epoch: ce, .. } = r.op {
16895 committed.insert(r.txn_id, ce);
16896 }
16897 }
16898 let catalog_snapshot_txns = records
16899 .iter()
16900 .filter_map(|record| {
16901 (committed.contains_key(&record.txn_id)
16902 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
16903 .then_some(record.txn_id)
16904 })
16905 .collect::<HashSet<_>>();
16906
16907 let mut changed = false;
16908 let mut applied_catalog_epoch = cat.db_epoch;
16909 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
16910 for r in records.iter().cloned() {
16911 let Some(&ce) = committed.get(&r.txn_id) else {
16912 continue;
16913 };
16914 let txn_id = r.txn_id;
16915 match r.op {
16916 Op::Ddl(DdlOp::CreateTable {
16917 table_id,
16918 ref name,
16919 ref schema_json,
16920 }) => {
16921 if cat.tables.iter().any(|t| t.table_id == table_id) {
16922 continue;
16923 }
16924 let schema = DdlOp::decode_schema(schema_json)?;
16925 validate_recovered_schema(&schema)?;
16926 created_table_ids.insert(table_id);
16927 cat.tables.push(CatalogEntry {
16928 table_id,
16929 name: name.clone(),
16930 schema,
16931 state: TableState::Live,
16932 created_epoch: ce,
16933 });
16934 cat.next_table_id =
16935 cat.next_table_id
16936 .max(table_id.checked_add(1).ok_or_else(|| {
16937 MongrelError::Full("table id namespace exhausted".into())
16938 })?);
16939 changed = true;
16940 }
16941 Op::Ddl(DdlOp::CreateBuildingTable {
16942 table_id,
16943 ref build_name,
16944 ref intended_name,
16945 ref query_id,
16946 created_at_unix_nanos,
16947 ref schema_json,
16948 }) => {
16949 if cat.tables.iter().any(|table| table.table_id == table_id) {
16950 continue;
16951 }
16952 let schema = DdlOp::decode_schema(schema_json)?;
16953 validate_recovered_schema(&schema)?;
16954 created_table_ids.insert(table_id);
16955 cat.tables.push(CatalogEntry {
16956 table_id,
16957 name: build_name.clone(),
16958 schema,
16959 state: TableState::Building {
16960 intended_name: intended_name.clone(),
16961 query_id: query_id.clone(),
16962 created_at_unix_nanos,
16963 replaces_table_id: None,
16964 },
16965 created_epoch: ce,
16966 });
16967 cat.next_table_id =
16968 cat.next_table_id
16969 .max(table_id.checked_add(1).ok_or_else(|| {
16970 MongrelError::Full("table id namespace exhausted".into())
16971 })?);
16972 changed = true;
16973 }
16974 Op::Ddl(DdlOp::CreateRebuildingTable {
16975 table_id,
16976 ref build_name,
16977 ref intended_name,
16978 ref query_id,
16979 created_at_unix_nanos,
16980 replaces_table_id,
16981 ref schema_json,
16982 }) => {
16983 if cat.tables.iter().any(|table| table.table_id == table_id) {
16984 continue;
16985 }
16986 let schema = DdlOp::decode_schema(schema_json)?;
16987 validate_recovered_schema(&schema)?;
16988 created_table_ids.insert(table_id);
16989 cat.tables.push(CatalogEntry {
16990 table_id,
16991 name: build_name.clone(),
16992 schema,
16993 state: TableState::Building {
16994 intended_name: intended_name.clone(),
16995 query_id: query_id.clone(),
16996 created_at_unix_nanos,
16997 replaces_table_id: Some(replaces_table_id),
16998 },
16999 created_epoch: ce,
17000 });
17001 cat.next_table_id =
17002 cat.next_table_id
17003 .max(table_id.checked_add(1).ok_or_else(|| {
17004 MongrelError::Full("table id namespace exhausted".into())
17005 })?);
17006 changed = true;
17007 }
17008 Op::Ddl(DdlOp::DropTable { table_id }) => {
17009 let mut dropped_name = None;
17010 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
17011 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
17012 dropped_name = Some(entry.name.clone());
17013 entry.state = TableState::Dropped { at_epoch: ce };
17014 changed = true;
17015 }
17016 }
17017 if let Some(name) = dropped_name {
17018 let before = cat.materialized_views.len();
17019 cat.materialized_views
17020 .retain(|definition| definition.name != name);
17021 changed |= before != cat.materialized_views.len();
17022 cat.security.rls_tables.retain(|table| table != &name);
17023 cat.security.policies.retain(|policy| policy.table != name);
17024 cat.security.masks.retain(|mask| mask.table != name);
17025 for role in &mut cat.roles {
17026 role.permissions
17027 .retain(|permission| permission_table(permission) != Some(&name));
17028 }
17029 if !catalog_snapshot_txns.contains(&txn_id) {
17030 advance_security_version(cat)?;
17031 }
17032 }
17033 }
17034 Op::Ddl(DdlOp::PublishBuildingTable {
17035 table_id,
17036 ref new_name,
17037 }) => {
17038 if let Some(entry) = cat
17039 .tables
17040 .iter_mut()
17041 .find(|table| table.table_id == table_id)
17042 {
17043 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
17044 entry.name = new_name.clone();
17045 entry.state = TableState::Live;
17046 changed = true;
17047 }
17048 }
17049 }
17050 Op::Ddl(DdlOp::ReplaceBuildingTable {
17051 table_id,
17052 replaced_table_id,
17053 ref new_name,
17054 }) => {
17055 changed |=
17056 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
17057 }
17058 Op::Ddl(DdlOp::RenameTable {
17059 table_id,
17060 ref new_name,
17061 }) => {
17062 let mut old_name = None;
17063 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
17064 if entry.name != *new_name {
17065 old_name = Some(entry.name.clone());
17066 entry.name = new_name.clone();
17067 changed = true;
17068 }
17069 }
17070 if let Some(old_name) = old_name {
17071 if let Some(definition) = cat
17072 .materialized_views
17073 .iter_mut()
17074 .find(|definition| definition.name == old_name)
17075 {
17076 definition.name = new_name.clone();
17077 }
17078 for table in &mut cat.security.rls_tables {
17079 if *table == old_name {
17080 *table = new_name.clone();
17081 }
17082 }
17083 for policy in &mut cat.security.policies {
17084 if policy.table == old_name {
17085 policy.table = new_name.clone();
17086 }
17087 }
17088 for mask in &mut cat.security.masks {
17089 if mask.table == old_name {
17090 mask.table = new_name.clone();
17091 }
17092 }
17093 for role in &mut cat.roles {
17094 for permission in &mut role.permissions {
17095 rename_permission_table(permission, &old_name, new_name);
17096 }
17097 }
17098 if !catalog_snapshot_txns.contains(&txn_id) {
17099 advance_security_version(cat)?;
17100 }
17101 }
17102 }
17106 Op::Ddl(DdlOp::AlterTable {
17107 table_id,
17108 ref column_json,
17109 }) => {
17110 let column = DdlOp::decode_column(column_json)?;
17111 let mut renamed = None;
17112 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
17113 renamed = entry
17114 .schema
17115 .columns
17116 .iter()
17117 .find(|existing| existing.id == column.id && existing.name != column.name)
17118 .map(|existing| {
17119 (
17120 entry.name.clone(),
17121 existing.name.clone(),
17122 column.name.clone(),
17123 )
17124 });
17125 if apply_recovered_column_def(&mut entry.schema, column)? {
17126 validate_recovered_schema(&entry.schema)?;
17127 changed = true;
17128 }
17129 }
17130 if let Some((table, old_name, new_name)) = renamed {
17131 for role in &mut cat.roles {
17132 for permission in &mut role.permissions {
17133 rename_permission_column(permission, &table, &old_name, &new_name);
17134 }
17135 }
17136 if !catalog_snapshot_txns.contains(&txn_id) {
17137 advance_security_version(cat)?;
17138 }
17139 }
17140 }
17141 Op::Ddl(DdlOp::SetTtl {
17142 table_id,
17143 ref policy_json,
17144 }) => {
17145 let policy = DdlOp::decode_ttl(policy_json)?;
17146 let entry = cat
17147 .tables
17148 .iter()
17149 .find(|entry| entry.table_id == table_id)
17150 .ok_or_else(|| {
17151 MongrelError::Schema(format!(
17152 "recovered TTL references unknown table id {table_id}"
17153 ))
17154 })?;
17155 if let Some(policy) = policy {
17156 let valid = entry
17157 .schema
17158 .columns
17159 .iter()
17160 .find(|column| column.id == policy.column_id)
17161 .is_some_and(|column| {
17162 column.ty == TypeId::TimestampNanos
17163 && policy.duration_nanos > 0
17164 && policy.duration_nanos <= i64::MAX as u64
17165 });
17166 if !valid {
17167 return Err(MongrelError::Schema(format!(
17168 "invalid recovered TTL policy for table id {table_id}"
17169 )));
17170 }
17171 }
17172 ttl_updates.insert(table_id, (policy, ce));
17173 }
17174 Op::Ddl(DdlOp::SetMaterializedView {
17175 ref name,
17176 ref definition_json,
17177 }) => {
17178 let definition = DdlOp::decode_materialized_view(definition_json)?;
17179 if definition.name != *name {
17180 return Err(MongrelError::Schema(format!(
17181 "materialized view WAL name mismatch: {name:?}"
17182 )));
17183 }
17184 if cat.live(name).is_some() {
17185 if let Some(existing) = cat
17186 .materialized_views
17187 .iter_mut()
17188 .find(|existing| existing.name == *name)
17189 {
17190 if *existing != definition {
17191 *existing = definition;
17192 changed = true;
17193 }
17194 } else {
17195 cat.materialized_views.push(definition);
17196 changed = true;
17197 }
17198 }
17199 }
17200 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
17201 let security = DdlOp::decode_security(security_json)?;
17202 validate_security_catalog(cat, &security)?;
17203 if cat.security != security {
17204 cat.security = security;
17205 if !catalog_snapshot_txns.contains(&txn_id) {
17206 advance_security_version(cat)?;
17207 }
17208 changed = true;
17209 }
17210 }
17211 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
17212 let target = match key.as_str() {
17213 "user_version" => &mut cat.user_version,
17214 "application_id" => &mut cat.application_id,
17215 _ => {
17216 return Err(MongrelError::InvalidArgument(format!(
17217 "unsupported recovered SQL pragma {key:?}"
17218 )))
17219 }
17220 };
17221 if *target != Some(value) {
17222 *target = Some(value);
17223 cat.db_epoch = cat.db_epoch.max(ce);
17224 changed = true;
17225 }
17226 }
17227 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
17228 if ce <= applied_catalog_epoch {
17229 continue;
17230 }
17231 let snapshot = DdlOp::decode_catalog(catalog_json)?;
17232 if snapshot.db_epoch != ce {
17233 return Err(MongrelError::Schema(format!(
17234 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
17235 snapshot.db_epoch
17236 )));
17237 }
17238 validate_recovered_catalog(&snapshot)?;
17239 validate_catalog_transition(cat, &snapshot)?;
17240 *cat = snapshot;
17241 applied_catalog_epoch = ce;
17242 changed = true;
17243 }
17244 _ => {}
17245 }
17246 }
17247
17248 if cat.db_epoch < max_committed_epoch {
17249 cat.db_epoch = max_committed_epoch;
17250 changed = true;
17251 }
17252 changed |= repair_catalog_allocator_counters(cat)?;
17253
17254 validate_recovered_catalog(cat)?;
17255 let storage_reconciliation = validate_recovered_storage_plan(
17256 root,
17257 durable_root,
17258 cat,
17259 &created_table_ids,
17260 &ttl_updates,
17261 meta_dek,
17262 )?;
17263
17264 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
17265 if apply && (changed || needs_storage_apply) {
17266 for table_id in storage_reconciliation {
17267 let entry = cat
17268 .tables
17269 .iter()
17270 .find(|entry| entry.table_id == table_id)
17271 .ok_or_else(|| MongrelError::CorruptWal {
17272 offset: table_id,
17273 reason: "recovery storage plan lost its catalog table".into(),
17274 })?;
17275 ensure_recovered_table_storage(
17276 table_roots
17277 .and_then(|roots| roots.get(&table_id))
17278 .map(Arc::as_ref),
17279 durable_root,
17280 &root.join(TABLES_DIR).join(table_id.to_string()),
17281 table_id,
17282 &entry.schema,
17283 meta_dek,
17284 )?;
17285 }
17286 for (table_id, (policy, ttl_epoch)) in ttl_updates {
17287 let Some(entry) = cat.tables.iter().find(|entry| {
17288 entry.table_id == table_id
17289 && matches!(entry.state, TableState::Live | TableState::Building { .. })
17290 }) else {
17291 continue;
17292 };
17293 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
17294 {
17295 root.try_clone()?
17296 } else if let Some(root) = durable_root {
17297 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
17298 } else {
17299 crate::durable_file::DurableRoot::open(
17300 root.join(TABLES_DIR).join(table_id.to_string()),
17301 )?
17302 };
17303 let table_dir = table_root.io_path()?;
17304 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
17305 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
17306 manifest.ttl = policy;
17307 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
17308 manifest.schema_id = entry.schema.schema_id;
17309 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
17310 }
17311 }
17312 if changed {
17313 match durable_root {
17314 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
17315 None => catalog::write_atomic(root, cat, meta_dek)?,
17316 }
17317 }
17318 }
17319 *target_catalog = recovered_catalog;
17320 Ok(())
17321}
17322
17323fn ensure_recovered_table_storage(
17324 pinned_table: Option<&crate::durable_file::DurableRoot>,
17325 durable_root: Option<&crate::durable_file::DurableRoot>,
17326 fallback_table_dir: &Path,
17327 table_id: u64,
17328 schema: &Schema,
17329 meta_dek: Option<&[u8; META_DEK_LEN]>,
17330) -> Result<()> {
17331 let table_root = if let Some(root) = pinned_table {
17332 root.try_clone()?
17333 } else if let Some(root) = durable_root {
17334 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
17335 match root.open_directory(&relative) {
17336 Ok(table) => table,
17337 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
17338 root.create_directory_all_pinned(relative)?
17339 }
17340 Err(error) => return Err(error.into()),
17341 }
17342 } else {
17343 crate::durable_file::create_directory_all(fallback_table_dir)?;
17344 crate::durable_file::DurableRoot::open(fallback_table_dir)?
17345 };
17346 let table_dir = table_root.io_path()?;
17347 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
17348 Ok(manifest) => {
17349 if manifest.table_id != table_id {
17350 return Err(MongrelError::Conflict(format!(
17351 "recovered table directory id mismatch: expected {table_id}, found {}",
17352 manifest.table_id
17353 )));
17354 }
17355 Some(manifest)
17356 }
17357 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
17358 Err(error) => return Err(error),
17359 };
17360
17361 table_root.create_directory_all(crate::engine::WAL_DIR)?;
17362 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
17363 crate::engine::write_schema(&table_dir, schema)?;
17364
17365 if let Some(mut manifest) = existing_manifest.take() {
17366 if manifest.schema_id != schema.schema_id {
17367 manifest.schema_id = schema.schema_id;
17368 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
17369 }
17370 } else {
17371 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
17373 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
17374 }
17375 Ok(())
17376}
17377
17378fn validate_recovered_schema(schema: &Schema) -> Result<()> {
17379 schema.validate_auto_increment()?;
17380 schema.validate_defaults()?;
17381 schema.validate_ai()?;
17382 let mut column_ids = HashSet::new();
17383 let mut column_names = HashSet::new();
17384 for column in &schema.columns {
17385 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
17386 return Err(MongrelError::Schema(
17387 "recovered schema contains duplicate columns".into(),
17388 ));
17389 }
17390 match &column.ty {
17391 TypeId::Decimal128 { precision, scale }
17392 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
17393 {
17394 return Err(MongrelError::Schema(format!(
17395 "column {:?} has invalid decimal precision or scale",
17396 column.name
17397 )));
17398 }
17399 TypeId::Enum { variants }
17400 if variants.is_empty()
17401 || variants.iter().any(String::is_empty)
17402 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
17403 {
17404 return Err(MongrelError::Schema(format!(
17405 "column {:?} has invalid enum variants",
17406 column.name
17407 )));
17408 }
17409 _ => {}
17410 }
17411 }
17412 let mut index_names = HashSet::new();
17413 for index in &schema.indexes {
17414 index.validate_options()?;
17415 if index.name.is_empty()
17416 || !index_names.insert(index.name.as_str())
17417 || schema
17418 .columns
17419 .iter()
17420 .all(|column| column.id != index.column_id)
17421 {
17422 return Err(MongrelError::Schema(format!(
17423 "recovered index {:?} references missing column {}",
17424 index.name, index.column_id
17425 )));
17426 }
17427 }
17428 let mut colocated = HashSet::new();
17429 for group in &schema.colocation {
17430 if group.is_empty()
17431 || group.iter().any(|id| !column_ids.contains(id))
17432 || group.iter().any(|id| !colocated.insert(*id))
17433 {
17434 return Err(MongrelError::Schema(
17435 "recovered schema contains invalid column co-location groups".into(),
17436 ));
17437 }
17438 }
17439
17440 let mut constraint_ids = HashSet::new();
17441 let mut constraint_names = HashSet::<String>::new();
17442 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
17443 if name.is_empty()
17444 || !constraint_ids.insert(id)
17445 || !constraint_names.insert(name.to_owned())
17446 {
17447 return Err(MongrelError::Schema(
17448 "recovered schema contains duplicate or empty constraint identities".into(),
17449 ));
17450 }
17451 Ok(())
17452 };
17453 for unique in &schema.constraints.uniques {
17454 validate_constraint_identity(unique.id, &unique.name)?;
17455 if unique.columns.is_empty()
17456 || unique.columns.iter().any(|id| !column_ids.contains(id))
17457 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
17458 {
17459 return Err(MongrelError::Schema(format!(
17460 "unique constraint {:?} has invalid columns",
17461 unique.name
17462 )));
17463 }
17464 }
17465 for foreign_key in &schema.constraints.foreign_keys {
17466 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
17467 if foreign_key.ref_table.is_empty()
17468 || foreign_key.columns.is_empty()
17469 || foreign_key.columns.len() != foreign_key.ref_columns.len()
17470 || foreign_key
17471 .columns
17472 .iter()
17473 .any(|id| !column_ids.contains(id))
17474 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
17475 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
17476 != foreign_key.ref_columns.len()
17477 {
17478 return Err(MongrelError::Schema(format!(
17479 "foreign key {:?} has invalid columns",
17480 foreign_key.name
17481 )));
17482 }
17483 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
17484 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
17485 && foreign_key.columns.iter().any(|id| {
17486 schema
17487 .columns
17488 .iter()
17489 .find(|column| column.id == *id)
17490 .is_none_or(|column| {
17491 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
17492 })
17493 })
17494 {
17495 return Err(MongrelError::Schema(format!(
17496 "foreign key {:?} uses SET NULL on a non-nullable column",
17497 foreign_key.name
17498 )));
17499 }
17500 }
17501 for check in &schema.constraints.checks {
17502 validate_constraint_identity(check.id, &check.name)?;
17503 check.expr.validate()?;
17504 validate_check_columns(&check.expr, &column_ids)?;
17505 }
17506 Ok(())
17507}
17508
17509fn validate_check_columns(
17510 expression: &crate::constraint::CheckExpr,
17511 column_ids: &HashSet<u16>,
17512) -> Result<()> {
17513 use crate::constraint::CheckExpr;
17514 match expression {
17515 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
17516 if column_ids.contains(id) {
17517 Ok(())
17518 } else {
17519 Err(MongrelError::Schema(format!(
17520 "check constraint references unknown column {id}"
17521 )))
17522 }
17523 }
17524 CheckExpr::Regex { col, .. } => {
17525 if column_ids.contains(col) {
17526 Ok(())
17527 } else {
17528 Err(MongrelError::Schema(format!(
17529 "check constraint references unknown column {col}"
17530 )))
17531 }
17532 }
17533 CheckExpr::Add(left, right)
17534 | CheckExpr::Sub(left, right)
17535 | CheckExpr::Mul(left, right)
17536 | CheckExpr::Div(left, right)
17537 | CheckExpr::Mod(left, right)
17538 | CheckExpr::Eq(left, right)
17539 | CheckExpr::Ne(left, right)
17540 | CheckExpr::Lt(left, right)
17541 | CheckExpr::Le(left, right)
17542 | CheckExpr::Gt(left, right)
17543 | CheckExpr::Ge(left, right)
17544 | CheckExpr::And(left, right)
17545 | CheckExpr::Or(left, right) => {
17546 validate_check_columns(left, column_ids)?;
17547 validate_check_columns(right, column_ids)
17548 }
17549 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
17550 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
17551 }
17552}
17553
17554fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
17555 for (name, prior, candidate) in [
17556 ("db_epoch", current.db_epoch, next.db_epoch),
17557 ("next_table_id", current.next_table_id, next.next_table_id),
17558 (
17559 "next_segment_no",
17560 current.next_segment_no,
17561 next.next_segment_no,
17562 ),
17563 ("next_user_id", current.next_user_id, next.next_user_id),
17564 (
17565 "security_version",
17566 current.security_version,
17567 next.security_version,
17568 ),
17569 ] {
17570 if candidate < prior {
17571 return Err(MongrelError::Schema(format!(
17572 "catalog snapshot rolls back {name} from {prior} to {candidate}"
17573 )));
17574 }
17575 }
17576 for prior in ¤t.tables {
17577 let Some(candidate) = next
17578 .tables
17579 .iter()
17580 .find(|entry| entry.table_id == prior.table_id)
17581 else {
17582 return Err(MongrelError::Schema(format!(
17583 "catalog snapshot removes table identity {}",
17584 prior.table_id
17585 )));
17586 };
17587 if candidate.created_epoch != prior.created_epoch
17588 || candidate.schema.schema_id < prior.schema.schema_id
17589 || matches!(prior.state, TableState::Dropped { .. })
17590 && !matches!(candidate.state, TableState::Dropped { .. })
17591 {
17592 return Err(MongrelError::Schema(format!(
17593 "catalog snapshot rolls back table identity {}",
17594 prior.table_id
17595 )));
17596 }
17597 }
17598 for prior in ¤t.users {
17599 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
17600 if candidate.username != prior.username
17601 || candidate.created_epoch != prior.created_epoch
17602 {
17603 return Err(MongrelError::Schema(format!(
17604 "catalog snapshot reuses user identity {}",
17605 prior.id
17606 )));
17607 }
17608 }
17609 }
17610 Ok(())
17611}
17612
17613fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
17614 let mut table_ids = HashSet::new();
17615 let mut active_names = HashSet::new();
17616 let mut max_table_id = None::<u64>;
17617 for entry in &catalog.tables {
17618 if !table_ids.insert(entry.table_id) {
17619 return Err(MongrelError::Schema(format!(
17620 "catalog contains duplicate table id {}",
17621 entry.table_id
17622 )));
17623 }
17624 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
17625 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
17626 return Err(MongrelError::Schema(format!(
17627 "catalog table {} has invalid name or creation epoch",
17628 entry.table_id
17629 )));
17630 }
17631 validate_recovered_schema(&entry.schema)?;
17632 match &entry.state {
17633 TableState::Live => {
17634 if !active_names.insert(entry.name.as_str()) {
17635 return Err(MongrelError::Schema(format!(
17636 "catalog contains duplicate active table name {:?}",
17637 entry.name
17638 )));
17639 }
17640 }
17641 TableState::Dropped { at_epoch } => {
17642 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
17643 return Err(MongrelError::Schema(format!(
17644 "catalog table {} has invalid drop epoch {at_epoch}",
17645 entry.table_id
17646 )));
17647 }
17648 }
17649 TableState::Building {
17650 intended_name,
17651 query_id,
17652 replaces_table_id,
17653 ..
17654 } => {
17655 if intended_name.is_empty() || query_id.is_empty() {
17656 return Err(MongrelError::Schema(format!(
17657 "building table {} has empty identity fields",
17658 entry.table_id
17659 )));
17660 }
17661 if !active_names.insert(entry.name.as_str()) {
17662 return Err(MongrelError::Schema(format!(
17663 "catalog contains duplicate active/building table name {:?}",
17664 entry.name
17665 )));
17666 }
17667 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
17668 return Err(MongrelError::Schema(
17669 "building table cannot replace itself".into(),
17670 ));
17671 }
17672 }
17673 }
17674 }
17675 if let Some(maximum) = max_table_id {
17676 let required = maximum
17677 .checked_add(1)
17678 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
17679 if catalog.next_table_id < required {
17680 return Err(MongrelError::Schema(format!(
17681 "catalog next_table_id {} precedes required {required}",
17682 catalog.next_table_id
17683 )));
17684 }
17685 }
17686 for entry in &catalog.tables {
17687 if let TableState::Building {
17688 replaces_table_id: Some(replaced),
17689 ..
17690 } = entry.state
17691 {
17692 if !table_ids.contains(&replaced) {
17693 return Err(MongrelError::Schema(format!(
17694 "building table {} replaces unknown table {replaced}",
17695 entry.table_id
17696 )));
17697 }
17698 }
17699 }
17700 for entry in &catalog.tables {
17701 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
17702 validate_foreign_key_targets(catalog, &entry.schema)?;
17703 }
17704 }
17705
17706 let mut external_names = HashSet::new();
17707 for entry in &catalog.external_tables {
17708 entry.validate()?;
17709 validate_recovered_schema(&entry.declared_schema)?;
17710 if !entry.declared_schema.constraints.is_empty() {
17711 return Err(MongrelError::Schema(format!(
17712 "external table {:?} cannot carry engine-enforced constraints",
17713 entry.name
17714 )));
17715 }
17716 if entry.created_epoch > catalog.db_epoch
17717 || !external_names.insert(entry.name.as_str())
17718 || active_names.contains(entry.name.as_str())
17719 {
17720 return Err(MongrelError::Schema(format!(
17721 "invalid or duplicate external table {:?}",
17722 entry.name
17723 )));
17724 }
17725 }
17726
17727 let mut procedure_names = HashSet::new();
17728 for entry in &catalog.procedures {
17729 entry.procedure.validate()?;
17730 if entry.procedure.created_epoch > entry.procedure.updated_epoch
17731 || entry.procedure.updated_epoch > catalog.db_epoch
17732 || !procedure_names.insert(entry.procedure.name.as_str())
17733 {
17734 return Err(MongrelError::Schema(format!(
17735 "invalid or duplicate procedure {:?}",
17736 entry.procedure.name
17737 )));
17738 }
17739 validate_recovered_procedure_references(catalog, &entry.procedure)?;
17740 }
17741
17742 let mut trigger_names = HashSet::new();
17743 for entry in &catalog.triggers {
17744 entry.trigger.validate()?;
17745 if entry.trigger.created_epoch > entry.trigger.updated_epoch
17746 || entry.trigger.updated_epoch > catalog.db_epoch
17747 || !trigger_names.insert(entry.trigger.name.as_str())
17748 {
17749 return Err(MongrelError::Schema(format!(
17750 "invalid or duplicate trigger {:?}",
17751 entry.trigger.name
17752 )));
17753 }
17754 validate_recovered_trigger_references(catalog, &entry.trigger)?;
17755 }
17756
17757 let mut views = HashSet::new();
17758 for view in &catalog.materialized_views {
17759 let target = catalog.live(&view.name).ok_or_else(|| {
17760 MongrelError::Schema(format!(
17761 "materialized view {:?} has no live table",
17762 view.name
17763 ))
17764 })?;
17765 if view.name.is_empty()
17766 || view.query.trim().is_empty()
17767 || view.last_refresh_epoch > catalog.db_epoch
17768 || !views.insert(view.name.as_str())
17769 {
17770 return Err(MongrelError::Schema(format!(
17771 "materialized view {:?} has no unique live table",
17772 view.name
17773 )));
17774 }
17775 if let Some(incremental) = &view.incremental {
17776 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
17777 MongrelError::Schema(format!(
17778 "materialized view {:?} references missing source {:?}",
17779 view.name, incremental.source_table
17780 ))
17781 })?;
17782 if source.table_id != incremental.source_table_id
17783 || source
17784 .schema
17785 .columns
17786 .iter()
17787 .all(|column| column.id != incremental.group_column)
17788 {
17789 return Err(MongrelError::Schema(format!(
17790 "materialized view {:?} has invalid incremental source",
17791 view.name
17792 )));
17793 }
17794 let target_ids = target
17795 .schema
17796 .columns
17797 .iter()
17798 .map(|column| column.id)
17799 .collect::<HashSet<_>>();
17800 let mut output_ids = HashSet::new();
17801 let count_outputs = incremental
17802 .outputs
17803 .iter()
17804 .filter(|output| {
17805 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
17806 })
17807 .count();
17808 if incremental.checkpoint_event_id.is_empty()
17809 || !target_ids.contains(&incremental.group_output_column)
17810 || !target_ids.contains(&incremental.count_output_column)
17811 || incremental.outputs.is_empty()
17812 || count_outputs != 1
17813 || incremental.outputs.iter().any(|output| {
17814 !target_ids.contains(&output.output_column)
17815 || output.output_column == incremental.group_output_column
17816 || !output_ids.insert(output.output_column)
17817 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
17818 && output.output_column != incremental.count_output_column
17819 || match output.kind {
17820 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
17821 source
17822 .schema
17823 .columns
17824 .iter()
17825 .all(|column| column.id != source_column)
17826 }
17827 crate::catalog::IncrementalAggregateKind::Count => false,
17828 }
17829 })
17830 {
17831 return Err(MongrelError::Schema(format!(
17832 "materialized view {:?} has invalid incremental outputs",
17833 view.name
17834 )));
17835 }
17836 }
17837 }
17838
17839 validate_security_catalog(catalog, &catalog.security)?;
17840 validate_recovered_auth_catalog(catalog)?;
17841 Ok(())
17842}
17843
17844fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
17845 let mut changed = false;
17846 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
17847 let required = maximum
17848 .checked_add(1)
17849 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
17850 if catalog.next_table_id < required {
17851 catalog.next_table_id = required;
17852 changed = true;
17853 }
17854 }
17855 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
17856 let required = maximum
17857 .checked_add(1)
17858 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
17859 if catalog.next_user_id < required {
17860 catalog.next_user_id = required;
17861 changed = true;
17862 }
17863 }
17864 Ok(changed)
17865}
17866
17867fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
17868 for foreign_key in &schema.constraints.foreign_keys {
17869 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
17870 MongrelError::Schema(format!(
17871 "foreign key {:?} references unknown live table {:?}",
17872 foreign_key.name, foreign_key.ref_table
17873 ))
17874 })?;
17875 let referenced_unique = parent
17876 .schema
17877 .constraints
17878 .uniques
17879 .iter()
17880 .any(|unique| unique.columns == foreign_key.ref_columns)
17881 || foreign_key.ref_columns.len() == 1
17882 && parent
17883 .schema
17884 .primary_key()
17885 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
17886 if !referenced_unique {
17887 return Err(MongrelError::Schema(format!(
17888 "foreign key {:?} does not reference a unique key",
17889 foreign_key.name
17890 )));
17891 }
17892 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
17893 let local = schema.columns.iter().find(|column| column.id == *local_id);
17894 let referenced = parent
17895 .schema
17896 .columns
17897 .iter()
17898 .find(|column| column.id == *parent_id);
17899 if local
17900 .zip(referenced)
17901 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
17902 {
17903 return Err(MongrelError::Schema(format!(
17904 "foreign key {:?} has missing or incompatible columns",
17905 foreign_key.name
17906 )));
17907 }
17908 }
17909 }
17910 Ok(())
17911}
17912
17913fn validate_recovered_procedure_references(
17914 catalog: &Catalog,
17915 procedure: &StoredProcedure,
17916) -> Result<()> {
17917 for step in &procedure.body.steps {
17918 let Some(table_name) = step.table() else {
17919 continue;
17920 };
17921 let schema = &catalog
17922 .live(table_name)
17923 .ok_or_else(|| {
17924 MongrelError::Schema(format!(
17925 "procedure {:?} references unknown table {table_name:?}",
17926 procedure.name
17927 ))
17928 })?
17929 .schema;
17930 match step {
17931 ProcedureStep::NativeQuery {
17932 conditions,
17933 projection,
17934 ..
17935 } => {
17936 for condition in conditions {
17937 validate_condition_columns(condition, schema)?;
17938 }
17939 for id in projection.iter().flatten() {
17940 validate_column_id(*id, schema)?;
17941 }
17942 }
17943 ProcedureStep::Put { cells, .. } => {
17944 for cell in cells {
17945 validate_column_id(cell.column_id, schema)?;
17946 }
17947 }
17948 ProcedureStep::Upsert {
17949 cells,
17950 update_cells,
17951 ..
17952 } => {
17953 for cell in cells.iter().chain(update_cells.iter().flatten()) {
17954 validate_column_id(cell.column_id, schema)?;
17955 }
17956 }
17957 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
17958 return Err(MongrelError::Schema(format!(
17959 "procedure {:?} deletes by primary key on table without one",
17960 procedure.name
17961 )));
17962 }
17963 ProcedureStep::DeleteByPk { .. }
17964 | ProcedureStep::DeleteRows { .. }
17965 | ProcedureStep::SqlQuery { .. } => {}
17966 }
17967 }
17968 Ok(())
17969}
17970
17971fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
17972 let target_schema = match &trigger.target {
17973 TriggerTarget::Table(name) => catalog
17974 .live(name)
17975 .ok_or_else(|| {
17976 MongrelError::Schema(format!(
17977 "trigger {:?} references unknown table {name:?}",
17978 trigger.name
17979 ))
17980 })?
17981 .schema
17982 .clone(),
17983 TriggerTarget::View(_) => Schema {
17984 columns: trigger.target_columns.clone(),
17985 ..Schema::default()
17986 },
17987 };
17988 for column in &trigger.update_of {
17989 if target_schema.column(column).is_none() {
17990 return Err(MongrelError::Schema(format!(
17991 "trigger {:?} references unknown UPDATE OF column {column:?}",
17992 trigger.name
17993 )));
17994 }
17995 }
17996 if let Some(expr) = &trigger.when {
17997 validate_trigger_expr(expr, &target_schema, trigger.event)?;
17998 }
17999 let mut selects = HashMap::new();
18000 for step in &trigger.program.steps {
18001 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
18002 return Err(MongrelError::Schema(
18003 "SetNew is only valid in BEFORE triggers".into(),
18004 ));
18005 }
18006 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
18007 }
18008 Ok(())
18009}
18010
18011fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
18012 let mut role_names = HashSet::new();
18013 for role in &catalog.roles {
18014 if role.name.is_empty()
18015 || role.created_epoch > catalog.db_epoch
18016 || !role_names.insert(role.name.as_str())
18017 {
18018 return Err(MongrelError::Schema(format!(
18019 "invalid or duplicate role {:?}",
18020 role.name
18021 )));
18022 }
18023 for permission in &role.permissions {
18024 if let Some(table) = permission_table(permission) {
18025 let schema = catalog
18026 .live(table)
18027 .map(|entry| &entry.schema)
18028 .or_else(|| {
18029 catalog
18030 .external_tables
18031 .iter()
18032 .find(|entry| entry.name == table)
18033 .map(|entry| &entry.declared_schema)
18034 })
18035 .ok_or_else(|| {
18036 MongrelError::Schema(format!(
18037 "role {:?} references unknown table {table:?}",
18038 role.name
18039 ))
18040 })?;
18041 let columns = match permission {
18042 crate::auth::Permission::SelectColumns { columns, .. }
18043 | crate::auth::Permission::InsertColumns { columns, .. }
18044 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
18045 _ => None,
18046 };
18047 if columns.is_some_and(|columns| {
18048 columns.is_empty()
18049 || columns.iter().any(|column| schema.column(column).is_none())
18050 }) {
18051 return Err(MongrelError::Schema(format!(
18052 "role {:?} contains invalid column permissions",
18053 role.name
18054 )));
18055 }
18056 }
18057 }
18058 }
18059 let mut user_ids = HashSet::new();
18060 let mut usernames = HashSet::new();
18061 let mut maximum_user_id = 0;
18062 for user in &catalog.users {
18063 maximum_user_id = maximum_user_id.max(user.id);
18064 if user.id == 0
18065 || user.username.is_empty()
18066 || user.password_hash.is_empty()
18067 || user.created_epoch > catalog.db_epoch
18068 || !user_ids.insert(user.id)
18069 || !usernames.insert(user.username.as_str())
18070 || user
18071 .roles
18072 .iter()
18073 .any(|role| !role_names.contains(role.as_str()))
18074 {
18075 return Err(MongrelError::Schema(format!(
18076 "invalid or duplicate user {:?}",
18077 user.username
18078 )));
18079 }
18080 }
18081 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
18082 return Err(MongrelError::Schema(
18083 "catalog next_user_id does not advance beyond existing user ids".into(),
18084 ));
18085 }
18086 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
18087 return Err(MongrelError::Schema(
18088 "authenticated catalog has no administrator".into(),
18089 ));
18090 }
18091 Ok(())
18092}
18093
18094fn validate_recovered_storage_plan(
18095 root: &Path,
18096 durable_root: Option<&crate::durable_file::DurableRoot>,
18097 catalog: &Catalog,
18098 created_table_ids: &HashSet<u64>,
18099 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
18100 meta_dek: Option<&[u8; META_DEK_LEN]>,
18101) -> Result<Vec<u64>> {
18102 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
18103 let mut reconcile = Vec::new();
18104 for entry in &catalog.tables {
18105 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18106 continue;
18107 }
18108 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
18109 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
18110 let table_exists = match durable_root {
18111 Some(root) => match root.open_directory(&relative_dir) {
18112 Ok(_) => true,
18113 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
18114 Err(error) => return Err(error.into()),
18115 },
18116 None => table_dir.is_dir(),
18117 };
18118 if !table_exists {
18119 if created_table_ids.contains(&entry.table_id) {
18120 reconcile.push(entry.table_id);
18121 continue;
18122 }
18123 return Err(MongrelError::NotFound(format!(
18124 "catalog table {} storage is missing",
18125 entry.table_id
18126 )));
18127 }
18128 let manifest_result = match durable_root {
18129 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
18130 None => crate::manifest::read(&table_dir, meta_dek),
18131 };
18132 let manifest = match manifest_result {
18133 Ok(manifest) => manifest,
18134 Err(MongrelError::Io(error))
18135 if created_table_ids.contains(&entry.table_id)
18136 && error.kind() == std::io::ErrorKind::NotFound =>
18137 {
18138 reconcile.push(entry.table_id);
18139 continue;
18140 }
18141 Err(error) => return Err(error),
18142 };
18143 if manifest.table_id != entry.table_id {
18144 return Err(MongrelError::Conflict(format!(
18145 "catalog table {} storage identity mismatch",
18146 entry.table_id
18147 )));
18148 }
18149 let schema_result = match durable_root {
18150 Some(root) => root
18151 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
18152 .map_err(MongrelError::from),
18153 None => crate::durable_file::open_regular_nofollow(
18154 &table_dir.join(crate::engine::SCHEMA_FILENAME),
18155 ),
18156 };
18157 let file = match schema_result {
18158 Ok(file) => file,
18159 Err(MongrelError::Io(error))
18160 if created_table_ids.contains(&entry.table_id)
18161 && error.kind() == std::io::ErrorKind::NotFound =>
18162 {
18163 reconcile.push(entry.table_id);
18164 continue;
18165 }
18166 Err(error) => return Err(error),
18167 };
18168 let length = file.metadata()?.len();
18169 if length > MAX_SCHEMA_BYTES {
18170 return Err(MongrelError::ResourceLimitExceeded {
18171 resource: "recovered schema bytes",
18172 requested: usize::try_from(length).unwrap_or(usize::MAX),
18173 limit: MAX_SCHEMA_BYTES as usize,
18174 });
18175 }
18176 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
18177 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
18178 if manifest.schema_id != entry.schema.schema_id
18179 || crate::wal::DdlOp::encode_schema(&disk_schema)?
18180 != crate::wal::DdlOp::encode_schema(&entry.schema)?
18181 {
18182 reconcile.push(entry.table_id);
18183 }
18184 }
18185 for table_id in ttl_updates.keys() {
18186 if !catalog.tables.iter().any(|entry| {
18187 entry.table_id == *table_id
18188 && matches!(entry.state, TableState::Live | TableState::Building { .. })
18189 }) {
18190 continue;
18191 }
18192 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
18193 let table_exists = match durable_root {
18194 Some(root) => match root.open_directory(&relative_dir) {
18195 Ok(_) => true,
18196 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
18197 Err(error) => return Err(error.into()),
18198 },
18199 None => root.join(&relative_dir).is_dir(),
18200 };
18201 if !table_exists && !created_table_ids.contains(table_id) {
18202 return Err(MongrelError::NotFound(format!(
18203 "TTL recovery table {table_id} storage is missing"
18204 )));
18205 }
18206 }
18207 reconcile.sort_unstable();
18208 reconcile.dedup();
18209 Ok(reconcile)
18210}
18211
18212fn validate_catalog_table_storage(
18213 root: &crate::durable_file::DurableRoot,
18214 catalog: &Catalog,
18215 meta_dek: Option<&[u8; META_DEK_LEN]>,
18216) -> Result<()> {
18217 for entry in &catalog.tables {
18218 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18219 continue;
18220 }
18221 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
18222 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
18223 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
18224 return Err(MongrelError::Conflict(format!(
18225 "catalog table {} storage identity mismatch",
18226 entry.table_id
18227 )));
18228 }
18229 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
18230 }
18231 Ok(())
18232}
18233
18234fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
18235 match schema.columns.iter_mut().find(|c| c.id == column.id) {
18236 Some(existing) if *existing == column => Ok(false),
18237 Some(existing) => {
18238 *existing = column;
18239 schema.schema_id = schema
18240 .schema_id
18241 .checked_add(1)
18242 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
18243 Ok(true)
18244 }
18245 None => {
18246 schema.columns.push(column);
18247 schema.schema_id = schema
18248 .schema_id
18249 .checked_add(1)
18250 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
18251 Ok(true)
18252 }
18253 }
18254}
18255
18256fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
18257 use crate::auth::Permission;
18258 match permission {
18259 Permission::Select { table }
18260 | Permission::Insert { table }
18261 | Permission::Update { table }
18262 | Permission::Delete { table }
18263 | Permission::SelectColumns { table, .. }
18264 | Permission::InsertColumns { table, .. }
18265 | Permission::UpdateColumns { table, .. } => Some(table),
18266 Permission::All | Permission::Ddl | Permission::Admin => None,
18267 }
18268}
18269
18270fn apply_rebuilding_publish(
18271 catalog: &mut Catalog,
18272 table_id: u64,
18273 replaced_table_id: u64,
18274 new_name: &str,
18275 epoch: u64,
18276) -> Result<bool> {
18277 let already_published = catalog.tables.iter().any(|entry| {
18278 entry.table_id == table_id
18279 && entry.name == new_name
18280 && matches!(entry.state, TableState::Live)
18281 }) && catalog.tables.iter().any(|entry| {
18282 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
18283 });
18284 if already_published {
18285 return Ok(false);
18286 }
18287 let schema = catalog
18288 .tables
18289 .iter()
18290 .find(|entry| entry.table_id == table_id)
18291 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
18292 .schema
18293 .clone();
18294 let replaced = catalog
18295 .tables
18296 .iter_mut()
18297 .find(|entry| entry.table_id == replaced_table_id)
18298 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
18299 replaced.state = TableState::Dropped { at_epoch: epoch };
18300 let replacement = catalog
18301 .tables
18302 .iter_mut()
18303 .find(|entry| entry.table_id == table_id)
18304 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
18305 replacement.name = new_name.to_string();
18306 replacement.state = TableState::Live;
18307
18308 for role in &mut catalog.roles {
18309 role.permissions.retain_mut(|permission| {
18310 retain_rebuilt_permission_columns(permission, new_name, &schema)
18311 });
18312 }
18313 for definition in &mut catalog.materialized_views {
18314 if let Some(incremental) = definition.incremental.as_mut() {
18315 if incremental.source_table == new_name
18316 && incremental.source_table_id == replaced_table_id
18317 {
18318 incremental.source_table_id = table_id;
18319 }
18320 }
18321 }
18322 advance_security_version(catalog)?;
18323 Ok(true)
18324}
18325
18326fn retain_rebuilt_permission_columns(
18327 permission: &mut crate::auth::Permission,
18328 target_table: &str,
18329 schema: &Schema,
18330) -> bool {
18331 use crate::auth::Permission;
18332 let columns = match permission {
18333 Permission::SelectColumns { table, columns }
18334 | Permission::InsertColumns { table, columns }
18335 | Permission::UpdateColumns { table, columns }
18336 if table == target_table =>
18337 {
18338 Some(columns)
18339 }
18340 _ => None,
18341 };
18342 if let Some(columns) = columns {
18343 columns.retain(|column| schema.column(column).is_some());
18344 !columns.is_empty()
18345 } else {
18346 true
18347 }
18348}
18349
18350fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
18351 use crate::auth::Permission;
18352 let table = match permission {
18353 Permission::Select { table }
18354 | Permission::Insert { table }
18355 | Permission::Update { table }
18356 | Permission::Delete { table }
18357 | Permission::SelectColumns { table, .. }
18358 | Permission::InsertColumns { table, .. }
18359 | Permission::UpdateColumns { table, .. } => Some(table),
18360 Permission::All | Permission::Ddl | Permission::Admin => None,
18361 };
18362 if let Some(table) = table.filter(|table| table.as_str() == old) {
18363 *table = new.to_string();
18364 }
18365}
18366
18367fn rename_permission_column(
18368 permission: &mut crate::auth::Permission,
18369 target_table: &str,
18370 old: &str,
18371 new: &str,
18372) {
18373 use crate::auth::Permission;
18374 let columns = match permission {
18375 Permission::SelectColumns { table, columns }
18376 | Permission::InsertColumns { table, columns }
18377 | Permission::UpdateColumns { table, columns }
18378 if table == target_table =>
18379 {
18380 Some(columns)
18381 }
18382 _ => None,
18383 };
18384 if let Some(column) = columns
18385 .into_iter()
18386 .flatten()
18387 .find(|column| column.as_str() == old)
18388 {
18389 *column = new.to_string();
18390 }
18391}
18392
18393pub(crate) fn validate_security_catalog(
18394 catalog: &Catalog,
18395 security: &crate::security::SecurityCatalog,
18396) -> Result<()> {
18397 let mut policy_names = HashSet::new();
18398 for table in &security.rls_tables {
18399 if catalog.live(table).is_none() {
18400 return Err(MongrelError::NotFound(format!(
18401 "RLS table {table:?} not found"
18402 )));
18403 }
18404 }
18405 for policy in &security.policies {
18406 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
18407 return Err(MongrelError::InvalidArgument(format!(
18408 "duplicate policy {:?} on {:?}",
18409 policy.name, policy.table
18410 )));
18411 }
18412 let schema = &catalog
18413 .live(&policy.table)
18414 .ok_or_else(|| {
18415 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
18416 })?
18417 .schema;
18418 if let Some(expression) = &policy.using {
18419 validate_security_expression(expression, schema)?;
18420 }
18421 if let Some(expression) = &policy.with_check {
18422 validate_security_expression(expression, schema)?;
18423 }
18424 }
18425 let mut mask_names = HashSet::new();
18426 for mask in &security.masks {
18427 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
18428 return Err(MongrelError::InvalidArgument(format!(
18429 "duplicate mask {:?} on {:?}",
18430 mask.name, mask.table
18431 )));
18432 }
18433 let column = catalog
18434 .live(&mask.table)
18435 .and_then(|entry| {
18436 entry
18437 .schema
18438 .columns
18439 .iter()
18440 .find(|column| column.id == mask.column)
18441 })
18442 .ok_or_else(|| {
18443 MongrelError::NotFound(format!(
18444 "mask column {} on {:?} not found",
18445 mask.column, mask.table
18446 ))
18447 })?;
18448 if matches!(
18449 mask.strategy,
18450 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
18451 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
18452 {
18453 return Err(MongrelError::InvalidArgument(format!(
18454 "mask {:?} requires a string/bytes column",
18455 mask.name
18456 )));
18457 }
18458 }
18459 Ok(())
18460}
18461
18462fn validate_security_expression(
18463 expression: &crate::security::SecurityExpr,
18464 schema: &Schema,
18465) -> Result<()> {
18466 use crate::security::SecurityExpr;
18467 match expression {
18468 SecurityExpr::True => Ok(()),
18469 SecurityExpr::ColumnEqCurrentUser { column }
18470 | SecurityExpr::ColumnEqValue { column, .. } => {
18471 if schema
18472 .columns
18473 .iter()
18474 .any(|candidate| candidate.id == *column)
18475 {
18476 Ok(())
18477 } else {
18478 Err(MongrelError::InvalidArgument(format!(
18479 "security expression references unknown column id {column}"
18480 )))
18481 }
18482 }
18483 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
18484 validate_security_expression(left, schema)?;
18485 validate_security_expression(right, schema)
18486 }
18487 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
18488 }
18489}
18490
18491fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
18493 let referenced = cat
18494 .tables
18495 .iter()
18496 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
18497 .map(|entry| entry.table_id)
18498 .collect::<HashSet<_>>();
18499 let tables_dir = root.join(TABLES_DIR);
18500 let entries = match std::fs::read_dir(&tables_dir) {
18501 Ok(entries) => entries,
18502 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
18503 Err(error) => return Err(error.into()),
18504 };
18505 for entry in entries {
18506 let entry = entry?;
18507 if !entry.file_type()?.is_dir() {
18508 continue;
18509 }
18510 let file_name = entry.file_name();
18511 let Some(name) = file_name.to_str() else {
18512 continue;
18513 };
18514 let Ok(table_id) = name.parse::<u64>() else {
18515 continue;
18516 };
18517 if name != table_id.to_string() {
18518 continue;
18519 }
18520 if !referenced.contains(&table_id) {
18521 crate::durable_file::remove_directory_all(&entry.path())?;
18522 }
18523 }
18524 Ok(())
18525}
18526
18527fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
18532 for entry in &cat.tables {
18533 let txn_dir = root
18534 .join(TABLES_DIR)
18535 .join(entry.table_id.to_string())
18536 .join("_txn");
18537 if txn_dir.exists() {
18538 let _ = std::fs::remove_dir_all(&txn_dir);
18539 }
18540 }
18541}
18542
18543#[cfg(test)]
18544mod write_permission_tests {
18545 use super::*;
18546 use crate::txn::Staged;
18547
18548 struct NoopExternalBridge;
18549
18550 impl ExternalTriggerBridge for NoopExternalBridge {
18551 fn apply_trigger_external_write(
18552 &self,
18553 _entry: &ExternalTableEntry,
18554 base_state: Vec<u8>,
18555 _op: ExternalTriggerWrite,
18556 ) -> Result<ExternalTriggerWriteResult> {
18557 Ok(ExternalTriggerWriteResult::new(base_state))
18558 }
18559 }
18560
18561 fn assert_txn_namespace_full<T>(result: Result<T>) {
18562 assert!(matches!(result, Err(MongrelError::Full(_))));
18563 }
18564
18565 #[test]
18566 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
18567 let directory = tempfile::tempdir().unwrap();
18568 let database = Database::create(directory.path()).unwrap();
18569 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
18570 *database.next_txn_id.lock() = generation << 32;
18571 let before = crate::wal::SharedWal::replay(directory.path())
18572 .unwrap()
18573 .len();
18574 let bridge = NoopExternalBridge;
18575
18576 assert_txn_namespace_full(database.begin().commit());
18577 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
18578 assert_txn_namespace_full(
18579 database
18580 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
18581 .commit(),
18582 );
18583 assert_txn_namespace_full(
18584 database
18585 .begin_with_external_trigger_bridge(&bridge)
18586 .commit(),
18587 );
18588 assert_txn_namespace_full(
18589 database
18590 .begin_with_external_trigger_bridge_as(&bridge, None)
18591 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
18592 );
18593
18594 assert_eq!(
18595 crate::wal::SharedWal::replay(directory.path())
18596 .unwrap()
18597 .len(),
18598 before
18599 );
18600 drop(database);
18601 Database::open(directory.path()).unwrap();
18602 }
18603
18604 #[test]
18605 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
18606 let directory = tempfile::tempdir().unwrap();
18607 let table_dir = directory.path().join("7");
18608 crate::durable_file::create_directory_all(&table_dir).unwrap();
18609 let original_schema = test_schema();
18610 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
18611 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
18612 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
18613 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
18614 let original_bytes = std::fs::read(&schema_path).unwrap();
18615
18616 let mut replacement_schema = original_schema;
18617 replacement_schema.schema_id += 1;
18618 assert!(matches!(
18619 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
18620 Err(MongrelError::Conflict(_))
18621 ));
18622
18623 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
18624 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
18625 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
18626 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
18627 }
18628
18629 #[test]
18630 fn catalog_table_missing_storage_fails_without_recreating_it() {
18631 let directory = tempfile::tempdir().unwrap();
18632 let table_dir = {
18633 let database = Database::create(directory.path()).unwrap();
18634 database.create_table("docs", test_schema()).unwrap();
18635 directory
18636 .path()
18637 .join(TABLES_DIR)
18638 .join(database.table_id("docs").unwrap().to_string())
18639 };
18640 std::fs::remove_dir_all(&table_dir).unwrap();
18641
18642 assert!(matches!(
18643 Database::open(directory.path()),
18644 Err(MongrelError::NotFound(_))
18645 ));
18646 assert!(!table_dir.exists());
18647 }
18648
18649 #[test]
18650 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
18651 let directory = tempfile::tempdir().unwrap();
18652 let database = std::sync::Arc::new(
18653 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
18654 );
18655 database.create_user("alice", "old-password").unwrap();
18656 let old_identity = database.user_identity("alice").unwrap();
18657 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
18658 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
18659 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
18660 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
18661
18662 std::thread::scope(|scope| {
18663 let authenticate = {
18664 let database = std::sync::Arc::clone(&database);
18665 scope.spawn(move || {
18666 database.authenticate_principal_inner("alice", "old-password", || {
18667 verified_tx.send(()).unwrap();
18668 resume_rx.recv().unwrap();
18669 })
18670 })
18671 };
18672 verified_rx.recv().unwrap();
18673 let mutate = {
18674 let database = std::sync::Arc::clone(&database);
18675 scope.spawn(move || {
18676 mutation_started_tx.send(()).unwrap();
18677 database.drop_user("alice").unwrap();
18678 database.create_user("alice", "new-password").unwrap();
18679 mutation_done_tx.send(()).unwrap();
18680 })
18681 };
18682 mutation_started_rx.recv().unwrap();
18683 assert!(mutation_done_rx
18684 .recv_timeout(std::time::Duration::from_millis(50))
18685 .is_err());
18686 resume_tx.send(()).unwrap();
18687 let principal = authenticate.join().unwrap().unwrap().unwrap();
18688 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
18689 mutate.join().unwrap();
18690 });
18691
18692 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
18693 assert!(database
18694 .authenticate_principal("alice", "old-password")
18695 .unwrap()
18696 .is_none());
18697 assert!(database
18698 .authenticate_principal("alice", "new-password")
18699 .unwrap()
18700 .is_some());
18701 }
18702
18703 #[test]
18704 fn homogeneous_batch_summarizes_to_one_permission_decision() {
18705 let staging = (0..10_050)
18706 .map(|_| {
18707 (
18708 7,
18709 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
18710 )
18711 })
18712 .collect::<Vec<_>>();
18713
18714 let needs = summarize_write_permissions(&staging);
18715 let table = needs.get(&7).unwrap();
18716 assert_eq!(needs.len(), 1);
18717 assert!(table.insert);
18718 assert_eq!(table.insert_columns, [1, 2]);
18719 assert!(!table.update);
18720 assert!(!table.delete);
18721 assert!(!table.truncate);
18722 }
18723
18724 #[test]
18725 fn mixed_writes_union_columns_and_preserve_empty_operations() {
18726 let staging = vec![
18727 (7, Staged::Put(vec![(2, Value::Int64(2))])),
18728 (7, Staged::Put(vec![(1, Value::Int64(1))])),
18729 (
18730 7,
18731 Staged::Update {
18732 row_id: RowId(1),
18733 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
18734 changed_columns: vec![2],
18735 },
18736 ),
18737 (7, Staged::Delete(RowId(2))),
18738 (8, Staged::Truncate),
18739 ];
18740
18741 let needs = summarize_write_permissions(&staging);
18742 let table = needs.get(&7).unwrap();
18743 assert_eq!(table.insert_columns, [1, 2]);
18744 assert!(table.update);
18745 assert_eq!(table.update_columns, [2]);
18746 assert!(table.delete);
18747 assert!(needs.get(&8).unwrap().truncate);
18748 }
18749
18750 #[test]
18751 fn final_permission_decisions_do_not_scale_with_rows() {
18752 let credentialless_dir = tempfile::tempdir().unwrap();
18753 let credentialless = Database::create(credentialless_dir.path()).unwrap();
18754 credentialless.create_table("docs", test_schema()).unwrap();
18755 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18756 credentialless
18757 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
18758 .unwrap();
18759 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
18760
18761 let authenticated_dir = tempfile::tempdir().unwrap();
18762 let authenticated =
18763 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
18764 .unwrap();
18765 authenticated.create_table("docs", test_schema()).unwrap();
18766 let admin = authenticated.resolve_principal("admin").unwrap();
18767 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18768 authenticated
18769 .validate_write_permissions(
18770 &puts(authenticated.table_id("docs").unwrap()),
18771 Some(&admin),
18772 None,
18773 )
18774 .unwrap();
18775 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
18776 }
18777
18778 #[test]
18779 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
18780 let dir = tempfile::tempdir().unwrap();
18781 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
18782 db.create_table("docs", test_schema()).unwrap();
18783 let admin = db.resolve_principal("admin").unwrap();
18784 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18785
18786 let mut transaction = db.begin_as(Some(admin));
18787 transaction
18788 .delete_batch("docs", (0..100).map(RowId).collect())
18789 .unwrap();
18790 transaction.commit().unwrap();
18791
18792 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
18793 }
18794
18795 #[test]
18796 fn truncate_validation_checks_admin_once_for_all_tables() {
18797 let dir = tempfile::tempdir().unwrap();
18798 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
18799 db.create_table("first", test_schema()).unwrap();
18800 db.create_table("second", test_schema()).unwrap();
18801 let admin = db.resolve_principal("admin").unwrap();
18802 let staging = vec![
18803 (db.table_id("first").unwrap(), Staged::Truncate),
18804 (db.table_id("second").unwrap(), Staged::Truncate),
18805 ];
18806
18807 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18808 db.validate_write_permissions(&staging, Some(&admin), None)
18809 .unwrap();
18810 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
18811 }
18812
18813 #[test]
18814 fn one_table_commit_batches_structural_work() {
18815 let dir = tempfile::tempdir().unwrap();
18816 let db = Database::create(dir.path()).unwrap();
18817 db.create_table("docs", test_schema()).unwrap();
18818 let table_id = db.table_id("docs").unwrap();
18819
18820 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
18821 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
18822 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
18823 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
18824 db.transaction(|transaction| {
18825 for id in 0..100 {
18826 transaction.put("docs", vec![(1, Value::Int64(id))])?;
18827 }
18828 Ok(())
18829 })
18830 .unwrap();
18831
18832 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
18833 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18834 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18835 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
18836
18837 let puts = crate::wal::SharedWal::replay(dir.path())
18838 .unwrap()
18839 .into_iter()
18840 .filter_map(|record| match record.op {
18841 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
18842 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
18843 .unwrap()
18844 .len(),
18845 ),
18846 _ => None,
18847 })
18848 .collect::<Vec<_>>();
18849 assert_eq!(puts, [100]);
18850
18851 let row_ids = db
18852 .table("docs")
18853 .unwrap()
18854 .lock()
18855 .visible_rows(db.snapshot().0)
18856 .unwrap()
18857 .into_iter()
18858 .take(2)
18859 .map(|row| row.row_id)
18860 .collect::<Vec<_>>();
18861 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
18862 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
18863 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
18864 db.transaction(|transaction| {
18865 for row_id in row_ids {
18866 transaction.delete("docs", row_id)?;
18867 }
18868 Ok(())
18869 })
18870 .unwrap();
18871 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18872 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18873 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
18874
18875 let deletes = crate::wal::SharedWal::replay(dir.path())
18876 .unwrap()
18877 .into_iter()
18878 .filter_map(|record| match record.op {
18879 crate::wal::Op::Delete {
18880 table_id: id,
18881 row_ids,
18882 } if id == table_id => Some(row_ids.len()),
18883 _ => None,
18884 })
18885 .collect::<Vec<_>>();
18886 assert_eq!(deletes, [2]);
18887 }
18888
18889 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
18890 (0..10_050)
18891 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
18892 .collect()
18893 }
18894
18895 fn test_schema() -> Schema {
18896 Schema {
18897 columns: vec![ColumnDef {
18898 id: 1,
18899 name: "id".into(),
18900 ty: TypeId::Int64,
18901 flags: crate::schema::ColumnFlags::empty()
18902 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
18903 default_value: None,
18904 embedding_source: None,
18905 }],
18906 ..Schema::default()
18907 }
18908 }
18909}
18910
18911#[cfg(test)]
18912mod cdc_bounds_tests {
18913 use super::*;
18914
18915 #[test]
18916 fn retained_byte_limit_rejects_without_allocating_payload() {
18917 let mut retained = 0;
18918 let error = charge_cdc_bytes(
18919 &mut retained,
18920 CDC_MAX_RETAINED_BYTES.saturating_add(1),
18921 "CDC retained bytes",
18922 )
18923 .unwrap_err();
18924 assert!(matches!(
18925 error,
18926 MongrelError::ResourceLimitExceeded {
18927 resource: "CDC retained bytes",
18928 ..
18929 }
18930 ));
18931 }
18932
18933 #[test]
18934 fn row_json_estimate_accounts_for_byte_array_expansion() {
18935 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
18936 .with_column(1, Value::Bytes(vec![0; 1024]));
18937 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
18938 }
18939}
18940
18941#[cfg(test)]
18942mod generation_metrics_tests {
18943 use super::*;
18944 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
18945
18946 #[test]
18947 fn legacy_cow_fallback_is_measured() {
18948 let dir = tempfile::tempdir().unwrap();
18949 let table = Table::create(
18950 dir.path(),
18951 Schema {
18952 columns: vec![ColumnDef {
18953 id: 1,
18954 name: "id".into(),
18955 ty: TypeId::Int64,
18956 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
18957 default_value: None,
18958 embedding_source: None,
18959 }],
18960 ..Schema::default()
18961 },
18962 1,
18963 )
18964 .unwrap();
18965 let handle = TableHandle::from_table(table);
18966 let held = match &handle.inner {
18967 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
18968 TableHandleInner::Direct(_) => unreachable!(),
18969 };
18970
18971 handle.lock().set_sync_byte_threshold(1);
18972
18973 let stats = handle.generation_stats();
18974 assert_eq!(stats.cow_clone_count, 1);
18975 assert!(stats.estimated_cow_clone_bytes > 0);
18976 drop(held);
18977 }
18978}
18979
18980#[cfg(test)]
18981mod trigger_engine_tests {
18982 use super::*;
18983
18984 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
18985 WriteEvent {
18986 table: "test".into(),
18987 kind: TriggerEvent::Insert,
18988 new: Some(TriggerRowImage {
18989 columns: new_cells.iter().cloned().collect(),
18990 }),
18991 old: Some(TriggerRowImage {
18992 columns: old_cells.iter().cloned().collect(),
18993 }),
18994 changed_columns: Vec::new(),
18995 op_indices: Vec::new(),
18996 put_idx: None,
18997 trigger_stack: Vec::new(),
18998 }
18999 }
19000
19001 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
19002 WriteEvent {
19003 table: "test".into(),
19004 kind: TriggerEvent::Insert,
19005 new: Some(TriggerRowImage {
19006 columns: new_cells.iter().cloned().collect(),
19007 }),
19008 old: None,
19009 changed_columns: Vec::new(),
19010 op_indices: Vec::new(),
19011 put_idx: None,
19012 trigger_stack: Vec::new(),
19013 }
19014 }
19015
19016 #[test]
19017 fn value_order_int64_vs_float64() {
19018 assert_eq!(
19019 value_order(&Value::Int64(5), &Value::Float64(5.0)),
19020 Some(std::cmp::Ordering::Equal)
19021 );
19022 assert_eq!(
19023 value_order(&Value::Int64(5), &Value::Float64(3.0)),
19024 Some(std::cmp::Ordering::Greater)
19025 );
19026 assert_eq!(
19027 value_order(&Value::Int64(2), &Value::Float64(3.0)),
19028 Some(std::cmp::Ordering::Less)
19029 );
19030 }
19031
19032 #[test]
19033 fn value_order_null_returns_none() {
19034 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
19035 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
19036 assert_eq!(value_order(&Value::Null, &Value::Null), None);
19037 }
19038
19039 #[test]
19040 fn value_order_cross_group_returns_none() {
19041 assert_eq!(
19042 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
19043 None
19044 );
19045 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
19046 assert_eq!(
19047 value_order(
19048 &Value::Embedding(vec![1.0, 2.0]),
19049 &Value::Embedding(vec![1.0, 2.0])
19050 ),
19051 None
19052 );
19053 }
19054
19055 #[test]
19056 fn eval_trigger_expr_ranges_and_booleans() {
19057 let expr = TriggerExpr::And {
19058 left: Box::new(TriggerExpr::Gt {
19059 left: TriggerValue::NewColumn(1),
19060 right: TriggerValue::Literal(Value::Int64(0)),
19061 }),
19062 right: Box::new(TriggerExpr::Lte {
19063 left: TriggerValue::NewColumn(1),
19064 right: TriggerValue::Literal(Value::Int64(100)),
19065 }),
19066 };
19067 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
19068 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
19069 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
19070
19071 let or_expr = TriggerExpr::Or {
19072 left: Box::new(TriggerExpr::Lt {
19073 left: TriggerValue::NewColumn(1),
19074 right: TriggerValue::Literal(Value::Int64(0)),
19075 }),
19076 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
19077 TriggerValue::OldColumn(2),
19078 )))),
19079 };
19080 assert!(eval_trigger_expr(
19081 &or_expr,
19082 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
19083 )
19084 .unwrap());
19085 assert!(!eval_trigger_expr(
19086 &or_expr,
19087 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
19088 )
19089 .unwrap());
19090
19091 assert!(eval_trigger_expr(
19092 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
19093 &event_insert(&[])
19094 )
19095 .unwrap());
19096 assert!(!eval_trigger_expr(
19097 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
19098 &event_insert(&[])
19099 )
19100 .unwrap());
19101 assert!(!eval_trigger_expr(
19102 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
19103 &event_insert(&[])
19104 )
19105 .unwrap());
19106 }
19107}
19108
19109#[cfg(test)]
19110mod core_resource_tests {
19111 use super::*;
19112
19113 fn int_pk_schema() -> Schema {
19114 Schema {
19115 columns: vec![ColumnDef {
19116 id: 1,
19117 name: "id".into(),
19118 ty: TypeId::Int64,
19119 flags: crate::schema::ColumnFlags::empty()
19120 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19121 default_value: None,
19122 embedding_source: None,
19123 }],
19124 ..Schema::default()
19125 }
19126 }
19127
19128 #[test]
19129 fn open_constructs_governor_spill_and_jobs_with_documented_defaults() {
19130 let dir = tempfile::tempdir().unwrap();
19131 let db = Database::create(dir.path()).unwrap();
19132 assert_eq!(
19133 db.memory_governor().max_bytes(),
19134 DEFAULT_MEMORY_BUDGET_BYTES
19135 );
19136 assert_eq!(
19137 db.spill_manager().config().global_bytes,
19138 DEFAULT_TEMP_DISK_BUDGET_BYTES
19139 );
19140 assert!(db.job_registry().list().is_empty());
19141 assert_eq!(
19143 db.resource_groups().len(),
19144 crate::resource::WorkloadClass::ALL.len()
19145 );
19146 assert!(db.resource_groups().get("control").is_some());
19147 assert!(db.embedding_providers().list_ids().is_empty());
19148 let err = db
19150 .embedding_providers()
19151 .embed(
19152 &crate::embedding::EmbeddingSource::SuppliedByApplication,
19153 &["text"],
19154 4,
19155 )
19156 .unwrap_err();
19157 assert!(matches!(
19158 err,
19159 crate::embedding::EmbeddingError::SuppliedByApplication
19160 ));
19161 }
19162
19163 #[test]
19164 fn lock_rows_for_update_acquires_exclusive_row_locks() {
19165 use crate::locks::LockKey;
19166 use crate::rowid::RowId;
19167
19168 let dir = tempfile::tempdir().unwrap();
19169 let db = Database::create(dir.path()).unwrap();
19170 let txn_id = db.allocate_lock_txn_id().unwrap();
19171 let rid = RowId(42);
19172 db.lock_rows_for_update(txn_id, 7, &[rid], None).unwrap();
19173 assert!(db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
19174 db.release_txn_locks(txn_id);
19175 assert!(!db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
19176 }
19177
19178 #[test]
19179 fn open_with_options_sizes_the_core_budgets() {
19180 let dir = tempfile::tempdir().unwrap();
19181 let db = Database::create(dir.path()).unwrap();
19182 drop(db);
19183 let db = Database::open_with_options(
19184 dir.path(),
19185 OpenOptions::default()
19186 .with_memory_budget_bytes(256 * 1024 * 1024)
19187 .with_temp_disk_budget_bytes(16 * 1024 * 1024),
19188 )
19189 .unwrap();
19190 assert_eq!(db.memory_governor().max_bytes(), 256 * 1024 * 1024);
19191 assert_eq!(db.spill_manager().config().global_bytes, 16 * 1024 * 1024);
19192 }
19193
19194 #[test]
19195 fn zero_budgets_are_rejected() {
19196 let dir = tempfile::tempdir().unwrap();
19197 let db = Database::create(dir.path()).unwrap();
19198 drop(db);
19199 let result = Database::open_with_options(
19200 dir.path(),
19201 OpenOptions::default().with_memory_budget_bytes(0),
19202 );
19203 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
19204 let result = Database::open_with_options(
19205 dir.path(),
19206 OpenOptions::default().with_temp_disk_budget_bytes(0),
19207 );
19208 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
19209 }
19210
19211 #[test]
19212 fn page_caches_reserve_under_the_governor() {
19213 let dir = tempfile::tempdir().unwrap();
19214 let db = Database::create(dir.path()).unwrap();
19215 db.create_table("t", int_pk_schema()).unwrap();
19216 let mut txn = db.begin();
19217 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
19218 txn.commit().unwrap();
19219 let stats = db.memory_governor().stats();
19223 assert_eq!(
19224 stats.usage_for(crate::memory::MemoryClass::PageCache),
19225 db.page_cache.used_bytes()
19226 );
19227 assert_eq!(
19228 stats.usage_for(crate::memory::MemoryClass::DecodedCache),
19229 db.decoded_cache.used_bytes()
19230 );
19231 assert_eq!(
19232 db.memory_governor().reclaimable_bytes(),
19233 db.page_cache.used_bytes() + db.decoded_cache.used_bytes()
19234 );
19235 let _ = db.memory_governor().evict_reclaimable(1024 * 1024);
19237 }
19238
19239 #[test]
19240 fn job_registry_persists_across_reopen() {
19241 let dir = tempfile::tempdir().unwrap();
19242 let db = Database::create(dir.path()).unwrap();
19243 db.create_table("t", int_pk_schema()).unwrap();
19244 let job_id = db
19245 .job_registry()
19246 .submit(
19247 crate::jobs::JobKind::IndexBuild,
19248 crate::jobs::JobTarget {
19249 table: "t".to_string(),
19250 index: Some("idx".to_string()),
19251 },
19252 )
19253 .unwrap();
19254 drop(db);
19255 let db = Database::open(dir.path()).unwrap();
19256 let record = db.job_registry().get(job_id).expect("job survives reopen");
19257 assert_eq!(record.state, crate::jobs::JobState::Pending);
19258 }
19259
19260 #[test]
19261 fn spill_manager_open_sweeps_stale_temp_tree() {
19262 let dir = tempfile::tempdir().unwrap();
19263 let db = Database::create(dir.path()).unwrap();
19264 let stale = dir.path().join("temp").join("spill").join("q-deadbeef");
19265 std::fs::create_dir_all(&stale).unwrap();
19266 std::fs::write(stale.join("chunk-0"), b"stale").unwrap();
19267 drop(db);
19268 let db = Database::open(dir.path()).unwrap();
19269 assert!(
19270 !stale.exists(),
19271 "the startup sweep removes stale spill files (S1E-004)"
19272 );
19273 let session = db
19275 .spill_manager()
19276 .begin_query(
19277 mongreldb_types::ids::QueryId::from_bytes([7u8; 16]),
19278 1024 * 1024,
19279 )
19280 .unwrap();
19281 assert_eq!(session.used(), 0);
19282 }
19283}
19284
19285#[cfg(test)]
19286mod version_pin_tests {
19287 use super::*;
19288
19289 fn int_pk_schema() -> Schema {
19290 Schema {
19291 columns: vec![ColumnDef {
19292 id: 1,
19293 name: "id".into(),
19294 ty: TypeId::Int64,
19295 flags: crate::schema::ColumnFlags::empty()
19296 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19297 default_value: None,
19298 embedding_source: None,
19299 }],
19300 ..Schema::default()
19301 }
19302 }
19303
19304 fn pins_for(
19305 report: &[TablePinsReport],
19306 table: &str,
19307 source: crate::retention::PinSource,
19308 ) -> Option<crate::retention::PinInfo> {
19309 report
19310 .iter()
19311 .find(|entry| entry.table == table)
19312 .and_then(|entry| entry.pins.get(source).cloned())
19313 }
19314
19315 #[test]
19316 fn backup_boundary_registers_backup_pitr_pin() {
19317 let source = tempfile::tempdir().unwrap();
19318 let destination_parent = tempfile::tempdir().unwrap();
19319 let destination = destination_parent.path().join("backup");
19320 let db = Arc::new(Database::create(source.path()).unwrap());
19321 db.create_table("t", int_pk_schema()).unwrap();
19322 let mut txn = db.begin();
19323 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
19324 let boundary_epoch = txn.commit().unwrap();
19325
19326 let hold = Arc::new(std::sync::Barrier::new(2));
19327 let resume = Arc::new(std::sync::Barrier::new(2));
19328 db.__set_backup_hook({
19329 let hold = Arc::clone(&hold);
19330 let resume = Arc::clone(&resume);
19331 move || {
19332 hold.wait();
19333 resume.wait();
19334 }
19335 });
19336
19337 let backup = {
19338 let db = Arc::clone(&db);
19339 let destination = destination.clone();
19340 std::thread::spawn(move || db.hot_backup(destination))
19341 };
19342 hold.wait();
19343 let report = db.version_pins_report();
19346 let pin = pins_for(&report, "t", crate::retention::PinSource::BackupPitr)
19347 .expect("backup boundary must register a BackupPitr pin");
19348 assert_eq!(pin.oldest_epoch, boundary_epoch);
19349 assert!(pin.pin_count >= 1);
19350 resume.wait();
19351 backup.join().unwrap().unwrap();
19352
19353 let report = db.version_pins_report();
19354 assert!(
19355 pins_for(&report, "t", crate::retention::PinSource::BackupPitr).is_none(),
19356 "the BackupPitr pin releases when the backup finishes"
19357 );
19358 }
19359
19360 #[test]
19361 fn snapshot_and_read_generation_pins_surface_in_report() {
19362 let dir = tempfile::tempdir().unwrap();
19363 let db = Database::create(dir.path()).unwrap();
19364 db.create_table("t", int_pk_schema()).unwrap();
19365 let mut txn = db.begin();
19366 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
19367 txn.commit().unwrap();
19368
19369 let (_snapshot, guard) = db.snapshot();
19370 let report = db.version_pins_report();
19371 assert!(
19372 pins_for(
19373 &report,
19374 "t",
19375 crate::retention::PinSource::TransactionSnapshot
19376 )
19377 .is_some(),
19378 "a database snapshot projects the TransactionSnapshot source"
19379 );
19380 drop(guard);
19381
19382 let handle = db.table("t").unwrap();
19383 let (generation, _snapshot) = handle.read_generation_with_context(None).unwrap();
19384 let report = db.version_pins_report();
19385 assert!(
19386 pins_for(&report, "t", crate::retention::PinSource::ReadGeneration).is_some(),
19387 "a cloned read generation registers a ReadGeneration pin"
19388 );
19389 drop(generation);
19390
19391 let report = db.version_pins_report();
19392 let entry = report.iter().find(|entry| entry.table == "t").unwrap();
19393 assert!(
19394 entry
19395 .pins
19396 .get(crate::retention::PinSource::BackupPitr)
19397 .is_none()
19398 && entry
19399 .pins
19400 .get(crate::retention::PinSource::Replication)
19401 .is_none()
19402 && entry
19403 .pins
19404 .get(crate::retention::PinSource::OnlineIndexBuild)
19405 .is_none(),
19406 "untaken sources stay absent from the report"
19407 );
19408 }
19409}
19410
19411#[cfg(test)]
19412mod lock_manager_tests {
19413 use super::*;
19414 use crate::locks::LockKey;
19415
19416 fn col(id: u16, name: &str, ty: TypeId, flags: crate::schema::ColumnFlags) -> ColumnDef {
19417 ColumnDef {
19418 id,
19419 name: name.into(),
19420 ty,
19421 flags,
19422 default_value: None,
19423 embedding_source: None,
19424 }
19425 }
19426
19427 fn unique_schema() -> Schema {
19428 let mut constraints = crate::constraint::TableConstraints::default();
19429 constraints
19430 .uniques
19431 .push(crate::constraint::UniqueConstraint {
19432 id: 1,
19433 name: "users_email_unique".into(),
19434 columns: vec![1],
19435 });
19436 Schema {
19437 columns: vec![
19438 col(
19439 0,
19440 "id",
19441 TypeId::Int64,
19442 crate::schema::ColumnFlags::empty()
19443 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19444 ),
19445 col(
19446 1,
19447 "email",
19448 TypeId::Bytes,
19449 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
19450 ),
19451 ],
19452 constraints,
19453 ..Schema::default()
19454 }
19455 }
19456
19457 fn parent_schema() -> Schema {
19458 Schema {
19459 columns: vec![col(
19460 0,
19461 "id",
19462 TypeId::Int64,
19463 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::PRIMARY_KEY),
19464 )],
19465 ..Schema::default()
19466 }
19467 }
19468
19469 fn child_schema() -> Schema {
19470 let mut constraints = crate::constraint::TableConstraints::default();
19471 constraints
19472 .foreign_keys
19473 .push(crate::constraint::ForeignKey {
19474 id: 1,
19475 name: "child_parent_fk".into(),
19476 columns: vec![1],
19477 ref_table: "parent".into(),
19478 ref_columns: vec![0],
19479 on_delete: crate::constraint::FkAction::Restrict,
19480 on_update: crate::constraint::FkAction::Restrict,
19481 });
19482 Schema {
19483 columns: vec![
19484 col(
19485 0,
19486 "id",
19487 TypeId::Int64,
19488 crate::schema::ColumnFlags::empty()
19489 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19490 ),
19491 col(
19492 1,
19493 "pid",
19494 TypeId::Int64,
19495 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
19496 ),
19497 ],
19498 constraints,
19499 ..Schema::default()
19500 }
19501 }
19502
19503 fn auto_inc_schema() -> Schema {
19504 Schema {
19505 columns: vec![col(
19506 0,
19507 "id",
19508 TypeId::Int64,
19509 crate::schema::ColumnFlags::empty()
19510 .with(crate::schema::ColumnFlags::PRIMARY_KEY)
19511 .with(crate::schema::ColumnFlags::AUTO_INCREMENT),
19512 )],
19513 ..Schema::default()
19514 }
19515 }
19516
19517 fn pk_lock_key(table_id: u64, value: i64) -> LockKey {
19518 let mut key = b"pk:".to_vec();
19519 key.extend_from_slice(&Value::Int64(value).encode_key());
19520 LockKey::key(table_id, key)
19521 }
19522
19523 #[test]
19524 fn unique_claims_serialize_concurrent_commits() {
19525 let dir = tempfile::tempdir().unwrap();
19526 let db = Arc::new(Database::create(dir.path()).unwrap());
19527 let table_id = db.create_table("users", unique_schema()).unwrap();
19528 let pk_key = pk_lock_key(table_id, 1);
19529 let entered = Arc::new(std::sync::Barrier::new(2));
19530 let resume = Arc::new(std::sync::Barrier::new(2));
19531 let parked = Arc::new(AtomicBool::new(false));
19532 db.__set_catalog_commit_hook({
19533 let entered = Arc::clone(&entered);
19534 let resume = Arc::clone(&resume);
19535 let parked = Arc::clone(&parked);
19536 move || {
19537 if !parked.swap(true, Ordering::SeqCst) {
19540 entered.wait();
19541 resume.wait();
19542 }
19543 }
19544 });
19545
19546 let mut txn_a = db.begin();
19547 txn_a
19548 .put(
19549 "users",
19550 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
19551 )
19552 .unwrap();
19553 let a_id = txn_a.txn_id();
19554 let (a_tx, a_rx) = std::sync::mpsc::channel();
19555 let (b_tx, b_rx) = std::sync::mpsc::channel();
19556 std::thread::scope(|scope| {
19557 scope.spawn(|| {
19558 a_tx.send(txn_a.commit()).unwrap();
19559 });
19560 entered.wait();
19561 assert!(
19563 db.lock_manager().holds(a_id, &pk_key),
19564 "primary-key claim must be held until the commit ends"
19565 );
19566 let mut uq_key = format!("uq{}:", 1).into_bytes();
19567 let cells_map: HashMap<u16, Value> = [(1u16, Value::Bytes(b"a@x".to_vec()))]
19568 .into_iter()
19569 .collect();
19570 uq_key.extend_from_slice(
19571 &crate::constraint::encode_composite_key(&[1], &cells_map).unwrap(),
19572 );
19573 assert!(
19574 db.lock_manager()
19575 .holds(a_id, &LockKey::key(table_id, uq_key)),
19576 "declared-unique claim must be held until the commit ends"
19577 );
19578
19579 let mut txn_b = db.begin();
19580 txn_b
19581 .put(
19582 "users",
19583 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"b@x".to_vec()))],
19584 )
19585 .unwrap();
19586 scope.spawn(|| {
19587 b_tx.send(txn_b.commit()).unwrap();
19588 });
19589 std::thread::sleep(std::time::Duration::from_millis(100));
19590 assert!(
19591 b_rx.try_recv().is_err(),
19592 "the concurrent claim must block until A ends its transaction"
19593 );
19594 resume.wait();
19595 assert!(a_rx.recv().unwrap().is_ok());
19596 let b_result = b_rx.recv().unwrap();
19597 assert!(
19598 matches!(b_result, Err(MongrelError::Conflict(_))),
19599 "the loser surfaces a conflict after serializing: {b_result:?}"
19600 );
19601 });
19602 assert!(
19603 !db.lock_manager().holds(a_id, &pk_key),
19604 "no phantom holds remain after the commit"
19605 );
19606 }
19607
19608 #[test]
19609 fn ddl_waits_for_inflight_dml_commit_on_schema_barrier() {
19610 let dir = tempfile::tempdir().unwrap();
19611 let db = Arc::new(Database::create(dir.path()).unwrap());
19612 db.create_table("parent", parent_schema()).unwrap();
19613 db.create_table("child", child_schema()).unwrap();
19614 let mut seed = db.begin();
19615 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
19616 seed.commit().unwrap();
19617
19618 let entered = Arc::new(std::sync::Barrier::new(2));
19619 let resume = Arc::new(std::sync::Barrier::new(2));
19620 db.__set_fk_lock_hook({
19621 let entered = Arc::clone(&entered);
19622 let resume = Arc::clone(&resume);
19623 move || {
19624 entered.wait();
19625 resume.wait();
19626 }
19627 });
19628
19629 let mut txn_a = db.begin();
19630 txn_a
19631 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(1))])
19632 .unwrap();
19633 let a_id = txn_a.txn_id();
19634 let (a_tx, a_rx) = std::sync::mpsc::channel();
19635 let (ddl_tx, ddl_rx) = std::sync::mpsc::channel();
19636 std::thread::scope(|scope| {
19637 scope.spawn(|| {
19638 a_tx.send(txn_a.commit()).unwrap();
19639 });
19640 entered.wait();
19641 assert!(
19643 db.lock_manager().holds(a_id, &LockKey::schema_barrier()),
19644 "DML holds the schema barrier Shared for its commit"
19645 );
19646 let db = Arc::clone(&db);
19647 scope.spawn(move || {
19648 ddl_tx.send(db.drop_table("parent")).unwrap();
19649 });
19650 std::thread::sleep(std::time::Duration::from_millis(100));
19651 assert!(
19652 ddl_rx.try_recv().is_err(),
19653 "DDL must wait on the Exclusive schema barrier while DML is in flight"
19654 );
19655 resume.wait();
19656 let a_result = a_rx.recv().unwrap();
19662 match &a_result {
19663 Ok(_) => {}
19664 Err(MongrelError::Conflict(message)) => {
19665 assert!(
19666 message.contains("security policy changed during write"),
19667 "unexpected commit conflict: {message}"
19668 );
19669 }
19670 other => panic!("unexpected commit outcome: {other:?}"),
19671 }
19672 assert!(ddl_rx.recv().unwrap().is_ok());
19673 });
19674 assert!(!db.lock_manager().holds(a_id, &LockKey::schema_barrier()));
19675 }
19676
19677 #[test]
19678 fn auto_increment_sequence_barrier_held_until_commit() {
19679 let dir = tempfile::tempdir().unwrap();
19680 let db = Database::create(dir.path()).unwrap();
19681 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
19682 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
19683 let entered = Arc::new(std::sync::Barrier::new(2));
19684 let resume = Arc::new(std::sync::Barrier::new(2));
19685 let parked = Arc::new(AtomicBool::new(false));
19686 db.__set_catalog_commit_hook({
19687 let entered = Arc::clone(&entered);
19688 let resume = Arc::clone(&resume);
19689 let parked = Arc::clone(&parked);
19690 move || {
19691 if !parked.swap(true, Ordering::SeqCst) {
19692 entered.wait();
19693 resume.wait();
19694 }
19695 }
19696 });
19697
19698 let mut txn_a = db.begin();
19699 txn_a.put("seq_t", vec![(0, Value::Null)]).unwrap();
19700 let a_id = txn_a.txn_id();
19701 assert!(
19703 db.lock_manager().holds(a_id, &barrier_key),
19704 "sequence allocation takes the barrier at stage time"
19705 );
19706 let (a_tx, a_rx) = std::sync::mpsc::channel();
19707 std::thread::scope(|scope| {
19708 scope.spawn(|| {
19709 a_tx.send(txn_a.commit()).unwrap();
19710 });
19711 entered.wait();
19712 assert!(
19713 db.lock_manager().holds(a_id, &barrier_key),
19714 "the barrier is held through the commit"
19715 );
19716 resume.wait();
19717 assert!(a_rx.recv().unwrap().is_ok());
19718 });
19719 assert!(
19720 !db.lock_manager().holds(a_id, &barrier_key),
19721 "the barrier releases when the commit ends"
19722 );
19723 }
19724
19725 #[test]
19726 fn fk_wait_for_cycle_surfaces_deadlock_victim() {
19727 let dir = tempfile::tempdir().unwrap();
19728 let db = Database::create(dir.path()).unwrap();
19729 db.create_table("parent", parent_schema()).unwrap();
19730 db.create_table("child", child_schema()).unwrap();
19731 let mut seed = db.begin();
19732 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
19733 seed.put("parent", vec![(0, Value::Int64(2))]).unwrap();
19734 seed.commit().unwrap();
19735 let (rid1, rid2) = {
19736 let handle = db.table("parent").unwrap();
19737 let table = handle.lock();
19738 let rid = |pk: i64| {
19739 table
19740 .lookup_pk(&Value::Int64(pk).encode_key())
19741 .expect("seeded parent row")
19742 };
19743 (rid(1), rid(2))
19744 };
19745
19746 let rendezvous = Arc::new(std::sync::Barrier::new(2));
19751 let calls = Arc::new(AtomicUsize::new(0));
19752 db.__set_fk_lock_hook({
19753 let rendezvous = Arc::clone(&rendezvous);
19754 let calls = Arc::clone(&calls);
19755 move || {
19756 if calls.fetch_add(1, Ordering::SeqCst) < 2 {
19757 rendezvous.wait();
19758 }
19759 }
19760 });
19761
19762 let mut txn_a = db.begin();
19765 txn_a.delete("parent", rid1).unwrap();
19766 txn_a
19767 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(2))])
19768 .unwrap();
19769 let mut txn_b = db.begin();
19770 txn_b.delete("parent", rid2).unwrap();
19771 txn_b
19772 .put("child", vec![(0, Value::Int64(101)), (1, Value::Int64(1))])
19773 .unwrap();
19774 let b_id = txn_b.txn_id();
19775
19776 let (a_tx, a_rx) = std::sync::mpsc::channel();
19777 let (b_tx, b_rx) = std::sync::mpsc::channel();
19778 std::thread::scope(|scope| {
19779 scope.spawn(|| {
19780 a_tx.send(txn_a.commit()).unwrap();
19781 });
19782 scope.spawn(|| {
19783 b_tx.send(txn_b.commit()).unwrap();
19784 });
19785 let a_result = a_rx.recv().unwrap();
19786 let b_result = b_rx.recv().unwrap();
19787 assert!(
19788 a_result.is_ok(),
19789 "the survivor commits once the victim releases: {a_result:?}"
19790 );
19791 match b_result {
19792 Err(MongrelError::Deadlock { victim, .. }) => {
19793 assert_eq!(victim, b_id, "the youngest transaction is the victim");
19794 }
19795 other => panic!("the victim must surface a deadlock, got {other:?}"),
19796 }
19797 });
19798 let fk_key = |table: &str, pk: i64| {
19800 let table_id = db.table_id(table).unwrap();
19801 let mut key = b"fk:".to_vec();
19802 key.extend_from_slice(&Value::Int64(pk).encode_key());
19803 LockKey::key(table_id, key)
19804 };
19805 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 2)));
19806 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 1)));
19807 }
19808
19809 #[test]
19810 fn locks_release_after_commit_rollback_and_failed_commit() {
19811 let dir = tempfile::tempdir().unwrap();
19812 let db = Database::create(dir.path()).unwrap();
19813 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
19814 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
19815
19816 let mut txn = db.begin();
19818 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
19819 let committed_id = txn.txn_id();
19820 txn.commit().unwrap();
19821 assert!(!db.lock_manager().holds(committed_id, &barrier_key));
19822
19823 let mut txn = db.begin();
19825 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
19826 let rolled_back_id = txn.txn_id();
19827 assert!(db.lock_manager().holds(rolled_back_id, &barrier_key));
19828 txn.rollback();
19829 assert!(
19830 !db.lock_manager().holds(rolled_back_id, &barrier_key),
19831 "rollback must not leave phantom holds"
19832 );
19833
19834 db.create_table("users", unique_schema()).unwrap();
19837 let users_id = db.table_id("users").unwrap();
19838 let mut seed = db.begin();
19839 seed.put(
19840 "users",
19841 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
19842 )
19843 .unwrap();
19844 seed.commit().unwrap();
19845 let mut txn = db.begin();
19846 txn.put(
19849 "users",
19850 vec![(0, Value::Int64(2)), (1, Value::Bytes(b"a@x".to_vec()))],
19851 )
19852 .unwrap();
19853 let failed_id = txn.txn_id();
19854 let result = txn.commit();
19855 assert!(matches!(result, Err(MongrelError::Conflict(_))));
19856 assert!(
19857 !db.lock_manager()
19858 .holds(failed_id, &pk_lock_key(users_id, 2)),
19859 "a failed commit must not leave phantom holds"
19860 );
19861 }
19862}
19863
19864#[cfg(test)]
19865mod lifecycle_tests {
19866 use super::*;
19867
19868 fn int_pk_schema() -> Schema {
19869 Schema {
19870 columns: vec![ColumnDef {
19871 id: 1,
19872 name: "id".into(),
19873 ty: TypeId::Int64,
19874 flags: crate::schema::ColumnFlags::empty()
19875 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19876 default_value: None,
19877 embedding_source: None,
19878 }],
19879 ..Schema::default()
19880 }
19881 }
19882
19883 #[test]
19884 fn poisoned_core_rejects_operations_with_typed_errors() {
19885 let dir = tempfile::tempdir().unwrap();
19886 let db = Database::create(dir.path()).unwrap();
19887 db.create_table("t", int_pk_schema()).unwrap();
19888 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Open);
19889
19890 db.poisoned.store(true, Ordering::Relaxed);
19895 db.lifecycle.poison();
19896 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Poisoned);
19897
19898 let error = db.gc().unwrap_err();
19901 assert!(
19902 matches!(error, MongrelError::Conflict(_)),
19903 "gc must reject on a poisoned core: {error:?}"
19904 );
19905 let error = db.compact().unwrap_err();
19906 assert!(
19907 matches!(error, MongrelError::Conflict(_)),
19908 "compact must reject on a poisoned core: {error:?}"
19909 );
19910 assert!(db.operation_guard().is_err());
19911 let error = db.create_table("t2", int_pk_schema()).unwrap_err();
19914 assert!(
19915 error.to_string().contains("database poisoned"),
19916 "the legacy poison error still wins where it existed: {error:?}"
19917 );
19918 let mut txn = db.begin();
19919 txn.put("t", vec![(1, Value::Int64(2))]).unwrap();
19920 assert!(txn
19921 .commit()
19922 .unwrap_err()
19923 .to_string()
19924 .contains("database poisoned"));
19925 }
19926
19927 #[test]
19928 fn shutdown_waits_for_operation_guards_to_drain() {
19929 let dir = tempfile::tempdir().unwrap();
19930 let db = Arc::new(Database::create(dir.path()).unwrap());
19931 db.create_table("t", int_pk_schema()).unwrap();
19932 let guard = db.operation_guard().unwrap();
19935 let (started_tx, started_rx) = std::sync::mpsc::channel();
19936 let (done_tx, done_rx) = std::sync::mpsc::channel();
19937 let shutdown_thread = std::thread::spawn(move || {
19938 started_tx.send(()).unwrap();
19939 let result = db.shutdown();
19940 let _ = done_tx.send(result);
19941 });
19942 started_rx.recv().unwrap();
19943 std::thread::sleep(std::time::Duration::from_millis(100));
19944 assert!(
19945 done_rx.try_recv().is_err(),
19946 "shutdown must wait for the outstanding guard to drain"
19947 );
19948 drop(guard);
19949 shutdown_thread.join().unwrap();
19950 assert!(
19951 done_rx.recv().unwrap().is_ok(),
19952 "shutdown completes once the guard drops"
19953 );
19954 }
19955}
19956
19957#[cfg(test)]
19958mod commit_ts_ledger_tests {
19959 use super::*;
19960
19961 fn int_pk_schema() -> Schema {
19962 Schema {
19963 columns: vec![ColumnDef {
19964 id: 1,
19965 name: "id".into(),
19966 ty: TypeId::Int64,
19967 flags: crate::schema::ColumnFlags::empty()
19968 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19969 default_value: None,
19970 embedding_source: None,
19971 }],
19972 ..Schema::default()
19973 }
19974 }
19975
19976 fn commit_one(db: &Database) -> (Epoch, mongreldb_types::hlc::HlcTimestamp) {
19977 let mut txn = db.begin();
19978 let handle = txn.state_handle();
19979 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
19980 let epoch = txn.commit().unwrap();
19981 let crate::txn::TransactionState::Committed(receipt) = handle.state() else {
19982 panic!("expected Committed, got {:?}", handle.state());
19983 };
19984 (epoch, receipt.commit_ts)
19985 }
19986
19987 #[test]
19988 fn commit_ts_for_epoch_returns_the_exact_receipt_within_one_open() {
19989 let dir = tempfile::tempdir().unwrap();
19990 let db = Database::create(dir.path()).unwrap();
19991 db.create_table("t", int_pk_schema()).unwrap();
19992
19993 let (epoch, commit_ts) = commit_one(&db);
19994 assert_eq!(db.commit_ts_for_epoch(epoch), Some(commit_ts));
19995 assert_eq!(db.commit_ts_for_epoch(Epoch(epoch.0 + 100)), None);
19997 }
19998
19999 #[test]
20000 fn commit_ts_for_epoch_survives_reopen_with_the_physical_component() {
20001 let dir = tempfile::tempdir().unwrap();
20002 let (epoch, commit_ts) = {
20003 let db = Database::create(dir.path()).unwrap();
20004 db.create_table("t", int_pk_schema()).unwrap();
20005 commit_one(&db)
20006 };
20007
20008 let db = Database::open(dir.path()).unwrap();
20009 let reconstructed = db
20010 .commit_ts_for_epoch(epoch)
20011 .expect("the durable WAL CommitTimestamp ledger reconstructs the epoch");
20012 assert_eq!(reconstructed.physical_micros, commit_ts.physical_micros);
20013 assert_eq!(reconstructed.logical, 0);
20016 assert_eq!(reconstructed.node_tiebreaker, 0);
20017 }
20018
20019 #[test]
20020 fn recovery_ledger_keeps_only_newest_epochs_and_ignores_aborted_txns() {
20021 use crate::wal::Op;
20022 let records = vec![
20023 crate::wal::Record::new(Epoch(1), 7, Op::CommitTimestamp { unix_nanos: 1_000 }),
20024 crate::wal::Record::new(
20025 Epoch(2),
20026 7,
20027 Op::TxnCommit {
20028 epoch: 41,
20029 added_runs: vec![],
20030 },
20031 ),
20032 crate::wal::Record::new(
20034 Epoch(3),
20035 8,
20036 Op::TxnCommit {
20037 epoch: 42,
20038 added_runs: vec![],
20039 },
20040 ),
20041 crate::wal::Record::new(Epoch(4), 9, Op::CommitTimestamp { unix_nanos: 9_000 }),
20043 ];
20044 let ledger = commit_ts_ledger_from_recovery(&records);
20045 assert_eq!(ledger.len(), 1);
20046 assert_eq!(
20047 ledger.get(&41),
20048 Some(&mongreldb_types::hlc::HlcTimestamp {
20049 physical_micros: 1,
20050 logical: 0,
20051 node_tiebreaker: 0,
20052 })
20053 );
20054 }
20055}
20056
20057#[cfg(test)]
20058mod stage2e_storage_mode_tests {
20059 use super::*;
20060 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20061 use crate::storage_mode::{StorageMode, STORAGE_MODE_FILENAME};
20062 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
20063
20064 fn identity(seed: u8) -> (ClusterId, NodeId, DatabaseId) {
20065 (
20066 ClusterId::from_bytes([seed; 16]),
20067 NodeId::from_bytes([seed + 1; 16]),
20068 DatabaseId::from_bytes([seed + 2; 16]),
20069 )
20070 }
20071
20072 fn marker(root: &Path) -> Option<StorageMode> {
20073 let durable = crate::durable_file::DurableRoot::open(root).unwrap();
20074 crate::storage_mode::read(&durable).unwrap()
20075 }
20076
20077 fn simple_schema() -> Schema {
20078 Schema {
20079 columns: vec![ColumnDef {
20080 id: 1,
20081 name: "id".into(),
20082 ty: TypeId::Int64,
20083 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20084 default_value: None,
20085 embedding_source: None,
20086 }],
20087 ..Schema::default()
20088 }
20089 }
20090
20091 #[test]
20092 fn standalone_create_writes_marker_and_reopens() {
20093 let dir = tempfile::tempdir().unwrap();
20094 let root = dir.path().join("db");
20095 let db = Database::create(&root).unwrap();
20096 assert_eq!(marker(&root), Some(StorageMode::Standalone));
20097 assert_eq!(db.storage_mode().unwrap(), Some(StorageMode::Standalone));
20098 drop(db);
20099 let db = Database::open(&root).unwrap();
20100 assert_eq!(marker(&root), Some(StorageMode::Standalone));
20101 drop(db);
20102 }
20103
20104 #[test]
20105 fn legacy_database_without_marker_opens_and_gains_marker() {
20106 let dir = tempfile::tempdir().unwrap();
20107 let root = dir.path().join("db");
20108 let db = Database::create(&root).unwrap();
20109 drop(db);
20110 std::fs::remove_file(root.join(META_DIR).join(STORAGE_MODE_FILENAME)).unwrap();
20112 assert_eq!(marker(&root), None);
20113 let db = Database::open(&root).unwrap();
20114 assert_eq!(marker(&root), Some(StorageMode::Standalone));
20115 drop(db);
20116 }
20117
20118 #[test]
20119 fn server_owned_standalone_opens_embedded() {
20120 let dir = tempfile::tempdir().unwrap();
20121 let root = dir.path().join("db");
20122 let db = Database::create(&root).unwrap();
20123 drop(db);
20124 let durable = crate::durable_file::DurableRoot::open(&root).unwrap();
20125 crate::storage_mode::rewrite(&durable, &StorageMode::ServerOwnedStandalone).unwrap();
20126 let db = Database::open(&root).unwrap();
20127 assert_eq!(marker(&root), Some(StorageMode::ServerOwnedStandalone));
20128 drop(db);
20129 }
20130
20131 #[test]
20132 fn cluster_replica_is_rejected_by_normal_opens() {
20133 let dir = tempfile::tempdir().unwrap();
20134 let root = dir.path().join("db");
20135 let (cluster_id, node_id, database_id) = identity(10);
20136 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
20137 assert_eq!(
20138 marker(&root),
20139 Some(StorageMode::ClusterReplica {
20140 cluster_id,
20141 node_id,
20142 database_id,
20143 })
20144 );
20145 drop(db);
20146
20147 let error = Database::open(&root).unwrap_err();
20148 let message = error.to_string();
20149 assert!(
20150 matches!(error, MongrelError::InvalidArgument(_)),
20151 "unexpected error: {message}"
20152 );
20153 assert!(message.contains("cluster node runtime"), "{message}");
20154 assert!(message.contains(&cluster_id.to_hex()), "{message}");
20155 assert!(message.contains(&node_id.to_hex()), "{message}");
20156 assert!(message.contains(&database_id.to_hex()), "{message}");
20157
20158 let error = Database::open_with_options(&root, OpenOptions::default()).unwrap_err();
20159 assert!(error.to_string().contains("cluster node runtime"));
20160 assert_eq!(
20162 marker(&root),
20163 Some(StorageMode::ClusterReplica {
20164 cluster_id,
20165 node_id,
20166 database_id,
20167 })
20168 );
20169 }
20170
20171 #[test]
20172 fn offline_validation_opens_cluster_replica_read_only() {
20173 let dir = tempfile::tempdir().unwrap();
20174 let root = dir.path().join("db");
20175 let (cluster_id, node_id, database_id) = identity(20);
20176 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
20177 drop(db);
20178
20179 let options = OpenOptions::default().with_offline_validation(true);
20180 let db = Database::open_with_options(&root, options).unwrap();
20181 assert!(db.is_read_only_replica());
20182 let error = db.create_table("t", simple_schema()).unwrap_err();
20183 assert!(matches!(error, MongrelError::ReadOnlyReplica));
20184 drop(db);
20185 assert_eq!(
20187 marker(&root),
20188 Some(StorageMode::ClusterReplica {
20189 cluster_id,
20190 node_id,
20191 database_id,
20192 })
20193 );
20194 }
20195
20196 #[test]
20197 fn cluster_runtime_open_requires_exact_identity() {
20198 let dir = tempfile::tempdir().unwrap();
20199 let root = dir.path().join("db");
20200 let (cluster_id, node_id, database_id) = identity(30);
20201 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
20202 drop(db);
20203
20204 let error = Database::open_cluster_replica(&root, &StorageMode::Standalone).unwrap_err();
20206 assert!(matches!(error, MongrelError::InvalidArgument(_)));
20207 let wrong = StorageMode::ClusterReplica {
20209 cluster_id,
20210 node_id,
20211 database_id: DatabaseId::from_bytes([99; 16]),
20212 };
20213 let error = Database::open_cluster_replica(&root, &wrong).unwrap_err();
20214 assert!(error.to_string().contains("identity mismatch"), "{error}");
20215 let legacy = dir.path().join("legacy");
20217 let legacy_db = Database::create(&legacy).unwrap();
20218 drop(legacy_db);
20219 let expected = StorageMode::ClusterReplica {
20220 cluster_id,
20221 node_id,
20222 database_id,
20223 };
20224 let error = Database::open_cluster_replica(&legacy, &expected).unwrap_err();
20225 assert!(error.to_string().contains("identity mismatch"), "{error}");
20226
20227 let db = Database::open_cluster_replica(&root, &expected).unwrap();
20230 assert!(db.is_read_only_replica());
20231 let error = db.create_table("t", simple_schema()).unwrap_err();
20232 assert!(matches!(error, MongrelError::ReadOnlyReplica));
20233 drop(db);
20234 }
20235}
20236
20237#[cfg(test)]
20238mod stage2e_replicated_apply_tests {
20239 use super::*;
20240 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord, CatalogDelta};
20241 use crate::memtable::{Row, Value};
20242 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20243 use crate::wal::{Op, Record};
20244 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
20245 use std::sync::Arc;
20246
20247 fn ids() -> (ClusterId, NodeId, DatabaseId) {
20248 (
20249 ClusterId::from_bytes([1; 16]),
20250 NodeId::from_bytes([2; 16]),
20251 DatabaseId::from_bytes([3; 16]),
20252 )
20253 }
20254
20255 fn expected_mode() -> crate::storage_mode::StorageMode {
20256 let (cluster_id, node_id, database_id) = ids();
20257 crate::storage_mode::StorageMode::ClusterReplica {
20258 cluster_id,
20259 node_id,
20260 database_id,
20261 }
20262 }
20263
20264 fn simple_schema() -> Schema {
20265 Schema {
20266 columns: vec![ColumnDef {
20267 id: 1,
20268 name: "id".into(),
20269 ty: TypeId::Int64,
20270 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20271 default_value: None,
20272 embedding_source: None,
20273 }],
20274 ..Schema::default()
20275 }
20276 }
20277
20278 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
20279 CatalogCommandRecord {
20280 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
20281 catalog_version,
20282 command: CatalogCommand::CreateTable {
20283 name: name.to_string(),
20284 schema: simple_schema(),
20285 created_epoch: 1,
20286 },
20287 }
20288 }
20289
20290 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
20291 let rows: Vec<Row> = values
20292 .iter()
20293 .map(|value| {
20294 Row::new(crate::RowId(*value as u64), Epoch(epoch))
20297 .with_column(1, Value::Int64(*value))
20298 })
20299 .collect();
20300 vec![
20301 Record::new(
20302 Epoch(0),
20303 txn_id,
20304 Op::Put {
20305 table_id,
20306 rows: bincode::serialize(&rows).unwrap(),
20307 },
20308 ),
20309 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
20310 Record::new(
20311 Epoch(0),
20312 txn_id,
20313 Op::TxnCommit {
20314 epoch,
20315 added_runs: Vec::new(),
20316 },
20317 ),
20318 ]
20319 }
20320
20321 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
20322 let handle = db.table(table).unwrap();
20323 let rows = handle
20324 .lock()
20325 .visible_rows(crate::epoch::Snapshot::at(Epoch(u64::MAX)))
20326 .unwrap();
20327 let mut values: Vec<i64> = rows
20328 .iter()
20329 .map(|row| match row.columns.get(&1) {
20330 Some(Value::Int64(value)) => *value,
20331 other => panic!("unexpected column: {other:?}"),
20332 })
20333 .collect();
20334 values.sort_unstable();
20335 values
20336 }
20337
20338 #[test]
20339 fn catalog_command_mounts_table_and_replays_as_noop() {
20340 let dir = tempfile::tempdir().unwrap();
20341 let (cluster_id, node_id, database_id) = ids();
20342 let db =
20343 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20344
20345 let record = create_table_record("items", 1);
20346 let delta = db.apply_replicated_catalog_command(&record).unwrap();
20347 assert!(matches!(delta, CatalogDelta::TableCreated { .. }));
20348 assert_eq!(db.table_names(), vec!["items".to_string()]);
20349 assert_eq!(db.catalog_version(), 1);
20350
20351 let delta = db.apply_replicated_catalog_command(&record).unwrap();
20353 assert!(matches!(delta, CatalogDelta::NoOp));
20354 assert_eq!(db.table_names().len(), 1);
20355 drop(db);
20356
20357 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
20359 assert_eq!(db.table_names(), vec!["items".to_string()]);
20360 assert_eq!(db.catalog_version(), 1);
20361 }
20362
20363 #[test]
20364 fn records_apply_rows_and_skip_replays_across_restart() {
20365 let dir = tempfile::tempdir().unwrap();
20366 let (cluster_id, node_id, database_id) = ids();
20367 let db =
20368 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20369 db.apply_replicated_catalog_command(&create_table_record("items", 1))
20370 .unwrap();
20371
20372 let records = put_records(1, 0, 2, &[10, 20, 30]);
20373 assert!(db.apply_replicated_records(&records).unwrap());
20374 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
20375 assert_eq!(db.visible_epoch(), Epoch(2));
20376
20377 assert!(!db.apply_replicated_records(&records).unwrap());
20380 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
20381
20382 let later = put_records(2, 0, 3, &[40]);
20384 assert!(db.apply_replicated_records(&later).unwrap());
20385 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
20386 let db = Arc::new(db);
20387 db.shutdown().unwrap();
20388
20389 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
20392 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
20393 assert!(!db.apply_replicated_records(&later).unwrap());
20394 assert!(!db.apply_replicated_records(&records).unwrap());
20395 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
20396 }
20397
20398 #[test]
20399 fn spilled_run_commits_fail_closed_this_wave() {
20400 let dir = tempfile::tempdir().unwrap();
20401 let (cluster_id, node_id, database_id) = ids();
20402 let db =
20403 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20404 db.apply_replicated_catalog_command(&create_table_record("items", 1))
20405 .unwrap();
20406 let mut records = put_records(1, 0, 2, &[10]);
20407 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20408 panic!("put_records ends in TxnCommit");
20409 };
20410 added_runs.push(crate::wal::AddedRun {
20411 table_id: 0,
20412 run_id: 7,
20413 row_count: 1,
20414 level: 0,
20415 min_row_id: 1,
20416 max_row_id: 1,
20417 content_hash: [0; 32],
20418 });
20419 let error = db.apply_replicated_records(&records).unwrap_err();
20420 assert!(
20421 error.to_string().contains("spilled-run"),
20422 "unexpected error: {error}"
20423 );
20424 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
20425 }
20426
20427 #[test]
20428 fn records_without_commit_marker_fail_closed() {
20429 let dir = tempfile::tempdir().unwrap();
20430 let (cluster_id, node_id, database_id) = ids();
20431 let db =
20432 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20433 db.apply_replicated_catalog_command(&create_table_record("items", 1))
20434 .unwrap();
20435 let mut records = put_records(1, 0, 2, &[10]);
20436 records.pop(); let error = db.apply_replicated_records(&records).unwrap_err();
20438 assert!(matches!(error, MongrelError::InvalidArgument(_)));
20439 assert!(db.apply_replicated_records(&[]).is_err());
20440 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
20441 }
20442}
20443
20444#[cfg(test)]
20445mod stage2c_spill_translation_tests {
20446 use super::*;
20447 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord};
20448 use crate::memtable::{Row, Value};
20449 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20450 use crate::wal::{Op, Record};
20451 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
20452
20453 fn simple_schema() -> Schema {
20454 Schema {
20455 columns: vec![ColumnDef {
20456 id: 1,
20457 name: "id".into(),
20458 ty: TypeId::Int64,
20459 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20460 default_value: None,
20461 embedding_source: None,
20462 }],
20463 ..Schema::default()
20464 }
20465 }
20466
20467 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
20468 CatalogCommandRecord {
20469 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
20470 catalog_version,
20471 command: CatalogCommand::CreateTable {
20472 name: name.to_string(),
20473 schema: simple_schema(),
20474 created_epoch: 1,
20475 },
20476 }
20477 }
20478
20479 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
20480 let handle = db.table(table).unwrap();
20481 let rows = handle
20482 .lock()
20483 .visible_rows(crate::epoch::Snapshot::at(Epoch(u64::MAX)))
20484 .unwrap();
20485 let mut values: Vec<i64> = rows
20486 .iter()
20487 .map(|row| match row.columns.get(&1) {
20488 Some(Value::Int64(value)) => *value,
20489 other => panic!("unexpected column: {other:?}"),
20490 })
20491 .collect();
20492 values.sort_unstable();
20493 values
20494 }
20495
20496 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
20497 let rows: Vec<Row> = values
20498 .iter()
20499 .map(|value| {
20500 Row::new(crate::RowId(*value as u64), Epoch(epoch))
20501 .with_column(1, Value::Int64(*value))
20502 })
20503 .collect();
20504 vec![
20505 Record::new(
20506 Epoch(0),
20507 txn_id,
20508 Op::Put {
20509 table_id,
20510 rows: bincode::serialize(&rows).unwrap(),
20511 },
20512 ),
20513 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
20514 Record::new(
20515 Epoch(0),
20516 txn_id,
20517 Op::TxnCommit {
20518 epoch,
20519 added_runs: Vec::new(),
20520 },
20521 ),
20522 ]
20523 }
20524
20525 fn added_run(
20526 table_id: u64,
20527 row_count: u64,
20528 min_row_id: u64,
20529 max_row_id: u64,
20530 ) -> crate::wal::AddedRun {
20531 crate::wal::AddedRun {
20532 table_id,
20533 run_id: 7,
20534 row_count,
20535 level: 0,
20536 min_row_id,
20537 max_row_id,
20538 content_hash: [0; 32],
20539 }
20540 }
20541
20542 fn spilled_commit_records(db: &Database) -> Vec<Record> {
20545 let records = crate::wal::SharedWal::replay_with_dek(&db.root, None).unwrap();
20546 let txn_id = records
20547 .iter()
20548 .find_map(|record| match &record.op {
20549 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty() => Some(record.txn_id),
20550 _ => None,
20551 })
20552 .expect("a spilled commit is present in the WAL");
20553 records
20554 .into_iter()
20555 .filter(|record| record.txn_id == txn_id)
20556 .collect()
20557 }
20558
20559 #[test]
20560 fn non_spilled_records_translate_byte_identical() {
20561 let records = put_records(1, 0, 2, &[10, 20, 30]);
20562 let translated = translate_records_for_replication(&records).unwrap();
20563 assert_eq!(
20564 bincode::serialize(&translated).unwrap(),
20565 bincode::serialize(&records).unwrap(),
20566 "a commit without spill links must pass through byte-identical"
20567 );
20568 }
20569
20570 #[test]
20571 fn translation_rejects_uncovered_or_malformed_spills() {
20572 let mut records = put_records(1, 0, 2, &[10]);
20574 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20575 panic!("put_records ends in TxnCommit");
20576 };
20577 added_runs.push(added_run(0, 1, 10, 10));
20578 let error = translate_records_for_replication(&records).unwrap_err();
20579 assert!(
20580 error.to_string().contains("no logical row records"),
20581 "unexpected error: {error}"
20582 );
20583
20584 let mut records = put_records(1, 0, 2, &[10]);
20586 let spilled: Vec<Row> = (0..3_u64)
20587 .map(|value| {
20588 Row::new(crate::RowId(value), Epoch(2)).with_column(1, Value::Int64(value as i64))
20589 })
20590 .collect();
20591 records.insert(
20592 0,
20593 Record::new(
20594 Epoch(0),
20595 1,
20596 Op::SpilledRows {
20597 table_id: 0,
20598 rows: bincode::serialize(&spilled).unwrap(),
20599 },
20600 ),
20601 );
20602 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20603 panic!("put_records ends in TxnCommit");
20604 };
20605 added_runs.push(added_run(0, 4, 0, 3));
20606 let error = translate_records_for_replication(&records).unwrap_err();
20607 assert!(
20608 error.to_string().contains("cover 3 rows"),
20609 "unexpected error: {error}"
20610 );
20611
20612 let mut records = put_records(1, 0, 2, &[10]);
20614 records.insert(
20615 0,
20616 Record::new(
20617 Epoch(0),
20618 1,
20619 Op::SpilledRows {
20620 table_id: 0,
20621 rows: vec![0xFF, 0x01, 0x02],
20622 },
20623 ),
20624 );
20625 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20626 panic!("put_records ends in TxnCommit");
20627 };
20628 added_runs.push(added_run(0, 1, 0, 0));
20629 assert!(translate_records_for_replication(&records).is_err());
20630
20631 assert!(translate_records_for_replication(&[]).is_err());
20633 let mut mixed = put_records(1, 0, 2, &[10]);
20634 mixed[0].txn_id = 99;
20635 assert!(translate_records_for_replication(&mixed).is_err());
20636 let mut no_commit = put_records(1, 0, 2, &[10]);
20637 no_commit.pop();
20638 assert!(translate_records_for_replication(&no_commit).is_err());
20639 }
20640
20641 #[test]
20642 fn spilled_commit_translates_to_logical_rows_and_applies_on_replica() {
20643 let leader_dir = tempfile::tempdir().unwrap();
20645 let leader = Database::create(leader_dir.path()).unwrap();
20646 leader.create_table("t", simple_schema()).unwrap();
20647 leader.set_spill_threshold(1);
20648 let table_id = leader.table_id("t").unwrap();
20649 let values: Vec<i64> = (0..60).collect();
20650 leader
20651 .transaction(|txn| {
20652 for value in &values {
20653 txn.put("t", vec![(1, Value::Int64(*value))])?;
20654 }
20655 Ok(())
20656 })
20657 .unwrap();
20658 assert_eq!(visible_ids(&leader, "t"), values);
20659
20660 let records = spilled_commit_records(&leader);
20663 assert!(records
20664 .iter()
20665 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
20666 let Some(Op::TxnCommit { added_runs, epoch }) = records.last().map(|r| &r.op) else {
20667 panic!("a commit sequence ends in TxnCommit");
20668 };
20669 assert!(!added_runs.is_empty());
20670 let commit_epoch = *epoch;
20671
20672 let translated = translate_records_for_replication(&records).unwrap();
20675 assert!(records
20676 .iter()
20677 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
20678 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
20679 panic!("a commit sequence ends in TxnCommit");
20680 };
20681 assert!(!added_runs.is_empty(), "input records must be unchanged");
20682 assert_eq!(translated.len(), records.len());
20683 assert!(translated
20684 .iter()
20685 .all(|record| !matches!(record.op, Op::SpilledRows { .. })));
20686 assert!(translated
20687 .iter()
20688 .any(|record| matches!(record.op, Op::Put { .. })));
20689 let Some(Op::TxnCommit { added_runs, epoch }) = translated.last().map(|r| &r.op) else {
20690 panic!("a commit sequence ends in TxnCommit");
20691 };
20692 assert!(added_runs.is_empty(), "no added_runs may reach a replica");
20693 assert_eq!(*epoch, commit_epoch);
20694
20695 let replica_dir = tempfile::tempdir().unwrap();
20697 let replica = Database::create_cluster_replica(
20698 replica_dir.path(),
20699 ClusterId::from_bytes([1; 16]),
20700 NodeId::from_bytes([2; 16]),
20701 DatabaseId::from_bytes([3; 16]),
20702 )
20703 .unwrap();
20704 replica
20705 .apply_replicated_catalog_command(&create_table_record("t", 1))
20706 .unwrap();
20707 assert_eq!(replica.table_id("t").unwrap(), table_id);
20708 assert!(replica.apply_replicated_records(&translated).unwrap());
20709 assert_eq!(visible_ids(&replica, "t"), values);
20710 assert_eq!(visible_ids(&replica, "t"), visible_ids(&leader, "t"));
20711
20712 drop(leader);
20715 let leader = Database::open(leader_dir.path()).unwrap();
20716 assert_eq!(visible_ids(&leader, "t"), values);
20717 }
20718
20719 #[test]
20720 fn staged_txn_writes_validate_and_apply() {
20721 let dir = tempfile::tempdir().unwrap();
20722 let db = Database::create_cluster_replica(
20723 dir.path(),
20724 ClusterId::from_bytes([1; 16]),
20725 NodeId::from_bytes([2; 16]),
20726 DatabaseId::from_bytes([3; 16]),
20727 )
20728 .unwrap();
20729 db.apply_replicated_catalog_command(&create_table_record("t", 1))
20730 .unwrap();
20731 let table_id = db.table_id("t").unwrap();
20732
20733 assert!(db.validate_staged_txn_writes(&[vec![0xFF]]).is_err());
20735 let unknown_table = StagedTxnWrite::Put {
20736 table_id: 99,
20737 rows: bincode::serialize(&Vec::<Row>::new()).unwrap(),
20738 }
20739 .encode()
20740 .unwrap();
20741 assert!(db.validate_staged_txn_writes(&[unknown_table]).is_err());
20742 let good: Vec<Vec<u8>> = [10_i64, 20, 30]
20743 .iter()
20744 .map(|value| {
20745 let rows = vec![Row::new(crate::RowId(*value as u64), Epoch(0))
20746 .with_column(1, Value::Int64(*value))];
20747 StagedTxnWrite::Put {
20748 table_id,
20749 rows: bincode::serialize(&rows).unwrap(),
20750 }
20751 .encode()
20752 .unwrap()
20753 })
20754 .collect();
20755 db.validate_staged_txn_writes(&good).unwrap();
20756
20757 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
20760 physical_micros: 5_000,
20761 logical: 0,
20762 node_tiebreaker: 0,
20763 };
20764 assert!(db
20765 .apply_staged_txn_writes(1 << 63, &good, commit_ts)
20766 .unwrap());
20767 assert_eq!(visible_ids(&db, "t"), vec![10, 20, 30]);
20768 let delete = StagedTxnWrite::Delete {
20769 table_id,
20770 row_ids: vec![20],
20771 }
20772 .encode()
20773 .unwrap();
20774 assert!(db
20775 .apply_staged_txn_writes((1 << 63) + 1, &[delete], commit_ts)
20776 .unwrap());
20777 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
20778
20779 let db = Arc::new(db);
20782 db.shutdown().unwrap();
20783 let expected = crate::storage_mode::StorageMode::ClusterReplica {
20784 cluster_id: ClusterId::from_bytes([1; 16]),
20785 node_id: NodeId::from_bytes([2; 16]),
20786 database_id: DatabaseId::from_bytes([3; 16]),
20787 };
20788 let db = Database::open_cluster_replica(dir.path(), &expected).unwrap();
20789 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
20790 }
20791}