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
323fn read_encryption_salt(
324 root: &crate::durable_file::DurableRoot,
325) -> Result<[u8; crate::encryption::SALT_LEN]> {
326 let mut file = root
327 .open_regular(Path::new(META_DIR).join(KEYS_FILENAME))
328 .map_err(|error| MongrelError::NotFound(format!("encryption salt file: {error}")))?;
329 let length = file.metadata()?.len();
330 if length != crate::encryption::SALT_LEN as u64 {
331 return Err(MongrelError::Encryption(format!(
332 "invalid encryption salt length: got {length}, expected {}",
333 crate::encryption::SALT_LEN
334 )));
335 }
336 let mut salt = [0_u8; crate::encryption::SALT_LEN];
337 file.read_exact(&mut salt)?;
338 Ok(salt)
339}
340
341fn incremental_aggregate_cache_key(
342 table: &str,
343 conditions: &[crate::query::Condition],
344 column: Option<u16>,
345 agg: crate::engine::NativeAgg,
346 principal: Option<&crate::auth::Principal>,
347 security_version: u64,
348) -> u64 {
349 use std::hash::{Hash, Hasher};
350 let projection = column.as_ref().map(std::slice::from_ref);
351 let query_key = crate::query::canonical_query_key(conditions, projection, security_version);
352 let mut hasher = std::collections::hash_map::DefaultHasher::new();
353 table.hash(&mut hasher);
354 query_key.hash(&mut hasher);
355 match agg {
356 crate::engine::NativeAgg::Count => 0u8,
357 crate::engine::NativeAgg::Sum => 1,
358 crate::engine::NativeAgg::Min => 2,
359 crate::engine::NativeAgg::Max => 3,
360 crate::engine::NativeAgg::Avg => 4,
361 }
362 .hash(&mut hasher);
363 if let Some(principal) = principal {
364 principal.user_id.hash(&mut hasher);
365 principal.created_epoch.hash(&mut hasher);
366 principal.username.hash(&mut hasher);
367 principal.is_admin.hash(&mut hasher);
368 let mut roles = principal.roles.clone();
369 roles.sort_unstable();
370 roles.hash(&mut hasher);
371 }
372 hasher.finish()
373}
374
375fn read_history_retention(
376 root: &crate::durable_file::DurableRoot,
377 current_epoch: Epoch,
378) -> Result<(u64, Epoch)> {
379 const MAX_HISTORY_RETENTION_BYTES: u64 = 128;
380 let file = match root.open_regular(Path::new(META_DIR).join(HISTORY_RETENTION_FILENAME)) {
381 Ok(file) => file,
382 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
383 return Ok((0, current_epoch));
384 }
385 Err(error) => return Err(error.into()),
386 };
387 let length = file.metadata()?.len();
388 if length > MAX_HISTORY_RETENTION_BYTES {
389 return Err(MongrelError::ResourceLimitExceeded {
390 resource: "history retention bytes",
391 requested: usize::try_from(length).unwrap_or(usize::MAX),
392 limit: MAX_HISTORY_RETENTION_BYTES as usize,
393 });
394 }
395 let mut bytes = Vec::with_capacity(length as usize);
396 file.take(MAX_HISTORY_RETENTION_BYTES + 1)
397 .read_to_end(&mut bytes)?;
398 if bytes.len() as u64 != length {
399 return Err(MongrelError::Other(
400 "history retention length changed while reading".into(),
401 ));
402 }
403 let text = std::str::from_utf8(&bytes)
404 .map_err(|error| MongrelError::Other(format!("history retention encoding: {error}")))?;
405 let mut fields = text.split_whitespace();
406 let epochs = fields
407 .next()
408 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
409 .parse::<u64>()
410 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
411 let start = fields
412 .next()
413 .ok_or_else(|| MongrelError::Other("history retention start is missing".into()))?
414 .parse::<u64>()
415 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
416 if fields.next().is_some() || start > current_epoch.0 {
417 return Err(MongrelError::Other(
418 "history retention file has trailing fields or a future start epoch".into(),
419 ));
420 }
421 Ok((epochs, Epoch(start)))
422}
423
424fn write_history_retention<F>(
425 root: &Path,
426 epochs: u64,
427 start: Epoch,
428 after_publish: F,
429) -> Result<()>
430where
431 F: FnOnce(),
432{
433 let meta = root.join(META_DIR);
434 let path = meta.join(HISTORY_RETENTION_FILENAME);
435 let bytes = format!("{epochs} {}\n", start.0);
436 crate::durable_file::write_atomic_with_after(&path, bytes.as_bytes(), after_publish)?;
437 Ok(())
438}
439
440struct PreparedBackupDestination {
441 parent: crate::durable_file::DurableRoot,
442 destination_name: std::ffi::OsString,
443 destination_path: PathBuf,
444 stage_name: std::ffi::OsString,
445 stage: Option<Box<crate::durable_file::DurableRoot>>,
446}
447
448fn prepare_backup_destination(
449 source: &Path,
450 destination: &Path,
451) -> Result<PreparedBackupDestination> {
452 let destination_name = destination
453 .file_name()
454 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?
455 .to_os_string();
456 let requested_parent = destination
457 .parent()
458 .filter(|path| !path.as_os_str().is_empty())
459 .unwrap_or_else(|| Path::new("."));
460 crate::durable_file::create_directory_all(requested_parent)?;
461 let parent = crate::durable_file::DurableRoot::open(requested_parent)?;
462 prepare_backup_destination_in(source, &parent, &destination_name)
463}
464
465fn prepare_backup_destination_in(
466 source: &Path,
467 parent: &crate::durable_file::DurableRoot,
468 destination_name: &std::ffi::OsStr,
469) -> Result<PreparedBackupDestination> {
470 let source = source.canonicalize()?;
471 if parent.canonical_path().starts_with(&source) {
472 return Err(MongrelError::InvalidArgument(
473 "backup destination must not be inside the source database".into(),
474 ));
475 }
476 if parent.entry_exists(Path::new(&destination_name))? {
477 return Err(MongrelError::Conflict(format!(
478 "backup destination already exists: {}",
479 parent.canonical_path().join(destination_name).display()
480 )));
481 }
482 let mut stage_name = None;
483 for _ in 0..128 {
484 let mut nonce = [0_u8; 8];
485 crate::encryption::fill_random(&mut nonce)?;
486 let suffix = nonce
487 .iter()
488 .map(|byte| format!("{byte:02x}"))
489 .collect::<String>();
490 let name = std::ffi::OsString::from(format!(
491 ".{}.backup-stage-{}-{suffix}",
492 destination_name.to_string_lossy(),
493 std::process::id()
494 ));
495 match parent.create_directory_new(Path::new(&name)) {
496 Ok(()) => {
497 stage_name = Some(name);
498 break;
499 }
500 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
501 Err(error) => return Err(error.into()),
502 }
503 }
504 let stage_name = stage_name
505 .ok_or_else(|| MongrelError::Conflict("could not allocate backup staging path".into()))?;
506 let stage = parent.open_directory(Path::new(&stage_name))?;
507 Ok(PreparedBackupDestination {
508 destination_path: parent.canonical_path().join(destination_name),
509 destination_name: destination_name.to_os_string(),
510 stage_name,
511 stage: Some(Box::new(stage)),
512 parent: parent.try_clone()?,
513 })
514}
515
516fn copy_backup_boundary(
517 source_root: &Path,
518 destination_root: &crate::durable_file::DurableRoot,
519 deferred_runs: &HashSet<PathBuf>,
520 copied: &mut Vec<PathBuf>,
521 control: Option<&crate::ExecutionControl>,
522) -> Result<()> {
523 let mut visited = 0;
524 crate::durable_file::walk_regular_files_nofollow(
525 source_root,
526 |relative, is_directory| {
527 if visited % 256 == 0 {
528 if let Some(control) = control {
529 control.checkpoint()?;
530 }
531 }
532 visited += 1;
533 if backup_path_excluded(relative) {
534 return Ok(false);
535 }
536 if is_directory {
537 return Ok(true);
538 }
539 if deferred_runs.contains(relative) {
540 return Ok(false);
541 }
542 Ok(!(relative
543 .parent()
544 .and_then(Path::file_name)
545 .is_some_and(|parent| parent == "_runs")
546 && relative
547 .extension()
548 .is_some_and(|extension| extension == "sr")))
549 },
550 |relative| {
551 destination_root.create_directory_all(relative)?;
552 Ok(())
553 },
554 |relative, source| {
555 destination_root.copy_new_from(relative, source)?;
556 copied.push(relative.to_path_buf());
557 Ok(())
558 },
559 )
560}
561
562fn backup_path_excluded(relative: &Path) -> bool {
563 relative == Path::new("_meta/.lock")
564 || relative == Path::new("_meta/replica")
565 || relative == Path::new("_meta/repl_epoch")
566 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
567 || relative.components().any(|component| {
568 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
569 })
570}
571
572#[derive(Debug, Clone)]
573pub enum ExternalTriggerWrite {
574 Insert {
575 table: String,
576 cells: Vec<(u16, Value)>,
577 },
578 UpdateByPk {
579 table: String,
580 pk: Value,
581 cells: Vec<(u16, Value)>,
582 },
583 DeleteByPk {
584 table: String,
585 pk: Value,
586 },
587}
588
589impl ExternalTriggerWrite {
590 fn table(&self) -> &str {
591 match self {
592 Self::Insert { table, .. }
593 | Self::UpdateByPk { table, .. }
594 | Self::DeleteByPk { table, .. } => table,
595 }
596 }
597}
598
599#[derive(Debug, Clone, PartialEq)]
600pub enum ExternalTriggerBaseWrite {
601 Put {
602 table: String,
603 cells: Vec<(u16, Value)>,
604 },
605 Delete {
606 table: String,
607 row_id: RowId,
608 },
609}
610
611#[derive(Debug, Clone, PartialEq)]
612pub struct ExternalTriggerWriteResult {
613 pub state: Vec<u8>,
614 pub base_writes: Vec<ExternalTriggerBaseWrite>,
615}
616
617impl ExternalTriggerWriteResult {
618 pub fn new(state: Vec<u8>) -> Self {
619 Self {
620 state,
621 base_writes: Vec::new(),
622 }
623 }
624}
625
626pub trait ExternalTriggerBridge: Send + Sync {
627 fn apply_trigger_external_write(
628 &self,
629 entry: &ExternalTableEntry,
630 base_state: Vec<u8>,
631 op: ExternalTriggerWrite,
632 ) -> Result<ExternalTriggerWriteResult>;
633}
634
635struct SpilledRun {
637 table_id: u64,
638 run_id: u128,
639 pending_path: PathBuf,
640 final_path: PathBuf,
641 rows: Vec<crate::memtable::Row>,
642 row_count: u64,
643 min_rid: u64,
644 max_rid: u64,
645 content_hash: [u8; 32],
646}
647
648const SPILLED_WAL_PAYLOAD_MAX_BYTES: usize = 24 * 1024 * 1024;
649const SPILLED_WAL_TOTAL_MAX_BYTES: usize = 256 * 1024 * 1024;
650
651fn encode_spilled_row_chunks(
652 rows: &[crate::memtable::Row],
653 total_bytes: &mut usize,
654 total_limit: usize,
655 control: Option<&crate::ExecutionControl>,
656) -> Result<Vec<Vec<u8>>> {
657 let mut output = Vec::new();
658 let mut start = 0;
659 while start < rows.len() {
660 let mut estimated_bytes = std::mem::size_of::<u64>();
664 let mut end = start;
665 while end < rows.len() {
666 if end % 256 == 0 {
667 if let Some(control) = control {
668 control.checkpoint()?;
669 }
670 }
671 let row_bytes =
672 usize::try_from(bincode::serialized_size(&rows[end])?).map_err(|_| {
673 MongrelError::ResourceLimitExceeded {
674 resource: "spilled WAL row bytes",
675 requested: usize::MAX,
676 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
677 }
678 })?;
679 let next_bytes = estimated_bytes.checked_add(row_bytes).ok_or(
680 MongrelError::ResourceLimitExceeded {
681 resource: "spilled WAL row bytes",
682 requested: usize::MAX,
683 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
684 },
685 )?;
686 if next_bytes > SPILLED_WAL_PAYLOAD_MAX_BYTES {
687 break;
688 }
689 estimated_bytes = next_bytes;
690 end += 1;
691 }
692 if end == start {
693 return Err(MongrelError::ResourceLimitExceeded {
694 resource: "spilled WAL row bytes",
695 requested: estimated_bytes.saturating_add(1),
696 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
697 });
698 }
699 let payload = bincode::serialize(&rows[start..end])?;
700 if payload.len() > SPILLED_WAL_PAYLOAD_MAX_BYTES {
701 return Err(MongrelError::ResourceLimitExceeded {
702 resource: "spilled WAL row bytes",
703 requested: payload.len(),
704 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
705 });
706 }
707 let requested = total_bytes.checked_add(payload.len()).unwrap_or(usize::MAX);
708 if requested > total_limit {
709 return Err(MongrelError::ResourceLimitExceeded {
710 resource: "spilled WAL transaction bytes",
711 requested,
712 limit: total_limit,
713 });
714 }
715 *total_bytes = requested;
716 output.push(payload);
717 start = end;
718 }
719 Ok(output)
720}
721
722#[cfg(test)]
723mod spilled_wal_encoding_tests {
724 use super::*;
725
726 #[test]
727 fn logical_spill_payload_has_a_total_bound() {
728 let rows = (0..4)
729 .map(|row_id| crate::memtable::Row {
730 row_id: crate::rowid::RowId(row_id),
731 committed_epoch: Epoch::ZERO,
732 columns: [(1, Value::Bytes(vec![0; 64]))].into_iter().collect(),
733 deleted: false,
734 })
735 .collect::<Vec<_>>();
736 let mut total = 0;
737 let error = encode_spilled_row_chunks(&rows, &mut total, 32, None).unwrap_err();
738 assert!(matches!(
739 error,
740 MongrelError::ResourceLimitExceeded {
741 resource: "spilled WAL transaction bytes",
742 ..
743 }
744 ));
745 }
746}
747
748struct PreparedRunLinks {
753 links: Vec<(PathBuf, PathBuf)>,
754 armed: bool,
755}
756
757impl PreparedRunLinks {
758 fn prepare(spilled: &[SpilledRun]) -> Result<Self> {
759 let mut guard = Self {
760 links: Vec::with_capacity(spilled.len()),
761 armed: true,
762 };
763 for run in spilled {
764 crate::durable_file::rename(&run.pending_path, &run.final_path)?;
765 guard
766 .links
767 .push((run.pending_path.clone(), run.final_path.clone()));
768 }
769 Ok(guard)
770 }
771
772 fn disarm(&mut self) {
773 self.armed = false;
774 for (pending, _) in &self.links {
775 if let Some(parent) = pending.parent() {
776 let _ = std::fs::remove_dir_all(parent);
777 }
778 }
779 }
780}
781
782impl Drop for PreparedRunLinks {
783 fn drop(&mut self) {
784 if !self.armed {
785 return;
786 }
787 for (pending, final_path) in self.links.iter().rev() {
788 let _ = std::fs::rename(final_path, pending);
789 }
790 }
791}
792
793struct TableApplyBatch {
794 table_id: u64,
795 handle: TableHandle,
796 ops: Vec<crate::txn::StagedOp>,
797}
798
799#[derive(Debug, Clone)]
800struct TriggerRowImage {
801 columns: HashMap<u16, Value>,
802}
803
804impl TriggerRowImage {
805 fn from_row(row: crate::memtable::Row) -> Self {
806 Self {
807 columns: row.columns,
808 }
809 }
810
811 fn from_cells(cells: &[(u16, Value)]) -> Self {
812 Self {
813 columns: cells.iter().cloned().collect(),
814 }
815 }
816}
817
818#[derive(Debug, Clone)]
819struct WriteEvent {
820 table: String,
821 kind: TriggerEvent,
822 old: Option<TriggerRowImage>,
823 new: Option<TriggerRowImage>,
824 changed_columns: Vec<u16>,
825 op_indices: Vec<usize>,
826 put_idx: Option<usize>,
827 trigger_stack: Vec<String>,
828}
829
830#[derive(Default)]
831struct TriggerExpansion {
832 before: Vec<(u64, crate::txn::Staged)>,
833 before_stacks: Vec<Vec<String>>,
834 before_external: Vec<ExternalTriggerWrite>,
835 after: Vec<(u64, crate::txn::Staged)>,
836 after_stacks: Vec<Vec<String>>,
837 after_external: Vec<ExternalTriggerWrite>,
838 ignored_indices: std::collections::BTreeSet<usize>,
839}
840
841#[derive(Clone, PartialEq)]
842struct TriggerCatalogBinding {
843 triggers: Vec<TriggerEntry>,
844 tables: Vec<(String, u64, u64)>,
845 external_tables: Vec<(String, u64, u64)>,
846}
847
848fn trigger_catalog_binding(catalog: &Catalog) -> Option<TriggerCatalogBinding> {
849 let mut triggers = catalog
850 .triggers
851 .iter()
852 .filter(|entry| entry.trigger.enabled)
853 .cloned()
854 .collect::<Vec<_>>();
855 if triggers.is_empty() {
856 return None;
857 }
858 triggers.sort_by(|left, right| left.trigger.name.cmp(&right.trigger.name));
859 let mut tables = catalog
860 .tables
861 .iter()
862 .filter(|entry| matches!(entry.state, TableState::Live))
863 .map(|entry| (entry.name.clone(), entry.table_id, entry.schema.schema_id))
864 .collect::<Vec<_>>();
865 tables.sort_unstable();
866 let mut external_tables = catalog
867 .external_tables
868 .iter()
869 .map(|entry| {
870 (
871 entry.name.clone(),
872 entry.created_epoch,
873 entry.declared_schema.schema_id,
874 )
875 })
876 .collect::<Vec<_>>();
877 external_tables.sort_unstable();
878 Some(TriggerCatalogBinding {
879 triggers,
880 tables,
881 external_tables,
882 })
883}
884
885struct TriggerProgramOutput<'a> {
886 added: &'a mut Vec<(u64, crate::txn::Staged)>,
887 added_stacks: &'a mut Vec<Vec<String>>,
888 added_external: &'a mut Vec<ExternalTriggerWrite>,
889 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
890}
891
892#[derive(Debug, Clone, Copy, PartialEq, Eq)]
893enum TriggerProgramOutcome {
894 Continue,
895 Ignore,
896}
897
898#[derive(Debug, Clone)]
900pub struct CheckIssue {
901 pub table_id: u64,
902 pub table_name: String,
903 pub severity: String,
904 pub description: String,
905}
906
907#[derive(Debug, Clone)]
909pub struct AuthorizedReadSnapshot {
910 pub table: String,
911 pub table_snapshot: Snapshot,
912 pub data_generation: u64,
913 pub security_version: u64,
914 pub allowed_row_ids: Option<HashSet<RowId>>,
915}
916
917#[derive(Debug, Clone, Copy, PartialEq, Eq)]
919pub struct AuthorizedReadStamp {
920 pub table_id: u64,
921 pub schema_id: u64,
922 pub data_generation: u64,
923 pub security_version: u64,
924 pub snapshot: Snapshot,
925}
926
927type RlsCacheKey = (String, u64, u64, String);
928
929#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
931pub struct RlsCacheStats {
932 pub entries: usize,
933 pub bytes: usize,
934 pub hits: u64,
935 pub misses: u64,
936 pub evictions: u64,
937 pub build_nanos: u64,
938 pub rows_evaluated: u64,
939}
940
941const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
942const CDC_MAX_WAL_RECORDS: usize = 1_000_000;
943const CDC_MAX_WAL_REPLAY_BYTES: usize = 256 * 1024 * 1024;
944const CDC_MAX_EVENTS: usize = 100_000;
945const CDC_MAX_ROWS: usize = 1_000_000;
946const CDC_MAX_INLINE_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
947const CDC_MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
948
949fn charge_cdc_bytes(total: &mut usize, amount: usize, resource: &'static str) -> Result<()> {
950 let requested = total.saturating_add(amount);
951 if requested > CDC_MAX_RETAINED_BYTES {
952 return Err(MongrelError::ResourceLimitExceeded {
953 resource,
954 requested,
955 limit: CDC_MAX_RETAINED_BYTES,
956 });
957 }
958 *total = requested;
959 Ok(())
960}
961
962fn cdc_row_storage_bytes(row: &crate::memtable::Row) -> usize {
963 usize::try_from(row.estimated_bytes())
964 .unwrap_or(usize::MAX)
965 .saturating_add(std::mem::size_of::<crate::memtable::Row>())
966}
967
968fn cdc_row_json_bytes(row: &crate::memtable::Row) -> usize {
969 let value_slot = std::mem::size_of::<serde_json::Value>();
970 row.columns.values().fold(512_usize, |bytes, value| {
971 let values = match value {
972 Value::Bytes(values) => values.len(),
973 Value::Json(values) => values.len(),
974 Value::Embedding(values) => values.len(),
975 _ => 1,
976 };
977 bytes.saturating_add(values.saturating_mul(value_slot))
978 })
979}
980
981fn cdc_rows_json_bytes(rows: &[crate::memtable::Row]) -> usize {
982 rows.iter().fold(0_usize, |bytes, row| {
983 bytes.saturating_add(cdc_row_json_bytes(row))
984 })
985}
986
987#[derive(Default)]
988struct RlsCache {
989 entries: HashMap<RlsCacheKey, (Arc<HashSet<RowId>>, usize)>,
990 lru: VecDeque<RlsCacheKey>,
991 bytes: usize,
992 hits: u64,
993 misses: u64,
994 evictions: u64,
995 build_nanos: u64,
996 rows_evaluated: u64,
997}
998
999impl RlsCache {
1000 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
1001 let value = self.entries.get(key).map(|(value, _)| Arc::clone(value));
1002 if value.is_some() {
1003 self.hits = self.hits.saturating_add(1);
1004 self.touch(key);
1005 } else {
1006 self.misses = self.misses.saturating_add(1);
1007 }
1008 value
1009 }
1010
1011 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
1012 let bytes = key
1013 .0
1014 .len()
1015 .saturating_add(key.3.len())
1016 .saturating_add(
1017 value
1018 .capacity()
1019 .saturating_mul(std::mem::size_of::<RowId>() * 3),
1020 )
1021 .saturating_add(std::mem::size_of::<RlsCacheKey>());
1022 if bytes > RLS_CACHE_MAX_BYTES {
1023 return;
1024 }
1025 if let Some((_, old_bytes)) = self.entries.remove(&key) {
1026 self.bytes = self.bytes.saturating_sub(old_bytes);
1027 }
1028 self.lru.retain(|candidate| candidate != &key);
1029 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
1030 let Some(oldest) = self.lru.pop_front() else {
1031 break;
1032 };
1033 if let Some((_, old_bytes)) = self.entries.remove(&oldest) {
1034 self.bytes = self.bytes.saturating_sub(old_bytes);
1035 self.evictions = self.evictions.saturating_add(1);
1036 }
1037 }
1038 self.bytes = self.bytes.saturating_add(bytes);
1039 self.lru.push_back(key.clone());
1040 self.entries.insert(key, (value, bytes));
1041 }
1042
1043 fn touch(&mut self, key: &RlsCacheKey) {
1044 self.lru.retain(|candidate| candidate != key);
1045 self.lru.push_back(key.clone());
1046 }
1047
1048 fn stats(&self) -> RlsCacheStats {
1049 RlsCacheStats {
1050 entries: self.entries.len(),
1051 bytes: self.bytes,
1052 hits: self.hits,
1053 misses: self.misses,
1054 evictions: self.evictions,
1055 build_nanos: self.build_nanos,
1056 rows_evaluated: self.rows_evaluated,
1057 }
1058 }
1059}
1060
1061#[derive(Clone)]
1063pub struct TableHandle {
1064 inner: TableHandleInner,
1065 generation_metrics: Arc<TableGenerationMetrics>,
1066}
1067
1068#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1069pub struct TableGenerationStats {
1070 pub active_read_generations: usize,
1071 pub max_live_read_generations: usize,
1072 pub cow_clone_count: u64,
1073 pub cow_clone_nanos: u64,
1074 pub estimated_cow_clone_bytes: u64,
1075 pub writer_wait_nanos: u64,
1076}
1077
1078#[derive(Default)]
1079#[doc(hidden)]
1080pub struct TableGenerationMetrics {
1081 active_read_generations: AtomicUsize,
1082 max_live_read_generations: AtomicUsize,
1083 cow_clone_count: AtomicU64,
1084 cow_clone_nanos: AtomicU64,
1085 estimated_cow_clone_bytes: AtomicU64,
1086 writer_wait_nanos: AtomicU64,
1087}
1088
1089impl TableGenerationMetrics {
1090 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
1091 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
1092 self.max_live_read_generations
1093 .fetch_max(active, Ordering::Relaxed);
1094 Arc::new(TableReadGeneration {
1095 table,
1096 metrics: Arc::clone(self),
1097 })
1098 }
1099
1100 fn stats(&self) -> TableGenerationStats {
1101 TableGenerationStats {
1102 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
1103 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
1104 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
1105 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
1106 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
1107 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
1108 }
1109 }
1110}
1111
1112pub struct TableReadGeneration {
1114 table: Table,
1115 metrics: Arc<TableGenerationMetrics>,
1116}
1117
1118impl std::ops::Deref for TableReadGeneration {
1119 type Target = Table;
1120
1121 fn deref(&self) -> &Self::Target {
1122 &self.table
1123 }
1124}
1125
1126impl Drop for TableReadGeneration {
1127 fn drop(&mut self) {
1128 self.metrics
1129 .active_read_generations
1130 .fetch_sub(1, Ordering::Relaxed);
1131 }
1132}
1133
1134#[derive(Clone)]
1135enum TableHandleInner {
1136 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
1137 Direct(Arc<Mutex<Table>>),
1138}
1139
1140pub enum TableGuard<'a> {
1141 CopyOnWrite {
1142 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
1143 metrics: Arc<TableGenerationMetrics>,
1144 },
1145 Direct {
1146 table: parking_lot::MutexGuard<'a, Table>,
1147 },
1148}
1149
1150impl TableHandle {
1151 fn new(table: Table) -> Self {
1152 Self {
1153 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
1154 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1155 }
1156 }
1157
1158 pub fn from_table(table: Table) -> Self {
1159 Self::new(table)
1160 }
1161
1162 pub fn lock(&self) -> TableGuard<'_> {
1163 let started = std::time::Instant::now();
1164 let guard = match &self.inner {
1165 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
1166 table: table.write(),
1167 metrics: Arc::clone(&self.generation_metrics),
1168 },
1169 TableHandleInner::Direct(table) => TableGuard::Direct {
1170 table: table.lock(),
1171 },
1172 };
1173 self.generation_metrics.writer_wait_nanos.fetch_add(
1174 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1175 Ordering::Relaxed,
1176 );
1177 guard
1178 }
1179
1180 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
1181 let started = std::time::Instant::now();
1182 let guard = match &self.inner {
1183 TableHandleInner::CopyOnWrite(table) => {
1184 table
1185 .try_write_for(timeout)
1186 .map(|table| TableGuard::CopyOnWrite {
1187 table,
1188 metrics: Arc::clone(&self.generation_metrics),
1189 })
1190 }
1191 TableHandleInner::Direct(table) => table
1192 .try_lock_for(timeout)
1193 .map(|table| TableGuard::Direct { table }),
1194 };
1195 self.generation_metrics.writer_wait_nanos.fetch_add(
1196 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1197 Ordering::Relaxed,
1198 );
1199 guard
1200 }
1201
1202 pub fn ptr_eq(&self, other: &Self) -> bool {
1203 match (&self.inner, &other.inner) {
1204 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1205 Arc::ptr_eq(left, right)
1206 }
1207 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1208 Arc::ptr_eq(left, right)
1209 }
1210 _ => false,
1211 }
1212 }
1213
1214 pub fn read_generation_with_context(
1215 &self,
1216 context: Option<&crate::query::AiExecutionContext>,
1217 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1218 let mut table = if let Some(context) = context {
1219 loop {
1220 context.checkpoint()?;
1221 let wait = context
1222 .remaining_duration()
1223 .unwrap_or(std::time::Duration::from_millis(5))
1224 .min(std::time::Duration::from_millis(5));
1225 if let Some(table) = self.try_lock_for(wait) {
1226 break table;
1227 }
1228 }
1229 } else {
1230 self.lock()
1231 };
1232 let snapshot = table.snapshot();
1233 let generation = table.clone_read_generation()?;
1234 Ok((self.generation_metrics.activate(generation), snapshot))
1235 }
1236
1237 pub fn generation_stats(&self) -> TableGenerationStats {
1238 self.generation_metrics.stats()
1239 }
1240}
1241
1242impl From<Arc<Mutex<Table>>> for TableHandle {
1243 fn from(table: Arc<Mutex<Table>>) -> Self {
1244 Self {
1245 inner: TableHandleInner::Direct(table),
1246 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1247 }
1248 }
1249}
1250
1251impl std::ops::Deref for TableGuard<'_> {
1252 type Target = Table;
1253
1254 fn deref(&self) -> &Self::Target {
1255 match self {
1256 Self::CopyOnWrite { table, .. } => table.as_ref(),
1257 Self::Direct { table } => table,
1258 }
1259 }
1260}
1261
1262impl std::ops::DerefMut for TableGuard<'_> {
1263 fn deref_mut(&mut self) -> &mut Self::Target {
1264 match self {
1265 Self::CopyOnWrite { table, metrics } => {
1266 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1267 let estimated_bytes = table.estimated_clone_bytes();
1268 let started = std::time::Instant::now();
1269 let table = Arc::make_mut(table);
1270 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1271 metrics.cow_clone_nanos.fetch_add(
1272 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1273 Ordering::Relaxed,
1274 );
1275 metrics
1276 .estimated_cow_clone_bytes
1277 .fetch_add(estimated_bytes, Ordering::Relaxed);
1278 table
1279 } else {
1280 Arc::make_mut(table)
1281 }
1282 }
1283 Self::Direct { table } => table,
1284 }
1285 }
1286}
1287
1288#[derive(Clone, Debug)]
1289pub struct ReadAuthorization {
1290 pub operation: crate::auth::ColumnOperation,
1291 pub columns: Vec<u16>,
1292 pub permissions: Vec<crate::auth::Permission>,
1293}
1294
1295#[derive(Default, Debug)]
1296struct TableWritePermissionNeeds {
1297 insert: bool,
1298 insert_columns: Vec<u16>,
1299 update: bool,
1300 update_columns: Vec<u16>,
1301 delete: bool,
1302 truncate: bool,
1303}
1304
1305#[cfg(test)]
1306thread_local! {
1307 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1308 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1309 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1310 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1311 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1312 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1313}
1314
1315fn summarize_write_permissions(
1316 staging: &[(u64, crate::txn::Staged)],
1317) -> HashMap<u64, TableWritePermissionNeeds> {
1318 use crate::txn::Staged;
1319
1320 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1321 for (table_id, operation) in staging {
1322 let table = needs.entry(*table_id).or_default();
1323 match operation {
1324 Staged::Put(cells) => {
1325 table.insert = true;
1326 table
1327 .insert_columns
1328 .extend(cells.iter().map(|(column, _)| *column));
1329 }
1330 Staged::Update {
1331 changed_columns, ..
1332 } => {
1333 table.update = true;
1334 table.update_columns.extend(changed_columns);
1335 }
1336 Staged::Delete(_) => table.delete = true,
1337 Staged::Truncate => table.truncate = true,
1338 }
1339 }
1340 for table in needs.values_mut() {
1341 table.insert_columns.sort_unstable();
1342 table.insert_columns.dedup();
1343 table.update_columns.sort_unstable();
1344 table.update_columns.dedup();
1345 }
1346 needs
1347}
1348
1349struct SecurityCoordinator {
1350 gate: RwLock<()>,
1352 version: AtomicU64,
1353}
1354
1355fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1356 static COORDINATORS: std::sync::OnceLock<
1357 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1358 > = std::sync::OnceLock::new();
1359
1360 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1361 let mut coordinators = COORDINATORS
1362 .get_or_init(|| Mutex::new(HashMap::new()))
1363 .lock();
1364 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1365 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1366 return coordinator;
1367 }
1368 let coordinator = Arc::new(SecurityCoordinator {
1369 gate: RwLock::new(()),
1370 version: AtomicU64::new(version),
1371 });
1372 coordinators.insert(root, Arc::downgrade(&coordinator));
1373 coordinator
1374}
1375
1376pub fn lock_table_with_context<'a>(
1377 handle: &'a TableHandle,
1378 context: Option<&crate::query::AiExecutionContext>,
1379) -> Result<TableGuard<'a>> {
1380 let Some(context) = context else {
1381 return Ok(handle.lock());
1382 };
1383 loop {
1384 context.checkpoint()?;
1385 let wait = context
1386 .remaining_duration()
1387 .unwrap_or(std::time::Duration::from_millis(5))
1388 .min(std::time::Duration::from_millis(5));
1389 if let Some(guard) = handle.try_lock_for(wait) {
1390 return Ok(guard);
1391 }
1392 }
1393}
1394
1395#[derive(Clone, Debug, Default)]
1401pub struct OpenOptions {
1402 pub lock_timeout_ms: u32,
1418 pub memory_budget_bytes: Option<u64>,
1423 pub temp_disk_budget_bytes: Option<u64>,
1428 pub offline_validation: bool,
1435}
1436
1437impl OpenOptions {
1438 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1441 self.lock_timeout_ms = ms;
1442 self
1443 }
1444
1445 pub fn with_memory_budget_bytes(mut self, bytes: u64) -> Self {
1447 self.memory_budget_bytes = Some(bytes);
1448 self
1449 }
1450
1451 pub fn with_temp_disk_budget_bytes(mut self, bytes: u64) -> Self {
1453 self.temp_disk_budget_bytes = Some(bytes);
1454 self
1455 }
1456
1457 pub fn with_offline_validation(mut self, offline_validation: bool) -> Self {
1460 self.offline_validation = offline_validation;
1461 self
1462 }
1463}
1464
1465#[derive(Debug, Clone)]
1469pub(crate) enum OpenModeGate {
1470 Normal,
1473 OfflineValidation,
1475 ClusterRuntime {
1479 cluster_id: mongreldb_types::ids::ClusterId,
1481 node_id: mongreldb_types::ids::NodeId,
1483 database_id: mongreldb_types::ids::DatabaseId,
1485 },
1486 Create(crate::storage_mode::StorageMode),
1488}
1489
1490pub const DEFAULT_MEMORY_BUDGET_BYTES: u64 = 1024 * 1024 * 1024;
1494
1495pub const DEFAULT_TEMP_DISK_BUDGET_BYTES: u64 = 4 * 1024 * 1024 * 1024;
1498
1499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1503pub(crate) struct CoreResourceConfig {
1504 pub memory_budget_bytes: u64,
1505 pub temp_disk_budget_bytes: u64,
1506}
1507
1508impl Default for CoreResourceConfig {
1509 fn default() -> Self {
1510 Self {
1511 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
1512 temp_disk_budget_bytes: DEFAULT_TEMP_DISK_BUDGET_BYTES,
1513 }
1514 }
1515}
1516
1517impl CoreResourceConfig {
1518 fn from_options(options: &OpenOptions) -> Result<Self> {
1519 let config = Self {
1520 memory_budget_bytes: options
1521 .memory_budget_bytes
1522 .unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES),
1523 temp_disk_budget_bytes: options
1524 .temp_disk_budget_bytes
1525 .unwrap_or(DEFAULT_TEMP_DISK_BUDGET_BYTES),
1526 };
1527 if config.memory_budget_bytes == 0 {
1528 return Err(MongrelError::InvalidArgument(
1529 "memory_budget_bytes must be nonzero".into(),
1530 ));
1531 }
1532 if config.temp_disk_budget_bytes == 0 {
1533 return Err(MongrelError::InvalidArgument(
1534 "temp_disk_budget_bytes must be nonzero".into(),
1535 ));
1536 }
1537 Ok(config)
1538 }
1539}
1540
1541#[derive(Debug, Clone)]
1544pub struct TablePinsReport {
1545 pub table_id: u64,
1547 pub table: String,
1549 pub pins: crate::retention::PinsReport,
1551}
1552
1553pub struct DatabaseCore {
1566 root: PathBuf,
1567 durable_root: Arc<crate::durable_file::DurableRoot>,
1568 lifecycle: Arc<crate::core::LifecycleController>,
1571 registry: std::sync::OnceLock<crate::manager::CoreRegistration>,
1575 read_only: bool,
1577 catalog: RwLock<Catalog>,
1578 security_coordinator: Arc<SecurityCoordinator>,
1579 security_catalog_disk_reads: AtomicU64,
1580 rls_cache: Mutex<RlsCache>,
1581 epoch: Arc<EpochAuthority>,
1582 snapshots: Arc<SnapshotRegistry>,
1583 memory_governor: crate::memory::MemoryGovernor,
1588 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1589 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1590 spill_manager: crate::spill::SpillManager,
1594 resource_groups: crate::resource::ResourceGroupRegistry,
1598 embedding_providers: crate::embedding::EmbeddingProviderRegistry,
1602 job_registry: Arc<crate::jobs::JobRegistry>,
1605 commit_lock: Arc<Mutex<()>>,
1606 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1611 next_txn_id: Arc<Mutex<u64>>,
1615 tables: RwLock<HashMap<u64, TableHandle>>,
1616 kek: Option<Arc<crate::encryption::Kek>>,
1617 ddl_lock: Mutex<()>,
1620 meta_dek: Option<[u8; META_DEK_LEN]>,
1621 spill_threshold: std::sync::atomic::AtomicU64,
1624 conflicts: crate::txn::ConflictIndex,
1627 active_txns: crate::txn::ActiveTxns,
1630 lock_manager: Arc<crate::locks::LockManager>,
1636 poisoned: Arc<std::sync::atomic::AtomicBool>,
1639 group: Arc<crate::txn::GroupCommit>,
1643 commit_log: Arc<crate::commit_log::StandaloneCommitLog>,
1648 hlc: Arc<mongreldb_types::hlc::HlcClock>,
1654 idempotency: crate::txn::IdempotencyLedger,
1657 commit_ts_ledger: Mutex<std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp>>,
1665 active_spills: Arc<crate::retention::ActiveSpills>,
1668 replication_barrier: parking_lot::RwLock<()>,
1671 replication_wal_retention_segments: AtomicUsize,
1673 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1676 #[doc(hidden)]
1680 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1681 #[doc(hidden)]
1683 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1684 #[doc(hidden)]
1687 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1688 #[doc(hidden)]
1691 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1692 #[doc(hidden)]
1698 fk_lock_hook: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
1699 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1700 trigger_recursive: AtomicBool,
1701 trigger_max_depth: AtomicU32,
1702 trigger_max_loop_iterations: AtomicU32,
1703 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
1706 change_wake: tokio::sync::broadcast::Sender<()>,
1709 _lock: Mutex<Option<ExclusiveDatabaseLease>>,
1713}
1714
1715pub struct Database {
1725 core: Arc<DatabaseCore>,
1726 principal: RwLock<Option<crate::auth::Principal>>,
1736 auth_state: crate::auth_state::AuthState,
1742 shared: bool,
1749}
1750
1751impl std::ops::Deref for Database {
1752 type Target = DatabaseCore;
1753
1754 fn deref(&self) -> &DatabaseCore {
1755 &self.core
1756 }
1757}
1758
1759struct RunPins {
1760 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1761 runs: Vec<(u64, u128)>,
1762}
1763
1764struct TxnLockGuard<'a> {
1769 locks: &'a crate::locks::LockManager,
1770 txn_id: u64,
1771}
1772
1773impl Drop for TxnLockGuard<'_> {
1774 fn drop(&mut self) {
1775 self.locks.release_all(self.txn_id);
1776 }
1777}
1778
1779struct BackupFilePins {
1780 root: PathBuf,
1781}
1782
1783struct PendingTableDir {
1784 path: PathBuf,
1785 armed: bool,
1786}
1787
1788impl PendingTableDir {
1789 fn new(path: PathBuf) -> Self {
1790 Self { path, armed: true }
1791 }
1792
1793 fn disarm(&mut self) {
1794 self.armed = false;
1795 }
1796}
1797
1798impl Drop for PendingTableDir {
1799 fn drop(&mut self) {
1800 if self.armed {
1801 let _ = std::fs::remove_dir_all(&self.path);
1802 }
1803 }
1804}
1805
1806impl Drop for BackupFilePins {
1807 fn drop(&mut self) {
1808 let _ = std::fs::remove_dir_all(&self.root);
1809 }
1810}
1811
1812impl Drop for RunPins {
1813 fn drop(&mut self) {
1814 let mut pins = self.pins.lock();
1815 for run in &self.runs {
1816 if let Some(count) = pins.get_mut(run) {
1817 *count -= 1;
1818 if *count == 0 {
1819 pins.remove(run);
1820 }
1821 }
1822 }
1823 }
1824}
1825
1826#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1829pub struct ChangeEvent {
1830 pub id: Option<String>,
1831 pub channel: String,
1832 pub table_id: Option<u64>,
1833 pub table: String,
1834 pub op: String,
1835 pub epoch: u64,
1836 pub txn_id: Option<u64>,
1837 pub message: Option<String>,
1838 pub data: Option<serde_json::Value>,
1839}
1840
1841#[derive(Debug, Clone)]
1842pub struct CdcBatch {
1843 pub events: Vec<ChangeEvent>,
1844 pub current_epoch: u64,
1845 pub earliest_epoch: Option<u64>,
1846 pub gap: bool,
1847}
1848
1849impl std::fmt::Debug for Database {
1856 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1857 let cat = self.catalog.read();
1858 let principal_guard = self.principal.read();
1859 let principal: &str = principal_guard
1860 .as_ref()
1861 .map(|p| p.username.as_str())
1862 .unwrap_or("<none>");
1863 f.debug_struct("Database")
1864 .field("root", &self.root)
1865 .field("db_epoch", &cat.db_epoch)
1866 .field("open_generation", &"sidecar")
1867 .field("tables", &cat.tables.len())
1868 .field("visible_epoch", &self.epoch.visible().0)
1869 .field("encrypted", &self.kek.is_some())
1870 .field("require_auth", &cat.require_auth)
1871 .field("principal", &principal)
1872 .finish()
1873 }
1874}
1875
1876impl std::fmt::Debug for DatabaseCore {
1879 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1880 let cat = self.catalog.read();
1881 f.debug_struct("DatabaseCore")
1882 .field("root", &self.root)
1883 .field("lifecycle", &self.lifecycle.state())
1884 .field("db_epoch", &cat.db_epoch)
1885 .field("tables", &cat.tables.len())
1886 .field("visible_epoch", &self.epoch.visible().0)
1887 .field("encrypted", &self.kek.is_some())
1888 .field("require_auth", &cat.require_auth)
1889 .finish()
1890 }
1891}
1892
1893impl DatabaseCore {
1894 fn ensure_owner_process(&self) -> Result<()> {
1895 let current_pid = std::process::id();
1896 let owner_pid = self
1897 ._lock
1898 .lock()
1899 .as_ref()
1900 .map(|lease| lease.owner_pid)
1901 .unwrap_or(current_pid);
1902 if current_pid == owner_pid {
1903 Ok(())
1904 } else {
1905 Err(MongrelError::ForkedProcess {
1906 owner_pid,
1907 current_pid,
1908 })
1909 }
1910 }
1911
1912 pub fn root(&self) -> &Path {
1914 self.durable_root.canonical_path()
1915 }
1916
1917 pub fn file_identity(&self) -> Result<crate::core::DatabaseFileIdentity> {
1919 crate::core::DatabaseFileIdentity::from_durable_root(&self.durable_root)
1920 }
1921
1922 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
1924 self.lifecycle.state()
1925 }
1926
1927 pub fn is_open(&self) -> bool {
1929 self.lifecycle.is_open()
1930 }
1931
1932 pub fn operation_guard(self: &Arc<Self>) -> Result<crate::core::OperationGuard> {
1936 self.lifecycle.begin_operation()
1937 }
1938
1939 pub(crate) fn set_registry(
1943 &self,
1944 registration: crate::manager::CoreRegistration,
1945 ) -> Result<()> {
1946 self.registry
1947 .set(registration)
1948 .map_err(|_| MongrelError::Conflict("database core is already registry-bound".into()))
1949 }
1950
1951 pub fn shutdown(self: &Arc<Self>, drain_deadline: std::time::Duration) -> Result<()> {
1971 self.ensure_owner_process()?;
1972 let initiated = self.lifecycle.begin_shutdown();
1973 if !initiated {
1974 match self.lifecycle.state() {
1975 crate::core::LifecycleState::Closed => return Ok(()),
1976 crate::core::LifecycleState::Draining => {}
1979 state => {
1980 return Err(MongrelError::Conflict(format!(
1981 "database core cannot shut down from lifecycle state {state}"
1982 )))
1983 }
1984 }
1985 }
1986 if let Some(registration) = self.registry.get() {
1987 registration
1988 .manager
1989 .mark_closing(®istration.identity, self);
1990 }
1991 self.lifecycle.wait_drained(drain_deadline)?;
1992 let sync = self.shared_wal.lock().group_sync().map(|_| ());
1993 self.lifecycle.mark_closing();
1994 let lease = self._lock.lock().take();
1995 drop(lease);
1996 if let Some(registration) = self.registry.get() {
1997 registration
1998 .manager
1999 .entry_closed(®istration.identity, self);
2000 }
2001 self.lifecycle.mark_closed();
2002 sync
2003 }
2004}
2005
2006impl Database {
2007 pub fn open_metrics() -> DatabaseOpenMetrics {
2008 DatabaseOpenMetrics {
2009 lock_waits: DATABASE_OPEN_WAIT_COUNT.load(Ordering::Relaxed),
2010 failures: DATABASE_OPEN_FAILURE_COUNT.load(Ordering::Relaxed),
2011 }
2012 }
2013
2014 pub fn core(&self) -> Arc<DatabaseCore> {
2016 Arc::clone(&self.core)
2017 }
2018
2019 pub fn identity(&self) -> crate::handle::HandleIdentity {
2022 match self.principal.read().as_ref() {
2023 Some(principal) => crate::handle::HandleIdentity::CatalogUser {
2024 username: principal.username.clone(),
2025 user_id: principal.user_id,
2026 created_version: principal.created_epoch,
2027 },
2028 None => crate::handle::HandleIdentity::Credentialless,
2029 }
2030 }
2031
2032 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2034 self.core.lifecycle_state()
2035 }
2036
2037 pub fn operation_guard(&self) -> Result<crate::core::OperationGuard> {
2041 self.core.operation_guard()
2042 }
2043
2044 pub(crate) fn from_core(
2048 core: Arc<DatabaseCore>,
2049 principal: Option<crate::auth::Principal>,
2050 shared: bool,
2051 ) -> Self {
2052 let require_auth = core.catalog.read().require_auth;
2053 Self {
2054 core,
2055 principal: RwLock::new(principal.clone()),
2056 auth_state: crate::auth_state::AuthState::new(require_auth, principal),
2057 shared,
2058 }
2059 }
2060
2061 pub(crate) fn into_core(self) -> Arc<DatabaseCore> {
2063 self.core
2064 }
2065
2066 pub(crate) fn open_for_shared_core(root: impl AsRef<Path>) -> Result<Self> {
2071 Self::open_inner_with_lock_timeout(
2072 root,
2073 None,
2074 None,
2075 0,
2076 CoreResourceConfig::default(),
2077 OpenModeGate::Normal,
2078 )
2079 }
2080
2081 pub fn shutdown(self: Arc<Self>) -> Result<()> {
2087 if self.shared {
2088 return Err(MongrelError::Conflict(
2089 "shared-core facades do not own storage; use DatabaseHandle::shutdown".into(),
2090 ));
2091 }
2092 match Arc::try_unwrap(self) {
2093 Ok(database) => {
2094 database.ensure_owner_process()?;
2095 database.core.shutdown(std::time::Duration::from_secs(30))?;
2099 drop(database);
2100 Ok(())
2101 }
2102 Err(database) => Err(MongrelError::DatabaseBusy {
2103 strong_handles: Arc::strong_count(&database),
2104 }),
2105 }
2106 }
2107
2108 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
2109 if let Ok(canonical) = root.canonicalize() {
2110 let lock_dir = canonical.parent().ok_or_else(|| {
2111 std::io::Error::new(
2112 std::io::ErrorKind::InvalidInput,
2113 "database root must have a parent directory",
2114 )
2115 })?;
2116 return Ok((canonical.clone(), lock_dir.to_path_buf()));
2117 }
2118
2119 let absolute = if root.is_absolute() {
2120 root.to_path_buf()
2121 } else {
2122 std::env::current_dir()?.join(root)
2123 };
2124 let mut cursor = absolute.as_path();
2125 let mut suffix = Vec::new();
2126 while !cursor.exists() {
2127 let name = cursor.file_name().ok_or_else(|| {
2128 std::io::Error::new(
2129 std::io::ErrorKind::NotFound,
2130 format!("no existing ancestor for database root {}", root.display()),
2131 )
2132 })?;
2133 suffix.push(name.to_os_string());
2134 cursor = cursor.parent().ok_or_else(|| {
2135 std::io::Error::new(
2136 std::io::ErrorKind::NotFound,
2137 format!("no existing ancestor for database root {}", root.display()),
2138 )
2139 })?;
2140 }
2141 let lock_dir = cursor.canonicalize()?;
2142 let mut canonical = lock_dir.clone();
2143 for component in suffix.iter().rev() {
2144 canonical.push(component);
2145 }
2146 Ok((canonical, lock_dir))
2147 }
2148
2149 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<ExclusiveDatabaseLease> {
2150 use std::hash::{Hash, Hasher};
2151
2152 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
2153 let reservation =
2154 OpenReservation::acquire(DatabaseOpenKey::IntendedPath(canonical_path.clone()), root)?;
2155
2156 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2157 canonical_path.hash(&mut hasher);
2158 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
2159 let file = std::fs::OpenOptions::new()
2160 .create(true)
2161 .truncate(false)
2162 .write(true)
2163 .open(lock_path)?;
2164 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2165 return Err(MongrelError::DatabaseLocked {
2166 path: root.to_path_buf(),
2167 message: error.to_string(),
2168 });
2169 }
2170 Ok(reservation.into_lease(file, canonical_path))
2171 }
2172
2173 fn acquire_legacy_database_lock(
2174 lock: &mut ExclusiveDatabaseLease,
2175 root: &Path,
2176 timeout_ms: u32,
2177 ) -> Result<()> {
2178 let durable_root = lock
2179 .durable_root
2180 .as_ref()
2181 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
2182 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
2183 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2184 return Err(MongrelError::DatabaseLocked {
2185 path: root.to_path_buf(),
2186 message: error.to_string(),
2187 });
2188 }
2189 lock.legacy_file = Some(file);
2190 Ok(())
2191 }
2192
2193 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
2194 if path.exists() {
2195 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
2196 }
2197 let mut ancestor = path;
2198 while !ancestor.exists() {
2199 ancestor = ancestor.parent().ok_or_else(|| {
2200 MongrelError::NotFound(format!(
2201 "no existing ancestor for database root {}",
2202 path.display()
2203 ))
2204 })?;
2205 }
2206 let relative = path.strip_prefix(ancestor).map_err(|error| {
2207 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
2208 })?;
2209 crate::durable_file::DurableRoot::open(ancestor)?
2210 .create_directory_all_pinned(relative)
2211 .map_err(Into::into)
2212 }
2213
2214 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2215 let requested_root = root.as_ref();
2216 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
2217 let root = lock.canonical_path.clone();
2218 Self::reject_existing_database(&root)?;
2219 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
2220 if durable_root.canonical_path() != lock.canonical_path {
2221 return Err(MongrelError::Conflict(
2222 "database root changed while it was being created".into(),
2223 ));
2224 }
2225 lock.claim_root_identity(&durable_root)?;
2226 durable_root.create_directory_all(META_DIR)?;
2227 lock.durable_root = Some(durable_root);
2228 let io_root = lock
2229 .durable_root
2230 .as_ref()
2231 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2232 .io_path()?;
2233 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
2234 Self::reject_existing_database(&io_root)?;
2235 Ok((io_root, lock))
2236 }
2237
2238 fn begin_open(
2239 root: impl AsRef<Path>,
2240 lock_timeout_ms: u32,
2241 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2242 let root = root.as_ref();
2243 let canonical_root = root.canonicalize().map_err(|error| {
2244 if error.kind() == std::io::ErrorKind::NotFound {
2245 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
2246 } else {
2247 error.into()
2248 }
2249 })?;
2250 let durable_root = crate::durable_file::DurableRoot::open(&canonical_root)?;
2251 Self::begin_open_durable(durable_root, lock_timeout_ms)
2252 }
2253
2254 fn begin_open_durable(
2255 durable_root: crate::durable_file::DurableRoot,
2256 lock_timeout_ms: u32,
2257 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2258 let io_root = durable_root.io_path()?;
2259 let current_root = io_root.canonicalize()?;
2260 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
2261 lock.claim_root_identity(&durable_root)?;
2262 lock.durable_root = Some(Arc::new(durable_root));
2263 let io_root = lock
2264 .durable_root
2265 .as_ref()
2266 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2267 .io_path()?;
2268 if lock
2269 .durable_root
2270 .as_ref()
2271 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2272 .open_directory(META_DIR)
2273 .is_err()
2274 {
2275 return Err(MongrelError::NotFound(format!(
2276 "no database metadata found at {:?}",
2277 current_root
2278 )));
2279 }
2280 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
2281 Ok((io_root, lock))
2282 }
2283
2284 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
2286 let (root, lock) = Self::begin_create(root)?;
2287 Self::create_inner(
2288 root,
2289 None,
2290 lock,
2291 crate::storage_mode::StorageMode::Standalone,
2292 )
2293 }
2294
2295 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2298 let (root, lock) = Self::begin_create(root)?;
2299 let salt = crate::encryption::random_salt()?;
2300 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2301 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2302 Self::create_inner(
2303 root,
2304 Some(kek),
2305 lock,
2306 crate::storage_mode::StorageMode::Standalone,
2307 )
2308 }
2309
2310 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2313 let (root, lock) = Self::begin_create(root)?;
2314 let salt = crate::encryption::random_salt()?;
2315 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2316 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2317 Self::create_inner(
2318 root,
2319 Some(kek),
2320 lock,
2321 crate::storage_mode::StorageMode::Standalone,
2322 )
2323 }
2324
2325 pub fn create_cluster_replica(
2333 root: impl AsRef<Path>,
2334 cluster_id: mongreldb_types::ids::ClusterId,
2335 node_id: mongreldb_types::ids::NodeId,
2336 database_id: mongreldb_types::ids::DatabaseId,
2337 ) -> Result<Self> {
2338 let (root, lock) = Self::begin_create(root)?;
2339 Self::create_inner(
2340 root,
2341 None,
2342 lock,
2343 crate::storage_mode::StorageMode::ClusterReplica {
2344 cluster_id,
2345 node_id,
2346 database_id,
2347 },
2348 )
2349 }
2350
2351 pub fn open_cluster_replica(
2358 root: impl AsRef<Path>,
2359 expected: &crate::storage_mode::StorageMode,
2360 ) -> Result<Self> {
2361 let Some((cluster_id, node_id, database_id)) = expected.cluster_identity() else {
2362 return Err(MongrelError::InvalidArgument(format!(
2363 "open_cluster_replica requires a ClusterReplica identity, got {expected:?}"
2364 )));
2365 };
2366 let (root, lock) = Self::begin_open(root, 0)?;
2367 Self::open_inner_locked(
2368 root,
2369 None,
2370 lock,
2371 CoreResourceConfig::default(),
2372 OpenModeGate::ClusterRuntime {
2373 cluster_id,
2374 node_id,
2375 database_id,
2376 },
2377 )
2378 }
2379
2380 fn create_inner(
2381 root: PathBuf,
2382 kek: Option<Arc<crate::encryption::Kek>>,
2383 lock: ExclusiveDatabaseLease,
2384 storage_mode: crate::storage_mode::StorageMode,
2385 ) -> Result<Self> {
2386 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2387 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2388 let cat = Catalog::empty();
2389 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2390 Self::finish_open(
2391 root,
2392 cat,
2393 kek,
2394 meta_dek,
2395 false,
2396 None,
2397 None,
2398 None,
2399 lock,
2400 CoreResourceConfig::default(),
2401 OpenModeGate::Create(storage_mode),
2402 )
2403 }
2404
2405 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
2407 Self::open_inner(root, None, None)
2408 }
2409
2410 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2412 let (root, lock) = Self::begin_open(root, 0)?;
2413 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2414 MongrelError::Other("database root descriptor was not pinned".into())
2415 })?)?;
2416 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2417 Self::open_inner_locked(
2418 root,
2419 Some(kek),
2420 lock,
2421 CoreResourceConfig::default(),
2422 OpenModeGate::Normal,
2423 )
2424 }
2425
2426 pub fn open_encrypted_with_options(
2429 root: impl AsRef<Path>,
2430 passphrase: &str,
2431 options: OpenOptions,
2432 ) -> Result<Self> {
2433 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2434 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2435 MongrelError::Other("database root descriptor was not pinned".into())
2436 })?)?;
2437 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2438 let gate = if options.offline_validation {
2439 OpenModeGate::OfflineValidation
2440 } else {
2441 OpenModeGate::Normal
2442 };
2443 Self::open_inner_locked(
2444 root,
2445 Some(kek),
2446 lock,
2447 CoreResourceConfig::from_options(&options)?,
2448 gate,
2449 )
2450 }
2451
2452 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2454 let (root, lock) = Self::begin_open(root, 0)?;
2455 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2456 MongrelError::Other("database root descriptor was not pinned".into())
2457 })?)?;
2458 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2459 Self::open_inner_locked(
2460 root,
2461 Some(kek),
2462 lock,
2463 CoreResourceConfig::default(),
2464 OpenModeGate::Normal,
2465 )
2466 }
2467
2468 pub fn open_with_credentials(
2480 root: impl AsRef<Path>,
2481 username: &str,
2482 password: &str,
2483 ) -> Result<Self> {
2484 Self::open_inner_with_credentials(root, None, username, password)
2485 }
2486
2487 pub fn open_with_credentials_and_options(
2491 root: impl AsRef<Path>,
2492 username: &str,
2493 password: &str,
2494 options: OpenOptions,
2495 ) -> Result<Self> {
2496 let gate = if options.offline_validation {
2497 OpenModeGate::OfflineValidation
2498 } else {
2499 OpenModeGate::Normal
2500 };
2501 Self::open_inner_with_credentials_and_lock_timeout(
2502 root,
2503 None,
2504 username,
2505 password,
2506 options.lock_timeout_ms,
2507 CoreResourceConfig::from_options(&options)?,
2508 gate,
2509 )
2510 }
2511
2512 pub fn open_encrypted_with_credentials(
2515 root: impl AsRef<Path>,
2516 passphrase: &str,
2517 username: &str,
2518 password: &str,
2519 ) -> Result<Self> {
2520 let (root, lock) = Self::begin_open(root, 0)?;
2521 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2522 MongrelError::Other("database root descriptor was not pinned".into())
2523 })?)?;
2524 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2525 Self::open_inner_with_credentials_locked(
2526 root,
2527 Some(kek),
2528 username,
2529 password,
2530 lock,
2531 CoreResourceConfig::default(),
2532 OpenModeGate::Normal,
2533 )
2534 }
2535
2536 pub fn open_encrypted_with_credentials_and_options(
2540 root: impl AsRef<Path>,
2541 passphrase: &str,
2542 username: &str,
2543 password: &str,
2544 options: OpenOptions,
2545 ) -> Result<Self> {
2546 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2547 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2548 MongrelError::Other("database root descriptor was not pinned".into())
2549 })?)?;
2550 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2551 let gate = if options.offline_validation {
2552 OpenModeGate::OfflineValidation
2553 } else {
2554 OpenModeGate::Normal
2555 };
2556 Self::open_inner_with_credentials_locked(
2557 root,
2558 Some(kek),
2559 username,
2560 password,
2561 lock,
2562 CoreResourceConfig::from_options(&options)?,
2563 gate,
2564 )
2565 }
2566
2567 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
2574 let gate = if options.offline_validation {
2577 OpenModeGate::OfflineValidation
2578 } else {
2579 OpenModeGate::Normal
2580 };
2581 Self::open_inner_with_lock_timeout(
2582 root,
2583 None,
2584 None,
2585 options.lock_timeout_ms,
2586 CoreResourceConfig::from_options(&options)?,
2587 gate,
2588 )
2589 }
2590
2591 fn open_inner_with_lock_timeout(
2592 root: impl AsRef<Path>,
2593 kek: Option<Arc<crate::encryption::Kek>>,
2594 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2595 lock_timeout_ms: u32,
2596 resources: CoreResourceConfig,
2597 mode_gate: OpenModeGate,
2598 ) -> Result<Self> {
2599 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
2600 Self::open_inner_locked(root, kek, lock, resources, mode_gate)
2601 }
2602
2603 fn open_inner_locked(
2604 root: PathBuf,
2605 kek: Option<Arc<crate::encryption::Kek>>,
2606 lock: ExclusiveDatabaseLease,
2607 resources: CoreResourceConfig,
2608 mode_gate: OpenModeGate,
2609 ) -> Result<Self> {
2610 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2611 let mut cat = catalog::read_durable(
2612 lock.durable_root.as_deref().ok_or_else(|| {
2613 MongrelError::Other("database root descriptor was not pinned".into())
2614 })?,
2615 meta_dek.as_ref(),
2616 )?
2617 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2618 let recovery_checkpoint = cat.clone();
2619
2620 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2623 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2624 lock.durable_root.as_deref().ok_or_else(|| {
2625 MongrelError::Other("database root descriptor was not pinned".into())
2626 })?,
2627 wal_dek.as_ref(),
2628 )?;
2629 recover_ddl_from_records(
2630 &root,
2631 Some(lock.durable_root.as_deref().ok_or_else(|| {
2632 MongrelError::Other("database root descriptor was not pinned".into())
2633 })?),
2634 &mut cat,
2635 meta_dek.as_ref(),
2636 false,
2637 None,
2638 &recovery_records,
2639 )?;
2640 Self::finish_open(
2641 root,
2642 cat,
2643 kek,
2644 meta_dek,
2645 true,
2646 Some(recovery_checkpoint),
2647 Some(recovery_records),
2648 None,
2649 lock,
2650 resources,
2651 mode_gate,
2652 )
2653 }
2654
2655 fn open_inner_with_credentials(
2662 root: impl AsRef<Path>,
2663 kek: Option<Arc<crate::encryption::Kek>>,
2664 username: &str,
2665 password: &str,
2666 ) -> Result<Self> {
2667 Self::open_inner_with_credentials_and_lock_timeout(
2668 root,
2669 kek,
2670 username,
2671 password,
2672 0,
2673 CoreResourceConfig::default(),
2674 OpenModeGate::Normal,
2675 )
2676 }
2677
2678 #[allow(clippy::too_many_arguments)]
2682 fn open_inner_with_credentials_and_lock_timeout(
2683 root: impl AsRef<Path>,
2684 kek: Option<Arc<crate::encryption::Kek>>,
2685 username: &str,
2686 password: &str,
2687 lock_timeout_ms: u32,
2688 resources: CoreResourceConfig,
2689 mode_gate: OpenModeGate,
2690 ) -> Result<Self> {
2691 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
2692 Self::open_inner_with_credentials_locked(
2693 root, kek, username, password, lock, resources, mode_gate,
2694 )
2695 }
2696
2697 #[allow(clippy::too_many_arguments)]
2698 fn open_inner_with_credentials_locked(
2699 root: PathBuf,
2700 kek: Option<Arc<crate::encryption::Kek>>,
2701 username: &str,
2702 password: &str,
2703 lock: ExclusiveDatabaseLease,
2704 resources: CoreResourceConfig,
2705 mode_gate: OpenModeGate,
2706 ) -> Result<Self> {
2707 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2708 let mut cat = catalog::read_durable(
2709 lock.durable_root.as_deref().ok_or_else(|| {
2710 MongrelError::Other("database root descriptor was not pinned".into())
2711 })?,
2712 meta_dek.as_ref(),
2713 )?
2714 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2715 let recovery_checkpoint = cat.clone();
2716
2717 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2720 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2721 lock.durable_root.as_deref().ok_or_else(|| {
2722 MongrelError::Other("database root descriptor was not pinned".into())
2723 })?,
2724 wal_dek.as_ref(),
2725 )?;
2726 recover_ddl_from_records(
2727 &root,
2728 Some(lock.durable_root.as_deref().ok_or_else(|| {
2729 MongrelError::Other("database root descriptor was not pinned".into())
2730 })?),
2731 &mut cat,
2732 meta_dek.as_ref(),
2733 false,
2734 None,
2735 &recovery_records,
2736 )?;
2737
2738 if !cat.require_auth {
2741 return Err(MongrelError::AuthNotRequired);
2742 }
2743
2744 let user = cat
2749 .users
2750 .iter()
2751 .find(|u| u.username == username)
2752 .filter(|u| !u.password_hash.is_empty())
2753 .ok_or_else(|| MongrelError::InvalidCredentials {
2754 username: username.to_string(),
2755 })?;
2756 let password_ok = crate::auth::verify_password(password, &user.password_hash)
2757 .map_err(MongrelError::Other)?;
2758 if !password_ok {
2759 return Err(MongrelError::InvalidCredentials {
2760 username: username.to_string(),
2761 });
2762 }
2763
2764 let principal =
2766 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
2767 MongrelError::InvalidCredentials {
2768 username: username.to_string(),
2769 }
2770 })?;
2771
2772 Self::finish_open(
2773 root,
2774 cat,
2775 kek,
2776 meta_dek,
2777 true,
2778 Some(recovery_checkpoint),
2779 Some(recovery_records),
2780 Some(principal),
2781 lock,
2782 resources,
2783 mode_gate,
2784 )
2785 }
2786
2787 pub fn create_with_credentials(
2797 root: impl AsRef<Path>,
2798 admin_username: &str,
2799 admin_password: &str,
2800 ) -> Result<Self> {
2801 let (root, lock) = Self::begin_create(root)?;
2802 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
2803 }
2804
2805 pub fn create_encrypted_with_credentials(
2809 root: impl AsRef<Path>,
2810 passphrase: &str,
2811 admin_username: &str,
2812 admin_password: &str,
2813 ) -> Result<Self> {
2814 let (root, lock) = Self::begin_create(root)?;
2815 let salt = crate::encryption::random_salt()?;
2816 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2817 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2818 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
2819 }
2820
2821 fn create_inner_with_credentials(
2822 root: PathBuf,
2823 kek: Option<Arc<crate::encryption::Kek>>,
2824 admin_username: &str,
2825 admin_password: &str,
2826 lock: ExclusiveDatabaseLease,
2827 ) -> Result<Self> {
2828 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2829 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2830
2831 let password_hash =
2833 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2834 let mut cat = Catalog::empty();
2835 cat.require_auth = true;
2836 cat.next_user_id = 2;
2837 cat.users.push(crate::auth::UserEntry {
2838 id: 1,
2839 username: admin_username.to_string(),
2840 password_hash,
2841 roles: Vec::new(),
2842 is_admin: true,
2843 created_epoch: 0,
2844 });
2845 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2846
2847 let admin_principal = crate::auth::Principal {
2850 user_id: 1,
2851 created_epoch: 0,
2852 username: admin_username.to_string(),
2853 is_admin: true,
2854 roles: Vec::new(),
2855 permissions: Vec::new(),
2856 };
2857 Self::finish_open(
2858 root,
2859 cat,
2860 kek,
2861 meta_dek,
2862 false,
2863 None,
2864 None,
2865 Some(admin_principal),
2866 lock,
2867 CoreResourceConfig::default(),
2868 OpenModeGate::Create(crate::storage_mode::StorageMode::Standalone),
2869 )
2870 }
2871
2872 fn reject_existing_database(root: &Path) -> Result<()> {
2873 if root.join(catalog::CATALOG_FILENAME).exists() {
2876 return Err(MongrelError::InvalidArgument(format!(
2877 "database already exists at {}; use Database::open() to open it, \
2878 or remove the directory first",
2879 root.display()
2880 )));
2881 }
2882 Ok(())
2883 }
2884
2885 fn open_inner(
2886 root: impl AsRef<Path>,
2887 kek: Option<Arc<crate::encryption::Kek>>,
2888 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2889 ) -> Result<Self> {
2890 Self::open_inner_with_lock_timeout(
2891 root,
2892 kek,
2893 None,
2894 0,
2895 CoreResourceConfig::default(),
2896 OpenModeGate::Normal,
2897 )
2898 }
2899
2900 pub(crate) fn open_offline_validation(root: impl AsRef<Path>) -> Result<Self> {
2903 Self::open_inner_with_lock_timeout(
2904 root,
2905 None,
2906 None,
2907 0,
2908 CoreResourceConfig::default(),
2909 OpenModeGate::OfflineValidation,
2910 )
2911 }
2912
2913 pub(crate) fn open_replica_recovery_durable(
2917 root: &crate::durable_file::DurableRoot,
2918 ) -> Result<Self> {
2919 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2920 Self::open_replica_recovery_inner(root, None, lock)
2921 }
2922
2923 pub(crate) fn open_encrypted_replica_recovery_durable(
2924 root: &crate::durable_file::DurableRoot,
2925 passphrase: &str,
2926 ) -> Result<Self> {
2927 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2928 let salt = read_encryption_salt(root)?;
2929 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2930 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
2931 }
2932
2933 fn open_replica_recovery_inner(
2934 root: PathBuf,
2935 kek: Option<Arc<crate::encryption::Kek>>,
2936 lock: ExclusiveDatabaseLease,
2937 ) -> Result<Self> {
2938 if !root.join(META_DIR).join("replica").is_file() {
2939 return Err(MongrelError::InvalidArgument(
2940 "recovery auth bypass requires a marked replica staging directory".into(),
2941 ));
2942 }
2943 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2944 let mut cat = catalog::read_durable(
2945 lock.durable_root.as_deref().ok_or_else(|| {
2946 MongrelError::Other("database root descriptor was not pinned".into())
2947 })?,
2948 meta_dek.as_ref(),
2949 )?
2950 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2951 let recovery_checkpoint = cat.clone();
2952 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2953 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2954 lock.durable_root.as_deref().ok_or_else(|| {
2955 MongrelError::Other("database root descriptor was not pinned".into())
2956 })?,
2957 wal_dek.as_ref(),
2958 )?;
2959 recover_ddl_from_records(
2960 &root,
2961 Some(lock.durable_root.as_deref().ok_or_else(|| {
2962 MongrelError::Other("database root descriptor was not pinned".into())
2963 })?),
2964 &mut cat,
2965 meta_dek.as_ref(),
2966 false,
2967 None,
2968 &recovery_records,
2969 )?;
2970 let principal = if cat.require_auth {
2971 cat.users
2972 .iter()
2973 .find(|user| user.is_admin)
2974 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
2975 .ok_or_else(|| {
2976 MongrelError::Schema(
2977 "authenticated replica catalog has no recoverable admin".into(),
2978 )
2979 })?
2980 .into()
2981 } else {
2982 None
2983 };
2984 Self::finish_open(
2985 root,
2986 cat,
2987 kek,
2988 meta_dek,
2989 true,
2990 Some(recovery_checkpoint),
2991 Some(recovery_records),
2992 principal,
2993 lock,
2994 CoreResourceConfig::default(),
2995 OpenModeGate::Normal,
2996 )
2997 }
2998
2999 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
3011 use fs2::FileExt;
3012 if timeout_ms == 0 {
3013 return f.try_lock_exclusive();
3014 }
3015 let deadline =
3017 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
3018 let mut next_sleep = std::time::Duration::from_millis(1);
3019 let mut recorded_wait = false;
3020 loop {
3021 match f.try_lock_exclusive() {
3022 Ok(()) => return Ok(()),
3023 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
3024 if !recorded_wait {
3025 DATABASE_OPEN_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
3026 recorded_wait = true;
3027 }
3028 let now = std::time::Instant::now();
3029 if now >= deadline {
3030 return Err(std::io::Error::new(
3031 std::io::ErrorKind::WouldBlock,
3032 format!("could not acquire database lock within {timeout_ms}ms"),
3033 ));
3034 }
3035 let remaining = deadline - now;
3036 let sleep = next_sleep.min(remaining);
3037 std::thread::sleep(sleep);
3038 next_sleep = next_sleep
3041 .saturating_mul(10)
3042 .min(std::time::Duration::from_millis(50));
3043 }
3044 Err(e) => return Err(e),
3045 }
3046 }
3047 }
3048
3049 #[allow(clippy::too_many_arguments)]
3050 fn finish_open(
3051 root: PathBuf,
3052 cat: Catalog,
3053 kek: Option<Arc<crate::encryption::Kek>>,
3054 meta_dek: Option<[u8; META_DEK_LEN]>,
3055 existing: bool,
3056 recovery_checkpoint: Option<Catalog>,
3057 recovery_records: Option<Vec<crate::wal::Record>>,
3058 principal: Option<crate::auth::Principal>,
3059 lock: ExclusiveDatabaseLease,
3060 resources: CoreResourceConfig,
3061 mode_gate: OpenModeGate,
3062 ) -> Result<Self> {
3063 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
3064 MongrelError::Other("database root descriptor was not pinned".into())
3065 })?);
3066 let storage_mode = if existing {
3071 crate::storage_mode::read(&durable_root)?
3072 } else {
3073 None
3074 };
3075 match (&mode_gate, &storage_mode) {
3076 (OpenModeGate::Create(_), _) => {}
3077 (OpenModeGate::Normal, mode) => crate::storage_mode::check_open(mode.as_ref(), false)?,
3078 (OpenModeGate::OfflineValidation, mode) => {
3079 crate::storage_mode::check_open(mode.as_ref(), true)?
3080 }
3081 (
3082 OpenModeGate::ClusterRuntime {
3083 cluster_id,
3084 node_id,
3085 database_id,
3086 },
3087 Some(actual),
3088 ) => {
3089 let expected = crate::storage_mode::StorageMode::ClusterReplica {
3090 cluster_id: *cluster_id,
3091 node_id: *node_id,
3092 database_id: *database_id,
3093 };
3094 if *actual != expected {
3095 return Err(
3096 crate::storage_mode::StorageModeError::IdentityMismatch(format!(
3097 "expected {expected:?}, marker holds {actual:?}"
3098 ))
3099 .into(),
3100 );
3101 }
3102 }
3103 (OpenModeGate::ClusterRuntime { .. }, None) => {
3104 return Err(crate::storage_mode::StorageModeError::IdentityMismatch(
3105 "expected a ClusterReplica marker; directory has none".into(),
3106 )
3107 .into());
3108 }
3109 }
3110 let read_only = match &mode_gate {
3111 OpenModeGate::OfflineValidation | OpenModeGate::ClusterRuntime { .. } => true,
3112 OpenModeGate::Create(mode) => mode.cluster_identity().is_some(),
3115 OpenModeGate::Normal => false,
3116 } || if existing {
3117 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
3118 Ok(_) => true,
3119 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
3120 Err(error) => return Err(error.into()),
3121 }
3122 } else {
3123 false
3124 };
3125 let recovered_catalog = cat;
3126 let mut cat = recovered_catalog.clone();
3127 let abandoned = if existing && !read_only {
3128 let abandoned = cat
3129 .tables
3130 .iter()
3131 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
3132 .map(|entry| entry.table_id)
3133 .collect::<Vec<_>>();
3134 for entry in &mut cat.tables {
3135 if abandoned.contains(&entry.table_id) {
3136 entry.state = TableState::Dropped {
3137 at_epoch: cat.db_epoch,
3138 };
3139 }
3140 }
3141 abandoned
3142 } else {
3143 Vec::new()
3144 };
3145 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3146 let recovery_records = match (existing, recovery_records) {
3147 (true, Some(records)) => records,
3148 (true, None) => {
3149 return Err(MongrelError::Other(
3150 "existing open has no validated WAL recovery plan".into(),
3151 ))
3152 }
3153 (false, _) => Vec::new(),
3154 };
3155 let (history_epochs, history_start) =
3156 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
3157 let open_generation = if existing {
3158 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
3159 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3160 })?;
3161 let recovered_table_ids = cat
3162 .tables
3163 .iter()
3164 .filter(|entry| {
3165 checkpoint
3166 .tables
3167 .iter()
3168 .all(|checkpoint| checkpoint.table_id != entry.table_id)
3169 })
3170 .map(|entry| entry.table_id)
3171 .collect::<HashSet<_>>();
3172 let reconciled_table_ids = cat
3173 .tables
3174 .iter()
3175 .filter(|entry| {
3176 checkpoint
3177 .tables
3178 .iter()
3179 .find(|checkpoint| checkpoint.table_id == entry.table_id)
3180 .is_some_and(|checkpoint| {
3181 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
3182 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
3183 })
3184 })
3185 .map(|entry| entry.table_id)
3186 .collect::<HashSet<_>>();
3187 validate_shared_wal_recovery_plan(
3188 &durable_root,
3189 &cat,
3190 &recovered_table_ids,
3191 &reconciled_table_ids,
3192 meta_dek.as_ref(),
3193 kek.clone(),
3194 &recovery_records,
3195 )?;
3196 let retained_generation = recovery_records
3197 .iter()
3198 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
3199 .map(|record| record.txn_id >> 32)
3200 .max()
3201 .unwrap_or(0);
3202 let head_generation =
3203 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
3204 let durable_floor = match head_generation {
3205 Some(head) if retained_generation > head => {
3206 return Err(MongrelError::CorruptWal {
3207 offset: retained_generation,
3208 reason: format!(
3209 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
3210 ),
3211 })
3212 }
3213 Some(head) => head,
3214 None => retained_generation,
3215 };
3216 let stored = catalog::read_generation(&durable_root)?;
3217 if stored.is_some_and(|generation| generation < durable_floor) {
3218 return Err(MongrelError::Other(format!(
3219 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
3220 )));
3221 }
3222 let bumped = stored
3223 .unwrap_or(durable_floor)
3224 .max(durable_floor)
3225 .checked_add(1)
3226 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
3227 if bumped > u32::MAX as u64 {
3228 return Err(MongrelError::Full(
3229 "open-generation namespace exhausted".into(),
3230 ));
3231 }
3232 bumped
3233 } else {
3234 0
3235 };
3236 let principal = if cat.require_auth {
3237 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3238 Some(
3239 Self::resolve_bound_principal_from_catalog(&cat, supplied)
3240 .ok_or(MongrelError::AuthRequired)?,
3241 )
3242 } else {
3243 principal
3244 };
3245 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
3246 if existing {
3247 for entry in &cat.tables {
3248 if !matches!(entry.state, TableState::Live) {
3249 continue;
3250 }
3251 match durable_root
3252 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
3253 {
3254 Ok(root) => {
3255 table_roots.insert(entry.table_id, Arc::new(root));
3256 }
3257 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3258 Err(error) => return Err(error.into()),
3259 }
3260 }
3261 }
3262
3263 if existing {
3267 let mut applied = recovery_checkpoint.ok_or_else(|| {
3268 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3269 })?;
3270 recover_ddl_from_records(
3271 &root,
3272 Some(&durable_root),
3273 &mut applied,
3274 meta_dek.as_ref(),
3275 true,
3276 Some(&table_roots),
3277 &recovery_records,
3278 )?;
3279 let catalog_value = |catalog: &Catalog| {
3280 serde_json::to_value(catalog)
3281 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
3282 };
3283 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
3284 return Err(MongrelError::CorruptWal {
3285 offset: 0,
3286 reason: "validated and applied DDL recovery plans differ".into(),
3287 });
3288 }
3289 if catalog_value(&cat)? != catalog_value(&applied)? {
3290 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3291 }
3292 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
3293 if !read_only {
3294 sweep_unreferenced_table_dirs(&root, &cat)?;
3295 }
3296 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
3297 Ok(()) => {}
3298 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3299 Err(error) => return Err(error.into()),
3300 }
3301 }
3302
3303 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
3304 let snapshots = Arc::new(SnapshotRegistry::new());
3305 snapshots.configure_history(history_epochs, history_start);
3306 let memory_governor = crate::memory::MemoryGovernor::new(
3311 crate::memory::GovernorConfig::new(resources.memory_budget_bytes),
3312 )
3313 .map_err(|error| MongrelError::InvalidArgument(format!("memory governor: {error}")))?;
3314 let spill_manager = crate::spill::SpillManager::open(
3318 &durable_root,
3319 crate::spill::SpillConfig::new(resources.temp_disk_budget_bytes),
3320 meta_dek,
3321 )?;
3322 let job_registry = Arc::new(crate::jobs::JobRegistry::open(&root, meta_dek.as_ref())?);
3326 let page_cache = Arc::new(crate::cache::Sharded::new(
3327 crate::cache::CACHE_SHARDS,
3328 || {
3329 crate::cache::PageCache::new(
3330 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3331 )
3332 .with_governor(
3333 memory_governor.clone(),
3334 crate::memory::MemoryClass::PageCache,
3335 )
3336 },
3337 ));
3338 memory_governor.register_reclaimable(&page_cache);
3339 let decoded_cache = Arc::new(crate::cache::Sharded::new(
3340 crate::cache::CACHE_SHARDS,
3341 || {
3342 crate::cache::DecodedPageCache::new(
3343 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3344 )
3345 .with_governor(
3346 memory_governor.clone(),
3347 crate::memory::MemoryClass::DecodedCache,
3348 )
3349 },
3350 ));
3351 memory_governor.register_reclaimable(&decoded_cache);
3352 let commit_lock = Arc::new(Mutex::new(()));
3353 let shared_wal = Arc::new(Mutex::new(if existing {
3354 crate::wal::SharedWal::open_durable_root_validated(
3355 Arc::clone(&durable_root),
3356 Epoch(cat.db_epoch),
3357 wal_dek.clone(),
3358 Some(&recovery_records),
3359 )?
3360 } else {
3361 crate::wal::SharedWal::create_with_durable_root(
3362 Arc::clone(&durable_root),
3363 Epoch(cat.db_epoch),
3364 wal_dek.clone(),
3365 )?
3366 }));
3367 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
3371 let lifecycle = Arc::new(crate::core::LifecycleController::new());
3376 let group = Arc::new(
3377 crate::txn::GroupCommit::new(shared_wal.lock().durable_seq())
3378 .with_lifecycle(Arc::clone(&lifecycle)),
3379 );
3380 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
3381 let txn_ids = Arc::new(Mutex::new(1u64));
3385 let _ = abandoned;
3386 let hlc = Arc::new(mongreldb_types::hlc::HlcClock::new(
3393 0,
3394 std::time::Duration::from_millis(500),
3395 ));
3396 let idempotency = crate::txn::IdempotencyLedger::open(Arc::clone(&durable_root), meta_dek)?;
3399 let commit_log = Arc::new(crate::commit_log::StandaloneCommitLog::new(
3400 Arc::clone(&shared_wal),
3401 Arc::clone(&group),
3402 Arc::clone(&epoch),
3403 Arc::clone(&commit_lock),
3404 Arc::clone(&txn_ids),
3405 root.clone(),
3406 wal_dek.clone(),
3407 Arc::clone(&hlc),
3408 ));
3409
3410 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
3415 let security_coordinator = security_coordinator(&root, cat.security_version);
3416 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
3417 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
3418 ));
3419
3420 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
3426 for entry in &cat.tables {
3427 if !matches!(entry.state, TableState::Live) {
3428 continue;
3429 }
3430 let table_root = match table_roots.remove(&entry.table_id) {
3431 Some(root) => root,
3432 None => Arc::new(
3433 durable_root
3434 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
3435 ),
3436 };
3437 let tdir = table_root.io_path()?;
3438 let ctx = SharedCtx {
3439 root_guard: Some(table_root),
3440 epoch: Arc::clone(&epoch),
3441 page_cache: Arc::clone(&page_cache),
3442 decoded_cache: Arc::clone(&decoded_cache),
3443 snapshots: Arc::clone(&snapshots),
3444 kek: kek.clone(),
3445 commit_lock: Arc::clone(&commit_lock),
3446 shared: Some(crate::engine::SharedWalCtx {
3447 wal: Arc::clone(&shared_wal),
3448 group: Arc::clone(&group),
3449 poisoned: Arc::clone(&poisoned),
3450 txn_ids: Arc::clone(&txn_ids),
3451 change_wake: change_wake.clone(),
3452 lifecycle: Arc::clone(&lifecycle),
3453 }),
3454 table_name: Some(entry.name.clone()),
3455 auth: auth_checker.clone(),
3456 read_only,
3457 };
3458 let t = Table::open_in(&tdir, ctx)?;
3459 tables.insert(entry.table_id, TableHandle::new(t));
3460 }
3461
3462 if existing {
3468 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
3469 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
3470 if read_only {
3471 crate::replication::reconcile_replica_epoch_durable(
3472 &durable_root,
3473 epoch.visible().0,
3474 )?;
3475 }
3476 sweep_pending_txn_dirs(&root, &cat);
3479 }
3480
3481 catalog::write_generation(&durable_root, open_generation)?;
3483 match &mode_gate {
3488 OpenModeGate::Create(mode) => crate::storage_mode::write(&durable_root, mode)?,
3489 _ if existing && storage_mode.is_none() => {
3490 crate::storage_mode::write(
3491 &durable_root,
3492 &crate::storage_mode::StorageMode::Standalone,
3493 )?;
3494 }
3495 _ => {}
3496 }
3497 shared_wal.lock().seal_open_generation(open_generation)?;
3498 crate::replication::replication_identity_durable(&durable_root)?;
3499 let next_txn_id = (open_generation << 32) | 1;
3500 *txn_ids.lock() = next_txn_id;
3502 let mut lock = lock;
3503 lock.mark_open()?;
3504
3505 lifecycle.mark_open();
3509 let core = DatabaseCore {
3510 root,
3511 durable_root,
3512 lifecycle,
3513 registry: std::sync::OnceLock::new(),
3514 read_only,
3515 catalog: RwLock::new(cat),
3516 security_coordinator,
3517 security_catalog_disk_reads: AtomicU64::new(0),
3518 rls_cache: Mutex::new(RlsCache::default()),
3519 epoch,
3520 snapshots,
3521 memory_governor,
3522 page_cache,
3523 decoded_cache,
3524 spill_manager,
3525 resource_groups: crate::resource::ResourceGroupRegistry::with_defaults(),
3526 embedding_providers: crate::embedding::EmbeddingProviderRegistry::new(),
3527 job_registry,
3528 commit_lock,
3529 shared_wal,
3530 next_txn_id: txn_ids,
3531 tables: RwLock::new(tables),
3532 kek,
3533 ddl_lock: Mutex::new(()),
3534 meta_dek,
3535 conflicts: crate::txn::ConflictIndex::new(),
3536 active_txns: crate::txn::ActiveTxns::new(),
3537 lock_manager: Arc::new(crate::locks::LockManager::new()),
3538 poisoned,
3539 group,
3540 commit_log,
3541 hlc,
3542 idempotency,
3543 commit_ts_ledger: Mutex::new(commit_ts_ledger_from_recovery(&recovery_records)),
3544 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
3545 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
3546 replication_barrier: parking_lot::RwLock::new(()),
3547 replication_wal_retention_segments: AtomicUsize::new(0),
3548 backup_pins: Arc::new(Mutex::new(HashMap::new())),
3549 spill_hook: Mutex::new(None),
3550 security_commit_hook: Mutex::new(None),
3551 catalog_commit_hook: Mutex::new(None),
3552 backup_hook: Mutex::new(None),
3553 fk_lock_hook: Mutex::new(None),
3554 replication_hook: Mutex::new(None),
3555 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
3556 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
3557 trigger_max_loop_iterations: AtomicU32::new(
3558 TriggerConfig::default().max_loop_iterations,
3559 ),
3560 notify: {
3561 let (tx, _rx) = tokio::sync::broadcast::channel(256);
3562 tx
3563 },
3564 change_wake,
3565 _lock: Mutex::new(Some(lock)),
3566 };
3567 Ok(Self {
3568 core: Arc::new(core),
3569 principal: RwLock::new(principal),
3570 auth_state,
3571 shared: false,
3572 })
3573 }
3574
3575 pub fn visible_epoch(&self) -> Epoch {
3577 self.epoch.visible()
3578 }
3579
3580 pub fn catalog_version(&self) -> u64 {
3582 self.catalog.read().catalog_version
3583 }
3584
3585 pub fn storage_mode(&self) -> Result<Option<crate::storage_mode::StorageMode>> {
3589 Ok(crate::storage_mode::read(&self.durable_root)?)
3590 }
3591
3592 pub fn memory_governor(&self) -> &crate::memory::MemoryGovernor {
3595 &self.memory_governor
3596 }
3597
3598 pub fn resource_groups(&self) -> &crate::resource::ResourceGroupRegistry {
3601 &self.resource_groups
3602 }
3603
3604 pub fn embedding_providers(&self) -> &crate::embedding::EmbeddingProviderRegistry {
3608 &self.embedding_providers
3609 }
3610
3611 pub fn lock_rows_for_update(
3615 &self,
3616 txn_id: u64,
3617 table_id: u64,
3618 row_ids: &[crate::rowid::RowId],
3619 control: Option<&crate::ExecutionControl>,
3620 ) -> Result<()> {
3621 for &row_id in row_ids {
3622 self.acquire_txn_lock(
3623 txn_id,
3624 crate::locks::LockKey::row(table_id, row_id),
3625 crate::locks::LockMode::Exclusive,
3626 control,
3627 )?;
3628 }
3629 Ok(())
3630 }
3631
3632 pub fn spill_manager(&self) -> &crate::spill::SpillManager {
3636 &self.spill_manager
3637 }
3638
3639 pub fn job_registry(&self) -> &Arc<crate::jobs::JobRegistry> {
3641 &self.job_registry
3642 }
3643
3644 pub fn version_pins_report(&self) -> Vec<TablePinsReport> {
3649 let names: HashMap<u64, String> = self
3650 .catalog
3651 .read()
3652 .tables
3653 .iter()
3654 .map(|entry| (entry.table_id, entry.name.clone()))
3655 .collect();
3656 let handles: Vec<_> = self
3657 .tables
3658 .read()
3659 .iter()
3660 .map(|(table_id, handle)| (*table_id, handle.clone()))
3661 .collect();
3662 handles
3663 .into_iter()
3664 .map(|(table_id, handle)| {
3665 let pins = handle.lock().version_pins_report();
3666 TablePinsReport {
3667 table_id,
3668 table: names.get(&table_id).cloned().unwrap_or_default(),
3669 pins,
3670 }
3671 })
3672 .collect()
3673 }
3674
3675 pub fn lock_manager(&self) -> &Arc<crate::locks::LockManager> {
3677 &self.lock_manager
3678 }
3679
3680 pub fn release_txn_locks(&self, txn_id: u64) {
3685 self.lock_manager.release_all(txn_id);
3686 }
3687
3688 fn txn_lock_request(
3691 txn_id: u64,
3692 mode: crate::locks::LockMode,
3693 control: Option<&crate::ExecutionControl>,
3694 ) -> crate::locks::LockRequest {
3695 let control = control
3696 .cloned()
3697 .unwrap_or_else(|| crate::ExecutionControl::new(None));
3698 crate::locks::LockRequest::new(txn_id, mode, control)
3699 }
3700
3701 pub(crate) fn acquire_txn_lock(
3705 &self,
3706 txn_id: u64,
3707 key: crate::locks::LockKey,
3708 mode: crate::locks::LockMode,
3709 control: Option<&crate::ExecutionControl>,
3710 ) -> Result<()> {
3711 self.lock_manager
3712 .acquire(key, Self::txn_lock_request(txn_id, mode, control))
3713 .map_err(MongrelError::from)
3714 }
3715
3716 fn acquire_schema_barrier_exclusive(&self) -> Result<TxnLockGuard<'_>> {
3721 let txn_id = self.alloc_txn_id()?;
3722 self.acquire_txn_lock(
3723 txn_id,
3724 crate::locks::LockKey::schema_barrier(),
3725 crate::locks::LockMode::Exclusive,
3726 None,
3727 )?;
3728 Ok(TxnLockGuard {
3729 locks: &self.lock_manager,
3730 txn_id,
3731 })
3732 }
3733
3734 pub fn commit_log(&self) -> Arc<crate::commit_log::StandaloneCommitLog> {
3739 Arc::clone(&self.commit_log)
3740 }
3741
3742 pub(crate) fn hlc_clock(&self) -> &mongreldb_types::hlc::HlcClock {
3747 &self.hlc
3748 }
3749
3750 pub fn catalog_snapshot(&self) -> Catalog {
3752 self.catalog.read().clone()
3753 }
3754
3755 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
3757 let catalog = self.catalog.read();
3758 match key {
3759 "user_version" => Ok(catalog.user_version),
3760 "application_id" => Ok(catalog.application_id),
3761 _ => Err(MongrelError::InvalidArgument(format!(
3762 "unsupported persistent SQL pragma {key:?}"
3763 ))),
3764 }
3765 }
3766
3767 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
3770 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
3771 }
3772
3773 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
3774 &self,
3775 key: &str,
3776 value: i64,
3777 mut before_commit: F,
3778 ) -> Result<Option<Epoch>>
3779 where
3780 F: FnMut() -> Result<()>,
3781 {
3782 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
3783 }
3784
3785 fn set_sql_pragma_i64_with_epoch_inner(
3786 &self,
3787 key: &str,
3788 value: i64,
3789 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
3790 ) -> Result<Option<Epoch>> {
3791 use crate::wal::DdlOp;
3792
3793 self.require(&crate::auth::Permission::Ddl)?;
3794 if self.read_only {
3795 return Err(MongrelError::ReadOnlyReplica);
3796 }
3797 if self.poisoned.load(Ordering::Relaxed) {
3798 return Err(MongrelError::Other(
3799 "database poisoned by fsync error".into(),
3800 ));
3801 }
3802 let _ddl = self.ddl_lock.lock();
3803 let _security_write = self.security_write()?;
3804 self.require(&crate::auth::Permission::Ddl)?;
3805 let mut next_catalog = self.catalog.read().clone();
3806 let target = match key {
3807 "user_version" => &mut next_catalog.user_version,
3808 "application_id" => &mut next_catalog.application_id,
3809 _ => {
3810 return Err(MongrelError::InvalidArgument(format!(
3811 "unsupported persistent SQL pragma {key:?}"
3812 )))
3813 }
3814 };
3815 if *target == Some(value) {
3816 return Ok(None);
3817 }
3818 *target = Some(value);
3819
3820 let _commit = self.commit_lock.lock();
3821 let epoch = self.epoch.bump_assigned();
3822 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3823 let txn_id = self.alloc_txn_id()?;
3824 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
3825 let commit_seq = {
3826 let mut wal = self.shared_wal.lock();
3827 if let Some(before_commit) = before_commit {
3828 before_commit()?;
3829 }
3830 let append: Result<u64> = (|| {
3831 wal.append(
3832 txn_id,
3833 WAL_TABLE_ID,
3834 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
3835 key: key.to_string(),
3836 value,
3837 }),
3838 )?;
3839 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
3840 wal.append_commit(txn_id, epoch, &[])
3841 })();
3842 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
3843 };
3844 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
3845 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
3846 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
3847 Ok(Some(epoch))
3848 }
3849
3850 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
3851 self.catalog
3852 .read()
3853 .materialized_views
3854 .iter()
3855 .find(|definition| definition.name == name)
3856 .cloned()
3857 }
3858
3859 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
3860 self.catalog.read().materialized_views.clone()
3861 }
3862
3863 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
3864 self.catalog.read().security.clone()
3865 }
3866
3867 pub fn security_active_for(&self, table: &str) -> bool {
3868 self.catalog.read().security.table_has_security(table)
3869 }
3870
3871 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
3872 if self.catalog.read().security_version == expected_version {
3873 return Ok(());
3874 }
3875 self.security_catalog_disk_reads
3876 .fetch_add(1, Ordering::Relaxed);
3877 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
3878 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
3879 let principal = self.principal.read().clone();
3880 let principal = if fresh.require_auth {
3881 principal
3882 .as_ref()
3883 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
3884 } else {
3885 principal
3886 };
3887 self.auth_state.set_require_auth(fresh.require_auth);
3888 *self.catalog.write() = fresh;
3889 *self.principal.write() = principal.clone();
3890 self.auth_state.set_principal(principal);
3891 Ok(())
3892 }
3893
3894 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
3895 let guard = self.security_coordinator.gate.write();
3896 let version = self.security_coordinator.version.load(Ordering::Acquire);
3897 self.refresh_security_catalog_if_stale(version)?;
3898 Ok(guard)
3899 }
3900
3901 pub(crate) fn admit_operation(&self) -> Result<crate::core::OperationGuard> {
3928 self.core.operation_guard()
3929 }
3930
3931 fn apply_catalog_command_to(
3937 &self,
3938 next_catalog: &mut Catalog,
3939 command: crate::catalog_cmds::CatalogCommand,
3940 ) -> Result<crate::catalog_cmds::CatalogDelta> {
3941 let record = crate::catalog_cmds::CatalogCommandRecord::next(next_catalog, command);
3942 next_catalog.apply_command(&record)
3943 }
3944
3945 pub fn apply_replicated_records(&self, records: &[crate::wal::Record]) -> Result<bool> {
3963 use crate::wal::Op;
3964 use std::sync::atomic::Ordering;
3965
3966 let _operation = self.admit_operation()?;
3967 if self.poisoned.load(Ordering::Relaxed) {
3968 return Err(MongrelError::Other(
3969 "database poisoned by fsync error".into(),
3970 ));
3971 }
3972 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
3975 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
3976 })?;
3977 if records.iter().any(|record| record.txn_id != txn_id) {
3978 return Err(MongrelError::InvalidArgument(
3979 "replicated transaction payload mixes transaction ids".into(),
3980 ));
3981 }
3982 let commits = records
3983 .iter()
3984 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
3985 .count();
3986 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
3987 return Err(MongrelError::InvalidArgument(
3988 "replicated transaction payload must end in exactly one commit marker".into(),
3989 ));
3990 }
3991 let commit_epoch = match records.last().map(|r| &r.op) {
3992 Some(Op::TxnCommit { epoch, .. }) => *epoch,
3993 _ => unreachable!("validated above"),
3994 };
3995 if let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) {
4004 if !added_runs.is_empty() {
4005 return Err(MongrelError::InvalidArgument(
4006 "replicated spilled-run commits are not appliable: the leader must translate \
4007 the commit through translate_records_for_replication before proposal"
4008 .into(),
4009 ));
4010 }
4011 }
4012 if commit_epoch <= self.epoch.visible().0 {
4016 return Ok(false);
4017 }
4018 {
4023 let mut wal = self.shared_wal.lock();
4024 for record in records {
4025 wal.append(record.txn_id, 0, record.op.clone())?;
4026 }
4027 if let Err(error) = wal.group_sync() {
4028 self.poisoned.store(true, Ordering::Relaxed);
4029 return Err(error);
4030 }
4031 }
4032 let replication_pins: Vec<crate::retention::PinGuard> = {
4035 let tables = self.tables.read();
4036 let floor = Epoch(self.epoch.visible().0);
4037 tables
4038 .values()
4039 .map(|handle| {
4040 let t = handle.lock();
4041 Arc::clone(t.pin_registry())
4042 .pin(crate::retention::PinSource::Replication, floor)
4043 })
4044 .collect()
4045 };
4046 let tables = self.tables.read().clone();
4047 let catalog = self.catalog.read().clone();
4048 recover_shared_wal(&self.durable_root, &tables, &catalog, &self.epoch, records)?;
4049 if let Some(Op::TxnCommit { epoch, .. }) = records.last().map(|r| &r.op) {
4054 let commit_ts = records
4055 .iter()
4056 .rev()
4057 .find_map(|record| match &record.op {
4058 Op::CommitTimestamp { unix_nanos } => {
4059 Some(mongreldb_types::hlc::HlcTimestamp {
4060 physical_micros: unix_nanos / 1_000,
4061 logical: 0,
4062 node_tiebreaker: 0,
4063 })
4064 }
4065 _ => None,
4066 })
4067 .unwrap_or_else(|| {
4068 self.hlc
4069 .now()
4070 .unwrap_or(mongreldb_types::hlc::HlcTimestamp {
4071 physical_micros: 0,
4072 logical: 0,
4073 node_tiebreaker: 0,
4074 })
4075 });
4076 self.record_commit_ts(Epoch(*epoch), commit_ts);
4077 }
4078 drop(replication_pins);
4079 Ok(true)
4080 }
4081
4082 pub fn validate_staged_txn_writes(&self, staged: &[Vec<u8>]) -> Result<()> {
4092 let tables = self.tables.read();
4093 for payload in staged {
4094 match StagedTxnWrite::decode(payload)? {
4095 StagedTxnWrite::Put { table_id, rows } => {
4096 let handle = tables.get(&table_id).ok_or_else(|| {
4097 MongrelError::InvalidArgument(format!(
4098 "staged write targets unmounted table {table_id}"
4099 ))
4100 })?;
4101 let rows: Vec<crate::memtable::Row> =
4102 bincode::deserialize(&rows).map_err(|error| {
4103 MongrelError::InvalidArgument(format!(
4104 "staged put payload for table {table_id} cannot decode: {error}"
4105 ))
4106 })?;
4107 let schema = handle.lock().schema().clone();
4108 for row in &rows {
4109 validate_recovered_row(&schema, row).map_err(|error| {
4110 MongrelError::InvalidArgument(format!(
4111 "staged row for table {table_id} is not appliable: {error}"
4112 ))
4113 })?;
4114 }
4115 }
4116 StagedTxnWrite::Delete { table_id, row_ids } => {
4117 if !tables.contains_key(&table_id) {
4118 return Err(MongrelError::InvalidArgument(format!(
4119 "staged delete targets unmounted table {table_id}"
4120 )));
4121 }
4122 if row_ids.contains(&u64::MAX) {
4123 return Err(MongrelError::InvalidArgument(format!(
4124 "staged delete for table {table_id} names an exhausted row id"
4125 )));
4126 }
4127 }
4128 }
4129 }
4130 Ok(())
4131 }
4132
4133 pub fn apply_staged_txn_writes(
4152 &self,
4153 txn_tag: u64,
4154 staged: &[Vec<u8>],
4155 commit_ts: mongreldb_types::hlc::HlcTimestamp,
4156 ) -> Result<bool> {
4157 use crate::wal::Op;
4158
4159 let mut writes = Vec::with_capacity(staged.len());
4161 for payload in staged {
4162 writes.push(StagedTxnWrite::decode(payload)?);
4163 }
4164 let generation = *self.next_txn_id.lock() >> 32;
4170 let txn_id = (generation << 32) | (txn_tag & 0x7FFF_FFFF) | 0x8000_0000;
4171 let mut records = Vec::with_capacity(writes.len() + 2);
4172 for write in writes {
4173 let op = match write {
4174 StagedTxnWrite::Put { table_id, rows } => Op::Put { table_id, rows },
4175 StagedTxnWrite::Delete { table_id, row_ids } => Op::Delete {
4176 table_id,
4177 row_ids: row_ids.into_iter().map(crate::RowId).collect(),
4178 },
4179 };
4180 records.push(crate::wal::Record::new(Epoch(0), txn_id, op));
4181 }
4182 let epoch = self.epoch.visible().0 + 1;
4183 let unix_nanos = commit_ts.physical_micros.saturating_mul(1_000);
4187 records.push(crate::wal::Record::new(
4188 Epoch(0),
4189 txn_id,
4190 Op::CommitTimestamp { unix_nanos },
4191 ));
4192 records.push(crate::wal::Record::new(
4193 Epoch(0),
4194 txn_id,
4195 Op::TxnCommit {
4196 epoch,
4197 added_runs: Vec::new(),
4198 },
4199 ));
4200 self.apply_replicated_records(&records)
4201 }
4202
4203 pub fn apply_replicated_catalog_command(
4212 &self,
4213 record: &crate::catalog_cmds::CatalogCommandRecord,
4214 ) -> Result<crate::catalog_cmds::CatalogDelta> {
4215 let _operation = self.admit_operation()?;
4216 let _g = self.ddl_lock.lock();
4217 let mut next_catalog = self.catalog.read().clone();
4218 let delta = next_catalog.apply_command(record)?;
4219 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
4220 return Ok(delta);
4221 }
4222 let referenced_epoch = match &delta {
4228 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => Some(entry.created_epoch),
4229 crate::catalog_cmds::CatalogDelta::TableDropped { at_epoch, .. }
4230 | crate::catalog_cmds::CatalogDelta::TableRenamed { at_epoch, .. } => Some(*at_epoch),
4231 _ => None,
4232 };
4233 if let Some(referenced) = referenced_epoch {
4234 self.epoch.advance_recovered(Epoch(referenced));
4235 next_catalog.db_epoch = next_catalog.db_epoch.max(referenced);
4236 }
4237 match &delta {
4238 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => {
4239 if !self.tables.read().contains_key(&entry.table_id) {
4244 self.mount_catalog_entry(entry)?;
4245 }
4246 }
4247 crate::catalog_cmds::CatalogDelta::TableDropped { table_id, .. } => {
4248 self.tables.write().remove(table_id);
4249 }
4250 _ => {}
4251 }
4252 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref())?;
4256 *self.catalog.write() = next_catalog;
4257 Ok(delta)
4258 }
4259
4260 fn mount_catalog_entry(&self, entry: &crate::catalog::CatalogEntry) -> Result<()> {
4265 let table_relative = Path::new(TABLES_DIR).join(entry.table_id.to_string());
4266 let table_root = Arc::new(
4267 self.durable_root
4268 .create_directory_all_pinned(&table_relative)?,
4269 );
4270 let tdir = table_root.io_path()?;
4271 let ctx = SharedCtx {
4272 root_guard: Some(table_root),
4273 epoch: Arc::clone(&self.epoch),
4274 page_cache: Arc::clone(&self.page_cache),
4275 decoded_cache: Arc::clone(&self.decoded_cache),
4276 snapshots: Arc::clone(&self.snapshots),
4277 kek: self.kek.clone(),
4278 commit_lock: Arc::clone(&self.commit_lock),
4279 shared: Some(crate::engine::SharedWalCtx {
4280 wal: Arc::clone(&self.shared_wal),
4281 group: Arc::clone(&self.group),
4282 poisoned: Arc::clone(&self.poisoned),
4283 txn_ids: Arc::clone(&self.next_txn_id),
4284 change_wake: self.change_wake.clone(),
4285 lifecycle: Arc::clone(&self.lifecycle),
4286 }),
4287 table_name: Some(entry.name.clone()),
4288 auth: self.table_auth_checker(),
4289 read_only: self.read_only,
4290 };
4291 let table = Table::create_in(&tdir, entry.schema.clone(), entry.table_id, ctx)?;
4292 self.tables
4293 .write()
4294 .insert(entry.table_id, TableHandle::new(table));
4295 Ok(())
4296 }
4297
4298 fn publish_catalog_candidate(
4299 &self,
4300 catalog: Catalog,
4301 epoch: Epoch,
4302 epoch_guard: &mut EpochGuard<'_>,
4303 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
4304 ) -> Result<()> {
4305 self.publish_catalog_candidate_with_prelude(
4306 catalog,
4307 epoch,
4308 epoch_guard,
4309 before_publish,
4310 Vec::new(),
4311 )
4312 }
4313
4314 fn publish_catalog_candidate_with_prelude(
4315 &self,
4316 catalog: Catalog,
4317 epoch: Epoch,
4318 epoch_guard: &mut EpochGuard<'_>,
4319 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
4320 prelude: Vec<(u64, crate::wal::Op)>,
4321 ) -> Result<()> {
4322 use crate::wal::DdlOp;
4323
4324 if self.read_only {
4325 return Err(MongrelError::ReadOnlyReplica);
4326 }
4327 if self.poisoned.load(Ordering::Relaxed) {
4328 return Err(MongrelError::Other(
4329 "database poisoned by fsync error".into(),
4330 ));
4331 }
4332 let _operation = self.admit_operation()?;
4334 if let Some(before_publish) = before_publish.as_mut() {
4335 (**before_publish)()?;
4336 }
4337 if catalog.db_epoch != epoch.0 {
4338 return Err(MongrelError::InvalidArgument(format!(
4339 "catalog epoch {} does not match commit epoch {}",
4340 catalog.db_epoch, epoch.0
4341 )));
4342 }
4343 {
4344 let current = self.catalog.read();
4345 validate_catalog_transition(¤t, &catalog)?;
4346 }
4347 validate_recovered_catalog(&catalog)?;
4348 let catalog_json = DdlOp::encode_catalog(&catalog)?;
4349 let txn_id = self.alloc_txn_id()?;
4350 let commit_seq = {
4351 let mut wal = self.shared_wal.lock();
4352 let append: Result<u64> = (|| {
4353 for (table_id, op) in prelude {
4354 wal.append(txn_id, table_id, op)?;
4355 }
4356 wal.append(
4357 txn_id,
4358 WAL_TABLE_ID,
4359 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
4360 )?;
4361 wal.append_commit(txn_id, epoch, &[])
4362 })();
4363 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4364 };
4365 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4366 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
4367 self.finish_durable_publish(epoch, epoch_guard, &receipt, checkpoint)
4368 }
4369
4370 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
4374 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
4375 let version = catalog.security_version;
4376 let principal = self.principal.read().clone();
4377 let principal = if catalog.require_auth {
4378 principal.as_ref().and_then(|principal| {
4379 Self::resolve_bound_principal_from_catalog(&catalog, principal)
4380 })
4381 } else {
4382 principal
4383 };
4384 *self.catalog.write() = catalog;
4385 self.security_coordinator
4386 .version
4387 .store(version, Ordering::Release);
4388 self.auth_state
4389 .set_require_auth(self.catalog.read().require_auth);
4390 *self.principal.write() = principal.clone();
4391 self.auth_state.set_principal(principal);
4392 checkpoint
4393 }
4394
4395 fn finish_durable_publish(
4396 &self,
4397 epoch: Epoch,
4398 epoch_guard: &mut EpochGuard<'_>,
4399 receipt: &mongreldb_log::CommitReceipt,
4400 post_step: Result<()>,
4401 ) -> Result<()> {
4402 if let Err(error) = self.publish_committed(receipt, epoch) {
4403 self.poisoned.store(true, Ordering::Relaxed);
4408 self.lifecycle.poison();
4409 return Err(MongrelError::DurableCommit {
4410 epoch: epoch.0,
4411 message: error.to_string(),
4412 });
4413 }
4414 epoch_guard.disarm();
4415 match post_step {
4416 Ok(()) => Ok(()),
4417 Err(error) => {
4418 self.poisoned.store(true, Ordering::Relaxed);
4419 self.lifecycle.poison();
4420 Err(MongrelError::DurableCommit {
4421 epoch: epoch.0,
4422 message: error.to_string(),
4423 })
4424 }
4425 }
4426 }
4427
4428 fn publish_committed(
4434 &self,
4435 receipt: &mongreldb_log::CommitReceipt,
4436 epoch: Epoch,
4437 ) -> Result<()> {
4438 debug_assert_eq!(
4439 receipt.log_position.index, epoch.0,
4440 "commit receipt position must match the published epoch"
4441 );
4442 mongreldb_fault::inject("commit.publish.before").map_err(crate::commit_log::fault_as_io)?;
4443 self.epoch.publish_in_order(epoch);
4444 mongreldb_fault::inject("commit.publish.after").map_err(crate::commit_log::fault_as_io)?;
4445 Ok(())
4446 }
4447
4448 fn await_durable_commit(
4457 &self,
4458 txn_id: u64,
4459 commit_seq: u64,
4460 epoch: Epoch,
4461 ) -> Result<mongreldb_log::CommitReceipt> {
4462 match self
4463 .commit_log
4464 .seal_transaction(txn_id, epoch, commit_seq, None)
4465 {
4466 Ok(receipt) => {
4467 self.record_commit_ts(epoch, receipt.commit_ts);
4468 Ok(receipt)
4469 }
4470 Err(error) => {
4471 self.poisoned.store(true, Ordering::Relaxed);
4472 self.lifecycle.poison();
4473 Err(MongrelError::CommitOutcomeUnknown {
4474 epoch: epoch.0,
4475 message: error.to_string(),
4476 })
4477 }
4478 }
4479 }
4480
4481 fn await_durable_commit_with_ts(
4486 &self,
4487 txn_id: u64,
4488 commit_seq: u64,
4489 epoch: Epoch,
4490 commit_ts: mongreldb_types::hlc::HlcTimestamp,
4491 ) -> Result<mongreldb_log::CommitReceipt> {
4492 match self
4493 .commit_log
4494 .seal_transaction(txn_id, epoch, commit_seq, Some(commit_ts))
4495 {
4496 Ok(receipt) => {
4497 self.record_commit_ts(epoch, receipt.commit_ts);
4498 Ok(receipt)
4499 }
4500 Err(error) => {
4501 self.poisoned.store(true, Ordering::Relaxed);
4502 self.lifecycle.poison();
4503 Err(MongrelError::CommitOutcomeUnknown {
4504 epoch: epoch.0,
4505 message: error.to_string(),
4506 })
4507 }
4508 }
4509 }
4510
4511 fn record_commit_ts(&self, epoch: Epoch, commit_ts: mongreldb_types::hlc::HlcTimestamp) {
4516 let mut ledger = self.commit_ts_ledger.lock();
4517 ledger.insert(epoch.0, commit_ts);
4518 while ledger.len() > COMMIT_TS_LEDGER_CAP {
4519 ledger.pop_first();
4520 }
4521 }
4522
4523 pub fn commit_ts_for_epoch(&self, epoch: Epoch) -> Option<mongreldb_types::hlc::HlcTimestamp> {
4537 self.commit_ts_ledger.lock().get(&epoch.0).copied()
4538 }
4539
4540 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
4541 self.poisoned.store(true, Ordering::Relaxed);
4542 self.lifecycle.poison();
4543 MongrelError::CommitOutcomeUnknown {
4544 epoch: epoch.0,
4545 message: error.to_string(),
4546 }
4547 }
4548
4549 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
4551 self.set_security_catalog_as_with_epoch(security, None)
4552 .map(|_| ())
4553 }
4554
4555 pub fn set_security_catalog_as(
4557 &self,
4558 security: crate::security::SecurityCatalog,
4559 principal: Option<&crate::auth::Principal>,
4560 ) -> Result<()> {
4561 self.set_security_catalog_as_with_epoch(security, principal)
4562 .map(|_| ())
4563 }
4564
4565 pub fn set_security_catalog_as_with_epoch(
4567 &self,
4568 security: crate::security::SecurityCatalog,
4569 principal: Option<&crate::auth::Principal>,
4570 ) -> Result<Epoch> {
4571 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
4572 }
4573
4574 pub fn set_security_catalog_as_with_epoch_controlled<F>(
4577 &self,
4578 security: crate::security::SecurityCatalog,
4579 principal: Option<&crate::auth::Principal>,
4580 mut before_commit: F,
4581 ) -> Result<Epoch>
4582 where
4583 F: FnMut() -> Result<()>,
4584 {
4585 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
4586 }
4587
4588 fn set_security_catalog_as_with_epoch_inner(
4589 &self,
4590 security: crate::security::SecurityCatalog,
4591 principal: Option<&crate::auth::Principal>,
4592 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
4593 ) -> Result<Epoch> {
4594 use crate::wal::DdlOp;
4595 use std::sync::atomic::Ordering;
4596
4597 let command = crate::catalog_cmds::CatalogCommand::SetSecurityCatalog {
4601 security: security.clone(),
4602 };
4603 self.require_for(
4604 principal,
4605 &crate::catalog_cmds::required_permission(&command),
4606 )?;
4607 if self.poisoned.load(Ordering::Relaxed) {
4608 return Err(MongrelError::Other(
4609 "database poisoned by fsync error".into(),
4610 ));
4611 }
4612 let _operation = self.admit_operation()?;
4615 let _ddl = self.ddl_lock.lock();
4616 let _security_write = self.security_write()?;
4619 self.require_for(
4620 principal,
4621 &crate::catalog_cmds::required_permission(&command),
4622 )?;
4623 let mut next_catalog = self.catalog.read().clone();
4624 validate_security_catalog(&next_catalog, &security)?;
4625 let payload = DdlOp::encode_security(&security)?;
4626 let _commit = self.commit_lock.lock();
4627 let epoch = self.epoch.bump_assigned();
4628 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4629 let txn_id = self.alloc_txn_id()?;
4630 self.apply_catalog_command_to(&mut next_catalog, command)?;
4631 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4632 let commit_seq = {
4633 let mut wal = self.shared_wal.lock();
4634 if let Some(before_commit) = before_commit {
4635 before_commit()?;
4636 }
4637 let append: Result<u64> = (|| {
4638 wal.append(
4639 txn_id,
4640 WAL_TABLE_ID,
4641 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
4642 security_json: payload,
4643 }),
4644 )?;
4645 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4646 wal.append_commit(txn_id, epoch, &[])
4647 })();
4648 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4649 };
4650 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4651 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4652 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
4653 Ok(epoch)
4654 }
4655
4656 pub fn require_for(
4657 &self,
4658 principal: Option<&crate::auth::Principal>,
4659 permission: &crate::auth::Permission,
4660 ) -> Result<()> {
4661 let Some(principal) = principal else {
4662 return self.require(permission);
4663 };
4664 let resolved;
4665 let principal = if self.auth_state.require_auth() || principal.user_id != 0 {
4666 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
4667 .ok_or(MongrelError::AuthRequired)?;
4668 &resolved
4669 } else {
4670 principal
4671 };
4672 #[cfg(test)]
4673 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
4674 if principal.has_permission(permission) {
4675 Ok(())
4676 } else {
4677 Err(MongrelError::PermissionDenied {
4678 required: permission.clone(),
4679 principal: principal.username.clone(),
4680 })
4681 }
4682 }
4683
4684 fn require_exact_principal_current(
4688 &self,
4689 principal: Option<&crate::auth::Principal>,
4690 permission: &crate::auth::Permission,
4691 ) -> Result<()> {
4692 let catalog = self.catalog.read();
4693 if !catalog.require_auth {
4694 return Ok(());
4695 }
4696 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
4697 let current = Self::resolve_bound_principal_from_catalog(&catalog, supplied)
4698 .ok_or(MongrelError::AuthRequired)?;
4699 if current.has_permission(permission) {
4700 Ok(())
4701 } else {
4702 Err(MongrelError::PermissionDenied {
4703 required: permission.clone(),
4704 principal: current.username,
4705 })
4706 }
4707 }
4708
4709 pub(crate) fn with_exact_principal_current<T, F>(
4710 &self,
4711 principal: Option<&crate::auth::Principal>,
4712 permission: &crate::auth::Permission,
4713 operation: F,
4714 ) -> Result<T>
4715 where
4716 F: FnOnce() -> Result<T>,
4717 {
4718 let _security = self.security_coordinator.gate.read();
4719 self.require_exact_principal_current(principal, permission)?;
4720 operation()
4721 }
4722
4723 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
4724 self.principal.read().clone()
4725 }
4726
4727 #[cfg(test)]
4728 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
4729 *self.principal.write() = principal.clone();
4730 self.auth_state.set_principal(principal);
4731 }
4732
4733 pub fn require_columns_for(
4734 &self,
4735 table: &str,
4736 operation: crate::auth::ColumnOperation,
4737 column_ids: &[u16],
4738 principal: Option<&crate::auth::Principal>,
4739 ) -> Result<()> {
4740 if principal.is_none() && !self.auth_state.require_auth() {
4741 return Ok(());
4742 }
4743 let cached = self.principal.read().clone();
4744 let principal = principal.or(cached.as_ref());
4745 let Some(principal) = principal else {
4746 let permission = match operation {
4747 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
4748 table: table.to_string(),
4749 },
4750 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
4751 table: table.to_string(),
4752 },
4753 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
4754 table: table.to_string(),
4755 },
4756 };
4757 return self.require(&permission);
4758 };
4759 let catalog = self.catalog.read();
4760 let resolved;
4761 let principal = if catalog.require_auth || principal.user_id != 0 {
4762 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
4763 .ok_or(MongrelError::AuthRequired)?;
4764 &resolved
4765 } else {
4766 principal
4767 };
4768 let schema = &catalog
4769 .live(table)
4770 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
4771 .schema;
4772 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
4773 }
4774
4775 fn require_columns_for_principal(
4776 table: &str,
4777 schema: &Schema,
4778 operation: crate::auth::ColumnOperation,
4779 column_ids: &[u16],
4780 principal: &crate::auth::Principal,
4781 ) -> Result<()> {
4782 #[cfg(test)]
4783 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
4784 match principal.column_access(table, operation) {
4785 crate::auth::ColumnAccess::All => Ok(()),
4786 crate::auth::ColumnAccess::Columns(allowed) => {
4787 let denied = column_ids.iter().find_map(|column_id| {
4788 schema
4789 .columns
4790 .iter()
4791 .find(|column| column.id == *column_id)
4792 .filter(|column| !allowed.contains(&column.name))
4793 });
4794 if denied.is_none() {
4795 Ok(())
4796 } else {
4797 Err(MongrelError::PermissionDenied {
4798 required: match operation {
4799 crate::auth::ColumnOperation::Select => {
4800 crate::auth::Permission::SelectColumns {
4801 table: table.to_string(),
4802 columns: denied
4803 .into_iter()
4804 .map(|column| column.name.clone())
4805 .collect(),
4806 }
4807 }
4808 crate::auth::ColumnOperation::Insert => {
4809 crate::auth::Permission::InsertColumns {
4810 table: table.to_string(),
4811 columns: denied
4812 .into_iter()
4813 .map(|column| column.name.clone())
4814 .collect(),
4815 }
4816 }
4817 crate::auth::ColumnOperation::Update => {
4818 crate::auth::Permission::UpdateColumns {
4819 table: table.to_string(),
4820 columns: denied
4821 .into_iter()
4822 .map(|column| column.name.clone())
4823 .collect(),
4824 }
4825 }
4826 },
4827 principal: principal.username.clone(),
4828 })
4829 }
4830 }
4831 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
4832 required: match operation {
4833 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
4834 table: table.to_string(),
4835 },
4836 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
4837 table: table.to_string(),
4838 },
4839 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
4840 table: table.to_string(),
4841 },
4842 },
4843 principal: principal.username.clone(),
4844 }),
4845 }
4846 }
4847
4848 pub fn select_column_ids_for(
4849 &self,
4850 table: &str,
4851 principal: Option<&crate::auth::Principal>,
4852 ) -> Result<Vec<u16>> {
4853 let catalog = self.catalog.read();
4854 let columns = catalog
4855 .live(table)
4856 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
4857 .schema
4858 .columns
4859 .iter()
4860 .map(|column| (column.id, column.name.clone()))
4861 .collect::<Vec<_>>();
4862 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
4863 drop(catalog);
4864 let Some(principal) = principal.as_ref() else {
4865 self.require(&crate::auth::Permission::Select {
4866 table: table.to_string(),
4867 })?;
4868 return Ok(columns.iter().map(|(id, _)| *id).collect());
4869 };
4870 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
4871 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
4872 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
4873 .iter()
4874 .filter(|(_, name)| allowed.contains(name))
4875 .map(|(id, _)| *id)
4876 .collect()),
4877 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
4878 required: crate::auth::Permission::Select {
4879 table: table.to_string(),
4880 },
4881 principal: principal.username.clone(),
4882 }),
4883 }
4884 }
4885
4886 pub fn secure_rows_for(
4887 &self,
4888 table: &str,
4889 rows: Vec<crate::memtable::Row>,
4890 principal: Option<&crate::auth::Principal>,
4891 ) -> Result<Vec<crate::memtable::Row>> {
4892 self.secure_rows_for_with_context(table, rows, principal, None)
4893 }
4894
4895 pub fn secure_rows_for_with_context(
4896 &self,
4897 table: &str,
4898 rows: Vec<crate::memtable::Row>,
4899 principal: Option<&crate::auth::Principal>,
4900 context: Option<&crate::query::AiExecutionContext>,
4901 ) -> Result<Vec<crate::memtable::Row>> {
4902 let (security, principal) = {
4903 let catalog = self.catalog.read();
4904 (
4905 catalog.security.clone(),
4906 self.principal_for_authorized_read(&catalog, principal, false)?,
4907 )
4908 };
4909 if !security.table_has_security(table) {
4910 return Ok(rows);
4911 }
4912 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
4913 let mut output = Vec::new();
4914 for mut row in rows {
4915 if let Some(context) = context {
4916 context.consume(1)?;
4917 }
4918 if security.row_allowed(
4919 table,
4920 crate::security::PolicyCommand::Select,
4921 &row,
4922 principal,
4923 false,
4924 ) {
4925 security.apply_masks(table, &mut row, principal);
4926 output.push(row);
4927 }
4928 }
4929 Ok(output)
4930 }
4931
4932 pub fn mask_search_hits_for(
4935 &self,
4936 table: &str,
4937 hits: &mut [crate::query::SearchHit],
4938 principal: Option<&crate::auth::Principal>,
4939 ) -> Result<()> {
4940 let (security, principal) = {
4941 let catalog = self.catalog.read();
4942 (
4943 catalog.security.clone(),
4944 self.principal_for_authorized_read(&catalog, principal, false)?,
4945 )
4946 };
4947 if !security.table_has_security(table) {
4948 return Ok(());
4949 }
4950 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
4951 for hit in hits {
4952 security.apply_masks_to_cells(table, &mut hit.cells, principal);
4953 }
4954 Ok(())
4955 }
4956
4957 pub fn mask_rows_for(
4959 &self,
4960 table: &str,
4961 rows: &mut [crate::memtable::Row],
4962 principal: Option<&crate::auth::Principal>,
4963 ) -> Result<()> {
4964 let (security, principal) = {
4965 let catalog = self.catalog.read();
4966 (
4967 catalog.security.clone(),
4968 self.principal_for_authorized_read(&catalog, principal, false)?,
4969 )
4970 };
4971 if !security.table_has_security(table) {
4972 return Ok(());
4973 }
4974 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
4975 for row in rows {
4976 security.apply_masks(table, row, principal);
4977 }
4978 Ok(())
4979 }
4980
4981 pub fn authorized_candidate_ids_for(
4983 &self,
4984 table: &str,
4985 principal: Option<&crate::auth::Principal>,
4986 ) -> Result<Option<std::collections::HashSet<RowId>>> {
4987 Ok(self
4988 .authorized_read_snapshot(table, principal)?
4989 .allowed_row_ids)
4990 }
4991
4992 fn allowed_row_ids_locked(
4993 &self,
4994 table_name: &str,
4995 table: &Table,
4996 table_snapshot: Snapshot,
4997 security_state: (&crate::security::SecurityCatalog, u64),
4998 principal: Option<&crate::auth::Principal>,
4999 context: Option<&crate::query::AiExecutionContext>,
5000 ) -> Result<Option<Arc<HashSet<RowId>>>> {
5001 let (security, security_version) = security_state;
5002 if !security.rls_enabled(table_name) {
5003 return Ok(None);
5004 }
5005 let authorization_started = std::time::Instant::now();
5006 let principal = principal.ok_or(MongrelError::AuthRequired)?;
5007 let mut roles = principal.roles.clone();
5008 roles.sort_unstable();
5009 let principal_key = format!(
5010 "{}:{}:{}:{}:{roles:?}",
5011 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
5012 );
5013 let cache_key = (
5014 table_name.to_string(),
5015 table.data_generation(),
5016 security_version,
5017 principal_key,
5018 );
5019 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
5020 crate::trace::QueryTrace::record(|trace| {
5021 trace.rls_cache_hit = true;
5022 trace.authorization_nanos = trace
5023 .authorization_nanos
5024 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5025 });
5026 return Ok(Some(allowed));
5027 }
5028 if let Some(context) = context {
5029 context.checkpoint()?;
5030 }
5031 let started = std::time::Instant::now();
5033 let rows = table.visible_rows(table_snapshot)?;
5034 let rows_evaluated = rows.len() as u64;
5035 let mut allowed = HashSet::new();
5036 for chunk in rows.chunks(256) {
5037 if let Some(context) = context {
5038 context.consume(chunk.len())?;
5039 }
5040 allowed.extend(chunk.iter().filter_map(|row| {
5041 security
5042 .row_allowed(
5043 table_name,
5044 crate::security::PolicyCommand::Select,
5045 row,
5046 principal,
5047 false,
5048 )
5049 .then_some(row.row_id)
5050 }));
5051 }
5052 let allowed = Arc::new(allowed);
5053 let mut cache = self.rls_cache.lock();
5054 cache.build_nanos = cache
5055 .build_nanos
5056 .saturating_add(started.elapsed().as_nanos() as u64);
5057 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
5058 cache.insert(cache_key, Arc::clone(&allowed));
5059 crate::trace::QueryTrace::record(|trace| {
5060 trace.rls_rows_evaluated = trace
5061 .rls_rows_evaluated
5062 .saturating_add(rows_evaluated as usize);
5063 trace.authorization_nanos = trace
5064 .authorization_nanos
5065 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5066 });
5067 Ok(Some(allowed))
5068 }
5069
5070 fn principal_for_authorized_read(
5071 &self,
5072 catalog: &Catalog,
5073 principal: Option<&crate::auth::Principal>,
5074 catalog_bound: bool,
5075 ) -> Result<Option<crate::auth::Principal>> {
5076 let principal = principal.cloned().or_else(|| self.principal.read().clone());
5077 let Some(principal) = principal else {
5078 return Ok(None);
5079 };
5080 if catalog.require_auth || catalog_bound || principal.user_id != 0 {
5081 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
5082 .map(Some)
5083 .ok_or(MongrelError::AuthRequired);
5084 }
5085 Ok(Some(principal))
5086 }
5087
5088 pub fn with_authorized_read<T, F>(
5092 &self,
5093 table_name: &str,
5094 principal: Option<&crate::auth::Principal>,
5095 catalog_bound: bool,
5096 read: F,
5097 ) -> Result<T>
5098 where
5099 F: FnMut(
5100 &mut Table,
5101 Snapshot,
5102 Option<&HashSet<RowId>>,
5103 Option<&crate::auth::Principal>,
5104 ) -> Result<T>,
5105 {
5106 self.with_authorized_read_context(
5107 table_name,
5108 principal,
5109 catalog_bound,
5110 None,
5111 None,
5112 None,
5113 read,
5114 )
5115 }
5116
5117 #[allow(clippy::too_many_arguments)]
5118 pub fn with_authorized_read_context<T, F>(
5119 &self,
5120 table_name: &str,
5121 principal: Option<&crate::auth::Principal>,
5122 catalog_bound: bool,
5123 authorization: Option<&ReadAuthorization>,
5124 context: Option<&crate::query::AiExecutionContext>,
5125 snapshot_override: Option<Snapshot>,
5126 read: F,
5127 ) -> Result<T>
5128 where
5129 F: FnMut(
5130 &mut Table,
5131 Snapshot,
5132 Option<&HashSet<RowId>>,
5133 Option<&crate::auth::Principal>,
5134 ) -> Result<T>,
5135 {
5136 self.with_authorized_read_context_stamped(
5137 table_name,
5138 principal,
5139 catalog_bound,
5140 authorization,
5141 context,
5142 snapshot_override,
5143 read,
5144 )
5145 .map(|(result, _)| result)
5146 }
5147
5148 #[allow(clippy::too_many_arguments)]
5149 pub fn with_authorized_read_context_stamped<T, F>(
5150 &self,
5151 table_name: &str,
5152 principal: Option<&crate::auth::Principal>,
5153 catalog_bound: bool,
5154 authorization: Option<&ReadAuthorization>,
5155 context: Option<&crate::query::AiExecutionContext>,
5156 snapshot_override: Option<Snapshot>,
5157 mut read: F,
5158 ) -> Result<(T, AuthorizedReadStamp)>
5159 where
5160 F: FnMut(
5161 &mut Table,
5162 Snapshot,
5163 Option<&HashSet<RowId>>,
5164 Option<&crate::auth::Principal>,
5165 ) -> Result<T>,
5166 {
5167 if principal.is_none() && self.principal.read().is_some() {
5168 self.refresh_principal()?;
5169 }
5170 const RETRIES: usize = 3;
5171 let handle = self.table(table_name)?;
5172 for attempt in 0..RETRIES {
5173 crate::trace::QueryTrace::record(|trace| {
5174 trace.authorization_retries = attempt;
5175 });
5176 let (security, security_version, effective_principal) = {
5177 let catalog = self.catalog.read();
5178 (
5179 catalog.security.clone(),
5180 catalog.security_version,
5181 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
5182 )
5183 };
5184 if let Some(authorization) = authorization {
5185 for permission in &authorization.permissions {
5186 self.require_for(effective_principal.as_ref(), permission)?;
5187 }
5188 self.require_columns_for(
5189 table_name,
5190 authorization.operation,
5191 &authorization.columns,
5192 effective_principal.as_ref(),
5193 )?;
5194 }
5195 let result = {
5196 let mut table = lock_table_with_context(&handle, context)?;
5197 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
5198 let allowed = self.allowed_row_ids_locked(
5199 table_name,
5200 &table,
5201 snapshot,
5202 (&security, security_version),
5203 effective_principal.as_ref(),
5204 context,
5205 )?;
5206 let stamp = AuthorizedReadStamp {
5207 table_id: table.table_id(),
5208 schema_id: table.schema().schema_id,
5209 data_generation: table.data_generation(),
5210 security_version,
5211 snapshot,
5212 };
5213 let result = read(
5214 &mut table,
5215 snapshot,
5216 allowed.as_deref(),
5217 effective_principal.as_ref(),
5218 )?;
5219 (result, stamp)
5220 };
5221 if let Some(context) = context {
5222 context.checkpoint()?;
5223 }
5224 if self.catalog.read().security_version == security_version {
5225 return Ok(result);
5226 }
5227 if attempt + 1 == RETRIES {
5228 return Err(MongrelError::Conflict(
5229 "security policy changed during scored read".into(),
5230 ));
5231 }
5232 }
5233 Err(MongrelError::Conflict(
5234 "authorization retry loop exhausted".into(),
5235 ))
5236 }
5237
5238 fn with_authorized_aggregate_table<T, F>(
5239 &self,
5240 table_name: &str,
5241 columns: &[u16],
5242 principal: Option<&crate::auth::Principal>,
5243 catalog_bound: bool,
5244 allow_table_security: bool,
5245 mut aggregate: F,
5246 ) -> Result<T>
5247 where
5248 F: FnMut(
5249 &mut Table,
5250 Option<&crate::security::CandidateAuthorization<'_>>,
5251 Option<&crate::auth::Principal>,
5252 u64,
5253 ) -> Result<T>,
5254 {
5255 if principal.is_none() && self.principal.read().is_some() {
5256 self.refresh_principal()?;
5257 }
5258 const RETRIES: usize = 3;
5259 let handle = self.table(table_name)?;
5260 for attempt in 0..RETRIES {
5261 let (security, security_version, effective_principal) = {
5262 let catalog = self.catalog.read();
5263 (
5264 catalog.security.clone(),
5265 catalog.security_version,
5266 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
5267 )
5268 };
5269 self.require_columns_for(
5270 table_name,
5271 crate::auth::ColumnOperation::Select,
5272 columns,
5273 effective_principal.as_ref(),
5274 )?;
5275 if !allow_table_security && security.table_has_security(table_name) {
5276 return Err(MongrelError::InvalidArgument(
5277 "incremental aggregate is unsupported while RLS or column masks are active"
5278 .into(),
5279 ));
5280 }
5281 let result = {
5282 let mut table = handle.lock();
5283 let authorization = if security.rls_enabled(table_name) {
5284 Some(crate::security::CandidateAuthorization {
5285 table: table_name,
5286 security: &security,
5287 principal: effective_principal
5288 .as_ref()
5289 .ok_or(MongrelError::AuthRequired)?,
5290 })
5291 } else {
5292 None
5293 };
5294 aggregate(
5295 &mut table,
5296 authorization.as_ref(),
5297 effective_principal.as_ref(),
5298 security_version,
5299 )?
5300 };
5301 if self.catalog.read().security_version == security_version {
5302 return Ok(result);
5303 }
5304 if attempt + 1 == RETRIES {
5305 return Err(MongrelError::Conflict(
5306 "security policy changed during aggregate read".into(),
5307 ));
5308 }
5309 }
5310 Err(MongrelError::Conflict(
5311 "aggregate authorization retry loop exhausted".into(),
5312 ))
5313 }
5314
5315 pub fn with_authorized_scored_read_context<T, F>(
5319 &self,
5320 table_name: &str,
5321 principal: Option<&crate::auth::Principal>,
5322 catalog_bound: bool,
5323 authorization: Option<&ReadAuthorization>,
5324 context: Option<&crate::query::AiExecutionContext>,
5325 mut read: F,
5326 ) -> Result<T>
5327 where
5328 F: FnMut(
5329 &mut Table,
5330 Snapshot,
5331 Option<&crate::security::CandidateAuthorization<'_>>,
5332 Option<&crate::auth::Principal>,
5333 ) -> Result<T>,
5334 {
5335 self.with_authorized_scored_read_context_at(
5336 table_name,
5337 principal,
5338 catalog_bound,
5339 authorization,
5340 context,
5341 None,
5342 |table, snapshot, authorization, principal| {
5343 let mut table = table.clone();
5344 read(&mut table, snapshot, authorization, principal)
5345 },
5346 )
5347 }
5348
5349 #[allow(clippy::too_many_arguments)]
5350 pub fn with_authorized_scored_read_context_at<T, F>(
5351 &self,
5352 table_name: &str,
5353 principal: Option<&crate::auth::Principal>,
5354 catalog_bound: bool,
5355 authorization: Option<&ReadAuthorization>,
5356 context: Option<&crate::query::AiExecutionContext>,
5357 snapshot_override: Option<Snapshot>,
5358 read: F,
5359 ) -> Result<T>
5360 where
5361 F: FnMut(
5362 &Table,
5363 Snapshot,
5364 Option<&crate::security::CandidateAuthorization<'_>>,
5365 Option<&crate::auth::Principal>,
5366 ) -> Result<T>,
5367 {
5368 self.with_authorized_scored_read_context_at_stamped(
5369 table_name,
5370 principal,
5371 catalog_bound,
5372 authorization,
5373 context,
5374 snapshot_override,
5375 read,
5376 )
5377 .map(|(result, _)| result)
5378 }
5379
5380 #[allow(clippy::too_many_arguments)]
5381 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
5382 &self,
5383 table_name: &str,
5384 principal: Option<&crate::auth::Principal>,
5385 catalog_bound: bool,
5386 authorization: Option<&ReadAuthorization>,
5387 context: Option<&crate::query::AiExecutionContext>,
5388 snapshot_override: Option<Snapshot>,
5389 mut read: F,
5390 ) -> Result<(T, AuthorizedReadStamp)>
5391 where
5392 F: FnMut(
5393 &Table,
5394 Snapshot,
5395 Option<&crate::security::CandidateAuthorization<'_>>,
5396 Option<&crate::auth::Principal>,
5397 ) -> Result<T>,
5398 {
5399 if principal.is_none() && self.principal.read().is_some() {
5400 self.refresh_principal()?;
5401 }
5402 const RETRIES: usize = 3;
5403 let handle = self.table(table_name)?;
5404 for attempt in 0..RETRIES {
5405 if let Some(context) = context {
5406 context.checkpoint()?;
5407 }
5408 crate::trace::QueryTrace::record(|trace| {
5409 trace.authorization_retries = attempt;
5410 });
5411 let (security, security_version, effective_principal) = {
5412 let catalog = self.catalog.read();
5413 (
5414 catalog.security.clone(),
5415 catalog.security_version,
5416 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
5417 )
5418 };
5419 if let Some(authorization) = authorization {
5420 for permission in &authorization.permissions {
5421 self.require_for(effective_principal.as_ref(), permission)?;
5422 }
5423 self.require_columns_for(
5424 table_name,
5425 authorization.operation,
5426 &authorization.columns,
5427 effective_principal.as_ref(),
5428 )?;
5429 }
5430 let result = {
5431 let (table, snapshot, _snapshot_guard, _run_pins) =
5432 self.scored_read_generation(&handle, context, snapshot_override)?;
5433 let candidate_authorization = if security.rls_enabled(table_name) {
5434 Some(crate::security::CandidateAuthorization {
5435 table: table_name,
5436 security: &security,
5437 principal: effective_principal
5438 .as_ref()
5439 .ok_or(MongrelError::AuthRequired)?,
5440 })
5441 } else {
5442 None
5443 };
5444 let stamp = AuthorizedReadStamp {
5445 table_id: table.table_id(),
5446 schema_id: table.schema().schema_id,
5447 data_generation: table.data_generation(),
5448 security_version,
5449 snapshot,
5450 };
5451 let result = read(
5452 table.as_ref(),
5453 snapshot,
5454 candidate_authorization.as_ref(),
5455 effective_principal.as_ref(),
5456 )?;
5457 (result, stamp)
5458 };
5459 if let Some(context) = context {
5460 context.checkpoint()?;
5461 }
5462 if self.catalog.read().security_version == security_version {
5463 return Ok(result);
5464 }
5465 if attempt + 1 == RETRIES {
5466 return Err(MongrelError::Conflict(
5467 "security policy changed during scored read".into(),
5468 ));
5469 }
5470 }
5471 Err(MongrelError::Conflict(
5472 "scored-read authorization retry loop exhausted".into(),
5473 ))
5474 }
5475
5476 fn scored_read_generation(
5477 &self,
5478 handle: &TableHandle,
5479 context: Option<&crate::query::AiExecutionContext>,
5480 snapshot_override: Option<Snapshot>,
5481 ) -> Result<(
5482 Arc<TableReadGeneration>,
5483 Snapshot,
5484 crate::retention::OwnedSnapshotGuard,
5485 RunPins,
5486 )> {
5487 let mut table = if let Some(context) = context {
5488 loop {
5489 context.checkpoint()?;
5490 let wait = context
5491 .remaining_duration()
5492 .unwrap_or(std::time::Duration::from_millis(5))
5493 .min(std::time::Duration::from_millis(5));
5494 if let Some(table) = handle.try_lock_for(wait) {
5495 break table;
5496 }
5497 }
5498 } else {
5499 handle.lock()
5500 };
5501 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
5502 self.snapshot_at_owned(snapshot.epoch)?
5503 } else {
5504 let snapshot = table.snapshot();
5505 let guard = self.snapshots.register_owned(snapshot.epoch);
5506 (snapshot, guard)
5507 };
5508 let table_id = table.table_id();
5509 let run_keys: Vec<_> = table
5510 .active_run_ids()
5511 .map(|run_id| (table_id, run_id))
5512 .collect();
5513 let generation = handle
5514 .generation_metrics
5515 .activate(table.clone_read_generation()?);
5516 let run_pins = self.pin_runs(&run_keys);
5517 Ok((generation, snapshot, snapshot_guard, run_pins))
5518 }
5519
5520 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
5521 let mut pins = self.backup_pins.lock();
5522 for run in runs {
5523 *pins.entry(*run).or_insert(0) += 1;
5524 }
5525 drop(pins);
5526 RunPins {
5527 pins: Arc::clone(&self.backup_pins),
5528 runs: runs.to_vec(),
5529 }
5530 }
5531
5532 pub fn query_for_current_principal(
5536 &self,
5537 table_name: &str,
5538 query: &crate::query::Query,
5539 projection: Option<&[u16]>,
5540 ) -> Result<Vec<crate::memtable::Row>> {
5541 let condition_columns = crate::query::condition_columns(&query.conditions);
5542 self.with_authorized_read(
5543 table_name,
5544 None,
5545 true,
5546 |table, snapshot, allowed, principal| {
5547 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5548 self.require_columns_for(
5549 table_name,
5550 crate::auth::ColumnOperation::Select,
5551 &condition_columns,
5552 principal,
5553 )?;
5554 if let Some(projection) = projection {
5555 self.require_columns_for(
5556 table_name,
5557 crate::auth::ColumnOperation::Select,
5558 projection,
5559 principal,
5560 )?;
5561 }
5562 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
5563 let projection =
5564 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
5565 for row in &mut rows {
5566 row.columns.retain(|column, _| {
5567 allowed_columns.contains(column)
5568 && projection
5569 .as_ref()
5570 .is_none_or(|projection| projection.contains(column))
5571 });
5572 }
5573 self.secure_rows_for(table_name, rows, principal)
5574 },
5575 )
5576 }
5577
5578 pub fn query_for_current_principal_controlled(
5582 &self,
5583 table_name: &str,
5584 query: &crate::query::Query,
5585 projection: Option<&[u16]>,
5586 control: &crate::ExecutionControl,
5587 ) -> Result<Vec<crate::memtable::Row>> {
5588 self.query_for_principal_controlled(table_name, query, projection, None, true, control)
5589 }
5590
5591 fn query_for_principal_controlled(
5592 &self,
5593 table_name: &str,
5594 query: &crate::query::Query,
5595 projection: Option<&[u16]>,
5596 principal: Option<&crate::auth::Principal>,
5597 catalog_bound: bool,
5598 control: &crate::ExecutionControl,
5599 ) -> Result<Vec<crate::memtable::Row>> {
5600 control.checkpoint()?;
5601 let context = crate::query::AiExecutionContext::with_control(
5602 control.clone(),
5603 usize::MAX,
5604 crate::query::MAX_FUSED_CANDIDATES,
5605 );
5606 let condition_columns = crate::query::condition_columns(&query.conditions);
5607 self.with_authorized_read_context(
5608 table_name,
5609 principal,
5610 catalog_bound,
5611 None,
5612 Some(&context),
5613 None,
5614 |table, snapshot, allowed, principal| {
5615 control.checkpoint()?;
5616 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5617 self.require_columns_for(
5618 table_name,
5619 crate::auth::ColumnOperation::Select,
5620 &condition_columns,
5621 principal,
5622 )?;
5623 if let Some(projection) = projection {
5624 self.require_columns_for(
5625 table_name,
5626 crate::auth::ColumnOperation::Select,
5627 projection,
5628 principal,
5629 )?;
5630 }
5631 let rows =
5632 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
5633 let projection =
5634 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
5635 let mut projected = Vec::with_capacity(rows.len());
5636 for (index, mut row) in rows.into_iter().enumerate() {
5637 if index & 255 == 0 {
5638 control.checkpoint()?;
5639 }
5640 row.columns.retain(|column, _| {
5641 allowed_columns.contains(column)
5642 && projection
5643 .as_ref()
5644 .is_none_or(|projection| projection.contains(column))
5645 });
5646 projected.push(row);
5647 }
5648 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
5649 },
5650 )
5651 }
5652
5653 pub fn approx_aggregate_for_current_principal(
5656 &self,
5657 table_name: &str,
5658 conditions: &[crate::query::Condition],
5659 column: Option<u16>,
5660 agg: crate::engine::ApproxAgg,
5661 z: f64,
5662 ) -> Result<Option<crate::engine::ApproxResult>> {
5663 if !z.is_finite() || z <= 0.0 {
5664 return Err(MongrelError::InvalidArgument(
5665 "z must be finite and > 0".into(),
5666 ));
5667 }
5668 let mut columns = crate::query::condition_columns(conditions);
5669 columns.extend(column);
5670 columns.sort_unstable();
5671 columns.dedup();
5672 self.with_authorized_aggregate_table(
5673 table_name,
5674 &columns,
5675 None,
5676 true,
5677 true,
5678 |table, authorization, _, _| {
5679 table.approx_aggregate_with_candidate_authorization(
5680 conditions,
5681 column,
5682 agg,
5683 z,
5684 authorization,
5685 )
5686 },
5687 )
5688 }
5689
5690 pub fn incremental_aggregate_for_current_principal(
5694 &self,
5695 table_name: &str,
5696 conditions: &[crate::query::Condition],
5697 column: Option<u16>,
5698 agg: crate::engine::NativeAgg,
5699 ) -> Result<crate::engine::IncrementalAggResult> {
5700 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
5701 }
5702
5703 pub fn incremental_aggregate_for_principal(
5707 &self,
5708 table_name: &str,
5709 conditions: &[crate::query::Condition],
5710 column: Option<u16>,
5711 agg: crate::engine::NativeAgg,
5712 principal: Option<&crate::auth::Principal>,
5713 catalog_bound: bool,
5714 ) -> Result<crate::engine::IncrementalAggResult> {
5715 let mut columns = crate::query::condition_columns(conditions);
5716 columns.extend(column);
5717 columns.sort_unstable();
5718 columns.dedup();
5719 self.with_authorized_aggregate_table(
5720 table_name,
5721 &columns,
5722 principal,
5723 catalog_bound,
5724 false,
5725 |table, _, principal, security_version| {
5726 let cache_key = incremental_aggregate_cache_key(
5727 table_name,
5728 conditions,
5729 column,
5730 agg,
5731 principal,
5732 security_version,
5733 );
5734 table.aggregate_incremental(cache_key, conditions, column, agg)
5735 },
5736 )
5737 }
5738
5739 pub fn get_for_current_principal(
5742 &self,
5743 table_name: &str,
5744 row_id: RowId,
5745 ) -> Result<Option<crate::memtable::Row>> {
5746 self.with_authorized_read(
5747 table_name,
5748 None,
5749 true,
5750 |table, snapshot, allowed, principal| {
5751 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5752 let Some(row) = table.get(row_id, snapshot) else {
5753 return Ok(None);
5754 };
5755 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
5756 return Ok(None);
5757 }
5758 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
5759 if let Some(row) = rows.first_mut() {
5760 row.columns
5761 .retain(|column, _| allowed_columns.contains(column));
5762 }
5763 Ok(rows.pop())
5764 },
5765 )
5766 }
5767
5768 pub fn ann_rerank_for_current_principal(
5771 &self,
5772 table_name: &str,
5773 request: &crate::query::AnnRerankRequest,
5774 ) -> Result<Vec<crate::query::AnnRerankHit>> {
5775 self.with_authorized_scored_read_context_at(
5776 table_name,
5777 None,
5778 true,
5779 Some(&ReadAuthorization {
5780 operation: crate::auth::ColumnOperation::Select,
5781 columns: vec![request.column_id],
5782 permissions: Vec::new(),
5783 }),
5784 None,
5785 None,
5786 |table, snapshot, authorization, principal| {
5787 self.require_columns_for(
5788 table_name,
5789 crate::auth::ColumnOperation::Select,
5790 &[request.column_id],
5791 principal,
5792 )?;
5793 table.ann_rerank_at_with_candidate_authorization_on_generation(
5794 request,
5795 snapshot,
5796 authorization,
5797 None,
5798 )
5799 },
5800 )
5801 }
5802
5803 pub fn search_for_current_principal(
5807 &self,
5808 table_name: &str,
5809 request: &crate::query::SearchRequest,
5810 ) -> Result<Vec<crate::query::SearchHit>> {
5811 let mut columns = crate::query::condition_columns(&request.must);
5812 for named in &request.retrievers {
5813 columns.push(named.retriever.column_id());
5814 }
5815 if let Some(crate::query::Rerank::ExactVector {
5816 embedding_column, ..
5817 }) = &request.rerank
5818 {
5819 columns.push(*embedding_column);
5820 }
5821 if let Some(proj) = &request.projection {
5822 columns.extend(proj);
5823 }
5824 columns.sort_unstable();
5825 columns.dedup();
5826 self.with_authorized_scored_read_context_at(
5827 table_name,
5828 None,
5829 true,
5830 Some(&ReadAuthorization {
5831 operation: crate::auth::ColumnOperation::Select,
5832 columns: columns.clone(),
5833 permissions: Vec::new(),
5834 }),
5835 None,
5836 None,
5837 |table, snapshot, authorization, principal| {
5838 self.require_columns_for(
5839 table_name,
5840 crate::auth::ColumnOperation::Select,
5841 &columns,
5842 principal,
5843 )?;
5844 let hits = table.search_at_with_candidate_authorization_on_generation(
5845 request,
5846 snapshot,
5847 authorization,
5848 None,
5849 )?;
5850 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5851 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
5852 for mut hit in hits {
5853 let row = crate::memtable::Row {
5854 row_id: hit.row_id,
5855 committed_epoch: crate::Epoch(0),
5856 columns: hit
5857 .cells
5858 .iter()
5859 .cloned()
5860 .collect::<std::collections::HashMap<u16, crate::Value>>(),
5861 deleted: false,
5862 };
5863 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
5864 if let Some(mut row) = rows.pop() {
5865 row.columns
5866 .retain(|column, _| allowed_columns.contains(column));
5867 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
5868 hit.cells.sort_by_key(|(column, _)| *column);
5869 secured.push(hit);
5870 }
5871 }
5872 Ok(secured)
5873 },
5874 )
5875 }
5876
5877 pub fn authorized_read_snapshot(
5880 &self,
5881 table: &str,
5882 principal: Option<&crate::auth::Principal>,
5883 ) -> Result<AuthorizedReadSnapshot> {
5884 let (security, security_version, effective_principal) = {
5885 let catalog = self.catalog.read();
5886 (
5887 catalog.security.clone(),
5888 catalog.security_version,
5889 self.principal_for_authorized_read(&catalog, principal, false)?,
5890 )
5891 };
5892 let handle = self.table(table)?;
5893 let (table_snapshot, data_generation, allowed_row_ids) = {
5894 let table_handle = handle.lock();
5895 let table_snapshot = table_handle.snapshot();
5896 let data_generation = table_handle.data_generation();
5897 let allowed = self.allowed_row_ids_locked(
5898 table,
5899 &table_handle,
5900 table_snapshot,
5901 (&security, security_version),
5902 effective_principal.as_ref(),
5903 None,
5904 )?;
5905 (
5906 table_snapshot,
5907 data_generation,
5908 allowed.map(|allowed| (*allowed).clone()),
5909 )
5910 };
5911 Ok(AuthorizedReadSnapshot {
5912 table: table.to_string(),
5913 table_snapshot,
5914 data_generation,
5915 security_version,
5916 allowed_row_ids,
5917 })
5918 }
5919
5920 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
5921 if self.catalog.read().security_version != snapshot.security_version {
5922 return false;
5923 }
5924 self.table(&snapshot.table)
5925 .ok()
5926 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
5927 }
5928
5929 pub fn rls_cache_stats(&self) -> RlsCacheStats {
5930 self.rls_cache.lock().stats()
5931 }
5932
5933 pub fn rows_for(
5935 &self,
5936 table: &str,
5937 principal: Option<&crate::auth::Principal>,
5938 ) -> Result<Vec<crate::memtable::Row>> {
5939 if principal.is_none() && self.principal.read().is_some() {
5940 self.refresh_principal()?;
5941 }
5942 let allowed = self.select_column_ids_for(table, principal)?;
5943 let handle = self.table(table)?;
5944 let rows = {
5945 let table = handle.lock();
5946 table.visible_rows(table.snapshot())?
5947 };
5948 let mut rows = self.secure_rows_for(table, rows, principal)?;
5949 for row in &mut rows {
5950 row.columns.retain(|column, _| allowed.contains(column));
5951 }
5952 Ok(rows)
5953 }
5954
5955 pub fn rows_at_epoch_for_current_principal(
5958 &self,
5959 table_name: &str,
5960 snapshot: Snapshot,
5961 ) -> Result<Vec<crate::memtable::Row>> {
5962 self.with_authorized_read_context(
5963 table_name,
5964 None,
5965 true,
5966 Some(&ReadAuthorization {
5967 operation: crate::auth::ColumnOperation::Select,
5968 columns: Vec::new(),
5969 permissions: Vec::new(),
5970 }),
5971 None,
5972 Some(snapshot),
5973 |table, snapshot, allowed, principal| {
5974 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
5975 let mut rows = table.visible_rows(snapshot)?;
5976 if let Some(allowed) = allowed {
5977 rows.retain(|row| allowed.contains(&row.row_id));
5978 }
5979 rows = self.secure_rows_for(table_name, rows, principal)?;
5980 for row in &mut rows {
5981 row.columns
5982 .retain(|column, _| allowed_columns.contains(column));
5983 }
5984 Ok(rows)
5985 },
5986 )
5987 }
5988
5989 pub fn count_for(
5991 &self,
5992 table: &str,
5993 principal: Option<&crate::auth::Principal>,
5994 ) -> Result<u64> {
5995 if principal.is_none() && self.principal.read().is_some() {
5996 self.refresh_principal()?;
5997 }
5998 self.select_column_ids_for(table, principal)?;
5999 if self.security_active_for(table) {
6000 Ok(self.rows_for(table, principal)?.len() as u64)
6001 } else {
6002 Ok(self.table(table)?.lock().count())
6003 }
6004 }
6005
6006 pub fn put_for(
6008 &self,
6009 table: &str,
6010 mut cells: Vec<(u16, crate::memtable::Value)>,
6011 principal: Option<&crate::auth::Principal>,
6012 ) -> Result<RowId> {
6013 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
6014 self.require_columns_for(
6015 table,
6016 crate::auth::ColumnOperation::Insert,
6017 &columns,
6018 principal,
6019 )?;
6020 let handle = self.table(table)?;
6021 let mut table_handle = handle.lock();
6022 table_handle.fill_auto_inc(&mut cells)?;
6023 table_handle.apply_defaults(&mut cells)?;
6024 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
6025 row.columns.extend(cells.iter().cloned());
6026 self.check_row_policy_for(
6027 table,
6028 crate::security::PolicyCommand::Insert,
6029 &row,
6030 true,
6031 principal,
6032 )?;
6033 table_handle.put(cells)
6034 }
6035
6036 pub fn check_row_policy_for(
6037 &self,
6038 table: &str,
6039 command: crate::security::PolicyCommand,
6040 row: &crate::memtable::Row,
6041 check_new: bool,
6042 principal: Option<&crate::auth::Principal>,
6043 ) -> Result<()> {
6044 let security = self.catalog.read().security.clone();
6045 if !security.rls_enabled(table) {
6046 return Ok(());
6047 }
6048 let cached = self.principal.read().clone();
6049 let principal = principal
6050 .or(cached.as_ref())
6051 .ok_or(MongrelError::AuthRequired)?;
6052 if security.row_allowed(table, command, row, principal, check_new) {
6053 return Ok(());
6054 }
6055 let required = match command {
6056 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
6057 table: table.to_string(),
6058 },
6059 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
6060 table: table.to_string(),
6061 },
6062 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
6063 table: table.to_string(),
6064 },
6065 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
6066 crate::auth::Permission::Delete {
6067 table: table.to_string(),
6068 }
6069 }
6070 };
6071 Err(MongrelError::PermissionDenied {
6072 required,
6073 principal: principal.username.clone(),
6074 })
6075 }
6076
6077 pub fn set_materialized_view(
6080 &self,
6081 definition: crate::catalog::MaterializedViewEntry,
6082 ) -> Result<()> {
6083 self.set_materialized_view_with_epoch(definition)
6084 .map(|_| ())
6085 }
6086
6087 pub fn set_materialized_view_with_epoch(
6089 &self,
6090 definition: crate::catalog::MaterializedViewEntry,
6091 ) -> Result<Epoch> {
6092 use crate::wal::DdlOp;
6093 use std::sync::atomic::Ordering;
6094
6095 let command = crate::catalog_cmds::CatalogCommand::CreateMaterializedView {
6096 definition: definition.clone(),
6097 };
6098 self.require(&crate::catalog_cmds::required_permission(&command))?;
6099 if self.poisoned.load(Ordering::Relaxed) {
6100 return Err(MongrelError::Other(
6101 "database poisoned by fsync error".into(),
6102 ));
6103 }
6104 if definition.name.is_empty() || definition.query.trim().is_empty() {
6105 return Err(MongrelError::InvalidArgument(
6106 "materialized view name and query must not be empty".into(),
6107 ));
6108 }
6109 let _operation = self.admit_operation()?;
6112 let _ddl = self.ddl_lock.lock();
6113 let _security_write = self.security_write()?;
6114 self.require(&crate::catalog_cmds::required_permission(&command))?;
6115 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
6119 let table_id = self
6120 .catalog
6121 .read()
6122 .live(&definition.name)
6123 .ok_or_else(|| {
6124 MongrelError::NotFound(format!(
6125 "materialized view table {:?} not found",
6126 definition.name
6127 ))
6128 })?
6129 .table_id;
6130 let definition_json = DdlOp::encode_materialized_view(&definition)?;
6131 let _commit = self.commit_lock.lock();
6132 let epoch = self.epoch.bump_assigned();
6133 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6134 let txn_id = self.alloc_txn_id()?;
6135 let mut next_catalog = self.catalog.read().clone();
6136 self.apply_catalog_command_to(&mut next_catalog, command)?;
6137 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
6138 let commit_seq = {
6139 let mut wal = self.shared_wal.lock();
6140 let append: Result<u64> = (|| {
6141 wal.append(
6142 txn_id,
6143 table_id,
6144 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
6145 name: definition.name.clone(),
6146 definition_json,
6147 }),
6148 )?;
6149 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
6150 wal.append_commit(txn_id, epoch, &[])
6151 })();
6152 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
6153 };
6154 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
6155
6156 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
6157 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
6158 Ok(epoch)
6159 }
6160
6161 pub fn root(&self) -> &Path {
6163 self.durable_root.canonical_path()
6164 }
6165
6166 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
6169 Arc::clone(&self.durable_root)
6170 }
6171
6172 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
6176 self.kek
6177 .as_deref()
6178 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
6179 }
6180
6181 pub fn is_read_only_replica(&self) -> bool {
6182 self.read_only
6183 }
6184
6185 pub fn ensure_consistent_read(&self) -> Result<()> {
6189 self.ensure_owner_process()?;
6190 if self.poisoned.load(Ordering::Relaxed) {
6191 return Err(MongrelError::Other(
6192 "database poisoned by post-commit failure; reopen required".into(),
6193 ));
6194 }
6195 Ok(())
6196 }
6197
6198 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
6199 self.replication_wal_retention_segments
6200 .store(segments, std::sync::atomic::Ordering::Relaxed);
6201 }
6202
6203 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
6208 let admin = crate::auth::Permission::Admin;
6209 self.require(&admin)?;
6210 let _operation = self.admit_operation()?;
6212 let operation_principal = self.principal_snapshot();
6213 let _barrier = self.replication_barrier.write();
6214 let _ddl = self.ddl_lock.lock();
6215 let _security = self.security_coordinator.gate.read();
6216 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6217 let mut handles: Vec<_> = self
6218 .tables
6219 .read()
6220 .iter()
6221 .map(|(id, handle)| (*id, handle.clone()))
6222 .collect();
6223 handles.sort_by_key(|(id, _)| *id);
6224 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
6225 let _commit = self.commit_lock.lock();
6226 let mut wal = self.shared_wal.lock();
6227 wal.group_sync()?;
6228 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6229 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
6230 let epoch = records
6231 .iter()
6232 .filter_map(|record| match &record.op {
6233 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
6234 _ => None,
6235 })
6236 .max()
6237 .unwrap_or(0)
6238 .max(self.epoch.committed().0);
6239 let files = crate::replication::capture_files(&self.root)?;
6240 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
6241 drop(wal);
6242 Ok(crate::replication::ReplicationSnapshot::new(
6243 source_id, epoch, files,
6244 ))
6245 }
6246
6247 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
6255 let control = crate::ExecutionControl::new(None);
6256 self.hot_backup_controlled(destination, &control, || true)
6257 }
6258
6259 pub(crate) fn hot_backup_to_durable_child(
6260 &self,
6261 parent: &crate::durable_file::DurableRoot,
6262 child: &Path,
6263 control: &crate::ExecutionControl,
6264 ) -> Result<crate::backup::BackupReport> {
6265 let mut components = child.components();
6266 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
6267 || components.next().is_some()
6268 {
6269 return Err(MongrelError::InvalidArgument(
6270 "durable backup child must be one relative path component".into(),
6271 ));
6272 }
6273 let destination_name = child.file_name().ok_or_else(|| {
6274 MongrelError::InvalidArgument("durable backup child has no filename".into())
6275 })?;
6276 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
6277 self.hot_backup_prepared(prepared, control, || true)
6278 }
6279
6280 #[doc(hidden)]
6283 pub fn hot_backup_controlled<F>(
6284 &self,
6285 destination: impl AsRef<Path>,
6286 control: &crate::ExecutionControl,
6287 before_publish: F,
6288 ) -> Result<crate::backup::BackupReport>
6289 where
6290 F: FnOnce() -> bool,
6291 {
6292 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
6293 self.hot_backup_prepared(prepared, control, before_publish)
6294 }
6295
6296 fn hot_backup_prepared<F>(
6297 &self,
6298 mut prepared: PreparedBackupDestination,
6299 control: &crate::ExecutionControl,
6300 before_publish: F,
6301 ) -> Result<crate::backup::BackupReport>
6302 where
6303 F: FnOnce() -> bool,
6304 {
6305 let admin = crate::auth::Permission::Admin;
6306 self.require(&admin)?;
6307 let _operation = self.admit_operation()?;
6310 let operation_principal = self.principal_snapshot();
6311 control.checkpoint()?;
6312 let destination = prepared.destination_path.clone();
6313 let mut before_publish = Some(before_publish);
6314
6315 let outcome = (|| {
6316 control.checkpoint()?;
6317 let barrier = self.replication_barrier.write();
6318 let ddl = self.ddl_lock.lock();
6319 let security = self.security_coordinator.gate.read();
6320 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6321 let mut handles: Vec<_> = self
6322 .tables
6323 .read()
6324 .iter()
6325 .map(|(id, handle)| (*id, handle.clone()))
6326 .collect();
6327 handles.sort_by_key(|(id, _)| *id);
6328 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
6329 let commit = self.commit_lock.lock();
6330 let mut wal = self.shared_wal.lock();
6331 wal.group_sync()?;
6332 let epoch = self.epoch.committed().0;
6333 let boundary_unix_nanos = current_unix_nanos();
6334
6335 let pin_nonce = std::time::SystemTime::now()
6336 .duration_since(std::time::UNIX_EPOCH)
6337 .unwrap_or_default()
6338 .as_nanos();
6339 let file_pin_root = self
6340 .root
6341 .join(META_DIR)
6342 .join("backup-pins")
6343 .join(format!("{}-{pin_nonce}", std::process::id()));
6344 std::fs::create_dir_all(&file_pin_root)?;
6345 let _file_pins = BackupFilePins {
6346 root: file_pin_root.clone(),
6347 };
6348 let mut run_files = Vec::new();
6349 for (index, (table_id, _)) in handles.iter().enumerate() {
6350 if index % 256 == 0 {
6351 control.checkpoint()?;
6352 }
6353 let table = &table_guards[index];
6354 for (run_index, run) in table.run_refs().iter().enumerate() {
6355 if run_index % 256 == 0 {
6356 control.checkpoint()?;
6357 }
6358 let source = table.run_path(run.run_id as u64);
6359 let relative = Path::new(TABLES_DIR)
6360 .join(table_id.to_string())
6361 .join(crate::engine::RUNS_DIR)
6362 .join(format!("r-{}.sr", run.run_id));
6363 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
6364 if std::fs::hard_link(&source, &pinned).is_err() {
6365 crate::backup::copy_file_synced(&source, &pinned)?;
6366 }
6367 run_files.push(((*table_id, run.run_id), pinned, relative));
6368 }
6369 }
6370 crate::durable_file::sync_directory(&file_pin_root)?;
6371 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
6372 {
6373 let mut pins = self.backup_pins.lock();
6374 for key in &run_keys {
6375 *pins.entry(*key).or_insert(0) += 1;
6376 }
6377 }
6378 let _run_pins = RunPins {
6379 pins: Arc::clone(&self.backup_pins),
6380 runs: run_keys,
6381 };
6382 let _registry_pins: Vec<crate::retention::PinGuard> = table_guards
6389 .iter()
6390 .map(|table| {
6391 table
6392 .pin_registry()
6393 .pin(crate::retention::PinSource::BackupPitr, Epoch(epoch))
6394 })
6395 .collect();
6396 let deferred: HashSet<_> = run_files
6397 .iter()
6398 .map(|(_, _, relative)| relative.clone())
6399 .collect();
6400 let mut copied = Vec::new();
6401 copy_backup_boundary(
6402 &self.root,
6403 prepared.stage.as_deref().ok_or_else(|| {
6404 MongrelError::Other("backup staging root was already released".into())
6405 })?,
6406 &deferred,
6407 &mut copied,
6408 Some(control),
6409 )?;
6410
6411 drop(wal);
6412 drop(commit);
6413 drop(table_guards);
6414 drop(security);
6415 drop(ddl);
6416 drop(barrier);
6417
6418 if let Some(hook) = self.backup_hook.lock().as_ref() {
6419 hook();
6420 }
6421 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
6422 if index % 256 == 0 {
6423 control.checkpoint()?;
6424 }
6425 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
6426 prepared
6427 .stage
6428 .as_deref()
6429 .ok_or_else(|| {
6430 MongrelError::Other("backup staging root was already released".into())
6431 })?
6432 .copy_new_from(&relative, &mut source)?;
6433 copied.push(relative);
6434 }
6435
6436 let manifest = crate::backup::BackupManifest::create_controlled_durable(
6437 prepared.stage.as_deref().ok_or_else(|| {
6438 MongrelError::Other("backup staging root was already released".into())
6439 })?,
6440 epoch,
6441 &copied,
6442 control,
6443 )?;
6444 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
6445 MongrelError::Other("backup staging root was already released".into())
6446 })?)?;
6447 control.checkpoint()?;
6448 let publish = before_publish.take().ok_or_else(|| {
6449 MongrelError::Other("backup publication callback already consumed".into())
6450 })?;
6451 if !publish() {
6452 return Err(MongrelError::Cancelled);
6453 }
6454 let final_security = self.security_coordinator.gate.read();
6455 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6456 drop(prepared.stage.take().ok_or_else(|| {
6460 MongrelError::Other("backup staging root was already released".into())
6461 })?);
6462 let published = std::cell::Cell::new(false);
6463 if let Err(error) = prepared.parent.rename_directory_new_with_after(
6464 Path::new(&prepared.stage_name),
6465 &prepared.parent,
6466 Path::new(&prepared.destination_name),
6467 || published.set(true),
6468 ) {
6469 if published.get() {
6470 return Err(MongrelError::CommitOutcomeUnknown {
6471 epoch,
6472 message: format!("backup publication was not durable: {error}"),
6473 });
6474 }
6475 return Err(error.into());
6476 }
6477 drop(final_security);
6478 Ok(crate::backup::BackupReport {
6479 destination,
6480 epoch,
6481 boundary_unix_nanos,
6482 files: manifest.files.len(),
6483 bytes: manifest.total_bytes(),
6484 })
6485 })();
6486
6487 if outcome.is_err() {
6488 drop(prepared.stage.take());
6489 let _ = prepared
6490 .parent
6491 .remove_directory_all(Path::new(&prepared.stage_name));
6492 }
6493 outcome
6494 }
6495
6496 pub fn replication_batch_since(
6499 &self,
6500 since_epoch: u64,
6501 ) -> Result<crate::replication::ReplicationBatch> {
6502 use crate::wal::Op;
6503
6504 let admin = crate::auth::Permission::Admin;
6505 self.require(&admin)?;
6506 let _operation = self.admit_operation()?;
6508 let operation_principal = self.principal_snapshot();
6509
6510 let mut wal = self.shared_wal.lock();
6511 wal.group_sync()?;
6512 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6513 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
6514 drop(wal);
6515
6516 let commits: HashMap<u64, u64> = records
6517 .iter()
6518 .filter_map(|record| match &record.op {
6519 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
6520 _ => None,
6521 })
6522 .collect();
6523 let earliest_epoch = commits.values().copied().min();
6524 let current_epoch = commits
6525 .values()
6526 .copied()
6527 .max()
6528 .unwrap_or(0)
6529 .max(self.epoch.committed().0);
6530 let selected: HashSet<u64> = commits
6531 .iter()
6532 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
6533 .collect();
6534 let retention_gap = since_epoch < current_epoch
6535 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
6536 let spilled = records.iter().any(|record| {
6537 selected.contains(&record.txn_id)
6538 && matches!(
6539 &record.op,
6540 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
6541 )
6542 });
6543 let records = records
6544 .into_iter()
6545 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
6546 .filter(|record| selected.contains(&record.txn_id))
6547 .collect();
6548 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
6549 let batch = crate::replication::ReplicationBatch::complete_for_source(
6550 source_id,
6551 since_epoch,
6552 current_epoch,
6553 earliest_epoch,
6554 retention_gap,
6555 spilled,
6556 records,
6557 )?;
6558 if let Some(hook) = self.replication_hook.lock().as_ref() {
6559 hook();
6560 }
6561 let _security = self.security_coordinator.gate.read();
6562 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
6563 Ok(batch)
6564 }
6565
6566 pub fn append_replication_batch(
6570 &self,
6571 batch: &crate::replication::ReplicationBatch,
6572 ) -> Result<u64> {
6573 use crate::wal::Op;
6574
6575 if !self.read_only {
6576 return Err(MongrelError::InvalidArgument(
6577 "replication batches may only target a marked replica".into(),
6578 ));
6579 }
6580 let _operation = self.admit_operation()?;
6582 let current = crate::replication::replica_epoch(&self.root)?;
6583 if batch.is_source_bound() {
6584 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
6585 if batch.source_id != source_id {
6586 return Err(MongrelError::Conflict(
6587 "replication batch source does not match follower binding".into(),
6588 ));
6589 }
6590 }
6591 if batch.requires_snapshot {
6592 return Err(MongrelError::Conflict(
6593 "replication snapshot required for this batch".into(),
6594 ));
6595 }
6596 batch.validate_proof()?;
6597 if batch.from_epoch != current {
6598 if batch.from_epoch < current && batch.current_epoch == current {
6599 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6600 let _wal = self.shared_wal.lock();
6601 let existing: HashSet<(u64, u64)> =
6602 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
6603 .into_iter()
6604 .filter_map(|record| match record.op {
6605 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
6606 _ => None,
6607 })
6608 .collect();
6609 let already_applied = batch.records.iter().all(|record| match &record.op {
6610 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
6611 _ => true,
6612 });
6613 if already_applied {
6614 return Ok(current);
6615 }
6616 }
6617 return Err(MongrelError::Conflict(format!(
6618 "replication batch starts at epoch {}, follower is at epoch {current}",
6619 batch.from_epoch
6620 )));
6621 }
6622 if batch.current_epoch < current {
6623 return Err(MongrelError::InvalidArgument(format!(
6624 "replication batch current epoch {} precedes follower epoch {current}",
6625 batch.current_epoch
6626 )));
6627 }
6628 let records = &batch.records;
6629 let mut commits = HashMap::new();
6630 let mut commit_epochs = HashSet::new();
6631 let mut commit_timestamps = HashMap::new();
6632 for record in records {
6633 match &record.op {
6634 Op::TxnCommit { epoch, added_runs } => {
6635 if !added_runs.is_empty() {
6636 return Err(MongrelError::Conflict(
6637 "replication snapshot required for spilled-run transaction".into(),
6638 ));
6639 }
6640 if commits.insert(record.txn_id, *epoch).is_some() {
6641 return Err(MongrelError::InvalidArgument(format!(
6642 "duplicate commit for replication transaction {}",
6643 record.txn_id
6644 )));
6645 }
6646 if *epoch <= current || *epoch > batch.current_epoch {
6647 return Err(MongrelError::InvalidArgument(format!(
6648 "replication commit epoch {epoch} is outside ({current}, {}]",
6649 batch.current_epoch
6650 )));
6651 }
6652 if !commit_epochs.insert(*epoch) {
6653 return Err(MongrelError::InvalidArgument(format!(
6654 "duplicate replication commit epoch {epoch}"
6655 )));
6656 }
6657 }
6658 Op::CommitTimestamp { unix_nanos } => {
6659 commit_timestamps.insert(record.txn_id, *unix_nanos);
6660 }
6661 _ => {}
6662 }
6663 }
6664 for record in records {
6665 if record.txn_id == crate::wal::SYSTEM_TXN_ID
6666 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
6667 {
6668 return Err(MongrelError::InvalidArgument(
6669 "replication batch contains a non-committed record".into(),
6670 ));
6671 }
6672 if !commits.contains_key(&record.txn_id) {
6673 return Err(MongrelError::InvalidArgument(format!(
6674 "incomplete replication transaction {}",
6675 record.txn_id
6676 )));
6677 }
6678 }
6679 let target_epoch = commits
6680 .values()
6681 .copied()
6682 .filter(|epoch| *epoch > current)
6683 .max()
6684 .unwrap_or(current);
6685 if target_epoch != batch.current_epoch {
6686 return Err(MongrelError::InvalidArgument(format!(
6687 "replication batch ends at epoch {target_epoch}, expected {}",
6688 batch.current_epoch
6689 )));
6690 }
6691 let mut selected: HashSet<u64> = commits
6692 .iter()
6693 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
6694 .collect();
6695 if selected.is_empty() {
6696 return Ok(current);
6697 }
6698 let mut wal = self.shared_wal.lock();
6699 wal.group_sync()?;
6700 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6701 let existing: HashSet<(u64, u64)> =
6702 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
6703 .into_iter()
6704 .filter_map(|record| match record.op {
6705 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
6706 _ => None,
6707 })
6708 .collect();
6709 selected.retain(|txn_id| {
6710 commits
6711 .get(txn_id)
6712 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
6713 });
6714 for record in records {
6715 if !selected.contains(&record.txn_id) {
6716 continue;
6717 }
6718 match &record.op {
6719 Op::TxnCommit { epoch, added_runs } => {
6720 let timestamp = commit_timestamps
6721 .get(&record.txn_id)
6722 .copied()
6723 .unwrap_or_else(current_unix_nanos);
6724 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
6725 }
6726 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
6727 op => {
6728 wal.append(record.txn_id, 0, op.clone())?;
6729 }
6730 }
6731 }
6732 if !selected.is_empty() {
6733 wal.group_sync()?;
6734 }
6735 drop(wal);
6736
6737 let mut recovered_catalog = self.catalog.read().clone();
6741 if let Err(error) = recover_ddl_from_wal(
6742 &self.root,
6743 Some(&self.durable_root),
6744 &mut recovered_catalog,
6745 self.meta_dek.as_ref(),
6746 wal_dek.as_ref(),
6747 true,
6748 None,
6749 ) {
6750 return Err(MongrelError::DurableCommit {
6751 epoch: target_epoch,
6752 message: format!(
6753 "replication WAL is durable but catalog checkpoint failed: {error}"
6754 ),
6755 });
6756 }
6757 let _security = self.security_coordinator.gate.write();
6758 let old_security_version = self.catalog.read().security_version;
6759 let security_changed = old_security_version != recovered_catalog.security_version
6760 || self.catalog.read().require_auth != recovered_catalog.require_auth;
6761 let require_auth = recovered_catalog.require_auth;
6762 let principal = if security_changed {
6763 None
6764 } else {
6765 self.principal.read().as_ref().and_then(|principal| {
6766 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
6767 })
6768 };
6769 if require_auth {
6770 self.auth_state.set_require_auth(true);
6771 }
6772 self.auth_state.set_principal(principal.clone());
6773 *self.principal.write() = principal;
6774 let security_version = recovered_catalog.security_version;
6775 *self.catalog.write() = recovered_catalog;
6776 self.security_coordinator
6777 .version
6778 .store(security_version, Ordering::Release);
6779 if !require_auth {
6780 self.auth_state.set_require_auth(false);
6781 }
6782 if let Err(error) =
6783 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
6784 {
6785 return Err(MongrelError::DurableCommit {
6786 epoch: target_epoch,
6787 message: format!(
6788 "replication WAL and catalog are durable but follower watermark failed: {error}"
6789 ),
6790 });
6791 }
6792 Ok(target_epoch)
6793 }
6794
6795 pub fn table_id(&self, name: &str) -> Result<u64> {
6798 let cat = self.catalog.read();
6799 cat.live(name)
6800 .map(|e| e.table_id)
6801 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
6802 }
6803
6804 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
6809 let catalog = self.catalog.read();
6810 catalog
6811 .live(name)
6812 .map(|entry| (entry.table_id, entry.schema.schema_id))
6813 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
6814 }
6815
6816 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
6817 self.catalog
6818 .read()
6819 .building(name)
6820 .map(|entry| entry.table_id)
6821 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
6822 }
6823
6824 pub fn procedures(&self) -> Vec<StoredProcedure> {
6825 self.catalog
6826 .read()
6827 .procedures
6828 .iter()
6829 .map(|p| p.procedure.clone())
6830 .collect()
6831 }
6832
6833 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
6834 self.catalog
6835 .read()
6836 .procedures
6837 .iter()
6838 .find(|p| p.procedure.name == name)
6839 .map(|p| p.procedure.clone())
6840 }
6841
6842 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
6843 self.create_procedure_inner(procedure, None)
6844 }
6845
6846 pub fn create_procedure_controlled<F>(
6847 &self,
6848 procedure: StoredProcedure,
6849 mut before_publish: F,
6850 ) -> Result<StoredProcedure>
6851 where
6852 F: FnMut() -> Result<()>,
6853 {
6854 self.create_procedure_inner(procedure, Some(&mut before_publish))
6855 }
6856
6857 fn create_procedure_inner(
6858 &self,
6859 mut procedure: StoredProcedure,
6860 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6861 ) -> Result<StoredProcedure> {
6862 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
6863 procedure: procedure.clone(),
6864 };
6865 self.require(&crate::catalog_cmds::required_permission(&command))?;
6866 let _g = self.ddl_lock.lock();
6867 let _security_write = self.security_write()?;
6868 self.require(&crate::catalog_cmds::required_permission(&command))?;
6869 procedure.validate()?;
6870 self.validate_procedure_references(&procedure)?;
6871 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
6875 let commit_lock = Arc::clone(&self.commit_lock);
6876 let _c = commit_lock.lock();
6877 let epoch = self.epoch.bump_assigned();
6878 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6879 procedure.created_epoch = epoch.0;
6880 procedure.updated_epoch = epoch.0;
6881 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
6882 procedure: procedure.clone(),
6883 };
6884 let mut next_catalog = self.catalog.read().clone();
6885 self.apply_catalog_command_to(&mut next_catalog, command)?;
6886 next_catalog.db_epoch = epoch.0;
6887 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6888 Ok(procedure)
6889 }
6890
6891 pub fn create_or_replace_procedure(
6892 &self,
6893 procedure: StoredProcedure,
6894 ) -> Result<StoredProcedure> {
6895 self.create_or_replace_procedure_inner(procedure, None)
6896 }
6897
6898 pub fn create_or_replace_procedure_controlled<F>(
6899 &self,
6900 procedure: StoredProcedure,
6901 mut before_publish: F,
6902 ) -> Result<StoredProcedure>
6903 where
6904 F: FnMut() -> Result<()>,
6905 {
6906 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
6907 }
6908
6909 fn create_or_replace_procedure_inner(
6910 &self,
6911 procedure: StoredProcedure,
6912 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6913 ) -> Result<StoredProcedure> {
6914 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
6915 procedure: procedure.clone(),
6916 };
6917 self.require(&crate::catalog_cmds::required_permission(&command))?;
6918 let _g = self.ddl_lock.lock();
6919 let _security_write = self.security_write()?;
6920 self.require(&crate::catalog_cmds::required_permission(&command))?;
6921 procedure.validate()?;
6922 self.validate_procedure_references(&procedure)?;
6923 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
6927 let commit_lock = Arc::clone(&self.commit_lock);
6928 let _c = commit_lock.lock();
6929 let epoch = self.epoch.bump_assigned();
6930 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6931 let mut next_catalog = self.catalog.read().clone();
6932 let replaced = match next_catalog
6937 .procedures
6938 .iter()
6939 .position(|p| p.procedure.name == procedure.name)
6940 {
6941 Some(idx) => next_catalog.procedures[idx]
6942 .procedure
6943 .replaced(procedure.clone(), epoch.0)?,
6944 None => {
6945 let mut next = procedure;
6946 next.created_epoch = epoch.0;
6947 next.updated_epoch = epoch.0;
6948 next
6949 }
6950 };
6951 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
6952 procedure: replaced.clone(),
6953 };
6954 self.apply_catalog_command_to(&mut next_catalog, command)?;
6955 next_catalog.db_epoch = epoch.0;
6956 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6957 Ok(replaced)
6958 }
6959
6960 pub fn drop_procedure(&self, name: &str) -> Result<()> {
6961 self.drop_procedure_with_epoch(name).map(|_| ())
6962 }
6963
6964 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
6965 self.drop_procedure_with_epoch_inner(name, None)
6966 }
6967
6968 pub fn drop_procedure_with_epoch_controlled<F>(
6969 &self,
6970 name: &str,
6971 mut before_publish: F,
6972 ) -> Result<Epoch>
6973 where
6974 F: FnMut() -> Result<()>,
6975 {
6976 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
6977 }
6978
6979 fn drop_procedure_with_epoch_inner(
6980 &self,
6981 name: &str,
6982 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6983 ) -> Result<Epoch> {
6984 let command = crate::catalog_cmds::CatalogCommand::DropProcedure {
6985 name: name.to_string(),
6986 };
6987 self.require(&crate::catalog_cmds::required_permission(&command))?;
6988 let _g = self.ddl_lock.lock();
6989 let _security_write = self.security_write()?;
6990 self.require(&crate::catalog_cmds::required_permission(&command))?;
6991 let commit_lock = Arc::clone(&self.commit_lock);
6992 let _c = commit_lock.lock();
6993 let epoch = self.epoch.bump_assigned();
6994 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6995 let mut next_catalog = self.catalog.read().clone();
6996 self.apply_catalog_command_to(&mut next_catalog, command)?;
6997 next_catalog.db_epoch = epoch.0;
6998 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6999 Ok(epoch)
7000 }
7001
7002 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
7007 self.catalog.read().users.clone()
7008 }
7009
7010 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
7013 self.catalog
7014 .read()
7015 .users
7016 .iter()
7017 .find(|user| user.username == username)
7018 .map(|user| (user.id, user.created_epoch))
7019 }
7020
7021 pub fn security_version(&self) -> u64 {
7024 self.catalog.read().security_version
7025 }
7026
7027 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
7029 self.catalog.read().roles.clone()
7030 }
7031
7032 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
7034 self.require(&crate::auth::Permission::Admin)?;
7035 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
7036 self.create_user_with_password_hash(username, hash)
7037 }
7038
7039 pub fn create_user_with_password_hash(
7041 &self,
7042 username: &str,
7043 hash: String,
7044 ) -> Result<crate::auth::UserEntry> {
7045 self.create_user_with_password_hash_inner(username, hash, None)
7046 }
7047
7048 pub fn create_user_with_password_hash_controlled<F>(
7049 &self,
7050 username: &str,
7051 hash: String,
7052 mut before_publish: F,
7053 ) -> Result<crate::auth::UserEntry>
7054 where
7055 F: FnMut() -> Result<()>,
7056 {
7057 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
7058 }
7059
7060 fn create_user_with_password_hash_inner(
7061 &self,
7062 username: &str,
7063 hash: String,
7064 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7065 ) -> Result<crate::auth::UserEntry> {
7066 let command = crate::catalog_cmds::CatalogCommand::CreateUser {
7070 username: username.to_string(),
7071 password_hash: hash.clone(),
7072 is_admin: false,
7073 created_epoch: 0, };
7075 self.require(&crate::catalog_cmds::required_permission(&command))?;
7076 let _ddl = self.ddl_lock.lock();
7077 let _security_write = self.security_write()?;
7078 self.require(&crate::catalog_cmds::required_permission(&command))?;
7079 let _commit = self.commit_lock.lock();
7080 let epoch = self.epoch.bump_assigned();
7081 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7082 let command = crate::catalog_cmds::CatalogCommand::CreateUser {
7083 username: username.to_string(),
7084 password_hash: hash,
7085 is_admin: false,
7086 created_epoch: epoch.0,
7087 };
7088 let mut next_catalog = self.catalog.read().clone();
7089 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
7090 let crate::catalog_cmds::CatalogDelta::UserUpserted(entry) = delta else {
7091 return Err(MongrelError::Other(
7092 "CreateUser resolved to an unexpected catalog delta".into(),
7093 ));
7094 };
7095 next_catalog.db_epoch = epoch.0;
7096 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7097 Ok(entry)
7098 }
7099
7100 pub fn drop_user(&self, username: &str) -> Result<()> {
7102 self.drop_user_with_epoch(username).map(|_| ())
7103 }
7104
7105 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
7106 self.drop_user_with_epoch_inner(username, None)
7107 }
7108
7109 pub fn drop_user_with_epoch_controlled<F>(
7110 &self,
7111 username: &str,
7112 mut before_publish: F,
7113 ) -> Result<Epoch>
7114 where
7115 F: FnMut() -> Result<()>,
7116 {
7117 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
7118 }
7119
7120 fn drop_user_with_epoch_inner(
7121 &self,
7122 username: &str,
7123 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7124 ) -> Result<Epoch> {
7125 let command = crate::catalog_cmds::CatalogCommand::DropUser {
7126 username: username.to_string(),
7127 };
7128 self.require(&crate::catalog_cmds::required_permission(&command))?;
7129 let _ddl = self.ddl_lock.lock();
7130 let _security_write = self.security_write()?;
7131 self.require(&crate::catalog_cmds::required_permission(&command))?;
7132 let _commit = self.commit_lock.lock();
7133 let epoch = self.epoch.bump_assigned();
7134 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7135 let mut next_catalog = self.catalog.read().clone();
7136 self.apply_catalog_command_to(&mut next_catalog, command)?;
7137 next_catalog.db_epoch = epoch.0;
7138 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7139 Ok(epoch)
7140 }
7141
7142 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
7144 self.alter_user_password_with_epoch(username, new_password)
7145 .map(|_| ())
7146 }
7147
7148 pub fn alter_user_password_with_epoch(
7149 &self,
7150 username: &str,
7151 new_password: &str,
7152 ) -> Result<Epoch> {
7153 self.require(&crate::auth::Permission::Admin)?;
7154 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
7155 self.alter_user_password_hash_with_epoch(username, hash)
7156 }
7157
7158 pub fn alter_user_password_hash_with_epoch(
7159 &self,
7160 username: &str,
7161 hash: String,
7162 ) -> Result<Epoch> {
7163 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
7164 }
7165
7166 pub fn alter_user_password_hash_with_epoch_controlled<F>(
7167 &self,
7168 username: &str,
7169 hash: String,
7170 mut before_publish: F,
7171 ) -> Result<Epoch>
7172 where
7173 F: FnMut() -> Result<()>,
7174 {
7175 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
7176 }
7177
7178 fn alter_user_password_hash_with_epoch_inner(
7179 &self,
7180 username: &str,
7181 hash: String,
7182 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7183 ) -> Result<Epoch> {
7184 let command = crate::catalog_cmds::CatalogCommand::AlterUserPassword {
7185 username: username.to_string(),
7186 password_hash: hash,
7187 };
7188 self.require(&crate::catalog_cmds::required_permission(&command))?;
7189 let _ddl = self.ddl_lock.lock();
7190 let _security_write = self.security_write()?;
7191 self.require(&crate::catalog_cmds::required_permission(&command))?;
7192 let _commit = self.commit_lock.lock();
7193 let epoch = self.epoch.bump_assigned();
7194 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7195 let mut next_catalog = self.catalog.read().clone();
7196 self.apply_catalog_command_to(&mut next_catalog, command)?;
7197 next_catalog.db_epoch = epoch.0;
7198 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7199 Ok(epoch)
7200 }
7201
7202 pub fn verify_user(
7205 &self,
7206 username: &str,
7207 password: &str,
7208 ) -> Result<Option<crate::auth::UserEntry>> {
7209 let cat = self.catalog.read();
7210 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
7211 return Ok(None);
7212 };
7213 if user.password_hash.is_empty() {
7214 return Ok(None);
7215 }
7216 let ok = crate::auth::verify_password(password, &user.password_hash)
7217 .map_err(MongrelError::Other)?;
7218 if ok {
7219 Ok(Some(user.clone()))
7220 } else {
7221 Ok(None)
7222 }
7223 }
7224
7225 pub fn authenticate_principal(
7229 &self,
7230 username: &str,
7231 password: &str,
7232 ) -> Result<Option<crate::auth::Principal>> {
7233 self.authenticate_principal_inner(username, password, || {})
7234 }
7235
7236 fn authenticate_principal_inner<F>(
7237 &self,
7238 username: &str,
7239 password: &str,
7240 after_verify: F,
7241 ) -> Result<Option<crate::auth::Principal>>
7242 where
7243 F: FnOnce(),
7244 {
7245 let catalog = self.catalog.read();
7246 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
7247 return Ok(None);
7248 };
7249 if user.password_hash.is_empty()
7250 || !crate::auth::verify_password(password, &user.password_hash)
7251 .map_err(MongrelError::Other)?
7252 {
7253 return Ok(None);
7254 }
7255 after_verify();
7256 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
7257 }
7258
7259 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
7261 self.set_user_admin_with_epoch(username, is_admin)
7262 .map(|_| ())
7263 }
7264
7265 pub fn set_user_admin_with_epoch(
7266 &self,
7267 username: &str,
7268 is_admin: bool,
7269 ) -> Result<Option<Epoch>> {
7270 self.set_user_admin_with_epoch_inner(username, is_admin, None)
7271 }
7272
7273 pub fn set_user_admin_with_epoch_controlled<F>(
7274 &self,
7275 username: &str,
7276 is_admin: bool,
7277 mut before_publish: F,
7278 ) -> Result<Option<Epoch>>
7279 where
7280 F: FnMut() -> Result<()>,
7281 {
7282 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
7283 }
7284
7285 fn set_user_admin_with_epoch_inner(
7286 &self,
7287 username: &str,
7288 is_admin: bool,
7289 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7290 ) -> Result<Option<Epoch>> {
7291 let command = crate::catalog_cmds::CatalogCommand::SetUserAdmin {
7292 username: username.to_string(),
7293 is_admin,
7294 };
7295 self.require(&crate::catalog_cmds::required_permission(&command))?;
7296 let _ddl = self.ddl_lock.lock();
7297 let _security_write = self.security_write()?;
7298 self.require(&crate::catalog_cmds::required_permission(&command))?;
7299 let _commit = self.commit_lock.lock();
7300 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7304 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7305 return Ok(None);
7306 }
7307 let epoch = self.epoch.bump_assigned();
7308 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7309 let mut next_catalog = self.catalog.read().clone();
7310 self.apply_catalog_command_to(&mut next_catalog, command)?;
7311 next_catalog.db_epoch = epoch.0;
7312 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7313 Ok(Some(epoch))
7314 }
7315
7316 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
7318 self.create_role_inner(name, None)
7319 }
7320
7321 pub fn create_role_controlled<F>(
7322 &self,
7323 name: &str,
7324 mut before_publish: F,
7325 ) -> Result<crate::auth::RoleEntry>
7326 where
7327 F: FnMut() -> Result<()>,
7328 {
7329 self.create_role_inner(name, Some(&mut before_publish))
7330 }
7331
7332 fn create_role_inner(
7333 &self,
7334 name: &str,
7335 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7336 ) -> Result<crate::auth::RoleEntry> {
7337 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
7338 name: name.to_string(),
7339 created_epoch: 0, };
7341 self.require(&crate::catalog_cmds::required_permission(&command))?;
7342 let _ddl = self.ddl_lock.lock();
7343 let _security_write = self.security_write()?;
7344 self.require(&crate::catalog_cmds::required_permission(&command))?;
7345 let _commit = self.commit_lock.lock();
7346 let epoch = self.epoch.bump_assigned();
7347 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7348 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
7349 name: name.to_string(),
7350 created_epoch: epoch.0,
7351 };
7352 let mut next_catalog = self.catalog.read().clone();
7353 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
7354 let crate::catalog_cmds::CatalogDelta::RoleUpserted(entry) = delta else {
7355 return Err(MongrelError::Other(
7356 "CreateRole resolved to an unexpected catalog delta".into(),
7357 ));
7358 };
7359 next_catalog.db_epoch = epoch.0;
7360 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7361 Ok(entry)
7362 }
7363
7364 pub fn drop_role(&self, name: &str) -> Result<()> {
7366 self.drop_role_with_epoch(name).map(|_| ())
7367 }
7368
7369 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
7370 self.drop_role_with_epoch_inner(name, None)
7371 }
7372
7373 pub fn drop_role_with_epoch_controlled<F>(
7374 &self,
7375 name: &str,
7376 mut before_publish: F,
7377 ) -> Result<Epoch>
7378 where
7379 F: FnMut() -> Result<()>,
7380 {
7381 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
7382 }
7383
7384 fn drop_role_with_epoch_inner(
7385 &self,
7386 name: &str,
7387 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7388 ) -> Result<Epoch> {
7389 let command = crate::catalog_cmds::CatalogCommand::DropRole {
7390 name: name.to_string(),
7391 };
7392 self.require(&crate::catalog_cmds::required_permission(&command))?;
7393 let _ddl = self.ddl_lock.lock();
7394 let _security_write = self.security_write()?;
7395 self.require(&crate::catalog_cmds::required_permission(&command))?;
7396 let _commit = self.commit_lock.lock();
7397 let epoch = self.epoch.bump_assigned();
7398 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7399 let mut next_catalog = self.catalog.read().clone();
7400 self.apply_catalog_command_to(&mut next_catalog, command)?;
7401 next_catalog.db_epoch = epoch.0;
7402 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7403 Ok(epoch)
7404 }
7405
7406 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
7408 self.grant_role_with_epoch(username, role_name).map(|_| ())
7409 }
7410
7411 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
7412 self.grant_role_with_epoch_inner(username, role_name, None)
7413 }
7414
7415 pub fn grant_role_with_epoch_controlled<F>(
7416 &self,
7417 username: &str,
7418 role_name: &str,
7419 mut before_publish: F,
7420 ) -> Result<Option<Epoch>>
7421 where
7422 F: FnMut() -> Result<()>,
7423 {
7424 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
7425 }
7426
7427 fn grant_role_with_epoch_inner(
7428 &self,
7429 username: &str,
7430 role_name: &str,
7431 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7432 ) -> Result<Option<Epoch>> {
7433 let command = crate::catalog_cmds::CatalogCommand::GrantRole {
7434 username: username.to_string(),
7435 role: role_name.to_string(),
7436 };
7437 self.require(&crate::catalog_cmds::required_permission(&command))?;
7438 let _ddl = self.ddl_lock.lock();
7439 let _security_write = self.security_write()?;
7440 self.require(&crate::catalog_cmds::required_permission(&command))?;
7441 let _commit = self.commit_lock.lock();
7442 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7446 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7447 return Ok(None);
7448 }
7449 let epoch = self.epoch.bump_assigned();
7450 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7451 let mut next_catalog = self.catalog.read().clone();
7452 self.apply_catalog_command_to(&mut next_catalog, command)?;
7453 next_catalog.db_epoch = epoch.0;
7454 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7455 Ok(Some(epoch))
7456 }
7457
7458 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
7460 self.revoke_role_with_epoch(username, role_name).map(|_| ())
7461 }
7462
7463 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
7464 self.revoke_role_with_epoch_inner(username, role_name, None)
7465 }
7466
7467 pub fn revoke_role_with_epoch_controlled<F>(
7468 &self,
7469 username: &str,
7470 role_name: &str,
7471 mut before_publish: F,
7472 ) -> Result<Option<Epoch>>
7473 where
7474 F: FnMut() -> Result<()>,
7475 {
7476 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
7477 }
7478
7479 fn revoke_role_with_epoch_inner(
7480 &self,
7481 username: &str,
7482 role_name: &str,
7483 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7484 ) -> Result<Option<Epoch>> {
7485 let command = crate::catalog_cmds::CatalogCommand::RevokeRole {
7486 username: username.to_string(),
7487 role: role_name.to_string(),
7488 };
7489 self.require(&crate::catalog_cmds::required_permission(&command))?;
7490 let _ddl = self.ddl_lock.lock();
7491 let _security_write = self.security_write()?;
7492 self.require(&crate::catalog_cmds::required_permission(&command))?;
7493 let _commit = self.commit_lock.lock();
7494 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7498 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7499 return Ok(None);
7500 }
7501 let epoch = self.epoch.bump_assigned();
7502 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7503 let mut next_catalog = self.catalog.read().clone();
7504 self.apply_catalog_command_to(&mut next_catalog, command)?;
7505 next_catalog.db_epoch = epoch.0;
7506 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7507 Ok(Some(epoch))
7508 }
7509
7510 pub fn grant_permission(
7512 &self,
7513 role_name: &str,
7514 permission: crate::auth::Permission,
7515 ) -> Result<()> {
7516 self.grant_permission_with_epoch(role_name, permission)
7517 .map(|_| ())
7518 }
7519
7520 pub fn grant_permission_with_epoch(
7521 &self,
7522 role_name: &str,
7523 permission: crate::auth::Permission,
7524 ) -> Result<Option<Epoch>> {
7525 self.grant_permission_with_epoch_inner(role_name, permission, None)
7526 }
7527
7528 pub fn grant_permission_with_epoch_controlled<F>(
7529 &self,
7530 role_name: &str,
7531 permission: crate::auth::Permission,
7532 mut before_publish: F,
7533 ) -> Result<Option<Epoch>>
7534 where
7535 F: FnMut() -> Result<()>,
7536 {
7537 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
7538 }
7539
7540 fn grant_permission_with_epoch_inner(
7541 &self,
7542 role_name: &str,
7543 permission: crate::auth::Permission,
7544 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7545 ) -> Result<Option<Epoch>> {
7546 let command = crate::catalog_cmds::CatalogCommand::GrantPermission {
7547 role: role_name.to_string(),
7548 permission,
7549 };
7550 self.require(&crate::catalog_cmds::required_permission(&command))?;
7551 let _ddl = self.ddl_lock.lock();
7552 let _security_write = self.security_write()?;
7553 self.require(&crate::catalog_cmds::required_permission(&command))?;
7554 let _commit = self.commit_lock.lock();
7555 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7559 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7560 return Ok(None);
7561 }
7562 let epoch = self.epoch.bump_assigned();
7563 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7564 let mut next_catalog = self.catalog.read().clone();
7565 self.apply_catalog_command_to(&mut next_catalog, command)?;
7566 next_catalog.db_epoch = epoch.0;
7567 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7568 Ok(Some(epoch))
7569 }
7570
7571 pub fn revoke_permission(
7573 &self,
7574 role_name: &str,
7575 permission: crate::auth::Permission,
7576 ) -> Result<()> {
7577 self.revoke_permission_with_epoch(role_name, permission)
7578 .map(|_| ())
7579 }
7580
7581 pub fn revoke_permission_with_epoch(
7582 &self,
7583 role_name: &str,
7584 permission: crate::auth::Permission,
7585 ) -> Result<Option<Epoch>> {
7586 self.revoke_permission_with_epoch_inner(role_name, permission, None)
7587 }
7588
7589 pub fn revoke_permission_with_epoch_controlled<F>(
7590 &self,
7591 role_name: &str,
7592 permission: crate::auth::Permission,
7593 mut before_publish: F,
7594 ) -> Result<Option<Epoch>>
7595 where
7596 F: FnMut() -> Result<()>,
7597 {
7598 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
7599 }
7600
7601 fn revoke_permission_with_epoch_inner(
7602 &self,
7603 role_name: &str,
7604 permission: crate::auth::Permission,
7605 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7606 ) -> Result<Option<Epoch>> {
7607 let command = crate::catalog_cmds::CatalogCommand::RevokePermission {
7608 role: role_name.to_string(),
7609 permission,
7610 };
7611 self.require(&crate::catalog_cmds::required_permission(&command))?;
7612 let _ddl = self.ddl_lock.lock();
7613 let _security_write = self.security_write()?;
7614 self.require(&crate::catalog_cmds::required_permission(&command))?;
7615 let _commit = self.commit_lock.lock();
7616 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7620 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
7621 return Ok(None);
7622 }
7623 let epoch = self.epoch.bump_assigned();
7624 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7625 let mut next_catalog = self.catalog.read().clone();
7626 self.apply_catalog_command_to(&mut next_catalog, command)?;
7627 next_catalog.db_epoch = epoch.0;
7628 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7629 Ok(Some(epoch))
7630 }
7631
7632 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
7635 let cat = self.catalog.read();
7636 Self::resolve_principal_from_catalog(&cat, username)
7637 }
7638
7639 pub fn resolve_current_principal(
7642 &self,
7643 principal: &crate::auth::Principal,
7644 ) -> Option<crate::auth::Principal> {
7645 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
7646 }
7647
7648 fn resolve_principal_from_catalog(
7653 cat: &Catalog,
7654 username: &str,
7655 ) -> Option<crate::auth::Principal> {
7656 let user = cat.users.iter().find(|u| u.username == username)?;
7657 Self::resolve_user_principal_from_catalog(cat, user)
7658 }
7659
7660 fn resolve_bound_principal_from_catalog(
7661 cat: &Catalog,
7662 principal: &crate::auth::Principal,
7663 ) -> Option<crate::auth::Principal> {
7664 let user = cat.users.iter().find(|user| {
7665 user.id == principal.user_id
7666 && user.created_epoch == principal.created_epoch
7667 && user.username == principal.username
7668 })?;
7669 Self::resolve_user_principal_from_catalog(cat, user)
7670 }
7671
7672 fn resolve_user_principal_from_catalog(
7673 cat: &Catalog,
7674 user: &crate::auth::UserEntry,
7675 ) -> Option<crate::auth::Principal> {
7676 let mut permissions = Vec::new();
7677 for role_name in &user.roles {
7678 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
7679 permissions.extend(role.permissions.iter().cloned());
7680 }
7681 }
7682 Some(crate::auth::Principal {
7683 user_id: user.id,
7684 created_epoch: user.created_epoch,
7685 username: user.username.clone(),
7686 is_admin: user.is_admin,
7687 roles: user.roles.clone(),
7688 permissions,
7689 })
7690 }
7691
7692 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
7694 match self.resolve_principal(username) {
7695 Some(p) => p.has_permission(permission),
7696 None => false,
7697 }
7698 }
7699
7700 pub fn require_auth_enabled(&self) -> bool {
7704 self.catalog.read().require_auth
7705 }
7706
7707 pub fn principal(&self) -> Option<crate::auth::Principal> {
7711 self.principal.read().clone()
7712 }
7713
7714 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
7720 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
7721 self.auth_state.clone(),
7722 )))
7723 }
7724
7725 pub fn refresh_principal(&self) -> Result<()> {
7739 let previous = match self.principal.read().clone() {
7740 Some(principal) => principal,
7741 None => return Ok(()),
7742 };
7743 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
7744 self.refresh_security_catalog_if_stale(observed_version)?;
7745 let cat = self.catalog.read();
7746 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
7747 Some(p) => {
7748 *self.principal.write() = Some(p.clone());
7749 self.auth_state.set_principal(Some(p));
7753 Ok(())
7754 }
7755 None => Err(MongrelError::InvalidCredentials {
7756 username: previous.username,
7757 }),
7758 }
7759 }
7760
7761 pub fn security_catalog_disk_read_count(&self) -> u64 {
7764 self.security_catalog_disk_reads.load(Ordering::Relaxed)
7765 }
7766
7767 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
7779 if self.shared {
7780 return Err(MongrelError::Conflict(
7786 "enable_auth requires an exclusive Database owner; shared cores reject \
7787 auth-mode transitions"
7788 .into(),
7789 ));
7790 }
7791 let password_hash =
7792 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
7793 let _ddl = self.ddl_lock.lock();
7794 let _security_write = self.security_write()?;
7795 let _commit = self.commit_lock.lock();
7796 let epoch = self.epoch.bump_assigned();
7797 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7798 let mut next_catalog = self.catalog.read().clone();
7799 if next_catalog.require_auth {
7800 return Err(MongrelError::InvalidArgument(
7801 "database already has require_auth enabled".into(),
7802 ));
7803 }
7804 if next_catalog
7805 .users
7806 .iter()
7807 .any(|u| u.username == admin_username)
7808 {
7809 return Err(MongrelError::InvalidArgument(format!(
7810 "user {admin_username:?} already exists"
7811 )));
7812 }
7813 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
7814 let id = next_catalog.next_user_id;
7815 next_catalog.next_user_id = id
7816 .checked_add(1)
7817 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
7818 next_catalog.users.push(crate::auth::UserEntry {
7819 id,
7820 username: admin_username.to_string(),
7821 password_hash,
7822 roles: Vec::new(),
7823 is_admin: true,
7824 created_epoch: epoch.0,
7825 });
7826 next_catalog.require_auth = true;
7827 advance_security_version(&mut next_catalog)?;
7828 next_catalog.db_epoch = epoch.0;
7829 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
7830 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
7834 let principal = crate::auth::Principal {
7835 user_id: id,
7836 created_epoch: epoch.0,
7837 username: admin_username.to_string(),
7838 is_admin: true,
7839 roles: Vec::new(),
7840 permissions: Vec::new(),
7841 };
7842 *self.principal.write() = Some(principal.clone());
7843 self.auth_state.set_principal(Some(principal));
7844 }
7845 publish
7846 }
7847
7848 pub fn disable_auth(&self) -> Result<()> {
7865 let _ddl = self.ddl_lock.lock();
7866 let _security_write = self.security_write()?;
7867 let _commit = self.commit_lock.lock();
7868 let epoch = self.epoch.bump_assigned();
7869 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7870 let mut next_catalog = self.catalog.read().clone();
7871 if !next_catalog.require_auth {
7872 return Err(MongrelError::InvalidArgument(
7873 "database does not have require_auth enabled".into(),
7874 ));
7875 }
7876 next_catalog.require_auth = false;
7877 advance_security_version(&mut next_catalog)?;
7878 next_catalog.db_epoch = epoch.0;
7879 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
7880 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
7882 *self.principal.write() = None;
7883 }
7884 publish
7885 }
7886
7887 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
7894 self.ensure_owner_process()?;
7895 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
7896 return Err(MongrelError::ReadOnlyReplica);
7897 }
7898 if self.principal.read().is_some() {
7899 self.refresh_principal().map_err(|error| match error {
7900 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
7901 error => error,
7902 })?;
7903 }
7904 if !self.catalog.read().require_auth {
7905 return Ok(());
7906 }
7907 let guard = self.principal.read();
7908 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
7909 if p.has_permission(perm) {
7910 Ok(())
7911 } else {
7912 Err(MongrelError::PermissionDenied {
7913 required: perm.clone(),
7914 principal: p.username.clone(),
7915 })
7916 }
7917 }
7918
7919 pub fn require_table(
7924 &self,
7925 table: &str,
7926 perm: crate::auth_state::RequiredPermission,
7927 ) -> Result<()> {
7928 self.require(&perm.into_permission(table))
7929 }
7930
7931 pub fn triggers(&self) -> Vec<StoredTrigger> {
7932 self.catalog
7933 .read()
7934 .triggers
7935 .iter()
7936 .map(|t| t.trigger.clone())
7937 .collect()
7938 }
7939
7940 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
7941 self.catalog
7942 .read()
7943 .triggers
7944 .iter()
7945 .find(|t| t.trigger.name == name)
7946 .map(|t| t.trigger.clone())
7947 }
7948
7949 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
7950 self.create_trigger_inner(trigger, None, None)
7951 }
7952
7953 pub fn create_trigger_controlled<F>(
7954 &self,
7955 trigger: StoredTrigger,
7956 mut before_publish: F,
7957 ) -> Result<StoredTrigger>
7958 where
7959 F: FnMut() -> Result<()>,
7960 {
7961 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
7962 }
7963
7964 pub fn create_trigger_as_controlled<F>(
7965 &self,
7966 trigger: StoredTrigger,
7967 principal: Option<&crate::auth::Principal>,
7968 mut before_publish: F,
7969 ) -> Result<StoredTrigger>
7970 where
7971 F: FnMut() -> Result<()>,
7972 {
7973 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
7974 }
7975
7976 fn create_trigger_inner(
7977 &self,
7978 mut trigger: StoredTrigger,
7979 principal: Option<&crate::auth::Principal>,
7980 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7981 ) -> Result<StoredTrigger> {
7982 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
7983 trigger: trigger.clone(),
7984 };
7985 self.require_for(
7986 principal,
7987 &crate::catalog_cmds::required_permission(&command),
7988 )?;
7989 let _g = self.ddl_lock.lock();
7990 let _security_write = self.security_write()?;
7991 self.require_for(
7992 principal,
7993 &crate::catalog_cmds::required_permission(&command),
7994 )?;
7995 trigger.validate()?;
7996 self.validate_trigger_references(&trigger)
7997 .map_err(trigger_validation_error)?;
7998 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8002 let commit_lock = Arc::clone(&self.commit_lock);
8003 let _c = commit_lock.lock();
8004 let epoch = self.epoch.bump_assigned();
8005 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8006 trigger.created_epoch = epoch.0;
8007 trigger.updated_epoch = epoch.0;
8008 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
8009 trigger: trigger.clone(),
8010 };
8011 let mut next_catalog = self.catalog.read().clone();
8012 self.apply_catalog_command_to(&mut next_catalog, command)?;
8013 next_catalog.db_epoch = epoch.0;
8014 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8015 Ok(trigger)
8016 }
8017
8018 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
8019 self.create_or_replace_trigger_inner(trigger, None, None)
8020 }
8021
8022 pub fn create_or_replace_trigger_controlled<F>(
8023 &self,
8024 trigger: StoredTrigger,
8025 mut before_publish: F,
8026 ) -> Result<StoredTrigger>
8027 where
8028 F: FnMut() -> Result<()>,
8029 {
8030 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
8031 }
8032
8033 pub fn create_or_replace_trigger_as_controlled<F>(
8034 &self,
8035 trigger: StoredTrigger,
8036 principal: Option<&crate::auth::Principal>,
8037 mut before_publish: F,
8038 ) -> Result<StoredTrigger>
8039 where
8040 F: FnMut() -> Result<()>,
8041 {
8042 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
8043 }
8044
8045 fn create_or_replace_trigger_inner(
8046 &self,
8047 trigger: StoredTrigger,
8048 principal: Option<&crate::auth::Principal>,
8049 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8050 ) -> Result<StoredTrigger> {
8051 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
8052 trigger: trigger.clone(),
8053 };
8054 self.require_for(
8055 principal,
8056 &crate::catalog_cmds::required_permission(&command),
8057 )?;
8058 let _g = self.ddl_lock.lock();
8059 let _security_write = self.security_write()?;
8060 self.require_for(
8061 principal,
8062 &crate::catalog_cmds::required_permission(&command),
8063 )?;
8064 trigger.validate()?;
8065 self.validate_trigger_references(&trigger)
8066 .map_err(trigger_validation_error)?;
8067 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8071 let commit_lock = Arc::clone(&self.commit_lock);
8072 let _c = commit_lock.lock();
8073 let epoch = self.epoch.bump_assigned();
8074 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8075 let mut next_catalog = self.catalog.read().clone();
8076 let replaced = match next_catalog
8081 .triggers
8082 .iter()
8083 .position(|t| t.trigger.name == trigger.name)
8084 {
8085 Some(idx) => next_catalog.triggers[idx]
8086 .trigger
8087 .replaced(trigger.clone(), epoch.0)?,
8088 None => {
8089 let mut next = trigger;
8090 next.created_epoch = epoch.0;
8091 next.updated_epoch = epoch.0;
8092 next
8093 }
8094 };
8095 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
8096 trigger: replaced.clone(),
8097 };
8098 self.apply_catalog_command_to(&mut next_catalog, command)?;
8099 next_catalog.db_epoch = epoch.0;
8100 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8101 Ok(replaced)
8102 }
8103
8104 pub fn drop_trigger(&self, name: &str) -> Result<()> {
8105 self.drop_trigger_with_epoch(name).map(|_| ())
8106 }
8107
8108 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
8110 self.drop_triggers_with_epoch(&[name.to_string()])
8111 }
8112
8113 pub fn drop_trigger_with_epoch_controlled<F>(
8114 &self,
8115 name: &str,
8116 before_publish: F,
8117 ) -> Result<Epoch>
8118 where
8119 F: FnMut() -> Result<()>,
8120 {
8121 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
8122 }
8123
8124 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
8126 self.drop_triggers_with_epoch_inner(names, None, None)
8127 }
8128
8129 pub fn drop_triggers_with_epoch_controlled<F>(
8130 &self,
8131 names: &[String],
8132 mut before_publish: F,
8133 ) -> Result<Epoch>
8134 where
8135 F: FnMut() -> Result<()>,
8136 {
8137 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
8138 }
8139
8140 pub fn drop_triggers_with_epoch_as_controlled<F>(
8141 &self,
8142 names: &[String],
8143 principal: Option<&crate::auth::Principal>,
8144 mut before_publish: F,
8145 ) -> Result<Epoch>
8146 where
8147 F: FnMut() -> Result<()>,
8148 {
8149 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
8150 }
8151
8152 fn drop_triggers_with_epoch_inner(
8153 &self,
8154 names: &[String],
8155 principal: Option<&crate::auth::Principal>,
8156 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8157 ) -> Result<Epoch> {
8158 if names.is_empty() {
8159 return Err(MongrelError::InvalidArgument(
8160 "at least one trigger name is required".into(),
8161 ));
8162 }
8163 let commands: Vec<crate::catalog_cmds::CatalogCommand> = names
8164 .iter()
8165 .map(|name| crate::catalog_cmds::CatalogCommand::DropTrigger { name: name.clone() })
8166 .collect();
8167 for command in &commands {
8168 self.require_for(
8169 principal,
8170 &crate::catalog_cmds::required_permission(command),
8171 )?;
8172 }
8173 let _g = self.ddl_lock.lock();
8174 let _security_write = self.security_write()?;
8175 for command in &commands {
8176 self.require_for(
8177 principal,
8178 &crate::catalog_cmds::required_permission(command),
8179 )?;
8180 }
8181 {
8186 let cat = self.catalog.read();
8187 for command in &commands {
8188 crate::catalog_cmds::apply(&cat, command)?;
8189 }
8190 }
8191 let commit_lock = Arc::clone(&self.commit_lock);
8192 let _c = commit_lock.lock();
8193 let epoch = self.epoch.bump_assigned();
8194 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8195 let mut next_catalog = self.catalog.read().clone();
8196 for command in commands {
8197 self.apply_catalog_command_to(&mut next_catalog, command)?;
8198 }
8199 next_catalog.db_epoch = epoch.0;
8200 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8201 Ok(epoch)
8202 }
8203
8204 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
8205 self.catalog.read().external_tables.clone()
8206 }
8207
8208 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
8209 self.catalog
8210 .read()
8211 .external_tables
8212 .iter()
8213 .find(|t| t.name == name)
8214 .cloned()
8215 }
8216
8217 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
8218 self.create_external_table_inner(entry, None)
8219 }
8220
8221 pub fn create_external_table_controlled<F>(
8222 &self,
8223 entry: ExternalTableEntry,
8224 mut before_publish: F,
8225 ) -> Result<ExternalTableEntry>
8226 where
8227 F: FnMut() -> Result<()>,
8228 {
8229 self.create_external_table_inner(entry, Some(&mut before_publish))
8230 }
8231
8232 fn create_external_table_inner(
8233 &self,
8234 mut entry: ExternalTableEntry,
8235 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8236 ) -> Result<ExternalTableEntry> {
8237 self.require(&crate::auth::Permission::Ddl)?;
8238 let _g = self.ddl_lock.lock();
8239 let _security_write = self.security_write()?;
8240 self.require(&crate::auth::Permission::Ddl)?;
8241 entry.validate()?;
8242 {
8243 let cat = self.catalog.read();
8244 if cat.live(&entry.name).is_some()
8245 || cat.external_tables.iter().any(|t| t.name == entry.name)
8246 {
8247 return Err(MongrelError::InvalidArgument(format!(
8248 "table {:?} already exists",
8249 entry.name
8250 )));
8251 }
8252 }
8253 let commit_lock = Arc::clone(&self.commit_lock);
8254 let _c = commit_lock.lock();
8255 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
8259 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
8260 let epoch = self.epoch.bump_assigned();
8261 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8262 entry.created_epoch = epoch.0;
8263 let mut next_catalog = self.catalog.read().clone();
8264 next_catalog.external_tables.push(entry.clone());
8265 next_catalog.db_epoch = epoch.0;
8266 self.publish_catalog_candidate_with_prelude(
8267 next_catalog,
8268 epoch,
8269 &mut _epoch_guard,
8270 before_publish,
8271 vec![(
8272 EXTERNAL_TABLE_ID,
8273 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
8274 name: entry.name.clone(),
8275 generation_epoch: epoch.0,
8276 }),
8277 )],
8278 )?;
8279 Ok(entry)
8280 }
8281
8282 pub fn drop_external_table(&self, name: &str) -> Result<()> {
8283 self.drop_external_table_with_epoch(name).map(|_| ())
8284 }
8285
8286 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
8288 self.drop_external_table_with_epoch_inner(name, None)
8289 }
8290
8291 pub fn drop_external_table_with_epoch_controlled<F>(
8292 &self,
8293 name: &str,
8294 mut before_publish: F,
8295 ) -> Result<Epoch>
8296 where
8297 F: FnMut() -> Result<()>,
8298 {
8299 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
8300 }
8301
8302 fn drop_external_table_with_epoch_inner(
8303 &self,
8304 name: &str,
8305 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8306 ) -> Result<Epoch> {
8307 self.require(&crate::auth::Permission::Ddl)?;
8308 let _g = self.ddl_lock.lock();
8309 let _security_write = self.security_write()?;
8310 self.require(&crate::auth::Permission::Ddl)?;
8311 let commit_lock = Arc::clone(&self.commit_lock);
8312 let _c = commit_lock.lock();
8313 let epoch = self.epoch.bump_assigned();
8314 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8315 let mut next_catalog = self.catalog.read().clone();
8316 let before = next_catalog.external_tables.len();
8317 next_catalog.external_tables.retain(|t| t.name != name);
8318 if next_catalog.external_tables.len() == before {
8319 return Err(MongrelError::NotFound(format!(
8320 "external table {name:?} not found"
8321 )));
8322 }
8323 next_catalog.db_epoch = epoch.0;
8324 self.publish_catalog_candidate_with_prelude(
8325 next_catalog,
8326 epoch,
8327 &mut _epoch_guard,
8328 before_publish,
8329 vec![(
8330 EXTERNAL_TABLE_ID,
8331 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
8332 name: name.to_string(),
8333 generation_epoch: epoch.0,
8334 }),
8335 )],
8336 )?;
8337 let state_dir = self.root.join(VTAB_DIR).join(name);
8338 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
8339 return Err(MongrelError::DurableCommit {
8340 epoch: epoch.0,
8341 message: format!(
8342 "external table was dropped but connector-state cleanup failed: {error}"
8343 ),
8344 });
8345 }
8346 Ok(epoch)
8347 }
8348
8349 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
8350 let txn_id = self.alloc_txn_id()?;
8351 let (principal, catalog_bound) = self.transaction_principal_snapshot();
8352 self.commit_transaction_with_external_states(
8353 txn_id,
8354 self.epoch.visible(),
8355 Vec::new(),
8356 vec![(name.to_string(), state.to_vec())],
8357 Vec::new(),
8358 principal,
8359 catalog_bound,
8360 None,
8361 crate::txn::TxnCommitContext::internal(),
8362 )
8363 .map(|(epoch, _)| epoch)
8364 }
8365
8366 pub fn trigger_config(&self) -> TriggerConfig {
8367 use std::sync::atomic::Ordering;
8368 TriggerConfig {
8369 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
8370 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
8371 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
8372 }
8373 }
8374
8375 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
8376 use std::sync::atomic::Ordering;
8377 if config.max_depth == 0 {
8378 return Err(MongrelError::InvalidArgument(
8379 "trigger max_depth must be greater than 0".into(),
8380 ));
8381 }
8382 self.trigger_recursive
8383 .store(config.recursive_triggers, Ordering::Relaxed);
8384 self.trigger_max_depth
8385 .store(config.max_depth, Ordering::Relaxed);
8386 self.trigger_max_loop_iterations
8387 .store(config.max_loop_iterations, Ordering::Relaxed);
8388 Ok(())
8389 }
8390
8391 pub fn set_recursive_triggers(&self, recursive: bool) {
8392 use std::sync::atomic::Ordering;
8393 self.trigger_recursive.store(recursive, Ordering::Relaxed);
8394 }
8395
8396 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
8400 self.notify.subscribe()
8401 }
8402
8403 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
8404 self.change_wake.subscribe()
8405 }
8406
8407 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
8412 let control = crate::ExecutionControl::new(None);
8413 self.change_events_since_controlled(last_event_id, &control)
8414 }
8415
8416 pub fn change_events_since_controlled(
8418 &self,
8419 last_event_id: Option<&str>,
8420 control: &crate::ExecutionControl,
8421 ) -> Result<CdcBatch> {
8422 use crate::wal::Op;
8423
8424 control.checkpoint()?;
8425 let resume = match last_event_id {
8426 Some(id) => {
8427 let (epoch, index) = id.split_once(':').ok_or_else(|| {
8428 MongrelError::InvalidArgument(format!(
8429 "invalid CDC event id {id:?}; expected <epoch>:<index>"
8430 ))
8431 })?;
8432 Some((
8433 epoch.parse::<u64>().map_err(|error| {
8434 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
8435 })?,
8436 index.parse::<u32>().map_err(|error| {
8437 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
8438 })?,
8439 ))
8440 }
8441 None => None,
8442 };
8443
8444 let mut wal = self.shared_wal.lock();
8445 wal.group_sync()?;
8446 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
8447 let records = crate::wal::SharedWal::replay_with_dek_controlled(
8448 &self.root,
8449 wal_dek.as_ref(),
8450 control,
8451 CDC_MAX_WAL_RECORDS,
8452 CDC_MAX_WAL_REPLAY_BYTES,
8453 )?;
8454 drop(wal);
8455 control.checkpoint()?;
8456
8457 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
8458 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
8459 for (index, record) in records.iter().enumerate() {
8460 if index % 256 == 0 {
8461 control.checkpoint()?;
8462 }
8463 if let Op::TxnCommit { epoch, added_runs } = &record.op {
8464 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
8465 }
8466 if let Op::SpilledRows { table_id, rows } = &record.op {
8467 spilled_payloads
8468 .entry((record.txn_id, *table_id))
8469 .or_default()
8470 .push(rows);
8471 }
8472 }
8473 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
8474 let current_epoch = self.epoch.committed().0;
8475 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
8476 let gap = resume.is_some_and(|(epoch, _)| {
8477 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
8478 });
8479 if gap {
8480 return Ok(CdcBatch {
8481 events: Vec::new(),
8482 current_epoch,
8483 earliest_epoch,
8484 gap: true,
8485 });
8486 }
8487
8488 let table_names: HashMap<u64, String> = self
8489 .catalog
8490 .read()
8491 .tables
8492 .iter()
8493 .map(|entry| (entry.table_id, entry.name.clone()))
8494 .collect();
8495 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
8496 let mut retained_bytes = 0_usize;
8497 for (index, record) in records.iter().enumerate() {
8498 if index % 256 == 0 {
8499 control.checkpoint()?;
8500 }
8501 if !commits.contains_key(&record.txn_id) {
8502 continue;
8503 }
8504 let Op::BeforeImage {
8505 table_id,
8506 row_id,
8507 row,
8508 } = &record.op
8509 else {
8510 continue;
8511 };
8512 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
8513 return Err(MongrelError::ResourceLimitExceeded {
8514 resource: "CDC before-image bytes",
8515 requested: row.len(),
8516 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
8517 });
8518 }
8519 let before: crate::memtable::Row = bincode::deserialize(row)?;
8520 if before_images.len() >= CDC_MAX_ROWS {
8521 return Err(MongrelError::ResourceLimitExceeded {
8522 resource: "CDC before-image rows",
8523 requested: before_images.len().saturating_add(1),
8524 limit: CDC_MAX_ROWS,
8525 });
8526 }
8527 charge_cdc_bytes(
8528 &mut retained_bytes,
8529 cdc_row_storage_bytes(&before),
8530 "CDC retained bytes",
8531 )?;
8532 before_images.insert((record.txn_id, *table_id, row_id.0), before);
8533 }
8534 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
8535 let mut events = Vec::new();
8536 let mut decoded_rows = before_images.len();
8537 for (record_index, record) in records.iter().enumerate() {
8538 if record_index % 256 == 0 {
8539 control.checkpoint()?;
8540 }
8541 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
8542 continue;
8543 };
8544 let event = match &record.op {
8545 Op::Put { table_id, rows } => {
8546 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
8547 return Err(MongrelError::ResourceLimitExceeded {
8548 resource: "CDC inline row bytes",
8549 requested: rows.len(),
8550 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
8551 });
8552 }
8553 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
8554 decoded_rows = decoded_rows.saturating_add(rows.len());
8555 if decoded_rows > CDC_MAX_ROWS {
8556 return Err(MongrelError::ResourceLimitExceeded {
8557 resource: "CDC decoded rows",
8558 requested: decoded_rows,
8559 limit: CDC_MAX_ROWS,
8560 });
8561 }
8562 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
8563 let mut peak_bytes = retained_bytes;
8564 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
8565 let data = serde_json::to_value(rows)
8566 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
8567 Some((*table_id, "put", data, event_bytes))
8568 }
8569 Op::Delete { table_id, row_ids } => {
8570 let before = row_ids
8571 .iter()
8572 .filter_map(|row_id| {
8573 before_images
8574 .get(&(record.txn_id, *table_id, row_id.0))
8575 .cloned()
8576 })
8577 .collect::<Vec<_>>();
8578 let event_bytes = cdc_rows_json_bytes(&before)
8579 .saturating_add(
8580 row_ids
8581 .len()
8582 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
8583 )
8584 .saturating_add(512);
8585 let mut peak_bytes = retained_bytes;
8586 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
8587 Some((
8588 *table_id,
8589 "delete",
8590 serde_json::json!({
8591 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
8592 "before": before,
8593 }),
8594 event_bytes,
8595 ))
8596 }
8597 Op::TruncateTable { table_id } => {
8598 Some((*table_id, "truncate", serde_json::Value::Null, 512))
8599 }
8600 _ => None,
8601 };
8602 if let Some((table_id, op, data, event_bytes)) = event {
8603 let index = operation_indices.entry(record.txn_id).or_insert(0);
8604 let event_position = (*commit_epoch, *index);
8605 *index = index.saturating_add(1);
8606 if resume.is_some_and(|position| event_position <= position) {
8607 continue;
8608 }
8609 if events.len() >= CDC_MAX_EVENTS {
8610 return Err(MongrelError::ResourceLimitExceeded {
8611 resource: "CDC events",
8612 requested: events.len().saturating_add(1),
8613 limit: CDC_MAX_EVENTS,
8614 });
8615 }
8616 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
8617 events.push(ChangeEvent {
8618 id: Some(format!("{}:{}", event_position.0, event_position.1)),
8619 channel: "changes".into(),
8620 table_id: Some(table_id),
8621 table: table_names.get(&table_id).cloned().unwrap_or_default(),
8622 op: op.into(),
8623 epoch: *commit_epoch,
8624 txn_id: Some(record.txn_id),
8625 message: None,
8626 data: Some(data),
8627 });
8628 }
8629 if let Op::TxnCommit { added_runs, .. } = &record.op {
8630 for run in added_runs {
8631 control.checkpoint()?;
8632 let index = operation_indices.entry(record.txn_id).or_insert(0);
8633 let event_position = (*commit_epoch, *index);
8634 *index = index.saturating_add(1);
8635 if resume.is_some_and(|position| event_position <= position) {
8636 continue;
8637 }
8638 let mut rows = if let Some(payloads) =
8639 spilled_payloads.get(&(record.txn_id, run.table_id))
8640 {
8641 let mut rows = Vec::new();
8642 for payload in payloads {
8643 control.checkpoint()?;
8644 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
8645 return Err(MongrelError::ResourceLimitExceeded {
8646 resource: "CDC spilled row bytes",
8647 requested: payload.len(),
8648 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
8649 });
8650 }
8651 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
8652 if decoded_rows
8653 .saturating_add(rows.len())
8654 .saturating_add(chunk.len())
8655 > CDC_MAX_ROWS
8656 {
8657 return Err(MongrelError::ResourceLimitExceeded {
8658 resource: "CDC decoded rows",
8659 requested: decoded_rows
8660 .saturating_add(rows.len())
8661 .saturating_add(chunk.len()),
8662 limit: CDC_MAX_ROWS,
8663 });
8664 }
8665 rows.extend(chunk);
8666 }
8667 rows
8668 } else {
8669 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
8670 return Ok(CdcBatch {
8671 events: Vec::new(),
8672 current_epoch,
8673 earliest_epoch,
8674 gap: true,
8675 });
8676 };
8677 let table = handle.lock();
8678 let mut reader = match table.open_reader(run.run_id) {
8679 Ok(reader) => reader,
8680 Err(_) => {
8681 return Ok(CdcBatch {
8682 events: Vec::new(),
8683 current_epoch,
8684 earliest_epoch,
8685 gap: true,
8686 })
8687 }
8688 };
8689 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
8690 let rows = reader.all_rows_controlled(control, remaining)?;
8691 drop(reader);
8692 drop(table);
8693 rows
8694 };
8695 for row in &mut rows {
8696 row.committed_epoch = Epoch(*commit_epoch);
8697 }
8698 decoded_rows = decoded_rows.saturating_add(rows.len());
8699 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
8700 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
8701 if events.len() >= CDC_MAX_EVENTS {
8702 return Err(MongrelError::ResourceLimitExceeded {
8703 resource: "CDC events",
8704 requested: events.len().saturating_add(1),
8705 limit: CDC_MAX_EVENTS,
8706 });
8707 }
8708 events.push(ChangeEvent {
8709 id: Some(format!("{}:{}", event_position.0, event_position.1)),
8710 channel: "changes".into(),
8711 table_id: Some(run.table_id),
8712 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
8713 op: "put_run".into(),
8714 epoch: *commit_epoch,
8715 txn_id: Some(record.txn_id),
8716 message: None,
8717 data: Some(serde_json::json!({
8718 "run_id": run.run_id.to_string(),
8719 "row_count": run.row_count,
8720 "min_row_id": run.min_row_id,
8721 "max_row_id": run.max_row_id,
8722 "rows": rows,
8723 })),
8724 });
8725 }
8726 }
8727 }
8728 control.checkpoint()?;
8729 Ok(CdcBatch {
8730 events,
8731 current_epoch,
8732 earliest_epoch,
8733 gap: false,
8734 })
8735 }
8736
8737 pub fn notify(&self, channel: &str, message: Option<String>) {
8740 let _ = self.notify.send(ChangeEvent {
8741 id: None,
8742 channel: channel.to_string(),
8743 table_id: None,
8744 table: String::new(),
8745 op: "notify".into(),
8746 epoch: self.epoch.visible().0,
8747 txn_id: None,
8748 message,
8749 data: None,
8750 });
8751 }
8752
8753 pub fn call_procedure(
8754 &self,
8755 name: &str,
8756 args: HashMap<String, crate::Value>,
8757 ) -> Result<ProcedureCallResult> {
8758 self.call_procedure_as(name, args, None)
8759 }
8760
8761 pub fn call_procedure_as(
8762 &self,
8763 name: &str,
8764 args: HashMap<String, crate::Value>,
8765 principal: Option<&crate::auth::Principal>,
8766 ) -> Result<ProcedureCallResult> {
8767 let control = crate::ExecutionControl::new(None);
8768 self.call_procedure_as_controlled(name, args, principal, &control, || true)
8769 }
8770
8771 #[doc(hidden)]
8774 pub fn call_procedure_as_bound(
8775 &self,
8776 expected: &StoredProcedure,
8777 args: HashMap<String, crate::Value>,
8778 principal: Option<&crate::auth::Principal>,
8779 ) -> Result<ProcedureCallResult> {
8780 self.require_for(principal, &crate::auth::Permission::All)?;
8781 let procedure = self.procedure(&expected.name).ok_or_else(|| {
8782 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
8783 })?;
8784 if &procedure != expected {
8785 return Err(MongrelError::Conflict(format!(
8786 "procedure {:?} changed after request authorization",
8787 expected.name
8788 )));
8789 }
8790 let control = crate::ExecutionControl::new(None);
8791 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
8792 }
8793
8794 #[doc(hidden)]
8799 pub fn call_procedure_as_controlled<F>(
8800 &self,
8801 name: &str,
8802 args: HashMap<String, crate::Value>,
8803 principal: Option<&crate::auth::Principal>,
8804 control: &crate::ExecutionControl,
8805 before_commit: F,
8806 ) -> Result<ProcedureCallResult>
8807 where
8808 F: FnOnce() -> bool,
8809 {
8810 self.require_for(principal, &crate::auth::Permission::All)?;
8814 let procedure = self
8815 .procedure(name)
8816 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
8817 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
8818 }
8819
8820 fn execute_procedure_as_controlled<F>(
8821 &self,
8822 procedure: StoredProcedure,
8823 args: HashMap<String, crate::Value>,
8824 principal: Option<&crate::auth::Principal>,
8825 control: &crate::ExecutionControl,
8826 before_commit: F,
8827 ) -> Result<ProcedureCallResult>
8828 where
8829 F: FnOnce() -> bool,
8830 {
8831 let args = bind_procedure_args(&procedure, args)?;
8832 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
8833 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
8834 if has_writes {
8835 let mut tx = self.begin_as(principal.cloned());
8836 let run = (|| {
8837 for (step_index, step) in procedure.body.steps.iter().enumerate() {
8838 if step_index % 256 == 0 {
8839 control.checkpoint()?;
8840 }
8841 let output = self.execute_procedure_step(
8842 step,
8843 &args,
8844 &outputs,
8845 Some(&mut tx),
8846 principal,
8847 Some(control),
8848 )?;
8849 outputs.insert(step.id().to_string(), output);
8850 }
8851 control.checkpoint()?;
8852 eval_return_output(&procedure.body.return_value, &args, &outputs)
8853 })();
8854 match run {
8855 Ok(output) => {
8856 control.checkpoint()?;
8857 if !before_commit() {
8858 tx.rollback();
8859 return Err(MongrelError::Cancelled);
8860 }
8861 let epoch = tx.commit()?.0;
8862 Ok(ProcedureCallResult {
8863 epoch: Some(epoch),
8864 output,
8865 })
8866 }
8867 Err(e) => {
8868 tx.rollback();
8869 Err(e)
8870 }
8871 }
8872 } else {
8873 for (step_index, step) in procedure.body.steps.iter().enumerate() {
8874 if step_index % 256 == 0 {
8875 control.checkpoint()?;
8876 }
8877 let output = self.execute_procedure_step(
8878 step,
8879 &args,
8880 &outputs,
8881 None,
8882 principal,
8883 Some(control),
8884 )?;
8885 outputs.insert(step.id().to_string(), output);
8886 }
8887 control.checkpoint()?;
8888 Ok(ProcedureCallResult {
8889 epoch: None,
8890 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
8891 })
8892 }
8893 }
8894
8895 fn execute_procedure_step(
8896 &self,
8897 step: &ProcedureStep,
8898 args: &HashMap<String, crate::Value>,
8899 outputs: &HashMap<String, ProcedureCallOutput>,
8900 tx: Option<&mut crate::txn::Transaction<'_>>,
8901 principal: Option<&crate::auth::Principal>,
8902 control: Option<&crate::ExecutionControl>,
8903 ) -> Result<ProcedureCallOutput> {
8904 if let Some(control) = control {
8905 control.checkpoint()?;
8906 }
8907 match step {
8908 ProcedureStep::NativeQuery {
8909 table,
8910 conditions,
8911 projection,
8912 limit,
8913 ..
8914 } => {
8915 let mut q = crate::Query::new();
8916 for condition in conditions {
8917 q = q.and(eval_condition(condition, args, outputs)?);
8918 }
8919 let fallback_control = crate::ExecutionControl::new(None);
8920 let query_control = control.unwrap_or(&fallback_control);
8921 let mut rows = self.query_for_principal_controlled(
8922 table,
8923 &q,
8924 projection.as_deref(),
8925 principal,
8926 false,
8927 query_control,
8928 )?;
8929 if let Some(limit) = limit {
8930 rows.truncate(*limit);
8931 }
8932 let mut output = Vec::with_capacity(rows.len());
8933 for (row_index, row) in rows.into_iter().enumerate() {
8934 if row_index % 256 == 0 {
8935 if let Some(control) = control {
8936 control.checkpoint()?;
8937 }
8938 }
8939 output.push(ProcedureCallRow {
8940 row_id: Some(row.row_id),
8941 columns: row.columns,
8942 });
8943 }
8944 Ok(ProcedureCallOutput::Rows(output))
8945 }
8946 ProcedureStep::Put {
8947 table,
8948 cells,
8949 returning,
8950 ..
8951 } => {
8952 let tx = tx.ok_or_else(|| {
8953 MongrelError::InvalidArgument(
8954 "write procedure step requires a transaction".into(),
8955 )
8956 })?;
8957 let cells = eval_cells(cells, args, outputs)?;
8958 if *returning {
8959 let out = tx.put_returning(table, cells)?;
8960 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
8961 row_id: None,
8962 columns: out.row.columns.into_iter().collect(),
8963 }))
8964 } else {
8965 tx.put(table, cells)?;
8966 Ok(ProcedureCallOutput::Null)
8967 }
8968 }
8969 ProcedureStep::Upsert {
8970 table,
8971 cells,
8972 update_cells,
8973 returning,
8974 ..
8975 } => {
8976 let tx = tx.ok_or_else(|| {
8977 MongrelError::InvalidArgument(
8978 "write procedure step requires a transaction".into(),
8979 )
8980 })?;
8981 let cells = eval_cells(cells, args, outputs)?;
8982 let action = match update_cells {
8983 Some(update_cells) => {
8984 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
8985 }
8986 None => crate::UpsertAction::DoNothing,
8987 };
8988 let out = tx.upsert(table, cells, action)?;
8989 if *returning {
8990 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
8991 row_id: None,
8992 columns: out.row.columns.into_iter().collect(),
8993 }))
8994 } else {
8995 Ok(ProcedureCallOutput::Null)
8996 }
8997 }
8998 ProcedureStep::DeleteByPk { table, pk, .. } => {
8999 let tx = tx.ok_or_else(|| {
9000 MongrelError::InvalidArgument(
9001 "write procedure step requires a transaction".into(),
9002 )
9003 })?;
9004 let pk = eval_value(pk, args, outputs)?;
9005 let handle = self.table(table)?;
9006 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
9007 MongrelError::NotFound("procedure delete_by_pk target not found".into())
9008 })?;
9009 tx.delete(table, row_id)?;
9010 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
9011 }
9012 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
9013 "DeleteRows procedure step is not supported by the core executor yet".into(),
9014 )),
9015 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
9016 "SqlQuery procedure step must be executed by mongreldb-query".into(),
9017 )),
9018 }
9019 }
9020
9021 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
9022 let cat = self.catalog.read();
9023 for step in &procedure.body.steps {
9024 let Some(table_name) = step.table() else {
9025 continue;
9026 };
9027 let schema = &cat
9028 .live(table_name)
9029 .ok_or_else(|| {
9030 MongrelError::InvalidArgument(format!(
9031 "procedure {:?} references unknown table {table_name:?}",
9032 procedure.name
9033 ))
9034 })?
9035 .schema;
9036 match step {
9037 ProcedureStep::NativeQuery {
9038 conditions,
9039 projection,
9040 ..
9041 } => {
9042 for condition in conditions {
9043 validate_condition_columns(condition, schema)?;
9044 }
9045 if let Some(projection) = projection {
9046 for id in projection {
9047 validate_column_id(*id, schema)?;
9048 }
9049 }
9050 }
9051 ProcedureStep::Put { cells, .. } => {
9052 for cell in cells {
9053 validate_column_id(cell.column_id, schema)?;
9054 }
9055 }
9056 ProcedureStep::Upsert {
9057 cells,
9058 update_cells,
9059 ..
9060 } => {
9061 for cell in cells {
9062 validate_column_id(cell.column_id, schema)?;
9063 }
9064 if let Some(update_cells) = update_cells {
9065 for cell in update_cells {
9066 validate_column_id(cell.column_id, schema)?;
9067 }
9068 }
9069 }
9070 ProcedureStep::DeleteByPk { .. } => {
9071 if schema.primary_key().is_none() {
9072 return Err(MongrelError::InvalidArgument(format!(
9073 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
9074 procedure.name
9075 )));
9076 }
9077 }
9078 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
9079 }
9080 }
9081 Ok(())
9082 }
9083
9084 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
9085 let cat = self.catalog.read();
9086 let target_schema = match &trigger.target {
9087 TriggerTarget::Table(target_name) => cat
9088 .live(target_name)
9089 .ok_or_else(|| {
9090 MongrelError::InvalidArgument(format!(
9091 "trigger {:?} references unknown target table {target_name:?}",
9092 trigger.name
9093 ))
9094 })?
9095 .schema
9096 .clone(),
9097 TriggerTarget::View(_) => Schema {
9098 columns: trigger.target_columns.clone(),
9099 ..Schema::default()
9100 },
9101 };
9102 for col in &trigger.update_of {
9103 if target_schema.column(col).is_none() {
9104 return Err(MongrelError::InvalidArgument(format!(
9105 "trigger {:?} UPDATE OF references unknown column {col:?}",
9106 trigger.name
9107 )));
9108 }
9109 }
9110 if let Some(expr) = &trigger.when {
9111 validate_trigger_expr(expr, &target_schema, trigger.event)?;
9112 }
9113 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
9114 for step in &trigger.program.steps {
9115 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
9116 {
9117 return Err(MongrelError::InvalidArgument(
9118 "SetNew trigger steps are only valid in BEFORE triggers".into(),
9119 ));
9120 }
9121 validate_trigger_step(
9122 step,
9123 &cat,
9124 &target_schema,
9125 trigger.event,
9126 &mut select_schemas,
9127 )?;
9128 }
9129 Ok(())
9130 }
9131
9132 pub fn begin(&self) -> crate::txn::Transaction<'_> {
9134 self.begin_with_isolation(crate::txn::IsolationLevel::default())
9135 }
9136
9137 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
9138 let principal = self.principal.read().clone();
9139 let catalog_bound = principal.as_ref().is_some_and(|principal| {
9140 let catalog = self.catalog.read();
9141 catalog.require_auth || principal.user_id != 0
9142 });
9143 (principal, catalog_bound)
9144 }
9145
9146 pub fn begin_as(
9147 &self,
9148 principal: Option<crate::auth::Principal>,
9149 ) -> crate::txn::Transaction<'_> {
9150 let catalog_bound = principal.as_ref().is_some_and(|principal| {
9151 let catalog = self.catalog.read();
9152 catalog.require_auth || principal.user_id != 0
9153 });
9154 let txn_id = self.alloc_txn_id();
9155 let read = Snapshot::at(self.epoch.visible());
9156 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
9157 .with_principal(principal, catalog_bound)
9158 }
9159
9160 pub fn begin_with_isolation(
9162 &self,
9163 level: crate::txn::IsolationLevel,
9164 ) -> crate::txn::Transaction<'_> {
9165 let txn_id = self.alloc_txn_id();
9166 let read = Snapshot::at(self.epoch.visible());
9169 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9170 crate::txn::Transaction::new(self, txn_id, read, level)
9171 .with_principal(principal, catalog_bound)
9172 }
9173
9174 pub fn begin_with_external_trigger_bridge<'a>(
9177 &'a self,
9178 bridge: &'a dyn ExternalTriggerBridge,
9179 ) -> crate::txn::Transaction<'a> {
9180 let txn_id = self.alloc_txn_id();
9181 let read = Snapshot::at(self.epoch.visible());
9182 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9183 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
9184 .with_external_trigger_bridge(bridge)
9185 .with_principal(principal, catalog_bound)
9186 }
9187
9188 pub fn begin_with_external_trigger_bridge_as<'a>(
9189 &'a self,
9190 bridge: &'a dyn ExternalTriggerBridge,
9191 principal: Option<crate::auth::Principal>,
9192 ) -> crate::txn::Transaction<'a> {
9193 let catalog_bound = principal.as_ref().is_some_and(|principal| {
9194 let catalog = self.catalog.read();
9195 catalog.require_auth || principal.user_id != 0
9196 });
9197 let txn_id = self.alloc_txn_id();
9198 let read = Snapshot::at(self.epoch.visible());
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 transaction<T>(
9206 &self,
9207 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9208 ) -> Result<T> {
9209 let mut tx = self.begin();
9210 match f(&mut tx) {
9211 Ok(out) => {
9212 tx.commit()?;
9213 Ok(out)
9214 }
9215 Err(e) => {
9216 tx.rollback();
9217 Err(e)
9218 }
9219 }
9220 }
9221
9222 pub fn transaction_with_row_ids<T>(
9223 &self,
9224 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9225 ) -> Result<(T, Vec<RowId>)> {
9226 let mut tx = self.begin();
9227 match f(&mut tx) {
9228 Ok(output) => {
9229 let (_, row_ids) = tx.commit_with_row_ids()?;
9230 Ok((output, row_ids))
9231 }
9232 Err(error) => {
9233 tx.rollback();
9234 Err(error)
9235 }
9236 }
9237 }
9238
9239 pub fn transaction_for_current_principal<T>(
9240 &self,
9241 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9242 ) -> Result<T> {
9243 if self.principal.read().is_some() {
9244 self.refresh_principal()?;
9245 }
9246 let mut transaction = self.begin_as(self.principal.read().clone());
9247 match f(&mut transaction) {
9248 Ok(output) => {
9249 transaction.commit()?;
9250 Ok(output)
9251 }
9252 Err(error) => {
9253 transaction.rollback();
9254 Err(error)
9255 }
9256 }
9257 }
9258
9259 pub fn transaction_for_current_principal_with_epoch<T>(
9260 &self,
9261 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9262 ) -> Result<(Epoch, T)> {
9263 if self.principal.read().is_some() {
9264 self.refresh_principal()?;
9265 }
9266 let mut transaction = self.begin_as(self.principal.read().clone());
9267 match f(&mut transaction) {
9268 Ok(output) => {
9269 let epoch = transaction.commit()?;
9270 Ok((epoch, output))
9271 }
9272 Err(error) => {
9273 transaction.rollback();
9274 Err(error)
9275 }
9276 }
9277 }
9278
9279 pub fn transaction_with_row_ids_for_current_principal<T>(
9280 &self,
9281 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9282 ) -> Result<(T, Vec<RowId>)> {
9283 if self.principal.read().is_some() {
9284 self.refresh_principal()?;
9285 }
9286 let mut transaction = self.begin_as(self.principal.read().clone());
9287 match f(&mut transaction) {
9288 Ok(output) => {
9289 let (_, row_ids) = transaction.commit_with_row_ids()?;
9290 Ok((output, row_ids))
9291 }
9292 Err(error) => {
9293 transaction.rollback();
9294 Err(error)
9295 }
9296 }
9297 }
9298
9299 pub fn transaction_with_external_trigger_bridge<'a, T>(
9302 &'a self,
9303 bridge: &'a dyn ExternalTriggerBridge,
9304 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9305 ) -> Result<T> {
9306 let mut tx = self.begin_with_external_trigger_bridge(bridge);
9307 match f(&mut tx) {
9308 Ok(out) => {
9309 tx.commit()?;
9310 Ok(out)
9311 }
9312 Err(e) => {
9313 tx.rollback();
9314 Err(e)
9315 }
9316 }
9317 }
9318
9319 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
9320 &'a self,
9321 bridge: &'a dyn ExternalTriggerBridge,
9322 principal: Option<crate::auth::Principal>,
9323 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
9324 ) -> Result<T> {
9325 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
9326 match f(&mut tx) {
9327 Ok(output) => {
9328 tx.commit()?;
9329 Ok(output)
9330 }
9331 Err(error) => {
9332 tx.rollback();
9333 Err(error)
9334 }
9335 }
9336 }
9337
9338 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
9341 self.active_txns.register(epoch)
9342 }
9343
9344 fn fill_auto_increment_for_staging(
9345 &self,
9346 txn_id: u64,
9347 staging: &mut [(u64, crate::txn::Staged)],
9348 control: Option<&crate::ExecutionControl>,
9349 ) -> Result<()> {
9350 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9351 for (index, (table_id, staged)) in staging.iter().enumerate() {
9352 commit_prepare_checkpoint(control, index)?;
9353 if matches!(staged, crate::txn::Staged::Put(_)) {
9354 puts_by_table.entry(*table_id).or_default().push(index);
9355 }
9356 }
9357
9358 {
9365 let mut barrier_tables: Vec<u64> = puts_by_table
9366 .iter()
9367 .filter(|(table_id, indexes)| {
9368 indexes.iter().any(|index| {
9369 matches!(
9370 &staging[*index].1,
9371 crate::txn::Staged::Put(cells)
9372 if self.table_auto_inc_would_allocate(**table_id, cells)
9373 )
9374 })
9375 })
9376 .map(|(table_id, _)| *table_id)
9377 .collect();
9378 barrier_tables.sort_unstable();
9379 for table_id in barrier_tables {
9380 self.acquire_txn_lock(
9381 txn_id,
9382 crate::locks::LockKey::sequence_barrier(&format!("auto_inc:{table_id}")),
9383 crate::locks::LockMode::Exclusive,
9384 control,
9385 )?;
9386 }
9387 }
9388
9389 let tables = self.tables.read();
9390 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
9391 commit_prepare_checkpoint(control, table_index)?;
9392 if let Some(handle) = tables.get(&table_id) {
9393 #[cfg(test)]
9394 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9395 let mut t = handle.lock();
9396 for (fill_index, index) in indexes.into_iter().enumerate() {
9397 commit_prepare_checkpoint(control, fill_index)?;
9398 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
9399 t.fill_auto_inc(cells)?;
9400 }
9401 }
9402 }
9403 }
9404 Ok(())
9405 }
9406
9407 fn expand_table_triggers(
9408 &self,
9409 txn_id: u64,
9410 staging: &mut Vec<(u64, crate::txn::Staged)>,
9411 read_epoch: Epoch,
9412 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9413 external_states: &mut Vec<(String, Vec<u8>)>,
9414 control: Option<&crate::ExecutionControl>,
9415 ) -> Result<()> {
9416 commit_prepare_checkpoint(control, 0)?;
9417 let mut external_writes = Vec::new();
9418 let config = self.trigger_config();
9419 if config.recursive_triggers {
9420 let chunk = std::mem::take(staging);
9421 let stacks = vec![Vec::new(); chunk.len()];
9422 *staging = self.expand_trigger_chunk(
9423 txn_id,
9424 chunk,
9425 stacks,
9426 read_epoch,
9427 0,
9428 config.max_depth,
9429 &mut external_writes,
9430 &config,
9431 control,
9432 )?;
9433 self.apply_external_trigger_writes(
9434 external_writes,
9435 external_trigger_bridge,
9436 external_states,
9437 staging,
9438 control,
9439 )?;
9440 return Ok(());
9441 }
9442
9443 let mut expansion =
9444 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
9445 if !expansion.before.is_empty() {
9446 let mut final_staging = expansion.before;
9447 final_staging.extend(filter_ignored_staging(
9448 std::mem::take(staging),
9449 &expansion.ignored_indices,
9450 ));
9451 *staging = final_staging;
9452 } else if !expansion.ignored_indices.is_empty() {
9453 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
9454 }
9455 staging.append(&mut expansion.after);
9456 external_writes.append(&mut expansion.before_external);
9457 external_writes.append(&mut expansion.after_external);
9458 self.apply_external_trigger_writes(
9459 external_writes,
9460 external_trigger_bridge,
9461 external_states,
9462 staging,
9463 control,
9464 )?;
9465 Ok(())
9466 }
9467
9468 #[allow(clippy::too_many_arguments)]
9469 fn expand_trigger_chunk(
9470 &self,
9471 txn_id: u64,
9472 mut chunk: Vec<(u64, crate::txn::Staged)>,
9473 stacks: Vec<Vec<String>>,
9474 read_epoch: Epoch,
9475 depth: u32,
9476 max_depth: u32,
9477 external_writes: &mut Vec<ExternalTriggerWrite>,
9478 config: &TriggerConfig,
9479 control: Option<&crate::ExecutionControl>,
9480 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
9481 if chunk.is_empty() {
9482 return Ok(Vec::new());
9483 }
9484 commit_prepare_checkpoint(control, 0)?;
9485 self.fill_auto_increment_for_staging(txn_id, &mut chunk, control)?;
9486 let expansion = self.expand_table_triggers_once(
9487 &mut chunk,
9488 read_epoch,
9489 Some(&stacks),
9490 config,
9491 control,
9492 )?;
9493 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
9494 let stack = expansion
9495 .before_stacks
9496 .first()
9497 .or_else(|| expansion.after_stacks.first())
9498 .cloned()
9499 .unwrap_or_default();
9500 return Err(MongrelError::TriggerValidation(format!(
9501 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
9502 Self::format_trigger_stack(&stack)
9503 )));
9504 }
9505
9506 let mut out = Vec::new();
9507 external_writes.extend(expansion.before_external);
9508 out.extend(self.expand_trigger_chunk(
9509 txn_id,
9510 expansion.before,
9511 expansion.before_stacks,
9512 read_epoch,
9513 depth + 1,
9514 max_depth,
9515 external_writes,
9516 config,
9517 control,
9518 )?);
9519 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
9520 external_writes.extend(expansion.after_external);
9521 out.extend(self.expand_trigger_chunk(
9522 txn_id,
9523 expansion.after,
9524 expansion.after_stacks,
9525 read_epoch,
9526 depth + 1,
9527 max_depth,
9528 external_writes,
9529 config,
9530 control,
9531 )?);
9532 Ok(out)
9533 }
9534
9535 fn apply_external_trigger_writes(
9536 &self,
9537 writes: Vec<ExternalTriggerWrite>,
9538 bridge: Option<&dyn ExternalTriggerBridge>,
9539 external_states: &mut Vec<(String, Vec<u8>)>,
9540 staging: &mut Vec<(u64, crate::txn::Staged)>,
9541 control: Option<&crate::ExecutionControl>,
9542 ) -> Result<()> {
9543 if writes.is_empty() {
9544 return Ok(());
9545 }
9546 let bridge = bridge.ok_or_else(|| {
9547 MongrelError::TriggerValidation(
9548 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
9549 )
9550 })?;
9551 for (write_index, write) in writes.into_iter().enumerate() {
9552 commit_prepare_checkpoint(control, write_index)?;
9553 let table = write.table().to_string();
9554 let entry = self.external_table(&table).ok_or_else(|| {
9555 MongrelError::NotFound(format!("external table {table:?} not found"))
9556 })?;
9557 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
9558 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
9559 external_states.push((table, result.state));
9560 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
9561 commit_prepare_checkpoint(control, base_index)?;
9562 match base_write {
9563 ExternalTriggerBaseWrite::Put { table, cells } => {
9564 let table_id = self.table_id(&table)?;
9565 staging.push((table_id, crate::txn::Staged::Put(cells)));
9566 }
9567 ExternalTriggerBaseWrite::Delete { table, row_id } => {
9568 let table_id = self.table_id(&table)?;
9569 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
9570 }
9571 }
9572 }
9573 }
9574 dedup_external_states_in_place(external_states);
9575 Ok(())
9576 }
9577
9578 fn expand_table_triggers_once(
9579 &self,
9580 staging: &mut Vec<(u64, crate::txn::Staged)>,
9581 read_epoch: Epoch,
9582 trigger_stacks: Option<&[Vec<String>]>,
9583 config: &TriggerConfig,
9584 control: Option<&crate::ExecutionControl>,
9585 ) -> Result<TriggerExpansion> {
9586 commit_prepare_checkpoint(control, 0)?;
9587 let triggers: Vec<StoredTrigger> = self
9588 .catalog
9589 .read()
9590 .triggers
9591 .iter()
9592 .filter(|entry| {
9593 entry.trigger.enabled
9594 && matches!(
9595 entry.trigger.timing,
9596 TriggerTiming::Before | TriggerTiming::After
9597 )
9598 && matches!(entry.trigger.target, TriggerTarget::Table(_))
9599 })
9600 .map(|entry| entry.trigger.clone())
9601 .collect();
9602 if triggers.is_empty() || staging.is_empty() {
9603 return Ok(TriggerExpansion::default());
9604 }
9605
9606 let before_triggers = triggers
9607 .iter()
9608 .filter(|trigger| trigger.timing == TriggerTiming::Before)
9609 .cloned()
9610 .collect::<Vec<_>>();
9611 let after_triggers = triggers
9612 .iter()
9613 .filter(|trigger| trigger.timing == TriggerTiming::After)
9614 .cloned()
9615 .collect::<Vec<_>>();
9616
9617 let mut before_added = Vec::new();
9618 let mut before_stacks = Vec::new();
9619 let mut before_external = Vec::new();
9620 let mut ignored_indices = std::collections::BTreeSet::new();
9621 if !before_triggers.is_empty() {
9622 let before_events =
9623 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
9624 let mut out = TriggerProgramOutput {
9625 added: &mut before_added,
9626 added_stacks: &mut before_stacks,
9627 added_external: &mut before_external,
9628 ignored_indices: &mut ignored_indices,
9629 };
9630 self.execute_triggers_for_events(
9631 &before_triggers,
9632 &before_events,
9633 Some(staging),
9634 &mut out,
9635 config,
9636 read_epoch,
9637 control,
9638 )?;
9639 }
9640
9641 let after_events = if after_triggers.is_empty() {
9642 Vec::new()
9643 } else {
9644 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
9645 .into_iter()
9646 .filter(|event| {
9647 !event
9648 .op_indices
9649 .iter()
9650 .any(|idx| ignored_indices.contains(idx))
9651 })
9652 .collect()
9653 };
9654
9655 let mut after_added = Vec::new();
9656 let mut after_stacks = Vec::new();
9657 let mut after_external = Vec::new();
9658 let mut out = TriggerProgramOutput {
9659 added: &mut after_added,
9660 added_stacks: &mut after_stacks,
9661 added_external: &mut after_external,
9662 ignored_indices: &mut ignored_indices,
9663 };
9664 self.execute_triggers_for_events(
9665 &after_triggers,
9666 &after_events,
9667 None,
9668 &mut out,
9669 config,
9670 read_epoch,
9671 control,
9672 )?;
9673 Ok(TriggerExpansion {
9674 before: before_added,
9675 before_stacks,
9676 before_external,
9677 after: after_added,
9678 after_stacks,
9679 after_external,
9680 ignored_indices,
9681 })
9682 }
9683
9684 #[allow(clippy::too_many_arguments)]
9685 fn execute_triggers_for_events(
9686 &self,
9687 triggers: &[StoredTrigger],
9688 events: &[WriteEvent],
9689 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
9690 out: &mut TriggerProgramOutput<'_>,
9691 config: &TriggerConfig,
9692 read_epoch: Epoch,
9693 control: Option<&crate::ExecutionControl>,
9694 ) -> Result<()> {
9695 let mut checkpoint_index = 0_usize;
9696 for event in events {
9697 for trigger in triggers {
9698 commit_prepare_checkpoint(control, checkpoint_index)?;
9699 checkpoint_index += 1;
9700 if event
9701 .op_indices
9702 .iter()
9703 .any(|idx| out.ignored_indices.contains(idx))
9704 {
9705 break;
9706 }
9707 let matches = {
9708 let cat = self.catalog.read();
9709 trigger_matches_event(trigger, event, &cat)?
9710 };
9711 if !matches {
9712 continue;
9713 }
9714 if let Some(when) = &trigger.when {
9715 if !eval_trigger_expr(when, event)? {
9716 continue;
9717 }
9718 }
9719 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
9720 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
9721 return Err(MongrelError::TriggerValidation(format!(
9722 "trigger recursion cycle detected; trigger stack: {}",
9723 Self::format_trigger_stack(&trigger_stack)
9724 )));
9725 }
9726 let outcome = match staging.as_mut() {
9727 Some(staging) => self.execute_trigger_program(
9728 trigger,
9729 event,
9730 Some(&mut **staging),
9731 out,
9732 &trigger_stack,
9733 config,
9734 read_epoch,
9735 control,
9736 )?,
9737 None => self.execute_trigger_program(
9738 trigger,
9739 event,
9740 None,
9741 out,
9742 &trigger_stack,
9743 config,
9744 read_epoch,
9745 control,
9746 )?,
9747 };
9748 if outcome == TriggerProgramOutcome::Ignore {
9749 out.ignored_indices.extend(event.op_indices.iter().copied());
9750 break;
9751 }
9752 }
9753 }
9754 Ok(())
9755 }
9756
9757 fn trigger_events_for_staging(
9758 &self,
9759 staging: &[(u64, crate::txn::Staged)],
9760 read_epoch: Epoch,
9761 trigger_stacks: Option<&[Vec<String>]>,
9762 control: Option<&crate::ExecutionControl>,
9763 ) -> Result<Vec<WriteEvent>> {
9764 use crate::txn::Staged;
9765 use std::collections::{HashMap, VecDeque};
9766
9767 let snapshot = Snapshot::at(read_epoch);
9768 let cat = self.catalog.read();
9769 let mut table_names = HashMap::new();
9770 let mut table_schemas = HashMap::new();
9771 for entry in cat
9772 .tables
9773 .iter()
9774 .filter(|entry| matches!(entry.state, TableState::Live))
9775 {
9776 table_names.insert(entry.table_id, entry.name.clone());
9777 table_schemas.insert(entry.table_id, entry.schema.clone());
9778 }
9779 drop(cat);
9780
9781 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
9782 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
9783 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
9784
9785 for (idx, (table_id, staged)) in staging.iter().enumerate() {
9786 commit_prepare_checkpoint(control, idx)?;
9787 let Some(schema) = table_schemas.get(table_id) else {
9788 continue;
9789 };
9790 let Some(pk) = schema.primary_key() else {
9791 continue;
9792 };
9793 match staged {
9794 Staged::Delete(row_id) => {
9795 let handle = self.table_by_id(*table_id)?;
9796 let Some(row) = handle.lock().get(*row_id, snapshot) else {
9797 continue;
9798 };
9799 let Some(pk_value) = row.columns.get(&pk.id) else {
9800 continue;
9801 };
9802 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
9803 delete_by_key
9804 .entry((*table_id, pk_value.encode_key()))
9805 .or_default()
9806 .push_back(idx);
9807 }
9808 Staged::Put(cells) => {
9809 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
9810 put_by_key
9811 .entry((*table_id, value.encode_key()))
9812 .or_default()
9813 .push_back(idx);
9814 }
9815 }
9816 Staged::Update { row_id, .. } => {
9817 let handle = self.table_by_id(*table_id)?;
9818 let row = handle.lock().get(*row_id, snapshot);
9819 if let Some(row) = row {
9820 old_rows.insert(idx, TriggerRowImage::from_row(row));
9821 }
9822 }
9823 Staged::Truncate => {}
9824 }
9825 }
9826
9827 let mut paired_delete = std::collections::HashSet::new();
9828 let mut paired_put = std::collections::HashSet::new();
9829 let mut events = Vec::new();
9830
9831 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
9832 commit_prepare_checkpoint(control, pair_index)?;
9833 let Some(puts) = put_by_key.get_mut(key) else {
9834 continue;
9835 };
9836 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
9837 paired_delete.insert(delete_idx);
9838 paired_put.insert(put_idx);
9839 let (table_id, _) = &staging[put_idx];
9840 let Some(table_name) = table_names.get(table_id).cloned() else {
9841 continue;
9842 };
9843 let old = old_rows.get(&delete_idx).cloned();
9844 let new = match &staging[put_idx].1 {
9845 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
9846 _ => None,
9847 };
9848 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
9849 events.push(WriteEvent {
9850 table: table_name,
9851 kind: TriggerEvent::Update,
9852 old,
9853 new,
9854 changed_columns,
9855 op_indices: vec![delete_idx, put_idx],
9856 put_idx: Some(put_idx),
9857 trigger_stack: Self::trigger_stack_for_indices(
9858 trigger_stacks,
9859 &[delete_idx, put_idx],
9860 ),
9861 });
9862 }
9863 }
9864
9865 for (idx, (table_id, staged)) in staging.iter().enumerate() {
9866 commit_prepare_checkpoint(control, idx)?;
9867 let Some(table_name) = table_names.get(table_id).cloned() else {
9868 continue;
9869 };
9870 match staged {
9871 Staged::Put(cells) if !paired_put.contains(&idx) => {
9872 let new = Some(TriggerRowImage::from_cells(cells));
9873 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
9874 events.push(WriteEvent {
9875 table: table_name,
9876 kind: TriggerEvent::Insert,
9877 old: None,
9878 new,
9879 changed_columns,
9880 op_indices: vec![idx],
9881 put_idx: Some(idx),
9882 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
9883 });
9884 }
9885 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
9886 let old = match old_rows.get(&idx).cloned() {
9887 Some(old) => Some(old),
9888 None => {
9889 let handle = self.table_by_id(*table_id)?;
9890 let row = handle.lock().get(*row_id, snapshot);
9891 row.map(TriggerRowImage::from_row)
9892 }
9893 };
9894 let Some(old) = old else {
9895 continue;
9896 };
9897 let changed_columns = old.columns.keys().copied().collect();
9898 events.push(WriteEvent {
9899 table: table_name,
9900 kind: TriggerEvent::Delete,
9901 old: Some(old),
9902 new: None,
9903 changed_columns,
9904 op_indices: vec![idx],
9905 put_idx: None,
9906 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
9907 });
9908 }
9909 Staged::Update { new_row: cells, .. } => {
9910 let old = old_rows.get(&idx).cloned();
9911 let new = Some(TriggerRowImage::from_cells(cells));
9912 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
9913 events.push(WriteEvent {
9914 table: table_name,
9915 kind: TriggerEvent::Update,
9916 old,
9917 new,
9918 changed_columns,
9919 op_indices: vec![idx],
9920 put_idx: Some(idx),
9921 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
9922 });
9923 }
9924 Staged::Truncate => {}
9925 _ => {}
9926 }
9927 }
9928
9929 Ok(events)
9930 }
9931
9932 #[allow(clippy::too_many_arguments)]
9933 fn execute_trigger_program(
9934 &self,
9935 trigger: &StoredTrigger,
9936 event: &WriteEvent,
9937 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
9938 out: &mut TriggerProgramOutput<'_>,
9939 trigger_stack: &[String],
9940 config: &TriggerConfig,
9941 read_epoch: Epoch,
9942 control: Option<&crate::ExecutionControl>,
9943 ) -> Result<TriggerProgramOutcome> {
9944 let mut event = event.clone();
9945 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
9946 self.execute_trigger_steps(
9947 trigger,
9948 &trigger.program.steps,
9949 &mut event,
9950 staging,
9951 out,
9952 trigger_stack,
9953 config,
9954 &mut select_results,
9955 0,
9956 None,
9957 read_epoch,
9958 control,
9959 )
9960 }
9961
9962 #[allow(clippy::too_many_arguments)]
9963 fn execute_trigger_steps(
9964 &self,
9965 trigger: &StoredTrigger,
9966 steps: &[TriggerStep],
9967 event: &mut WriteEvent,
9968 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
9969 out: &mut TriggerProgramOutput<'_>,
9970 trigger_stack: &[String],
9971 config: &TriggerConfig,
9972 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
9973 depth: u32,
9974 selected: Option<&TriggerRowImage>,
9975 read_epoch: Epoch,
9976 control: Option<&crate::ExecutionControl>,
9977 ) -> Result<TriggerProgramOutcome> {
9978 let _ = depth;
9979 for (step_index, step) in steps.iter().enumerate() {
9980 commit_prepare_checkpoint(control, step_index)?;
9981 match step {
9982 TriggerStep::SetNew { cells } => {
9983 if trigger.timing != TriggerTiming::Before {
9984 return Err(MongrelError::InvalidArgument(
9985 "SetNew trigger step is only valid in BEFORE triggers".into(),
9986 ));
9987 }
9988 let put_idx = event.put_idx.ok_or_else(|| {
9989 MongrelError::InvalidArgument(
9990 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
9991 )
9992 })?;
9993 let staging = staging.as_deref_mut().ok_or_else(|| {
9994 MongrelError::InvalidArgument(
9995 "SetNew trigger step requires mutable trigger staging".into(),
9996 )
9997 })?;
9998 let mut update_changed_columns = None;
9999 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
10000 Some(crate::txn::Staged::Put(cells)) => cells,
10001 Some(crate::txn::Staged::Update {
10002 new_row,
10003 changed_columns,
10004 ..
10005 }) => {
10006 update_changed_columns = Some(changed_columns);
10007 new_row
10008 }
10009 _ => {
10010 return Err(MongrelError::InvalidArgument(
10011 "SetNew trigger step target row is not mutable".into(),
10012 ))
10013 }
10014 };
10015 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
10016 row_cells.retain(|(id, _)| *id != column_id);
10017 row_cells.push((column_id, value.clone()));
10018 if let Some(changed_columns) = &mut update_changed_columns {
10019 changed_columns.push(column_id);
10020 }
10021 if let Some(new) = &mut event.new {
10022 new.columns.insert(column_id, value);
10023 }
10024 }
10025 row_cells.sort_by_key(|(id, _)| *id);
10026 if let Some(changed_columns) = update_changed_columns {
10027 changed_columns.sort_unstable();
10028 changed_columns.dedup();
10029 }
10030 }
10031 TriggerStep::Insert { table, cells } => {
10032 let cells = eval_trigger_cells(cells, event, selected)?;
10033 if let Ok(table_id) = self.table_id(table) {
10034 out.added.push((table_id, crate::txn::Staged::Put(cells)));
10035 out.added_stacks.push(trigger_stack.to_vec());
10036 } else if self.external_table(table).is_some() {
10037 out.added_external.push(ExternalTriggerWrite::Insert {
10038 table: table.clone(),
10039 cells,
10040 });
10041 } else {
10042 return Err(MongrelError::NotFound(format!(
10043 "trigger {:?} insert target {table:?} not found",
10044 trigger.name
10045 )));
10046 }
10047 }
10048 TriggerStep::UpdateByPk { table, pk, cells } => {
10049 let pk = eval_trigger_value(pk, event, selected)?;
10050 let cells = eval_trigger_cells(cells, event, selected)?;
10051 if self.external_table(table).is_some() {
10052 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
10053 table: table.clone(),
10054 pk,
10055 cells,
10056 });
10057 } else {
10058 let row_id = self
10059 .table(table)?
10060 .lock()
10061 .lookup_pk(&pk.encode_key())
10062 .ok_or_else(|| {
10063 MongrelError::NotFound(format!(
10064 "trigger {:?} update target not found",
10065 trigger.name
10066 ))
10067 })?;
10068 let handle = self.table(table)?;
10069 let snapshot = Snapshot::at(self.epoch.visible());
10070 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
10071 MongrelError::NotFound(format!(
10072 "trigger {:?} update target not visible",
10073 trigger.name
10074 ))
10075 })?;
10076 let mut changed_columns = cells
10077 .iter()
10078 .map(|(column_id, _)| *column_id)
10079 .collect::<Vec<_>>();
10080 changed_columns.sort_unstable();
10081 changed_columns.dedup();
10082 let mut merged = old.columns;
10083 for (column_id, value) in cells {
10084 merged.insert(column_id, value);
10085 }
10086 out.added.push((
10087 self.table_id(table)?,
10088 crate::txn::Staged::Update {
10089 row_id,
10090 new_row: merged.into_iter().collect(),
10091 changed_columns,
10092 },
10093 ));
10094 out.added_stacks.push(trigger_stack.to_vec());
10095 }
10096 }
10097 TriggerStep::DeleteByPk { table, pk } => {
10098 let pk = eval_trigger_value(pk, event, selected)?;
10099 if self.external_table(table).is_some() {
10100 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
10101 table: table.clone(),
10102 pk,
10103 });
10104 } else {
10105 let row_id = self
10106 .table(table)?
10107 .lock()
10108 .lookup_pk(&pk.encode_key())
10109 .ok_or_else(|| {
10110 MongrelError::NotFound(format!(
10111 "trigger {:?} delete target not found",
10112 trigger.name
10113 ))
10114 })?;
10115 out.added
10116 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
10117 out.added_stacks.push(trigger_stack.to_vec());
10118 }
10119 }
10120 TriggerStep::Select {
10121 id,
10122 table,
10123 conditions,
10124 } => {
10125 let schema = self.table(table)?.lock().schema().clone();
10126 let snapshot = Snapshot::at(read_epoch);
10127 let handle = self.table(table)?;
10128 let rows = match control {
10129 Some(control) => {
10130 handle.lock().visible_rows_controlled(snapshot, control)?
10131 }
10132 None => handle.lock().visible_rows(snapshot)?,
10133 };
10134 let mut matched = Vec::new();
10135 for (row_index, row) in rows.into_iter().enumerate() {
10136 commit_prepare_checkpoint(control, row_index)?;
10137 let image = TriggerRowImage::from_row(row);
10138 let passes = conditions
10139 .iter()
10140 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
10141 .collect::<Result<Vec<_>>>()?
10142 .into_iter()
10143 .all(|b| b);
10144 if passes {
10145 matched.push(image);
10146 }
10147 }
10148 if let Some(pk) = schema.primary_key() {
10149 matched.sort_by(|a, b| {
10150 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
10151 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
10152 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
10153 });
10154 }
10155 select_results.insert(id.clone(), matched);
10156 }
10157 TriggerStep::Foreach { id, steps } => {
10158 let rows = select_results.get(id).ok_or_else(|| {
10159 MongrelError::InvalidArgument(format!(
10160 "trigger {:?} foreach references unknown select id {id:?}",
10161 trigger.name
10162 ))
10163 })?;
10164 if rows.len() > config.max_loop_iterations as usize {
10165 return Err(MongrelError::InvalidArgument(format!(
10166 "trigger {:?} foreach exceeded max_loop_iterations ({})",
10167 trigger.name, config.max_loop_iterations
10168 )));
10169 }
10170 for (row_index, row) in rows.clone().into_iter().enumerate() {
10171 commit_prepare_checkpoint(control, row_index)?;
10172 let result = self.execute_trigger_steps(
10173 trigger,
10174 steps,
10175 event,
10176 staging.as_deref_mut(),
10177 out,
10178 trigger_stack,
10179 config,
10180 select_results,
10181 depth + 1,
10182 Some(&row),
10183 read_epoch,
10184 control,
10185 )?;
10186 if result == TriggerProgramOutcome::Ignore {
10187 return Ok(TriggerProgramOutcome::Ignore);
10188 }
10189 }
10190 }
10191 TriggerStep::DeleteWhere { table, conditions } => {
10192 let schema = self.table(table)?.lock().schema().clone();
10193 let snapshot = Snapshot::at(read_epoch);
10194 let handle = self.table(table)?;
10195 let rows = match control {
10196 Some(control) => {
10197 handle.lock().visible_rows_controlled(snapshot, control)?
10198 }
10199 None => handle.lock().visible_rows(snapshot)?,
10200 };
10201 let table_id = self.table_id(table)?;
10202 let mut to_delete = Vec::new();
10203 for (row_index, row) in rows.into_iter().enumerate() {
10204 commit_prepare_checkpoint(control, row_index)?;
10205 let image = TriggerRowImage::from_row(row.clone());
10206 let passes = conditions
10207 .iter()
10208 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
10209 .collect::<Result<Vec<_>>>()?
10210 .into_iter()
10211 .all(|b| b);
10212 if passes {
10213 to_delete.push((table_id, row.row_id));
10214 }
10215 }
10216 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
10217 commit_prepare_checkpoint(control, row_index)?;
10218 out.added
10219 .push((table_id, crate::txn::Staged::Delete(row_id)));
10220 out.added_stacks.push(trigger_stack.to_vec());
10221 }
10222 }
10223 TriggerStep::UpdateWhere {
10224 table,
10225 conditions,
10226 cells,
10227 } => {
10228 let schema = self.table(table)?.lock().schema().clone();
10229 let snapshot = Snapshot::at(read_epoch);
10230 let handle = self.table(table)?;
10231 let rows = match control {
10232 Some(control) => {
10233 handle.lock().visible_rows_controlled(snapshot, control)?
10234 }
10235 None => handle.lock().visible_rows(snapshot)?,
10236 };
10237 let table_id = self.table_id(table)?;
10238 let mut changed_columns =
10239 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
10240 changed_columns.sort_unstable();
10241 changed_columns.dedup();
10242 let mut to_update = Vec::new();
10243 for (row_index, row) in rows.into_iter().enumerate() {
10244 commit_prepare_checkpoint(control, row_index)?;
10245 let image = TriggerRowImage::from_row(row.clone());
10246 let passes = conditions
10247 .iter()
10248 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
10249 .collect::<Result<Vec<_>>>()?
10250 .into_iter()
10251 .all(|b| b);
10252 if passes {
10253 let new_cells = cells
10254 .iter()
10255 .map(|cell| {
10256 Ok((
10257 cell.column_id,
10258 eval_trigger_value(&cell.value, event, Some(&image))?,
10259 ))
10260 })
10261 .collect::<Result<Vec<_>>>()?;
10262 let mut merged = row.columns.clone();
10263 for (column_id, value) in new_cells {
10264 merged.insert(column_id, value);
10265 }
10266 to_update.push((table_id, row.row_id, merged));
10267 }
10268 }
10269 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
10270 {
10271 commit_prepare_checkpoint(control, row_index)?;
10272 out.added.push((
10273 table_id,
10274 crate::txn::Staged::Update {
10275 row_id,
10276 new_row: merged.into_iter().collect(),
10277 changed_columns: changed_columns.clone(),
10278 },
10279 ));
10280 out.added_stacks.push(trigger_stack.to_vec());
10281 }
10282 }
10283 TriggerStep::Raise { action, message } => match action {
10284 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
10285 TriggerRaiseAction::Abort
10286 | TriggerRaiseAction::Fail
10287 | TriggerRaiseAction::Rollback => {
10288 let message = eval_trigger_value(message, event, selected)?;
10289 return Err(MongrelError::TriggerValidation(format!(
10290 "trigger {:?} raised: {}; trigger stack: {}",
10291 trigger.name,
10292 trigger_message(message),
10293 Self::format_trigger_stack(trigger_stack)
10294 )));
10295 }
10296 },
10297 }
10298 }
10299 Ok(TriggerProgramOutcome::Continue)
10300 }
10301
10302 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
10303 let Some(stacks) = stacks else {
10304 return Vec::new();
10305 };
10306 let mut out = Vec::new();
10307 for idx in indices {
10308 let Some(stack) = stacks.get(*idx) else {
10309 continue;
10310 };
10311 for name in stack {
10312 if !out.iter().any(|existing| existing == name) {
10313 out.push(name.clone());
10314 }
10315 }
10316 }
10317 out
10318 }
10319
10320 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
10321 let mut out = stack.to_vec();
10322 out.push(trigger_name.to_string());
10323 out
10324 }
10325
10326 fn format_trigger_stack(stack: &[String]) -> String {
10327 if stack.is_empty() {
10328 "<root>".into()
10329 } else {
10330 stack.join(" -> ")
10331 }
10332 }
10333
10334 fn acquire_unique_key_claims(
10355 &self,
10356 txn_id: u64,
10357 staging: &[(u64, crate::txn::Staged)],
10358 control: Option<&crate::ExecutionControl>,
10359 ) -> Result<()> {
10360 let catalog = self.catalog.read();
10361 let has_uniques = staging.iter().any(|(table_id, staged)| {
10362 matches!(
10363 staged,
10364 crate::txn::Staged::Put(_) | crate::txn::Staged::Update { .. }
10365 ) && catalog.tables.iter().any(|entry| {
10366 entry.table_id == *table_id
10367 && (entry.schema.primary_key().is_some()
10368 || !entry.schema.constraints.uniques.is_empty())
10369 })
10370 });
10371 if !has_uniques {
10372 return Ok(());
10373 }
10374 let mut claims: Vec<(u64, Vec<u8>)> = Vec::new();
10375 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
10376 commit_prepare_checkpoint(control, staged_index)?;
10377 let cells = match staged {
10378 crate::txn::Staged::Put(cells) => cells,
10379 crate::txn::Staged::Update { new_row, .. } => new_row,
10380 _ => continue,
10381 };
10382 let Some(entry) = catalog
10383 .tables
10384 .iter()
10385 .find(|entry| entry.table_id == *table_id)
10386 else {
10387 continue;
10388 };
10389 for column in &entry.schema.columns {
10390 if !column
10391 .flags
10392 .contains(crate::schema::ColumnFlags::PRIMARY_KEY)
10393 {
10394 continue;
10395 }
10396 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == column.id) {
10397 let mut key = b"pk:".to_vec();
10398 key.extend_from_slice(&value.encode_key());
10399 claims.push((*table_id, key));
10400 }
10401 }
10402 let cells_map: HashMap<u16, Value> = cells.iter().cloned().collect();
10407 for uc in &entry.schema.constraints.uniques {
10408 if let Some(composite) =
10409 crate::constraint::encode_composite_key(&uc.columns, &cells_map)
10410 {
10411 let mut key = format!("uq{}:", uc.id).into_bytes();
10412 key.extend_from_slice(&composite);
10413 claims.push((*table_id, key));
10414 }
10415 }
10416 }
10417 claims.sort();
10418 claims.dedup();
10419 for (table_id, key) in claims {
10420 self.acquire_txn_lock(
10421 txn_id,
10422 crate::locks::LockKey::key(table_id, key),
10423 crate::locks::LockMode::Exclusive,
10424 control,
10425 )?;
10426 }
10427 Ok(())
10428 }
10429
10430 fn acquire_fk_lock(
10434 &self,
10435 txn_id: u64,
10436 table_id: u64,
10437 key: &[u8],
10438 mode: crate::locks::LockMode,
10439 control: Option<&crate::ExecutionControl>,
10440 ) -> Result<()> {
10441 let mut namespaced = b"fk:".to_vec();
10442 namespaced.extend_from_slice(key);
10443 self.acquire_txn_lock(
10444 txn_id,
10445 crate::locks::LockKey::key(table_id, namespaced),
10446 mode,
10447 control,
10448 )?;
10449 let hook = self.fk_lock_hook.lock().clone();
10452 if let Some(hook) = hook {
10453 hook();
10454 }
10455 Ok(())
10456 }
10457
10458 fn validate_constraints(
10459 &self,
10460 txn_id: u64,
10461 staging: &mut Vec<(u64, crate::txn::Staged)>,
10462 read_epoch: Epoch,
10463 control: Option<&crate::ExecutionControl>,
10464 ) -> Result<()> {
10465 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
10466 use crate::memtable::Row;
10467 use crate::txn::Staged;
10468 use std::collections::HashSet;
10469
10470 commit_prepare_checkpoint(control, 0)?;
10471 let snapshot = Snapshot::at(read_epoch);
10472 let cat = self.catalog.read();
10473
10474 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
10476 .tables
10477 .iter()
10478 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
10479 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
10480 .collect();
10481
10482 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
10484 if !any_constraints {
10485 return Ok(());
10486 }
10487
10488 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
10490 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
10491 if let Some(r) = rows_cache.get(&table_id) {
10492 return Ok(r.clone());
10493 }
10494 let handle = self.table_by_id(table_id)?;
10495 let rows = match control {
10496 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
10497 None => handle.lock().visible_rows(snapshot)?,
10498 };
10499 rows_cache.insert(table_id, rows.clone());
10500 Ok(rows)
10501 };
10502
10503 let mut processed_updates = HashSet::new();
10508 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
10509 let mut update_pass = 0_usize;
10510 loop {
10511 commit_prepare_checkpoint(control, update_pass)?;
10512 update_pass += 1;
10513 let updates: Vec<PendingUpdate> = staging
10514 .iter()
10515 .enumerate()
10516 .filter_map(|(index, (table_id, op))| match op {
10517 Staged::Update {
10518 row_id,
10519 new_row: cells,
10520 ..
10521 } if !processed_updates.contains(&index) => {
10522 Some((index, *table_id, *row_id, cells.clone()))
10523 }
10524 _ => None,
10525 })
10526 .collect();
10527 if updates.is_empty() {
10528 break;
10529 }
10530 let mut new_ops = Vec::new();
10531 for (update_index, (index, table_id, row_id, new_cells)) in
10532 updates.into_iter().enumerate()
10533 {
10534 commit_prepare_checkpoint(control, update_index)?;
10535 processed_updates.insert(index);
10536 let Some(tname) = live
10537 .iter()
10538 .find(|(id, _, _)| *id == table_id)
10539 .map(|(_, name, _)| *name)
10540 else {
10541 continue;
10542 };
10543 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
10544 continue;
10545 };
10546 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
10547 for (child_id, _child_name, child_schema) in &live {
10548 for fk in &child_schema.constraints.foreign_keys {
10549 if fk.ref_table != tname {
10550 continue;
10551 }
10552 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
10553 else {
10554 continue;
10555 };
10556 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
10557 == Some(old_key.as_slice())
10558 {
10559 continue;
10560 }
10561 if fk.on_update == FkAction::Restrict {
10562 continue;
10563 }
10564 self.acquire_fk_lock(
10569 txn_id,
10570 table_id,
10571 &old_key,
10572 crate::locks::LockMode::Exclusive,
10573 control,
10574 )?;
10575 let child_rows = load_rows(*child_id)?;
10576 for (child_index, child) in child_rows.into_iter().enumerate() {
10577 commit_prepare_checkpoint(control, child_index)?;
10578 if encode_composite_key(&fk.columns, &child.columns).as_deref()
10579 != Some(old_key.as_slice())
10580 {
10581 continue;
10582 }
10583 if staging.iter().any(|(id, op)| {
10584 *id == *child_id
10585 && matches!(op, Staged::Delete(id) if *id == child.row_id)
10586 }) {
10587 continue;
10588 }
10589 let mut cells: Vec<(u16, Value)> = child
10590 .columns
10591 .iter()
10592 .map(|(column_id, value)| (*column_id, value.clone()))
10593 .collect();
10594 for (child_column, parent_column) in
10595 fk.columns.iter().zip(&fk.ref_columns)
10596 {
10597 cells.retain(|(column_id, _)| column_id != child_column);
10598 let value = match fk.on_update {
10599 FkAction::Cascade => {
10600 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
10601 }
10602 FkAction::SetNull => Value::Null,
10603 FkAction::Restrict => {
10604 return Err(MongrelError::Other(
10605 "restricted foreign-key update reached cascade preparation"
10606 .into(),
10607 ));
10608 }
10609 };
10610 cells.push((*child_column, value));
10611 }
10612 cells.sort_by_key(|(column_id, _)| *column_id);
10613 if let Some(existing_index) = staging.iter().position(|(id, op)| {
10614 *id == *child_id
10615 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
10616 }) {
10617 if let Staged::Update {
10618 new_row: existing,
10619 changed_columns,
10620 ..
10621 } = &mut staging[existing_index].1 {
10622 changed_columns.extend(fk.columns.iter().copied());
10623 changed_columns.sort_unstable();
10624 changed_columns.dedup();
10625 if *existing != cells {
10626 *existing = cells;
10627 processed_updates.remove(&existing_index);
10628 }
10629 }
10630 } else {
10631 new_ops.push((
10632 *child_id,
10633 Staged::Update {
10634 row_id: child.row_id,
10635 new_row: cells,
10636 changed_columns: fk.columns.clone(),
10637 },
10638 ));
10639 }
10640 }
10641 }
10642 }
10643 }
10644 staging.extend(new_ops);
10645 }
10646
10647 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
10652 let mut cascade_pass = 0_usize;
10653 loop {
10654 commit_prepare_checkpoint(control, cascade_pass)?;
10655 cascade_pass += 1;
10656 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
10657 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
10658 .iter()
10659 .filter_map(|(t, op)| match op {
10660 Staged::Delete(rid) => Some((*t, *rid)),
10661 _ => None,
10662 })
10663 .collect();
10664 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
10665 commit_prepare_checkpoint(control, delete_index)?;
10666 if !cascaded.insert((table_id, rid.0)) {
10667 continue;
10668 }
10669 let Some(tname) = live
10670 .iter()
10671 .find(|(t, _, _)| *t == table_id)
10672 .map(|(_, n, _)| *n)
10673 else {
10674 continue;
10675 };
10676 let parent_handle = self.table_by_id(table_id)?;
10677 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
10678 continue;
10679 };
10680 for (child_id, _child_name, child_schema) in &live {
10681 for fk in &child_schema.constraints.foreign_keys {
10682 if fk.ref_table != tname {
10683 continue;
10684 }
10685 let Some(parent_key) =
10686 encode_composite_key(&fk.ref_columns, &parent_row.columns)
10687 else {
10688 continue;
10689 };
10690 let key_preserved = staging.iter().any(|(t, op)| {
10699 if *t != table_id {
10700 return false;
10701 }
10702 let Staged::Put(cells) = op else {
10703 return false;
10704 };
10705 let map: HashMap<u16, crate::memtable::Value> =
10706 cells.iter().cloned().collect();
10707 encode_composite_key(&fk.ref_columns, &map).as_deref()
10708 == Some(parent_key.as_slice())
10709 });
10710 if key_preserved {
10711 continue;
10712 }
10713 self.acquire_fk_lock(
10720 txn_id,
10721 table_id,
10722 &parent_key,
10723 crate::locks::LockMode::Exclusive,
10724 control,
10725 )?;
10726 match fk.on_delete {
10727 FkAction::Restrict => continue,
10728 FkAction::Cascade => {
10729 let child_rows = load_rows(*child_id)?;
10730 for (child_index, cr) in child_rows.iter().enumerate() {
10731 commit_prepare_checkpoint(control, child_index)?;
10732 if !cascaded.contains(&(*child_id, cr.row_id.0))
10733 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
10734 == Some(parent_key.as_slice())
10735 {
10736 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
10737 }
10738 }
10739 }
10740 FkAction::SetNull => {
10741 let child_rows = load_rows(*child_id)?;
10742 for (child_index, cr) in child_rows.iter().enumerate() {
10743 commit_prepare_checkpoint(control, child_index)?;
10744 if !cascaded.contains(&(*child_id, cr.row_id.0))
10745 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
10746 == Some(parent_key.as_slice())
10747 {
10748 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
10751 .columns
10752 .iter()
10753 .map(|(k, v)| (*k, v.clone()))
10754 .collect();
10755 for cid in &fk.columns {
10756 cells.retain(|(k, _)| k != cid);
10757 cells.push((*cid, crate::memtable::Value::Null));
10758 }
10759 new_ops.push((
10760 *child_id,
10761 Staged::Update {
10762 row_id: cr.row_id,
10763 new_row: cells,
10764 changed_columns: fk.columns.clone(),
10765 },
10766 ));
10767 }
10768 }
10769 }
10770 }
10771 }
10772 }
10773 }
10774 if new_ops.is_empty() {
10775 break;
10776 }
10777 staging.extend(new_ops);
10778 }
10779
10780 let staged_deletes: HashSet<(u64, u64)> = staging
10784 .iter()
10785 .filter_map(|(t, op)| match op {
10786 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
10787 _ => None,
10788 })
10789 .collect();
10790
10791 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
10793
10794 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
10796 commit_prepare_checkpoint(control, operation_index)?;
10797 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
10798 else {
10799 continue;
10800 };
10801 let cells_map: HashMap<u16, crate::memtable::Value>;
10802 match op {
10803 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
10804 cells_map = cells.iter().cloned().collect();
10805
10806 if !schema.constraints.checks.is_empty() {
10808 validate_checks(&schema.constraints.checks, &cells_map)?;
10809 }
10810
10811 for uc in &schema.constraints.uniques {
10813 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
10814 continue; };
10816 let marker = (*table_id, uc.id, key.clone());
10817 if !seen_unique.insert(marker) {
10818 return Err(MongrelError::Conflict(format!(
10819 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
10820 uc.name
10821 )));
10822 }
10823 let rows = load_rows(*table_id)?;
10824 for (row_index, r) in rows.iter().enumerate() {
10825 commit_prepare_checkpoint(control, row_index)?;
10826 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
10829 continue;
10830 }
10831 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
10832 if theirs == key {
10833 return Err(MongrelError::Conflict(format!(
10834 "UNIQUE constraint '{}' on table '{tname}' violated",
10835 uc.name
10836 )));
10837 }
10838 }
10839 }
10840 }
10841
10842 for fk in &schema.constraints.foreign_keys {
10844 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
10845 continue; };
10847 let Some(parent_id) = cat
10848 .tables
10849 .iter()
10850 .find(|t| t.name == fk.ref_table)
10851 .map(|t| t.table_id)
10852 else {
10853 return Err(MongrelError::InvalidArgument(format!(
10854 "FOREIGN KEY '{}' references unknown table '{}'",
10855 fk.name, fk.ref_table
10856 )));
10857 };
10858 self.acquire_fk_lock(
10863 txn_id,
10864 parent_id,
10865 &child_key,
10866 crate::locks::LockMode::Shared,
10867 control,
10868 )?;
10869 let parent_rows = load_rows(parent_id)?;
10870 let mut found = false;
10871 for (row_index, r) in parent_rows.iter().enumerate() {
10872 commit_prepare_checkpoint(control, row_index)?;
10873 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
10874 continue;
10875 }
10876 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
10877 if pkey == child_key {
10878 found = true;
10879 break;
10880 }
10881 }
10882 }
10883 if !found {
10890 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
10891 commit_prepare_checkpoint(control, staged_index)?;
10892 if *st_table != parent_id {
10893 continue;
10894 }
10895 if let Staged::Put(pcells)
10896 | Staged::Update {
10897 new_row: pcells, ..
10898 } = st_op
10899 {
10900 let pmap: HashMap<u16, crate::memtable::Value> =
10901 pcells.iter().cloned().collect();
10902 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
10903 {
10904 if pkey == child_key {
10905 found = true;
10906 break;
10907 }
10908 }
10909 }
10910 }
10911 }
10912 if !found {
10913 return Err(MongrelError::Conflict(format!(
10914 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
10915 fk.name, fk.ref_table
10916 )));
10917 }
10918 }
10919
10920 if let Staged::Update { row_id, .. } = op {
10925 let parent_handle = self.table_by_id(*table_id)?;
10926 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
10927 continue;
10928 };
10929 for (child_id, child_name, child_schema) in &live {
10930 for fk in &child_schema.constraints.foreign_keys {
10931 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
10932 continue;
10933 }
10934 let Some(old_key) =
10935 encode_composite_key(&fk.ref_columns, &old_parent.columns)
10936 else {
10937 continue;
10938 };
10939 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
10940 == Some(old_key.as_slice())
10941 {
10942 continue;
10943 }
10944 self.acquire_fk_lock(
10949 txn_id,
10950 *table_id,
10951 &old_key,
10952 crate::locks::LockMode::Exclusive,
10953 control,
10954 )?;
10955 for (child_index, child) in
10956 load_rows(*child_id)?.into_iter().enumerate()
10957 {
10958 commit_prepare_checkpoint(control, child_index)?;
10959 if encode_composite_key(&fk.columns, &child.columns).as_deref()
10960 != Some(old_key.as_slice())
10961 {
10962 continue;
10963 }
10964 let replacement = staging.iter().find_map(|(id, op)| {
10965 if *id != *child_id {
10966 return None;
10967 }
10968 match op {
10969 Staged::Delete(id) if *id == child.row_id => Some(None),
10970 Staged::Update {
10971 row_id,
10972 new_row: cells,
10973 ..
10974 } if *row_id == child.row_id => {
10975 let map: HashMap<u16, Value> =
10976 cells.iter().cloned().collect();
10977 Some(encode_composite_key(&fk.columns, &map))
10978 }
10979 _ => None,
10980 }
10981 });
10982 if replacement.is_some_and(|key| {
10983 key.as_deref() != Some(old_key.as_slice())
10984 }) {
10985 continue;
10986 }
10987 return Err(MongrelError::Conflict(format!(
10988 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
10989 fk.name
10990 )));
10991 }
10992 }
10993 }
10994 }
10995 }
10996 Staged::Delete(rid) => {
10997 let parent_handle = self.table_by_id(*table_id)?;
11001 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
11002 continue;
11003 };
11004 for (child_id, child_name, child_schema) in &live {
11005 for fk in &child_schema.constraints.foreign_keys {
11006 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
11007 continue;
11008 }
11009 let Some(parent_key) =
11010 encode_composite_key(&fk.ref_columns, &parent_row.columns)
11011 else {
11012 continue;
11013 };
11014 let child_rows = load_rows(*child_id)?;
11015 for (row_index, r) in child_rows.iter().enumerate() {
11016 commit_prepare_checkpoint(control, row_index)?;
11017 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
11020 continue;
11021 }
11022 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
11023 if ck == parent_key {
11024 return Err(MongrelError::Conflict(format!(
11025 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
11026 fk.name
11027 )));
11028 }
11029 }
11030 }
11031 }
11032 }
11033 }
11034 Staged::Truncate => {
11035 for (child_id, child_name, child_schema) in &live {
11039 for fk in &child_schema.constraints.foreign_keys {
11040 if fk.ref_table != tname {
11041 continue;
11042 }
11043 let child_rows = load_rows(*child_id)?;
11044 if child_rows
11045 .iter()
11046 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
11047 {
11048 return Err(MongrelError::Conflict(format!(
11049 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
11050 fk.name
11051 )));
11052 }
11053 }
11054 }
11055 }
11056 }
11057 }
11058 Ok(())
11059 }
11060
11061 fn validate_write_permissions(
11062 &self,
11063 staging: &[(u64, crate::txn::Staged)],
11064 principal: Option<&crate::auth::Principal>,
11065 control: Option<&crate::ExecutionControl>,
11066 ) -> Result<()> {
11067 commit_prepare_checkpoint(control, 0)?;
11068 if principal.is_none() && !self.auth_state.require_auth() {
11069 return Ok(());
11070 }
11071 let principal = principal.ok_or(MongrelError::AuthRequired)?;
11072 let needs = summarize_write_permissions(staging);
11073 let catalog = self.catalog.read();
11074
11075 if needs.values().any(|need| need.truncate) {
11076 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
11077 }
11078 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
11079 commit_prepare_checkpoint(control, need_index)?;
11080 let entry = catalog
11081 .tables
11082 .iter()
11083 .find(|entry| {
11084 entry.table_id == table_id
11085 && matches!(entry.state, TableState::Live | TableState::Building { .. })
11086 })
11087 .ok_or_else(|| {
11088 MongrelError::NotFound(format!(
11089 "live table {table_id} not found during write validation"
11090 ))
11091 })?;
11092 if matches!(entry.state, TableState::Building { .. }) {
11093 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
11094 continue;
11095 }
11096 if need.insert {
11097 Self::require_columns_for_principal(
11098 &entry.name,
11099 &entry.schema,
11100 crate::auth::ColumnOperation::Insert,
11101 &need.insert_columns,
11102 principal,
11103 )?;
11104 }
11105 if need.update {
11106 Self::require_columns_for_principal(
11107 &entry.name,
11108 &entry.schema,
11109 crate::auth::ColumnOperation::Update,
11110 &need.update_columns,
11111 principal,
11112 )?;
11113 }
11114 if need.delete {
11115 self.require_for(
11116 Some(principal),
11117 &crate::auth::Permission::Delete {
11118 table: entry.name.clone(),
11119 },
11120 )?;
11121 }
11122 }
11123 Ok(())
11124 }
11125
11126 fn validate_security_writes(
11127 &self,
11128 staging: &[(u64, crate::txn::Staged)],
11129 read_epoch: Epoch,
11130 explicit_principal: Option<&crate::auth::Principal>,
11131 control: Option<&crate::ExecutionControl>,
11132 ) -> Result<()> {
11133 commit_prepare_checkpoint(control, 0)?;
11134 use crate::security::PolicyCommand;
11135 use crate::txn::Staged;
11136
11137 let catalog = self.catalog.read();
11138 if catalog.security.rls_tables.is_empty() {
11139 return Ok(());
11140 }
11141 let security = catalog.security.clone();
11142 let table_names = catalog
11143 .tables
11144 .iter()
11145 .filter(|entry| matches!(entry.state, TableState::Live))
11146 .map(|entry| (entry.table_id, entry.name.clone()))
11147 .collect::<HashMap<_, _>>();
11148 drop(catalog);
11149 if !staging.iter().any(|(table_id, _)| {
11150 table_names
11151 .get(table_id)
11152 .is_some_and(|table| security.rls_enabled(table))
11153 }) {
11154 return Ok(());
11155 }
11156 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
11157
11158 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
11159 commit_prepare_checkpoint(control, operation_index)?;
11160 let Some(table) = table_names.get(table_id) else {
11161 continue;
11162 };
11163 if !security.rls_enabled(table) || principal.is_admin {
11164 continue;
11165 }
11166 let denied = |command| MongrelError::PermissionDenied {
11167 required: match command {
11168 PolicyCommand::Insert => crate::auth::Permission::Insert {
11169 table: table.clone(),
11170 },
11171 PolicyCommand::Update => crate::auth::Permission::Update {
11172 table: table.clone(),
11173 },
11174 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
11175 crate::auth::Permission::Delete {
11176 table: table.clone(),
11177 }
11178 }
11179 },
11180 principal: principal.username.clone(),
11181 };
11182 match operation {
11183 Staged::Put(cells) => {
11184 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
11185 row.columns.extend(cells.iter().cloned());
11186 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
11187 return Err(denied(PolicyCommand::Insert));
11188 }
11189 }
11190 Staged::Update {
11191 row_id,
11192 new_row: cells,
11193 ..
11194 } => {
11195 let old = self
11196 .table_by_id(*table_id)?
11197 .lock()
11198 .get(*row_id, Snapshot::at(read_epoch))
11199 .ok_or_else(|| {
11200 MongrelError::NotFound(format!("row {} not found", row_id.0))
11201 })?;
11202 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
11203 return Err(denied(PolicyCommand::Update));
11204 }
11205 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
11206 new.columns.extend(cells.iter().cloned());
11207 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
11208 return Err(denied(PolicyCommand::Update));
11209 }
11210 }
11211 Staged::Delete(row_id) => {
11212 let old = self
11213 .table_by_id(*table_id)?
11214 .lock()
11215 .get(*row_id, Snapshot::at(read_epoch))
11216 .ok_or_else(|| {
11217 MongrelError::NotFound(format!("row {} not found", row_id.0))
11218 })?;
11219 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
11220 return Err(denied(PolicyCommand::Delete));
11221 }
11222 }
11223 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
11224 }
11225 }
11226 Ok(())
11227 }
11228
11229 #[allow(clippy::too_many_arguments)]
11236 pub(crate) fn commit_transaction_with_external_states(
11237 &self,
11238 txn_id: u64,
11239 read_epoch: Epoch,
11240 staging: Vec<(u64, crate::txn::Staged)>,
11241 external_states: Vec<(String, Vec<u8>)>,
11242 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
11243 security_principal: Option<crate::auth::Principal>,
11244 principal_catalog_bound: bool,
11245 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
11246 context: crate::txn::TxnCommitContext,
11247 ) -> Result<(Epoch, Vec<RowId>)> {
11248 self.commit_transaction_with_external_states_inner(
11249 txn_id,
11250 read_epoch,
11251 staging,
11252 external_states,
11253 materialized_view_updates,
11254 security_principal,
11255 principal_catalog_bound,
11256 external_trigger_bridge,
11257 context,
11258 None,
11259 None,
11260 )
11261 }
11262
11263 #[allow(clippy::too_many_arguments)]
11264 pub(crate) fn commit_transaction_with_external_states_controlled(
11265 &self,
11266 txn_id: u64,
11267 read_epoch: Epoch,
11268 staging: Vec<(u64, crate::txn::Staged)>,
11269 external_states: Vec<(String, Vec<u8>)>,
11270 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
11271 security_principal: Option<crate::auth::Principal>,
11272 principal_catalog_bound: bool,
11273 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
11274 context: crate::txn::TxnCommitContext,
11275 control: &crate::ExecutionControl,
11276 before_commit: &mut dyn FnMut() -> Result<()>,
11277 ) -> Result<(Epoch, Vec<RowId>)> {
11278 self.commit_transaction_with_external_states_inner(
11279 txn_id,
11280 read_epoch,
11281 staging,
11282 external_states,
11283 materialized_view_updates,
11284 security_principal,
11285 principal_catalog_bound,
11286 external_trigger_bridge,
11287 context,
11288 Some(control),
11289 Some(before_commit),
11290 )
11291 }
11292
11293 #[allow(clippy::too_many_arguments)]
11294 fn commit_transaction_with_external_states_inner(
11295 &self,
11296 txn_id: u64,
11297 read_epoch: Epoch,
11298 mut staging: Vec<(u64, crate::txn::Staged)>,
11299 external_states: Vec<(String, Vec<u8>)>,
11300 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
11301 mut security_principal: Option<crate::auth::Principal>,
11302 principal_catalog_bound: bool,
11303 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
11304 context: crate::txn::TxnCommitContext,
11305 control: Option<&crate::ExecutionControl>,
11306 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11307 ) -> Result<(Epoch, Vec<RowId>)> {
11308 use crate::memtable::Row;
11309 use crate::txn::{Staged, StagedOp, WriteKey};
11310 use crate::wal::Op;
11311 use std::collections::hash_map::DefaultHasher;
11312 use std::hash::{Hash, Hasher};
11313 use std::sync::atomic::Ordering;
11314
11315 if txn_id == crate::wal::SYSTEM_TXN_ID {
11316 return Err(MongrelError::Full(
11317 "per-open transaction id namespace exhausted; reopen the database".into(),
11318 ));
11319 }
11320 if self.read_only {
11321 return Err(MongrelError::ReadOnlyReplica);
11322 }
11323 commit_prepare_checkpoint(control, 0)?;
11324 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
11325 self.refresh_security_catalog_if_stale(observed_security_version)?;
11326 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
11327 if self.auth_state.require_auth() && security_principal.is_none() {
11328 return Err(MongrelError::AuthRequired);
11329 }
11330 {
11331 let catalog = self.catalog.read();
11332 if catalog.require_auth
11333 || principal_catalog_bound
11334 || security_principal
11335 .as_ref()
11336 .is_some_and(|principal| principal.user_id != 0)
11337 {
11338 let principal = security_principal
11339 .as_ref()
11340 .ok_or(MongrelError::AuthRequired)?;
11341 security_principal =
11342 Self::resolve_bound_principal_from_catalog(&catalog, principal);
11343 if security_principal.is_none() {
11344 return Err(MongrelError::AuthRequired);
11345 }
11346 }
11347 }
11348 let _replication_guard = self.replication_barrier.read();
11349 if self.poisoned.load(Ordering::Relaxed) {
11350 return Err(MongrelError::Other(
11351 "database poisoned by fsync error".into(),
11352 ));
11353 }
11354 let _operation = self.admit_operation()?;
11358
11359 if context.state.is_some() {
11368 self.acquire_txn_lock(
11369 txn_id,
11370 crate::locks::LockKey::schema_barrier(),
11371 crate::locks::LockMode::Shared,
11372 control,
11373 )?;
11374 }
11375 let _txn_lock_guard = TxnLockGuard {
11376 locks: &self.lock_manager,
11377 txn_id,
11378 };
11379
11380 let idempotency_request = match &context.idempotency {
11386 Some(request) => match self.idempotency.check_and_reserve(request)? {
11387 crate::txn::IdempotencyCheck::Replay(receipt) => {
11388 let epoch = Epoch(receipt.log_position.index);
11389 if let Some(state) = &context.state {
11390 state.committed(receipt);
11391 }
11392 return Ok((epoch, Vec::new()));
11393 }
11394 crate::txn::IdempotencyCheck::Reserved => Some(request.clone()),
11395 },
11396 None => None,
11397 };
11398 let mut idempotency_guard = idempotency_request.as_ref().map(|request| {
11400 crate::txn::IdempotencyReservationGuard::new(&self.idempotency, request.clone())
11401 });
11402 let mut external_states = dedup_external_states(external_states);
11403 if !external_states.is_empty() {
11404 let cat = self.catalog.read();
11405 for (name, _) in &external_states {
11406 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
11407 return Err(MongrelError::NotFound(format!(
11408 "external table {name:?} not found"
11409 )));
11410 }
11411 }
11412 }
11413 let prepared_materialized_views = {
11414 let mut deduplicated = HashMap::new();
11415 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
11416 {
11417 commit_prepare_checkpoint(control, definition_index)?;
11418 if definition.name.is_empty() || definition.query.trim().is_empty() {
11419 return Err(MongrelError::InvalidArgument(
11420 "materialized view name and query must not be empty".into(),
11421 ));
11422 }
11423 deduplicated.insert(definition.name.clone(), definition);
11424 }
11425 let catalog = self.catalog.read();
11426 let mut prepared = Vec::with_capacity(deduplicated.len());
11427 for (definition_index, definition) in deduplicated.into_values().enumerate() {
11428 commit_prepare_checkpoint(control, definition_index)?;
11429 let table_id = catalog
11430 .live(&definition.name)
11431 .ok_or_else(|| {
11432 MongrelError::NotFound(format!(
11433 "materialized view table {:?} not found",
11434 definition.name
11435 ))
11436 })?
11437 .table_id;
11438 prepared.push((table_id, definition));
11439 }
11440 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
11441 prepared
11442 };
11443
11444 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
11447 self.expand_table_triggers(
11448 txn_id,
11449 &mut staging,
11450 read_epoch,
11451 external_trigger_bridge,
11452 &mut external_states,
11453 control,
11454 )?;
11455 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
11456 external_states = dedup_external_states(external_states);
11457 let expected_external_generations = {
11458 let catalog = self.catalog.read();
11459 let mut generations = HashMap::with_capacity(external_states.len());
11460 for (name, _) in &external_states {
11461 let entry = catalog
11462 .external_tables
11463 .iter()
11464 .find(|entry| entry.name == *name)
11465 .ok_or_else(|| {
11466 MongrelError::NotFound(format!("external table {name:?} not found"))
11467 })?;
11468 generations.insert(name.clone(), entry.created_epoch);
11469 }
11470 generations
11471 };
11472
11473 self.acquire_unique_key_claims(txn_id, &staging, control)?;
11479 self.validate_constraints(txn_id, &mut staging, read_epoch, control)?;
11484 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
11485 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
11486 let mut normalized = Vec::with_capacity(staging.len() * 2);
11487 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
11488 commit_prepare_checkpoint(control, staged_index)?;
11489 match op {
11490 crate::txn::Staged::Update {
11491 row_id,
11492 new_row: cells,
11493 ..
11494 } => {
11495 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
11496 normalized.push((table_id, crate::txn::Staged::Put(cells)));
11497 }
11498 op => normalized.push((table_id, op)),
11499 }
11500 }
11501 staging = normalized;
11502 let has_changes = !staging.is_empty()
11503 || !external_states.is_empty()
11504 || !prepared_materialized_views.is_empty();
11505 let truncated_tables: HashSet<u64> = staging
11506 .iter()
11507 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
11508 .collect();
11509
11510 let write_keys = {
11511 let cat = self.catalog.read();
11512 let mut keys: Vec<WriteKey> = Vec::new();
11513 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11514 commit_prepare_checkpoint(control, staged_index)?;
11515 match staged {
11516 Staged::Put(cells) => {
11517 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
11518 for col in &entry.schema.columns {
11519 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
11520 if let Some((_, val)) =
11521 cells.iter().find(|(id, _)| *id == col.id)
11522 {
11523 let mut h = DefaultHasher::new();
11524 val.encode_key().hash(&mut h);
11525 keys.push(WriteKey::Unique {
11526 table_id: *table_id,
11527 index_id: 0,
11528 key_hash: h.finish(),
11529 });
11530 }
11531 }
11532 }
11533 for uc in &entry.schema.constraints.uniques {
11540 if let Some(key_bytes) = crate::constraint::encode_composite_key(
11541 &uc.columns,
11542 &cells.iter().cloned().collect(),
11543 ) {
11544 let mut h = DefaultHasher::new();
11545 key_bytes.hash(&mut h);
11546 keys.push(WriteKey::Unique {
11547 table_id: *table_id,
11548 index_id: uc.id | 0x8000,
11549 key_hash: h.finish(),
11550 });
11551 }
11552 }
11553 }
11554 }
11555 Staged::Delete(rid) => keys.push(WriteKey::Row {
11556 table_id: *table_id,
11557 row_id: rid.0,
11558 }),
11559 Staged::Truncate => keys.push(WriteKey::Table {
11560 table_id: *table_id,
11561 }),
11562 Staged::Update { .. } => {
11563 return Err(MongrelError::Other(
11564 "transaction contains an unnormalized update during preparation".into(),
11565 ));
11566 }
11567 }
11568 }
11569 for (external_index, (name, _)) in external_states.iter().enumerate() {
11570 commit_prepare_checkpoint(control, external_index)?;
11571 let mut h = DefaultHasher::new();
11572 name.hash(&mut h);
11573 keys.push(WriteKey::Unique {
11574 table_id: EXTERNAL_TABLE_ID,
11575 index_id: 0,
11576 key_hash: h.finish(),
11577 });
11578 }
11579 keys
11580 };
11581
11582 let min_active = self.active_txns.min_read_epoch();
11584 if min_active < u64::MAX {
11585 self.conflicts.prune_below(Epoch(min_active));
11586 }
11587
11588 let ssi_keys = match context.isolation.canonical() {
11595 crate::txn::IsolationLevel::Serializable => {
11596 crate::txn::ssi_validation_keys(&context.read_set, &context.predicate_set)
11597 }
11598 _ => Vec::new(),
11599 };
11600
11601 if self.conflicts.conflicts(&write_keys, read_epoch) {
11605 return Err(MongrelError::Conflict(
11606 "write-write conflict (pre-validate, first-committer-wins)".into(),
11607 ));
11608 }
11609 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
11610 return Err(MongrelError::SerializationFailure {
11611 message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
11612 .into(),
11613 });
11614 }
11615 let pre_validate_version = self.conflicts.version();
11616
11617 let mut spilled: Vec<SpilledRun> = Vec::new();
11621 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
11622 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
11626 {
11627 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
11628 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
11629 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11630 commit_prepare_checkpoint(control, staged_index)?;
11631 if let Staged::Put(cells) = staged {
11632 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
11633 bytes.saturating_add(value.estimated_bytes())
11634 });
11635 let table_bytes = table_bytes.entry(*table_id).or_default();
11636 *table_bytes = table_bytes.saturating_add(bytes);
11637 put_indexes.entry(*table_id).or_default().push(staged_index);
11638 }
11639 }
11640 let tables = self.tables.read();
11641 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
11642 commit_prepare_checkpoint(control, table_index)?;
11643 if bytes
11644 <= self
11645 .spill_threshold
11646 .load(std::sync::atomic::Ordering::Relaxed)
11647 {
11648 continue;
11649 }
11650 let Some(handle) = tables.get(&table_id) else {
11651 continue;
11652 };
11653 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
11654 let mut t = handle.lock();
11655 let tdir = t.table_dir().to_path_buf();
11656 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
11657 std::fs::create_dir_all(&txn_dir)?;
11658 let run_id = t.alloc_run_id()? as u128;
11659 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
11660 let final_path = t.run_path(run_id as u64);
11661
11662 let mut rows: Vec<Row> = Vec::new();
11663 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
11664 commit_prepare_checkpoint(control, put_index)?;
11665 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
11666 return Err(MongrelError::Other(
11667 "transaction put index no longer references a put".into(),
11668 ));
11669 };
11670 t.validate_cells_not_null(cells)?;
11671 let row_id = t.alloc_row_id()?;
11672 let mut row = Row::new(row_id, Epoch(0));
11673 row.columns.extend(std::mem::take(cells));
11674 rows.push(row);
11675 }
11676 let schema = t.schema_ref().clone();
11677 let kek = t.kek_ref().cloned();
11678 let specs = t.indexable_column_specs();
11679 drop(t);
11680
11681 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
11682 .uniform_epoch(true);
11683 if let Some(ref kek) = kek {
11684 writer = writer.with_encryption(kek.as_ref(), specs);
11685 }
11686 commit_prepare_checkpoint(control, 0)?;
11687 let header = writer.write(&pending_path, &rows)?;
11688 commit_prepare_checkpoint(control, 0)?;
11689 let row_count = header.row_count;
11690 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
11691 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
11692
11693 spilled.push(SpilledRun {
11694 table_id,
11695 run_id,
11696 pending_path,
11697 final_path,
11698 rows,
11699 row_count,
11700 min_rid,
11701 max_rid,
11702 content_hash: header.content_hash,
11703 });
11704 spilled_tables.insert(table_id);
11705 }
11706 }
11707
11708 if spill_guard.is_some() {
11710 if let Some(hook) = self.spill_hook.lock().as_ref() {
11711 hook();
11712 }
11713 }
11714
11715 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
11726 .take(staging.len())
11727 .collect();
11728 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
11729 .take(staging.len())
11730 .collect();
11731 {
11732 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
11733 for (index, (table_id, staged)) in staging.iter().enumerate() {
11734 commit_prepare_checkpoint(control, index)?;
11735 if matches!(staged, Staged::Delete(_))
11736 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
11737 {
11738 indexes_by_table.entry(*table_id).or_default().push(index);
11739 }
11740 }
11741 let tables = self.tables.read();
11742 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
11743 commit_prepare_checkpoint(control, table_index)?;
11744 let handle = tables.get(&table_id).ok_or_else(|| {
11745 MongrelError::NotFound(format!("table {table_id} not mounted"))
11746 })?;
11747 #[cfg(test)]
11748 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
11749 let mut t = handle.lock();
11750 for (prepare_index, index) in indexes.into_iter().enumerate() {
11751 commit_prepare_checkpoint(control, prepare_index)?;
11752 match &staging[index].1 {
11753 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
11754 t.validate_cells_not_null(cells)?;
11755 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
11756 for (column, value) in cells {
11757 row.columns.insert(*column, value.clone());
11758 }
11759 prebuilt[index] = Some(row);
11760 }
11761 Staged::Delete(row_id) => {
11762 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
11763 }
11764 Staged::Put(_) | Staged::Truncate => {}
11765 Staged::Update { .. } => {
11766 return Err(MongrelError::Other(
11767 "transaction contains an unnormalized update during row preparation"
11768 .into(),
11769 ));
11770 }
11771 }
11772 }
11773 }
11774 }
11775
11776 let prepared_table_handles = {
11780 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
11781 let put_table_ids: HashSet<u64> = staging
11782 .iter()
11783 .filter_map(|(table_id, staged)| {
11784 matches!(staged, Staged::Put(_)).then_some(*table_id)
11785 })
11786 .collect();
11787 let tables = self.tables.read();
11788 let mut handles = HashMap::with_capacity(table_ids.len());
11789 for (table_index, table_id) in table_ids.into_iter().enumerate() {
11790 commit_prepare_checkpoint(control, table_index)?;
11791 let handle = tables.get(&table_id).ok_or_else(|| {
11792 MongrelError::NotFound(format!("table {table_id} not mounted"))
11793 })?;
11794 if put_table_ids.contains(&table_id) {
11795 match control {
11796 Some(control) => {
11797 handle.lock().prepare_durable_publish_controlled(control)?
11798 }
11799 None => handle.lock().prepare_durable_publish()?,
11800 }
11801 }
11802 handles.insert(table_id, handle.clone());
11803 }
11804 handles
11805 };
11806
11807 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
11811
11812 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
11813 .iter()
11814 .map(|run| {
11815 (
11816 run.table_id,
11817 run.rows.iter().map(|row| row.row_id).collect(),
11818 )
11819 })
11820 .collect();
11821 let committed_row_ids = staging
11822 .iter()
11823 .enumerate()
11824 .filter_map(|(index, (table_id, staged))| {
11825 if !matches!(staged, Staged::Put(_)) {
11826 return None;
11827 }
11828 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
11829 spilled_row_ids
11830 .get_mut(table_id)
11831 .and_then(VecDeque::pop_front)
11832 })
11833 })
11834 .collect();
11835
11836 let mut prepared_external = Vec::with_capacity(external_states.len());
11837 for (external_index, (name, state)) in external_states.iter().enumerate() {
11838 commit_prepare_checkpoint(control, external_index)?;
11839 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
11840 prepared_external.push((name.clone(), state.clone(), pending));
11841 }
11842
11843 if context.state.is_some() {
11853 self.lock_manager
11854 .release(txn_id, &crate::locks::LockKey::schema_barrier());
11855 }
11856 let added_runs: Vec<crate::wal::AddedRun> = spilled
11857 .iter()
11858 .map(|s| crate::wal::AddedRun {
11859 table_id: s.table_id,
11860 run_id: s.run_id,
11861 row_count: s.row_count,
11862 level: 0,
11863 min_row_id: s.min_rid,
11864 max_row_id: s.max_rid,
11865 content_hash: s.content_hash,
11866 })
11867 .collect();
11868 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
11869 hook();
11870 }
11871 let security_guard = self.security_coordinator.gate.read();
11875 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
11876 return Err(MongrelError::Conflict(
11877 "security policy changed during write".into(),
11878 ));
11879 }
11880 if spill_guard.is_some() {
11881 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
11882 hook();
11883 }
11884 }
11885 let commit_guard = self.commit_lock.lock();
11886 let catalog_generation_result = (|| {
11887 {
11888 let catalog = self.catalog.read();
11889 for table_id in prepared_table_handles.keys() {
11890 let is_current = catalog.tables.iter().any(|entry| {
11891 entry.table_id == *table_id
11892 && matches!(entry.state, TableState::Live | TableState::Building { .. })
11893 });
11894 if !is_current {
11895 return Err(MongrelError::Conflict(format!(
11896 "table {table_id} changed during transaction preparation"
11897 )));
11898 }
11899 }
11900 for (name, created_epoch) in &expected_external_generations {
11901 let current = catalog
11902 .external_tables
11903 .iter()
11904 .find(|entry| entry.name == *name)
11905 .map(|entry| entry.created_epoch);
11906 if current != Some(*created_epoch) {
11907 return Err(MongrelError::Conflict(format!(
11908 "external table {name:?} changed during transaction preparation"
11909 )));
11910 }
11911 }
11912 for (table_id, definition) in &prepared_materialized_views {
11913 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
11914 if current != Some(*table_id) {
11915 return Err(MongrelError::Conflict(format!(
11916 "materialized view {:?} changed during transaction preparation",
11917 definition.name
11918 )));
11919 }
11920 }
11921 if trigger_catalog_binding(&catalog) != trigger_binding {
11922 return Err(MongrelError::Conflict(
11923 "trigger or referenced table generation changed during transaction preparation"
11924 .into(),
11925 ));
11926 }
11927 }
11928 let tables = self.tables.read();
11929 for (table_id, prepared) in &prepared_table_handles {
11930 if !tables
11931 .get(table_id)
11932 .is_some_and(|current| current.ptr_eq(prepared))
11933 {
11934 return Err(MongrelError::Conflict(format!(
11935 "table {table_id} mount changed during transaction preparation"
11936 )));
11937 }
11938 }
11939 Ok(())
11940 })();
11941 if let Err(error) = catalog_generation_result {
11942 drop(commit_guard);
11943 for (_, _, pending) in &prepared_external {
11944 let _ = std::fs::remove_file(pending);
11945 }
11946 return Err(error);
11947 }
11948 let new_epoch = self.epoch.assigned().next();
11952 let mut spilled_wal_bytes = 0;
11953 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
11954 let spill_prepare = (|| {
11955 for run in &mut spilled {
11956 for row in &mut run.rows {
11957 row.committed_epoch = new_epoch;
11958 }
11959 for rows in encode_spilled_row_chunks(
11960 &run.rows,
11961 &mut spilled_wal_bytes,
11962 SPILLED_WAL_TOTAL_MAX_BYTES,
11963 control,
11964 )? {
11965 spilled_wal_records.push((
11966 run.table_id,
11967 Op::SpilledRows {
11968 table_id: run.table_id,
11969 rows,
11970 },
11971 ));
11972 }
11973 }
11974 Result::<()>::Ok(())
11975 })();
11976 if let Err(error) = spill_prepare {
11977 for (_, _, pending) in &prepared_external {
11978 let _ = std::fs::remove_file(pending);
11979 }
11980 return Err(error);
11981 }
11982 let (
11983 new_epoch,
11984 mut _epoch_guard,
11985 applies,
11986 committed_materialized_views,
11987 commit_seq,
11988 commit_ts,
11989 ) = {
11990 let mut wal = self.shared_wal.lock();
11991
11992 if self.conflicts.version() != pre_validate_version {
11997 if self.conflicts.conflicts(&write_keys, read_epoch) {
11998 drop(wal);
12001 for (_, _, pending) in &prepared_external {
12002 let _ = std::fs::remove_file(pending);
12003 }
12004 return Err(MongrelError::Conflict(
12005 "write-write conflict (sequencer delta re-check)".into(),
12006 ));
12007 }
12008 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
12009 drop(wal);
12013 for (_, _, pending) in &prepared_external {
12014 let _ = std::fs::remove_file(pending);
12015 }
12016 return Err(MongrelError::SerializationFailure {
12017 message:
12018 "a concurrent commit invalidated this transaction's reads (sequencer re-check)"
12019 .into(),
12020 });
12021 }
12022 }
12023
12024 if let Some(control) = control {
12025 if let Err(error) = control.checkpoint() {
12026 drop(wal);
12027 for (_, _, pending) in &prepared_external {
12028 let _ = std::fs::remove_file(pending);
12029 }
12030 return Err(error);
12031 }
12032 }
12033 let mut applies = Vec::<TableApplyBatch>::new();
12034 let mut apply_indexes = HashMap::<u64, usize>::new();
12035 let mut committed_materialized_views = Vec::new();
12036 let mut wal_records = spilled_wal_records;
12037
12038 let mut index = 0;
12039 while index < staging.len() {
12040 let table_id = staging[index].0;
12041 let handle = prepared_table_handles
12042 .get(&table_id)
12043 .cloned()
12044 .ok_or_else(|| {
12045 MongrelError::NotFound(format!("table {table_id} not prepared"))
12046 })?;
12047 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
12048 let index = applies.len();
12049 applies.push(TableApplyBatch {
12050 table_id,
12051 handle,
12052 ops: Vec::new(),
12053 });
12054 index
12055 });
12056
12057 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
12060 {
12061 index += 1;
12062 continue;
12063 }
12064
12065 match &staging[index].1 {
12066 Staged::Put(_) => {
12067 let mut rows = Vec::new();
12068 while index < staging.len()
12069 && staging[index].0 == table_id
12070 && matches!(&staging[index].1, Staged::Put(_))
12071 {
12072 let mut row = prebuilt[index].take().ok_or_else(|| {
12073 MongrelError::Other(
12074 "transaction prepare lost a prebuilt put row".into(),
12075 )
12076 })?;
12077 row.committed_epoch = new_epoch;
12078 rows.push(row);
12079 index += 1;
12080 }
12081 let payload = bincode::serialize(&rows)
12082 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
12083 wal_records.push((
12084 table_id,
12085 Op::Put {
12086 table_id,
12087 rows: payload,
12088 },
12089 ));
12090 applies[batch_index].ops.push(StagedOp::Put(rows));
12091 }
12092 Staged::Delete(_) => {
12093 let mut row_ids = Vec::new();
12094 while index < staging.len()
12095 && staging[index].0 == table_id
12096 && matches!(&staging[index].1, Staged::Delete(_))
12097 {
12098 let Staged::Delete(row_id) = &staging[index].1 else {
12099 return Err(MongrelError::Other(
12100 "transaction delete batch changed during WAL preparation"
12101 .into(),
12102 ));
12103 };
12104 if let Some(before) = &delete_images[index] {
12105 wal_records.push((
12106 table_id,
12107 Op::BeforeImage {
12108 table_id,
12109 row_id: *row_id,
12110 row: bincode::serialize(before).map_err(|error| {
12111 MongrelError::Other(format!(
12112 "before-image serialize: {error}"
12113 ))
12114 })?,
12115 },
12116 ));
12117 }
12118 row_ids.push(*row_id);
12119 index += 1;
12120 }
12121 wal_records.push((
12122 table_id,
12123 Op::Delete {
12124 table_id,
12125 row_ids: row_ids.clone(),
12126 },
12127 ));
12128 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
12129 }
12130 Staged::Truncate => {
12131 wal_records.push((table_id, Op::TruncateTable { table_id }));
12132 applies[batch_index].ops.push(StagedOp::Truncate);
12133 index += 1;
12134 }
12135 Staged::Update { .. } => {
12136 return Err(MongrelError::Other(
12137 "transaction contains an unnormalized update at the sequencer".into(),
12138 ));
12139 }
12140 }
12141 }
12142
12143 for (name, state, _) in &prepared_external {
12144 wal_records.push((
12145 EXTERNAL_TABLE_ID,
12146 Op::ExternalTableState {
12147 name: name.clone(),
12148 state: state.clone(),
12149 },
12150 ));
12151 }
12152
12153 for (table_id, definition) in &prepared_materialized_views {
12154 let mut definition = definition.clone();
12155 definition.last_refresh_epoch = new_epoch.0;
12156 wal_records.push((
12157 *table_id,
12158 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
12159 name: definition.name.clone(),
12160 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
12161 }),
12162 ));
12163 committed_materialized_views.push(definition);
12164 }
12165 if !committed_materialized_views.is_empty() {
12166 let mut next_catalog = self.catalog.read().clone();
12167 for definition in &committed_materialized_views {
12168 if let Some(existing) = next_catalog
12169 .materialized_views
12170 .iter_mut()
12171 .find(|existing| existing.name == definition.name)
12172 {
12173 *existing = definition.clone();
12174 } else {
12175 next_catalog.materialized_views.push(definition.clone());
12176 }
12177 }
12178 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
12179 wal_records.push((
12180 WAL_TABLE_ID,
12181 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
12182 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
12183 }),
12184 ));
12185 }
12186
12187 if let Some(control) = control {
12188 if let Err(error) = control.checkpoint() {
12189 drop(wal);
12190 for (_, _, pending) in &prepared_external {
12191 let _ = std::fs::remove_file(pending);
12192 }
12193 return Err(error);
12194 }
12195 }
12196 if let Some(before_commit) = before_commit.as_mut() {
12197 if let Err(error) = before_commit() {
12198 drop(wal);
12199 for (_, _, pending) in &prepared_external {
12200 let _ = std::fs::remove_file(pending);
12201 }
12202 return Err(error);
12203 }
12204 }
12205
12206 let assigned_epoch = self.epoch.bump_assigned();
12207 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
12208 if assigned_epoch != new_epoch {
12209 for (_, _, pending) in &prepared_external {
12210 let _ = std::fs::remove_file(pending);
12211 }
12212 return Err(MongrelError::Conflict(
12213 "commit epoch changed while sequencer lock was held".into(),
12214 ));
12215 }
12216
12217 let commit_ts = match context.read_ts {
12225 Some(read_ts) => self.hlc.commit_timestamp([read_ts]),
12226 None => self.hlc.now().map_err(|skew| {
12227 MongrelError::Other(format!(
12228 "clock skew rejected commit timestamp allocation: {skew}"
12229 ))
12230 })?,
12231 };
12232 if let Some(state) = &context.state {
12233 state.enter_commit_critical();
12234 }
12235
12236 prepared_run_links.disarm();
12240
12241 let commit_seq = self
12245 .commit_log
12246 .append_transaction(
12247 &mut wal,
12248 txn_id,
12249 new_epoch,
12250 commit_ts,
12251 wal_records,
12252 &added_runs,
12253 )
12254 .map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
12255
12256 self.conflicts.record(&write_keys, new_epoch);
12261 (
12262 new_epoch,
12263 _epoch_guard,
12264 applies,
12265 committed_materialized_views,
12266 commit_seq,
12267 commit_ts,
12268 )
12269 };
12270 drop(commit_guard);
12271
12272 let receipt =
12274 self.await_durable_commit_with_ts(txn_id, commit_seq, new_epoch, commit_ts)?;
12275 drop(security_guard);
12276
12277 if let Some(request) = &idempotency_request {
12281 if let Err(error) =
12282 self.idempotency
12283 .complete(request, txn_id, new_epoch, receipt.commit_ts)
12284 {
12285 return Err(MongrelError::DurableCommit {
12286 epoch: new_epoch.0,
12287 message: format!("idempotency record was not durable: {error}"),
12288 });
12289 }
12290 if let Some(guard) = idempotency_guard.as_mut() {
12291 guard.disarm();
12292 }
12293 }
12294
12295 let publish_result: Result<()> = {
12297 let mut first_error = None;
12298 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
12299 for run in &spilled {
12300 spilled_by_table.entry(run.table_id).or_default().push(run);
12301 }
12302 let mut modified_tables = Vec::with_capacity(applies.len());
12303 for batch in applies {
12306 #[cfg(test)]
12307 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
12308 let mut t = batch.handle.lock();
12309 for op in batch.ops {
12310 match op {
12311 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
12312 StagedOp::Delete(row_ids) => {
12313 for row_id in row_ids {
12314 t.apply_delete(row_id, new_epoch);
12315 }
12316 }
12317 StagedOp::Truncate => t.apply_truncate(new_epoch),
12318 }
12319 }
12320 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
12321 for run in runs {
12322 t.link_run(crate::manifest::RunRef {
12323 run_id: run.run_id,
12324 level: 0,
12325 epoch_created: new_epoch.0,
12326 row_count: run.row_count,
12327 });
12328 t.apply_run_metadata_prepared(&run.rows)?;
12329 if truncated_tables.contains(&batch.table_id) {
12330 t.set_flushed_epoch(new_epoch);
12335 }
12336 }
12337 }
12338 t.invalidate_pending_cache();
12339 drop(t);
12340 modified_tables.push(batch.handle);
12341 }
12342
12343 for handle in modified_tables {
12347 #[cfg(test)]
12348 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
12349 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
12350 first_error.get_or_insert(error);
12351 }
12352 }
12353 for (name, _, pending) in &prepared_external {
12354 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
12355 first_error.get_or_insert(error);
12356 }
12357 }
12358 if !committed_materialized_views.is_empty() {
12359 let mut next_catalog = self.catalog.read().clone();
12360 for definition in committed_materialized_views {
12361 if let Some(existing) = next_catalog
12362 .materialized_views
12363 .iter_mut()
12364 .find(|existing| existing.name == definition.name)
12365 {
12366 *existing = definition;
12367 } else {
12368 next_catalog.materialized_views.push(definition);
12369 }
12370 }
12371 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
12372 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
12373 first_error.get_or_insert(error);
12374 }
12375 }
12376 match first_error {
12377 Some(error) => Err(error),
12378 None => Ok(()),
12379 }
12380 };
12381
12382 if has_changes {
12383 let _ = self.change_wake.send(());
12384 }
12385 self.finish_durable_publish(new_epoch, &mut _epoch_guard, &receipt, publish_result)?;
12386 if let Some(state) = &context.state {
12390 state.committed(receipt.clone());
12391 }
12392 Ok((new_epoch, committed_row_ids))
12393 }
12394
12395 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
12398 let e = self.epoch.visible();
12399 let g = self.snapshots.register(e);
12400 (Snapshot::at(e), g)
12401 }
12402
12403 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
12406 let e = self.epoch.visible();
12407 let g = self.snapshots.register_owned(e);
12408 (Snapshot::at(e), g)
12409 }
12410
12411 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
12416 let _guard = self.ddl_lock.lock();
12417 let current = self.epoch.visible();
12418 let (old_epochs, old_start) = self.snapshots.history_config();
12419 let earliest_already_guaranteed = if old_epochs == 0 {
12420 current
12421 } else {
12422 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
12423 };
12424 let start = if epochs == 0 {
12425 current
12426 } else {
12427 earliest_already_guaranteed
12428 };
12429 let published = std::cell::Cell::new(false);
12430 let result = write_history_retention(&self.root, epochs, start, || {
12431 self.snapshots.configure_history(epochs, start);
12432 published.set(true);
12433 });
12434 match result {
12435 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
12436 epoch: current.0,
12437 message: format!("history-retention publication was not durable: {error}"),
12438 }),
12439 result => result,
12440 }
12441 }
12442
12443 pub fn history_retention_epochs(&self) -> u64 {
12444 self.snapshots.history_config().0
12445 }
12446
12447 pub fn earliest_retained_epoch(&self) -> Epoch {
12448 let current = self.epoch.visible();
12449 self.snapshots.history_floor(current).unwrap_or(current)
12450 }
12451
12452 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
12455 let current = self.epoch.visible();
12456 if epoch > current {
12457 return Err(MongrelError::InvalidArgument(format!(
12458 "epoch {} is in the future; current epoch is {}",
12459 epoch.0, current.0
12460 )));
12461 }
12462 let earliest = self.earliest_retained_epoch();
12463 if epoch < earliest {
12464 return Err(MongrelError::InvalidArgument(format!(
12465 "epoch {} is no longer retained; earliest available epoch is {}",
12466 epoch.0, earliest.0
12467 )));
12468 }
12469 let guard = self.snapshots.register_owned(epoch);
12470 Ok((Snapshot::at(epoch), guard))
12471 }
12472
12473 pub fn table_names(&self) -> Vec<String> {
12475 self.catalog
12476 .read()
12477 .tables
12478 .iter()
12479 .filter(|t| matches!(t.state, TableState::Live))
12480 .map(|t| t.name.clone())
12481 .collect()
12482 }
12483
12484 pub fn close(&self) -> Result<()> {
12490 for name in self.table_names() {
12491 if let Ok(handle) = self.table(&name) {
12492 if let Err(e) = handle.lock().close() {
12493 eprintln!("[close] flush failed for {name}: {e}");
12494 }
12495 }
12496 }
12497 Ok(())
12498 }
12499
12500 pub fn compact(&self) -> Result<(usize, usize)> {
12507 self.require(&crate::auth::Permission::Ddl)?;
12508 let _operation = self.admit_operation()?;
12510 let mut compacted = 0;
12511 let mut skipped = 0;
12512 for name in self.table_names() {
12513 let Ok(handle) = self.table(&name) else {
12514 continue;
12515 };
12516 {
12517 let mut t = handle.lock();
12518 let before = t.run_count();
12519 if before < 2 && !t.should_compact() {
12520 skipped += 1;
12521 continue;
12522 }
12523 match t.compact() {
12524 Ok(()) => {
12525 let after = t.run_count();
12526 compacted += 1;
12527 eprintln!("[compact] {name}: {before} -> {after} runs");
12528 }
12529 Err(e) => {
12530 eprintln!("[compact] {name}: compaction failed: {e}");
12531 skipped += 1;
12532 }
12533 }
12534 }
12535 }
12536 Ok((compacted, skipped))
12537 }
12538
12539 pub fn compact_table(&self, name: &str) -> Result<bool> {
12542 self.require(&crate::auth::Permission::Ddl)?;
12543 let handle = self.table(name)?;
12544 let mut t = handle.lock();
12545 let before = t.run_count();
12546 if before < 2 {
12547 return Ok(false);
12548 }
12549 t.compact()?;
12550 Ok(t.run_count() < before)
12551 }
12552
12553 pub fn table(&self, name: &str) -> Result<TableHandle> {
12555 self.ensure_owner_process()?;
12556 let cat = self.catalog.read();
12557 let entry = cat
12558 .live(name)
12559 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
12560 let id = entry.table_id;
12561 drop(cat);
12562 self.tables
12563 .read()
12564 .get(&id)
12565 .cloned()
12566 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
12567 }
12568
12569 pub fn has_ttl_tables(&self) -> bool {
12572 self.tables
12573 .read()
12574 .values()
12575 .any(|table| table.lock().ttl().is_some())
12576 }
12577
12578 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
12581 self.tables
12582 .read()
12583 .get(&id)
12584 .cloned()
12585 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
12586 }
12587
12588 pub(crate) fn table_auto_inc_would_allocate(
12594 &self,
12595 table_id: u64,
12596 cells: &[(u16, Value)],
12597 ) -> bool {
12598 let catalog = self.catalog.read();
12599 let Some(entry) = catalog
12600 .tables
12601 .iter()
12602 .find(|entry| entry.table_id == table_id)
12603 else {
12604 return false;
12605 };
12606 let Some(column) = entry.schema.auto_increment_column() else {
12607 return false;
12608 };
12609 matches!(
12610 cells.iter().find(|(id, _)| *id == column.id),
12611 None | Some((_, Value::Null))
12612 )
12613 }
12614
12615 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
12621 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
12622 return Err(MongrelError::InvalidArgument(format!(
12623 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
12624 )));
12625 }
12626 self.create_table_with_state(name, schema, TableState::Live)
12627 }
12628
12629 #[doc(hidden)]
12631 pub fn create_building_table(
12632 &self,
12633 build_name: &str,
12634 intended_name: &str,
12635 query_id: &str,
12636 schema: Schema,
12637 ) -> Result<u64> {
12638 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12639 || intended_name.is_empty()
12640 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12641 || query_id.is_empty()
12642 {
12643 return Err(MongrelError::InvalidArgument(
12644 "invalid CTAS building-table identity".into(),
12645 ));
12646 }
12647 self.create_table_with_state(
12648 build_name,
12649 schema,
12650 TableState::Building {
12651 intended_name: intended_name.to_string(),
12652 query_id: query_id.to_string(),
12653 created_at_unix_nanos: current_unix_nanos(),
12654 replaces_table_id: None,
12655 },
12656 )
12657 }
12658
12659 #[doc(hidden)]
12662 pub fn create_rebuilding_table(
12663 &self,
12664 build_name: &str,
12665 intended_name: &str,
12666 query_id: &str,
12667 schema: Schema,
12668 ) -> Result<u64> {
12669 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12670 || intended_name.is_empty()
12671 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
12672 || query_id.is_empty()
12673 {
12674 return Err(MongrelError::InvalidArgument(
12675 "invalid rebuilding-table identity".into(),
12676 ));
12677 }
12678 let replaces_table_id = self
12679 .catalog
12680 .read()
12681 .live(intended_name)
12682 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
12683 .table_id;
12684 self.create_table_with_state(
12685 build_name,
12686 schema,
12687 TableState::Building {
12688 intended_name: intended_name.to_string(),
12689 query_id: query_id.to_string(),
12690 created_at_unix_nanos: current_unix_nanos(),
12691 replaces_table_id: Some(replaces_table_id),
12692 },
12693 )
12694 }
12695
12696 fn create_table_with_state(
12697 &self,
12698 name: &str,
12699 schema: Schema,
12700 state: TableState,
12701 ) -> Result<u64> {
12702 use crate::wal::DdlOp;
12703 use std::sync::atomic::Ordering;
12704
12705 self.require(&crate::auth::Permission::Ddl)?;
12706 if self.poisoned.load(Ordering::Relaxed) {
12707 return Err(MongrelError::Other(
12708 "database poisoned by fsync error".into(),
12709 ));
12710 }
12711 let _operation = self.admit_operation()?;
12713 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
12714 let _g = self.ddl_lock.lock();
12715 let _security_write = self.security_write()?;
12716 self.require(&crate::auth::Permission::Ddl)?;
12717 {
12718 let cat = self.catalog.read();
12719 match &state {
12720 TableState::Live => {
12721 if cat.live(name).is_some() || cat.building_for(name).is_some() {
12722 return Err(MongrelError::InvalidArgument(format!(
12723 "table {name:?} already exists or is being built"
12724 )));
12725 }
12726 }
12727 TableState::Building {
12728 intended_name,
12729 replaces_table_id,
12730 ..
12731 } => {
12732 let target_matches = match replaces_table_id {
12733 Some(table_id) => cat
12734 .live(intended_name)
12735 .is_some_and(|entry| entry.table_id == *table_id),
12736 None => cat.live(intended_name).is_none(),
12737 };
12738 if !target_matches || cat.building_for(intended_name).is_some() {
12739 return Err(MongrelError::InvalidArgument(format!(
12740 "table {intended_name:?} changed or is already being built"
12741 )));
12742 }
12743 if cat.building(name).is_some() {
12744 return Err(MongrelError::InvalidArgument(format!(
12745 "building table {name:?} already exists"
12746 )));
12747 }
12748 }
12749 TableState::Dropped { .. } => {
12750 return Err(MongrelError::InvalidArgument(
12751 "cannot create a dropped table".into(),
12752 ));
12753 }
12754 }
12755 }
12756
12757 let commit_lock = Arc::clone(&self.commit_lock);
12762 let _c = commit_lock.lock();
12763 let live_create = matches!(state, TableState::Live);
12764 let legacy_table_id = if live_create {
12765 None
12766 } else {
12767 let mut cat = self.catalog.write();
12768 let id = cat.next_table_id;
12769 cat.next_table_id = id
12770 .checked_add(1)
12771 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
12772 Some(id)
12773 };
12774 let epoch = self.epoch.bump_assigned();
12775 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
12776 let txn_id = self.alloc_txn_id()?;
12777
12778 let mut schema = schema;
12779 if let Some(table_id) = legacy_table_id {
12780 schema.schema_id = table_id;
12784 schema.validate_auto_increment()?;
12792 schema.validate_defaults()?;
12793 schema.validate_ai()?;
12794 for index in &schema.indexes {
12795 index.validate_options()?;
12796 }
12797 for constraint in &schema.constraints.checks {
12798 constraint.expr.validate()?;
12799 }
12800 }
12801
12802 let mut next_catalog = self.catalog.read().clone();
12803 let (table_id, schema) = match legacy_table_id {
12804 Some(table_id) => {
12805 next_catalog.tables.push(CatalogEntry {
12806 table_id,
12807 name: name.to_string(),
12808 schema: schema.clone(),
12809 state: state.clone(),
12810 created_epoch: epoch.0,
12811 });
12812 (table_id, schema)
12813 }
12814 None => {
12815 let command = crate::catalog_cmds::CatalogCommand::CreateTable {
12819 name: name.to_string(),
12820 schema,
12821 created_epoch: epoch.0,
12822 };
12823 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
12824 let crate::catalog_cmds::CatalogDelta::TableCreated { entry } = delta else {
12825 return Err(MongrelError::Other(
12826 "CreateTable resolved to an unexpected catalog delta".into(),
12827 ));
12828 };
12829 (entry.table_id, entry.schema)
12830 }
12831 };
12832 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
12833
12834 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
12837 let canonical_tdir = self.root.join(&table_relative);
12838 let table_root = Arc::new(
12839 self.durable_root
12840 .create_directory_all_pinned(&table_relative)?,
12841 );
12842 let tdir = table_root.io_path()?;
12843 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
12844 let ctx = SharedCtx {
12845 root_guard: Some(table_root),
12846 epoch: Arc::clone(&self.epoch),
12847 page_cache: Arc::clone(&self.page_cache),
12848 decoded_cache: Arc::clone(&self.decoded_cache),
12849 snapshots: Arc::clone(&self.snapshots),
12850 kek: self.kek.clone(),
12851 commit_lock: Arc::clone(&self.commit_lock),
12852 shared: Some(crate::engine::SharedWalCtx {
12853 wal: Arc::clone(&self.shared_wal),
12854 group: Arc::clone(&self.group),
12855 poisoned: Arc::clone(&self.poisoned),
12856 txn_ids: Arc::clone(&self.next_txn_id),
12857 change_wake: self.change_wake.clone(),
12858 lifecycle: Arc::clone(&self.lifecycle),
12859 }),
12860 table_name: Some(name.to_string()),
12861 auth: self.table_auth_checker(),
12862 read_only: self.read_only,
12863 };
12864 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
12865
12866 let schema_json = DdlOp::encode_schema(&schema)?;
12869 let ddl = match &state {
12870 TableState::Live => DdlOp::CreateTable {
12871 table_id,
12872 name: name.to_string(),
12873 schema_json,
12874 },
12875 TableState::Building {
12876 intended_name,
12877 query_id,
12878 created_at_unix_nanos,
12879 replaces_table_id,
12880 } => match replaces_table_id {
12881 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
12882 table_id,
12883 build_name: name.to_string(),
12884 intended_name: intended_name.clone(),
12885 query_id: query_id.clone(),
12886 created_at_unix_nanos: *created_at_unix_nanos,
12887 replaces_table_id: *replaces_table_id,
12888 schema_json,
12889 },
12890 None => DdlOp::CreateBuildingTable {
12891 table_id,
12892 build_name: name.to_string(),
12893 intended_name: intended_name.clone(),
12894 query_id: query_id.clone(),
12895 created_at_unix_nanos: *created_at_unix_nanos,
12896 schema_json,
12897 },
12898 },
12899 TableState::Dropped { .. } => {
12900 return Err(MongrelError::InvalidArgument(
12901 "cannot create a table in dropped state".into(),
12902 ));
12903 }
12904 };
12905 let commit_seq = {
12906 let mut wal = self.shared_wal.lock();
12907 let append: Result<u64> = (|| {
12908 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
12909 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12910 wal.append_commit(txn_id, epoch, &[])
12911 })();
12912 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
12913 };
12914 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
12915 pending_table_dir.disarm();
12916
12917 self.tables
12920 .write()
12921 .insert(table_id, TableHandle::new(table));
12922 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12923 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
12924 Ok(table_id)
12925 }
12926
12927 pub fn drop_table(&self, name: &str) -> Result<()> {
12929 self.drop_table_with_epoch(name).map(|_| ())
12930 }
12931
12932 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
12934 self.drop_table_with_state(name, false, None)
12935 }
12936
12937 pub fn drop_table_with_epoch_controlled<F>(
12938 &self,
12939 name: &str,
12940 mut before_commit: F,
12941 ) -> Result<Epoch>
12942 where
12943 F: FnMut() -> Result<()>,
12944 {
12945 self.drop_table_with_state(name, false, Some(&mut before_commit))
12946 }
12947
12948 #[doc(hidden)]
12950 pub fn discard_building_table(&self, name: &str) -> Result<()> {
12951 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
12952 return Err(MongrelError::InvalidArgument(
12953 "not a CTAS building table".into(),
12954 ));
12955 }
12956 self.drop_table_with_state(name, true, None).map(|_| ())
12957 }
12958
12959 fn drop_table_with_state(
12960 &self,
12961 name: &str,
12962 building: bool,
12963 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
12964 ) -> Result<Epoch> {
12965 use crate::wal::DdlOp;
12966 use std::sync::atomic::Ordering;
12967
12968 self.require(&crate::auth::Permission::Ddl)?;
12969 if self.poisoned.load(Ordering::Relaxed) {
12970 return Err(MongrelError::Other(
12971 "database poisoned by fsync error".into(),
12972 ));
12973 }
12974 let _operation = self.admit_operation()?;
12976 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
12977 let _g = self.ddl_lock.lock();
12978 let _security_write = self.security_write()?;
12979 self.require(&crate::auth::Permission::Ddl)?;
12980 let table_id = {
12981 let cat = self.catalog.read();
12982 if building {
12983 cat.building(name)
12984 } else {
12985 cat.live(name)
12986 }
12987 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
12988 .table_id
12989 };
12990
12991 let commit_lock = Arc::clone(&self.commit_lock);
12992 let _c = commit_lock.lock();
12993 let epoch = self.epoch.bump_assigned();
12994 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
12995 let txn_id = self.alloc_txn_id()?;
12996 let mut next_catalog = self.catalog.read().clone();
12997 if building {
12998 let entry = next_catalog
13001 .tables
13002 .iter_mut()
13003 .find(|t| t.table_id == table_id)
13004 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13005 entry.state = TableState::Dropped { at_epoch: epoch.0 };
13006 next_catalog.triggers.retain(|trigger| {
13007 !matches!(
13008 &trigger.trigger.target,
13009 TriggerTarget::Table(target) if target == name
13010 )
13011 });
13012 next_catalog
13013 .materialized_views
13014 .retain(|definition| definition.name != name);
13015 next_catalog
13016 .security
13017 .rls_tables
13018 .retain(|table| table != name);
13019 next_catalog
13020 .security
13021 .policies
13022 .retain(|policy| policy.table != name);
13023 next_catalog
13024 .security
13025 .masks
13026 .retain(|mask| mask.table != name);
13027 for role in &mut next_catalog.roles {
13028 role.permissions
13029 .retain(|permission| permission_table(permission) != Some(name));
13030 }
13031 advance_security_version(&mut next_catalog)?;
13032 } else {
13033 let command = crate::catalog_cmds::CatalogCommand::DropTable {
13037 name: name.to_string(),
13038 at_epoch: epoch.0,
13039 };
13040 self.apply_catalog_command_to(&mut next_catalog, command)?;
13041 }
13042 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13043 let commit_seq = {
13044 let mut wal = self.shared_wal.lock();
13045 if let Some(before_commit) = before_commit {
13046 before_commit()?;
13047 }
13048 let append: Result<u64> = (|| {
13049 wal.append(
13050 txn_id,
13051 table_id,
13052 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
13053 )?;
13054 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13055 wal.append_commit(txn_id, epoch, &[])
13056 })();
13057 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13058 };
13059 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13060
13061 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
13062 self.tables.write().remove(&table_id);
13063 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
13064 Ok(epoch)
13065 }
13066
13067 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
13076 self.rename_table_with_epoch(name, new_name).map(|_| ())
13077 }
13078
13079 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
13081 self.rename_table_with_epoch_inner(name, new_name, None)
13082 }
13083
13084 pub fn rename_table_with_epoch_controlled<F>(
13085 &self,
13086 name: &str,
13087 new_name: &str,
13088 mut before_commit: F,
13089 ) -> Result<Epoch>
13090 where
13091 F: FnMut() -> Result<()>,
13092 {
13093 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
13094 }
13095
13096 fn rename_table_with_epoch_inner(
13097 &self,
13098 name: &str,
13099 new_name: &str,
13100 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13101 ) -> Result<Epoch> {
13102 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13103 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13104 {
13105 return Err(MongrelError::InvalidArgument(
13106 "the CTAS building-table namespace is reserved".into(),
13107 ));
13108 }
13109 self.rename_table_with_state(name, new_name, false, None, before_commit)
13110 }
13111
13112 #[doc(hidden)]
13114 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
13115 self.publish_building_table_inner(build_name, new_name, None)
13116 }
13117
13118 #[doc(hidden)]
13119 pub fn publish_building_table_controlled<F>(
13120 &self,
13121 build_name: &str,
13122 new_name: &str,
13123 mut before_commit: F,
13124 ) -> Result<Epoch>
13125 where
13126 F: FnMut() -> Result<()>,
13127 {
13128 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
13129 }
13130
13131 fn publish_building_table_inner(
13132 &self,
13133 build_name: &str,
13134 new_name: &str,
13135 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13136 ) -> Result<Epoch> {
13137 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13138 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13139 {
13140 return Err(MongrelError::InvalidArgument(
13141 "invalid CTAS publish identity".into(),
13142 ));
13143 }
13144 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
13145 }
13146
13147 #[doc(hidden)]
13149 pub fn publish_materialized_building_table(
13150 &self,
13151 build_name: &str,
13152 new_name: &str,
13153 definition: crate::catalog::MaterializedViewEntry,
13154 ) -> Result<Epoch> {
13155 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
13156 }
13157
13158 #[doc(hidden)]
13159 pub fn publish_materialized_building_table_controlled<F>(
13160 &self,
13161 build_name: &str,
13162 new_name: &str,
13163 definition: crate::catalog::MaterializedViewEntry,
13164 mut before_commit: F,
13165 ) -> Result<Epoch>
13166 where
13167 F: FnMut() -> Result<()>,
13168 {
13169 self.publish_materialized_building_table_inner(
13170 build_name,
13171 new_name,
13172 definition,
13173 Some(&mut before_commit),
13174 )
13175 }
13176
13177 fn publish_materialized_building_table_inner(
13178 &self,
13179 build_name: &str,
13180 new_name: &str,
13181 definition: crate::catalog::MaterializedViewEntry,
13182 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13183 ) -> Result<Epoch> {
13184 if definition.name != new_name || definition.query.trim().is_empty() {
13185 return Err(MongrelError::InvalidArgument(
13186 "invalid materialized-view publication".into(),
13187 ));
13188 }
13189 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
13190 }
13191
13192 #[doc(hidden)]
13194 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
13195 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
13196 }
13197
13198 #[doc(hidden)]
13199 pub fn publish_rebuilding_table_controlled<F>(
13200 &self,
13201 build_name: &str,
13202 new_name: &str,
13203 mut before_commit: F,
13204 ) -> Result<Epoch>
13205 where
13206 F: FnMut() -> Result<()>,
13207 {
13208 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
13209 }
13210
13211 #[doc(hidden)]
13213 pub fn publish_materialized_rebuilding_table_controlled<F>(
13214 &self,
13215 build_name: &str,
13216 new_name: &str,
13217 definition: crate::catalog::MaterializedViewEntry,
13218 mut before_commit: F,
13219 ) -> Result<Epoch>
13220 where
13221 F: FnMut() -> Result<()>,
13222 {
13223 self.publish_rebuilding_table_inner(
13224 build_name,
13225 new_name,
13226 Some(definition),
13227 Some(&mut before_commit),
13228 )
13229 }
13230
13231 fn publish_rebuilding_table_inner(
13232 &self,
13233 build_name: &str,
13234 new_name: &str,
13235 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
13236 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13237 ) -> Result<Epoch> {
13238 use crate::wal::DdlOp;
13239
13240 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13241 || new_name.is_empty()
13242 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13243 {
13244 return Err(MongrelError::InvalidArgument(
13245 "invalid rebuilding-table publish identity".into(),
13246 ));
13247 }
13248 if materialized_view.as_ref().is_some_and(|definition| {
13249 definition.name != new_name || definition.query.trim().is_empty()
13250 }) {
13251 return Err(MongrelError::InvalidArgument(
13252 "invalid materialized-view replacement".into(),
13253 ));
13254 }
13255 self.require(&crate::auth::Permission::Ddl)?;
13256 if self.poisoned.load(Ordering::Relaxed) {
13257 return Err(MongrelError::Other(
13258 "database poisoned by fsync error".into(),
13259 ));
13260 }
13261 let _operation = self.admit_operation()?;
13263 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13264 let _ddl = self.ddl_lock.lock();
13265 let _security_write = self.security_write()?;
13266 let (table_id, replaced_table_id) = {
13267 let catalog = self.catalog.read();
13268 let build = catalog.building(build_name).ok_or_else(|| {
13269 MongrelError::NotFound(format!("building table {build_name:?} not found"))
13270 })?;
13271 let replaced_table_id = match &build.state {
13272 TableState::Building {
13273 intended_name,
13274 replaces_table_id: Some(replaced_table_id),
13275 ..
13276 } if intended_name == new_name => *replaced_table_id,
13277 _ => {
13278 return Err(MongrelError::InvalidArgument(format!(
13279 "building table {build_name:?} is not a replacement for {new_name:?}"
13280 )))
13281 }
13282 };
13283 if catalog
13284 .live(new_name)
13285 .is_none_or(|entry| entry.table_id != replaced_table_id)
13286 {
13287 return Err(MongrelError::Conflict(format!(
13288 "table {new_name:?} changed while its replacement was built"
13289 )));
13290 }
13291 (build.table_id, replaced_table_id)
13292 };
13293
13294 let _commit = self.commit_lock.lock();
13295 let epoch = self.epoch.assigned().next();
13296 let txn_id = self.alloc_txn_id()?;
13297 let mut next_catalog = self.catalog.read().clone();
13298 apply_rebuilding_publish(
13299 &mut next_catalog,
13300 table_id,
13301 replaced_table_id,
13302 new_name,
13303 epoch.0,
13304 )?;
13305 if let Some(definition) = materialized_view.as_mut() {
13306 definition.last_refresh_epoch = epoch.0;
13307 }
13308 let materialized_view_json = materialized_view
13309 .as_ref()
13310 .map(DdlOp::encode_materialized_view)
13311 .transpose()?;
13312 if let Some(definition) = materialized_view {
13313 if let Some(existing) = next_catalog
13314 .materialized_views
13315 .iter_mut()
13316 .find(|existing| existing.name == definition.name)
13317 {
13318 *existing = definition;
13319 } else {
13320 next_catalog.materialized_views.push(definition);
13321 }
13322 }
13323 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13324 if let Some(before_commit) = before_commit {
13325 before_commit()?;
13326 }
13327 let assigned_epoch = self.epoch.bump_assigned();
13328 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
13329 if assigned_epoch != epoch {
13330 return Err(MongrelError::Conflict(
13331 "commit epoch changed while sequencer lock was held".into(),
13332 ));
13333 }
13334 let commit_seq = {
13335 let mut wal = self.shared_wal.lock();
13336 let append: Result<u64> = (|| {
13337 wal.append(
13338 txn_id,
13339 table_id,
13340 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
13341 table_id,
13342 replaced_table_id,
13343 new_name: new_name.to_string(),
13344 }),
13345 )?;
13346 if let Some(definition_json) = materialized_view_json {
13347 wal.append(
13348 txn_id,
13349 table_id,
13350 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
13351 name: new_name.to_string(),
13352 definition_json,
13353 }),
13354 )?;
13355 }
13356 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13357 wal.append_commit(txn_id, epoch, &[])
13358 })();
13359 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13360 };
13361 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13362
13363 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
13364 self.tables.write().remove(&replaced_table_id);
13365 if let Some(table) = self.tables.read().get(&table_id) {
13366 table.lock().set_catalog_name(new_name.to_string());
13367 }
13368 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
13369 Ok(epoch)
13370 }
13371
13372 fn rename_table_with_state(
13373 &self,
13374 name: &str,
13375 new_name: &str,
13376 building: bool,
13377 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
13378 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13379 ) -> Result<Epoch> {
13380 use crate::wal::DdlOp;
13381 use std::sync::atomic::Ordering;
13382
13383 self.require(&crate::auth::Permission::Ddl)?;
13384 if self.poisoned.load(Ordering::Relaxed) {
13385 return Err(MongrelError::Other(
13386 "database poisoned by fsync error".into(),
13387 ));
13388 }
13389
13390 if name == new_name {
13393 return Ok(self.visible_epoch());
13394 }
13395 if new_name.is_empty() {
13396 return Err(MongrelError::InvalidArgument(
13397 "rename_table: new name must not be empty".into(),
13398 ));
13399 }
13400 let _operation = self.admit_operation()?;
13402 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13403 let _g = self.ddl_lock.lock();
13404 let _security_write = self.security_write()?;
13405 self.require(&crate::auth::Permission::Ddl)?;
13406 let table_id = {
13407 let cat = self.catalog.read();
13408 let src = if building {
13409 cat.building(name)
13410 } else {
13411 cat.live(name)
13412 }
13413 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13414 if building
13415 && !matches!(
13416 &src.state,
13417 TableState::Building { intended_name, .. } if intended_name == new_name
13418 )
13419 {
13420 return Err(MongrelError::InvalidArgument(format!(
13421 "building table {name:?} is not reserved for {new_name:?}"
13422 )));
13423 }
13424 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
13428 return Err(MongrelError::InvalidArgument(format!(
13429 "rename_table: a table named {new_name:?} already exists"
13430 )));
13431 }
13432 src.table_id
13433 };
13434
13435 let commit_lock = Arc::clone(&self.commit_lock);
13436 let _c = commit_lock.lock();
13437 let epoch = self.epoch.bump_assigned();
13438 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13439 let txn_id = self.alloc_txn_id()?;
13440 if let Some(definition) = materialized_view.as_mut() {
13441 definition.last_refresh_epoch = epoch.0;
13442 }
13443 let materialized_view_json = materialized_view
13444 .as_ref()
13445 .map(DdlOp::encode_materialized_view)
13446 .transpose()?;
13447 let mut next_catalog = self.catalog.read().clone();
13448 if building {
13449 let entry = next_catalog
13452 .tables
13453 .iter_mut()
13454 .find(|t| t.table_id == table_id)
13455 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13456 entry.name = new_name.to_string();
13457 entry.state = TableState::Live;
13458 for trigger in &mut next_catalog.triggers {
13459 if matches!(
13460 &trigger.trigger.target,
13461 TriggerTarget::Table(target) if target == name
13462 ) {
13463 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
13464 }
13465 }
13466 if let Some(definition) = next_catalog
13467 .materialized_views
13468 .iter_mut()
13469 .find(|definition| definition.name == name)
13470 {
13471 definition.name = new_name.to_string();
13472 }
13473 if let Some(definition) = materialized_view.take() {
13474 next_catalog.materialized_views.push(definition);
13475 }
13476 for table in &mut next_catalog.security.rls_tables {
13477 if table == name {
13478 *table = new_name.to_string();
13479 }
13480 }
13481 for policy in &mut next_catalog.security.policies {
13482 if policy.table == name {
13483 policy.table = new_name.to_string();
13484 }
13485 }
13486 for mask in &mut next_catalog.security.masks {
13487 if mask.table == name {
13488 mask.table = new_name.to_string();
13489 }
13490 }
13491 for role in &mut next_catalog.roles {
13492 for permission in &mut role.permissions {
13493 rename_permission_table(permission, name, new_name);
13494 }
13495 }
13496 advance_security_version(&mut next_catalog)?;
13497 } else {
13498 let command = crate::catalog_cmds::CatalogCommand::RenameTable {
13502 name: name.to_string(),
13503 new_name: new_name.to_string(),
13504 at_epoch: epoch.0,
13505 };
13506 self.apply_catalog_command_to(&mut next_catalog, command)?;
13507 }
13508 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13509 let ddl = if building {
13510 DdlOp::PublishBuildingTable {
13511 table_id,
13512 new_name: new_name.to_string(),
13513 }
13514 } else {
13515 DdlOp::RenameTable {
13516 table_id,
13517 new_name: new_name.to_string(),
13518 }
13519 };
13520 let commit_seq = {
13521 let mut wal = self.shared_wal.lock();
13522 if let Some(before_commit) = before_commit {
13523 before_commit()?;
13524 }
13525 let append: Result<u64> = (|| {
13526 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
13527 if let Some(definition_json) = materialized_view_json {
13528 wal.append(
13529 txn_id,
13530 table_id,
13531 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
13532 name: new_name.to_string(),
13533 definition_json,
13534 }),
13535 )?;
13536 }
13537 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13538 wal.append_commit(txn_id, epoch, &[])
13539 })();
13540 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13541 };
13542 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13543
13544 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
13545 if let Some(table) = self.tables.read().get(&table_id) {
13548 table.lock().set_catalog_name(new_name.to_string());
13549 }
13550 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
13551 Ok(epoch)
13552 }
13553
13554 pub fn alter_column(
13555 &self,
13556 table_name: &str,
13557 column_name: &str,
13558 change: AlterColumn,
13559 ) -> Result<ColumnDef> {
13560 self.alter_column_with_epoch(table_name, column_name, change)
13561 .map(|(column, _)| column)
13562 }
13563
13564 pub fn alter_column_with_epoch(
13565 &self,
13566 table_name: &str,
13567 column_name: &str,
13568 change: AlterColumn,
13569 ) -> Result<(ColumnDef, Option<Epoch>)> {
13570 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
13571 }
13572
13573 pub fn alter_column_with_epoch_controlled<B, A>(
13578 &self,
13579 table_name: &str,
13580 column_name: &str,
13581 change: AlterColumn,
13582 control: &crate::ExecutionControl,
13583 mut before_commit: B,
13584 mut after_commit: A,
13585 ) -> Result<(ColumnDef, Option<Epoch>)>
13586 where
13587 B: FnMut() -> Result<()>,
13588 A: FnMut(Option<Epoch>) -> Result<()>,
13589 {
13590 self.alter_column_with_epoch_inner(
13591 table_name,
13592 column_name,
13593 change,
13594 Some(control),
13595 Some(&mut before_commit),
13596 Some(&mut after_commit),
13597 )
13598 }
13599
13600 #[allow(clippy::too_many_arguments)]
13601 fn alter_column_with_epoch_inner(
13602 &self,
13603 table_name: &str,
13604 column_name: &str,
13605 change: AlterColumn,
13606 control: Option<&crate::ExecutionControl>,
13607 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13608 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
13609 ) -> Result<(ColumnDef, Option<Epoch>)> {
13610 use crate::wal::DdlOp;
13611 use std::sync::atomic::Ordering;
13612
13613 self.require(&crate::auth::Permission::Ddl)?;
13614 commit_prepare_checkpoint(control, 0)?;
13615 if self.poisoned.load(Ordering::Relaxed) {
13616 return Err(MongrelError::Other(
13617 "database poisoned by fsync error".into(),
13618 ));
13619 }
13620 let _operation = self.admit_operation()?;
13622 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13623 let _g = self.ddl_lock.lock();
13624 let table_id = {
13625 let cat = self.catalog.read();
13626 cat.live(table_name)
13627 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
13628 .table_id
13629 };
13630 let handle =
13631 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
13632 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
13633 })?;
13634
13635 let backfill = {
13641 let table = handle.lock();
13642 let old = table
13643 .schema()
13644 .column(column_name)
13645 .cloned()
13646 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
13647 let next_flags = change.flags.unwrap_or(old.flags);
13648 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
13649 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
13650 && old.default_value.is_some()
13651 {
13652 let snapshot = Snapshot::at(self.epoch.visible());
13653 let mut updates = Vec::new();
13654 let rows = match control {
13655 Some(control) => table.visible_rows_controlled(snapshot, control)?,
13656 None => table.visible_rows(snapshot)?,
13657 };
13658 for (row_index, row) in rows.into_iter().enumerate() {
13659 commit_prepare_checkpoint(control, row_index)?;
13660 if row
13661 .columns
13662 .get(&old.id)
13663 .is_some_and(|value| !matches!(value, Value::Null))
13664 {
13665 continue;
13666 }
13667 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
13668 table.apply_defaults(&mut cells)?;
13669 updates.push((
13670 table_id,
13671 crate::txn::Staged::Update {
13672 row_id: row.row_id,
13673 new_row: cells,
13674 changed_columns: vec![old.id],
13675 },
13676 ));
13677 }
13678 updates
13679 } else {
13680 Vec::new()
13681 }
13682 };
13683 let durable_epoch = std::cell::Cell::new(None);
13684 let backfill_epoch = if backfill.is_empty() {
13685 None
13686 } else {
13687 let (principal, catalog_bound) = self.transaction_principal_snapshot();
13688 let txn_id = self.alloc_txn_id()?;
13689 let mut entered_fence = false;
13690 let commit_result = match (control, before_commit.as_deref_mut()) {
13691 (Some(control), Some(before_commit)) => self
13692 .commit_transaction_with_external_states_controlled(
13693 txn_id,
13694 self.epoch.visible(),
13695 backfill,
13696 Vec::new(),
13697 Vec::new(),
13698 principal.clone(),
13699 catalog_bound,
13700 None,
13701 crate::txn::TxnCommitContext::internal(),
13702 control,
13703 &mut || {
13704 before_commit()?;
13705 entered_fence = true;
13706 Ok(())
13707 },
13708 )
13709 .map(|(epoch, _)| epoch),
13710 _ => self
13711 .commit_transaction_with_external_states(
13712 txn_id,
13713 self.epoch.visible(),
13714 backfill,
13715 Vec::new(),
13716 Vec::new(),
13717 principal,
13718 catalog_bound,
13719 None,
13720 crate::txn::TxnCommitContext::internal(),
13721 )
13722 .map(|(epoch, _)| epoch),
13723 };
13724 let commit_result = if entered_fence {
13725 finish_controlled_commit_attempt(commit_result, &mut after_commit)
13726 } else {
13727 commit_result
13728 };
13729 match &commit_result {
13730 Ok(epoch) => durable_epoch.set(Some(*epoch)),
13731 Err(MongrelError::DurableCommit { epoch, .. }) => {
13732 durable_epoch.set(Some(Epoch(*epoch)));
13733 }
13734 Err(_) => {}
13735 }
13736 Some(commit_result?)
13737 };
13738 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
13739 let _security_write = self.security_write()?;
13740 self.require(&crate::auth::Permission::Ddl)?;
13741 if self
13742 .catalog
13743 .read()
13744 .live(table_name)
13745 .is_none_or(|entry| entry.table_id != table_id)
13746 {
13747 return Err(MongrelError::Conflict(format!(
13748 "table {table_name:?} changed during ALTER"
13749 )));
13750 }
13751 let mut table = handle.lock();
13752 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
13753 let renamed_column = (column.name != column_name).then(|| column.name.clone());
13754 let Some(prepared_schema) = prepared_schema else {
13755 return Ok((column, backfill_epoch));
13756 };
13757 let command = crate::catalog_cmds::CatalogCommand::AlterColumn {
13762 table: table_name.to_string(),
13763 column: column.clone(),
13764 };
13765 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
13766
13767 let commit_lock = Arc::clone(&self.commit_lock);
13768 let _c = commit_lock.lock();
13769 let epoch = self.epoch.bump_assigned();
13770 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13771 let txn_id = self.alloc_txn_id()?;
13772 let column_json = DdlOp::encode_column(&column)?;
13773 let mut next_catalog = self.catalog.read().clone();
13774 let catalog_entry_index = next_catalog
13775 .tables
13776 .iter()
13777 .position(|entry| entry.table_id == table_id)
13778 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
13779 if let Some(new_column_name) = &renamed_column {
13780 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
13781 commit_prepare_checkpoint(control, trigger_index)?;
13782 if matches!(
13783 &trigger.trigger.target,
13784 TriggerTarget::Table(target) if target == table_name
13785 ) {
13786 trigger.trigger = trigger.trigger.renamed_update_column(
13787 column_name,
13788 new_column_name.clone(),
13789 epoch.0,
13790 )?;
13791 }
13792 }
13793 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
13794 commit_prepare_checkpoint(control, role_index)?;
13795 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
13796 commit_prepare_checkpoint(control, permission_index)?;
13797 rename_permission_column(
13798 permission,
13799 table_name,
13800 column_name,
13801 new_column_name,
13802 );
13803 }
13804 }
13805 advance_security_version(&mut next_catalog)?;
13806 }
13807 self.apply_catalog_command_to(&mut next_catalog, command)?;
13813 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
13814 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13815 commit_prepare_checkpoint(control, 0)?;
13816 let mut entered_fence = false;
13817 if let Some(before_commit) = before_commit.as_deref_mut() {
13818 before_commit()?;
13819 entered_fence = true;
13820 }
13821 let commit_result: Result<Epoch> = (|| {
13822 let commit_seq = {
13823 let mut wal = self.shared_wal.lock();
13824 let append: Result<u64> = (|| {
13825 wal.append(
13826 txn_id,
13827 table_id,
13828 crate::wal::Op::Ddl(DdlOp::AlterTable {
13829 table_id,
13830 column_json,
13831 }),
13832 )?;
13833 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13834 wal.append_commit(txn_id, epoch, &[])
13835 })();
13836 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13837 };
13838 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13839 durable_epoch.set(Some(epoch));
13840
13841 table.apply_altered_schema_prepared(prepared_schema);
13842 let schema = table.schema().clone();
13843 let table_checkpoint = table.checkpoint_altered_schema();
13844 drop(table);
13845 next_catalog.tables[catalog_entry_index].schema = schema;
13846 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13847 let catalog_result =
13848 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
13849 let security_version = next_catalog.security_version;
13850 *self.catalog.write() = next_catalog;
13851 if renamed_column.is_some() {
13852 self.security_coordinator
13853 .version
13854 .store(security_version, Ordering::Release);
13855 }
13856 self.publish_committed(&receipt, epoch)?;
13857 _epoch_guard.disarm();
13858 if let Err(error) = table_checkpoint.and(catalog_result) {
13859 self.poisoned.store(true, Ordering::Relaxed);
13860 self.lifecycle.poison();
13861 return Err(MongrelError::DurableCommit {
13862 epoch: epoch.0,
13863 message: error.to_string(),
13864 });
13865 }
13866 Ok(epoch)
13867 })();
13868 let commit_result = if entered_fence {
13869 finish_controlled_commit_attempt(commit_result, &mut after_commit)
13870 } else {
13871 commit_result
13872 };
13873 let epoch = commit_result?;
13874 Ok((column, Some(epoch)))
13875 })();
13876 result.map_err(|error| match (durable_epoch.get(), error) {
13877 (_, error @ MongrelError::DurableCommit { .. }) => error,
13878 (Some(epoch), error) => MongrelError::DurableCommit {
13879 epoch: epoch.0,
13880 message: error.to_string(),
13881 },
13882 (None, error) => error,
13883 })
13884 }
13885
13886 pub fn set_table_ttl(
13889 &self,
13890 table_name: &str,
13891 column_name: &str,
13892 duration_nanos: u64,
13893 ) -> Result<crate::manifest::TtlPolicy> {
13894 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
13895 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
13896 }
13897
13898 #[doc(hidden)]
13900 pub fn set_building_table_ttl(
13901 &self,
13902 table_name: &str,
13903 column_name: &str,
13904 duration_nanos: u64,
13905 ) -> Result<crate::manifest::TtlPolicy> {
13906 let policy = self.replace_table_ttl_with_state(
13907 table_name,
13908 Some((column_name, duration_nanos)),
13909 true,
13910 )?;
13911 policy
13912 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
13913 }
13914
13915 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
13916 self.replace_table_ttl(table_name, None)?;
13917 Ok(())
13918 }
13919
13920 fn replace_table_ttl(
13921 &self,
13922 table_name: &str,
13923 requested: Option<(&str, u64)>,
13924 ) -> Result<Option<crate::manifest::TtlPolicy>> {
13925 self.replace_table_ttl_with_state(table_name, requested, false)
13926 }
13927
13928 fn replace_table_ttl_with_state(
13929 &self,
13930 table_name: &str,
13931 requested: Option<(&str, u64)>,
13932 building: bool,
13933 ) -> Result<Option<crate::manifest::TtlPolicy>> {
13934 use crate::wal::DdlOp;
13935 use std::sync::atomic::Ordering;
13936
13937 self.require(&crate::auth::Permission::Ddl)?;
13938 if self.poisoned.load(Ordering::Relaxed) {
13939 return Err(MongrelError::Other(
13940 "database poisoned by fsync error".into(),
13941 ));
13942 }
13943
13944 let _g = self.ddl_lock.lock();
13945 let _security_write = self.security_write()?;
13946 self.require(&crate::auth::Permission::Ddl)?;
13947 let table_id = {
13948 let cat = self.catalog.read();
13949 if building {
13950 cat.building(table_name)
13951 } else {
13952 cat.live(table_name)
13953 }
13954 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
13955 .table_id
13956 };
13957 let handle =
13958 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
13959 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
13960 })?;
13961 let mut table = handle.lock();
13962 let policy = match requested {
13963 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
13964 None => None,
13965 };
13966 if table.ttl() == policy {
13967 return Ok(policy);
13968 }
13969
13970 let commit_lock = Arc::clone(&self.commit_lock);
13971 let _c = commit_lock.lock();
13972 let epoch = self.epoch.bump_assigned();
13973 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13974 let txn_id = self.alloc_txn_id()?;
13975 let policy_json = DdlOp::encode_ttl(policy)?;
13976 let mut next_catalog = self.catalog.read().clone();
13977 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13978 let commit_seq = {
13979 let mut wal = self.shared_wal.lock();
13980 let append: Result<u64> = (|| {
13981 wal.append(
13982 txn_id,
13983 table_id,
13984 crate::wal::Op::Ddl(DdlOp::SetTtl {
13985 table_id,
13986 policy_json,
13987 }),
13988 )?;
13989 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13990 wal.append_commit(txn_id, epoch, &[])
13991 })();
13992 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13993 };
13994 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13995
13996 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
13997 drop(table);
13998 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
13999 publish_error.get_or_insert(error);
14000 }
14001 self.finish_durable_publish(
14002 epoch,
14003 &mut _epoch_guard,
14004 &receipt,
14005 publish_error.map_or(Ok(()), Err),
14006 )?;
14007 Ok(policy)
14008 }
14009
14010 pub fn gc(&self) -> Result<usize> {
14016 let control = crate::ExecutionControl::new(None);
14017 self.gc_controlled(&control, || true)
14018 }
14019
14020 #[doc(hidden)]
14023 pub fn gc_controlled<F>(
14024 &self,
14025 control: &crate::ExecutionControl,
14026 before_publish: F,
14027 ) -> Result<usize>
14028 where
14029 F: FnOnce() -> bool,
14030 {
14031 self.gc_controlled_with_receipt(control, before_publish)
14032 .map(|(reclaimed, _)| reclaimed)
14033 }
14034
14035 #[doc(hidden)]
14038 pub fn gc_controlled_with_receipt<F>(
14039 &self,
14040 control: &crate::ExecutionControl,
14041 before_publish: F,
14042 ) -> Result<(usize, Option<MaintenanceReceipt>)>
14043 where
14044 F: FnOnce() -> bool,
14045 {
14046 enum Candidate {
14047 Directory(PathBuf),
14048 File(PathBuf),
14049 }
14050
14051 self.require(&crate::auth::Permission::Ddl)?;
14052 let _operation = self.admit_operation()?;
14054 let _ddl = self.ddl_lock.lock();
14055 self.require(&crate::auth::Permission::Ddl)?;
14056 control.checkpoint()?;
14057 let maintenance_epoch = self.epoch.visible();
14058 let min_active = self.snapshots.min_active(maintenance_epoch).0;
14059 let mut candidates = Vec::new();
14060
14061 let cat = self.catalog.read();
14063 for (entry_index, entry) in cat.tables.iter().enumerate() {
14064 if entry_index % 256 == 0 {
14065 control.checkpoint()?;
14066 }
14067 if let TableState::Dropped { at_epoch } = entry.state {
14068 if at_epoch <= min_active {
14069 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
14070 if tdir.exists() {
14071 candidates.push(Candidate::Directory(tdir));
14072 }
14073 }
14074 }
14075 }
14076 drop(cat);
14077
14078 let cat = self.catalog.read();
14083 for (entry_index, entry) in cat.tables.iter().enumerate() {
14084 if entry_index % 256 == 0 {
14085 control.checkpoint()?;
14086 }
14087 if !matches!(entry.state, TableState::Live) {
14088 continue;
14089 }
14090 let txn_dir = self
14091 .root
14092 .join(TABLES_DIR)
14093 .join(entry.table_id.to_string())
14094 .join("_txn");
14095 if !txn_dir.exists() {
14096 continue;
14097 }
14098 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
14099 if sub_index % 256 == 0 {
14100 control.checkpoint()?;
14101 }
14102 let sub = sub?;
14103 let name = sub.file_name();
14104 let Some(name) = name.to_str() else { continue };
14105 let is_active = name
14107 .parse::<u64>()
14108 .map(|id| self.active_spills.is_active(id))
14109 .unwrap_or(false);
14110 if is_active {
14111 continue;
14112 }
14113 candidates.push(Candidate::Directory(sub.path()));
14114 }
14115 }
14116 drop(cat);
14117
14118 let external_names = {
14119 let cat = self.catalog.read();
14120 cat.external_tables
14121 .iter()
14122 .map(|entry| entry.name.clone())
14123 .collect::<std::collections::HashSet<_>>()
14124 };
14125 let vtab_dir = self.root.join(VTAB_DIR);
14126 if vtab_dir.exists() {
14127 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
14128 if entry_index % 256 == 0 {
14129 control.checkpoint()?;
14130 }
14131 let entry = entry?;
14132 let name = entry.file_name();
14133 let Some(name) = name.to_str() else { continue };
14134 if external_names.contains(name) {
14135 continue;
14136 }
14137 let path = entry.path();
14138 if path.is_dir() {
14139 candidates.push(Candidate::Directory(path));
14140 } else {
14141 candidates.push(Candidate::File(path));
14142 }
14143 }
14144 }
14145
14146 let tables = self
14150 .tables
14151 .read()
14152 .iter()
14153 .map(|(table_id, handle)| (*table_id, handle.clone()))
14154 .collect::<Vec<_>>();
14155 let mut retiring = Vec::new();
14156 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
14157 if table_index % 256 == 0 {
14158 control.checkpoint()?;
14159 }
14160 let backup_pinned: HashSet<u128> = self
14161 .backup_pins
14162 .lock()
14163 .keys()
14164 .filter_map(|(pinned_table, run_id)| {
14165 (*pinned_table == *table_id).then_some(*run_id)
14166 })
14167 .collect();
14168 if handle
14169 .lock()
14170 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
14171 {
14172 retiring.push((handle.clone(), backup_pinned));
14173 }
14174 }
14175
14176 let all_durable = self.active_spills.is_idle()
14184 && tables.iter().all(|(_, handle)| {
14185 let g = handle.lock();
14186 g.memtable_len() == 0 && g.mutable_run_len() == 0
14187 });
14188 let retain = self
14189 .replication_wal_retention_segments
14190 .load(std::sync::atomic::Ordering::Relaxed);
14191 let reap_wal = all_durable
14192 && self
14193 .shared_wal
14194 .lock()
14195 .has_gc_segments_retain_recent(retain)?;
14196
14197 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
14198 return Ok((0, None));
14199 }
14200 control.checkpoint()?;
14201 if !before_publish() {
14202 return Err(MongrelError::Cancelled);
14203 }
14204
14205 let mut reclaimed = 0;
14206 for candidate in candidates {
14207 match candidate {
14208 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
14209 Candidate::File(path) => std::fs::remove_file(path)?,
14210 }
14211 reclaimed += 1;
14212 }
14213 for (handle, backup_pinned) in retiring {
14214 reclaimed += handle
14215 .lock()
14216 .reap_retiring(Epoch(min_active), &backup_pinned)?;
14217 }
14218 if reap_wal {
14219 reclaimed += self
14220 .shared_wal
14221 .lock()
14222 .gc_segments_retain_recent(u64::MAX, retain)?;
14223 }
14224
14225 Ok((
14226 reclaimed,
14227 Some(MaintenanceReceipt {
14228 epoch: maintenance_epoch,
14229 }),
14230 ))
14231 }
14232
14233 pub fn checkpoint(&self) -> Result<()> {
14251 self.checkpoint_controlled(|| Ok(()))
14252 }
14253
14254 #[doc(hidden)]
14257 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
14258 where
14259 F: FnOnce() -> Result<()>,
14260 {
14261 self.require(&crate::auth::Permission::Ddl)?;
14262 let _operation = self.admit_operation()?;
14264 let _replication = self.replication_barrier.write();
14268 let _ddl = self.ddl_lock.lock();
14269 let _security = self.security_coordinator.gate.read();
14270 self.require(&crate::auth::Permission::Ddl)?;
14271
14272 let mut handles = self
14273 .tables
14274 .read()
14275 .iter()
14276 .map(|(table_id, handle)| (*table_id, handle.clone()))
14277 .collect::<Vec<_>>();
14278 handles.sort_by_key(|(table_id, _)| *table_id);
14279 let mut tables = handles
14280 .iter()
14281 .map(|(table_id, handle)| (*table_id, handle.lock()))
14282 .collect::<Vec<_>>();
14283
14284 for (_, table) in &mut tables {
14286 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
14287 {
14288 table.force_flush()?;
14289 }
14290 }
14291
14292 for (_, table) in &mut tables {
14295 if table.run_count() >= 2 || table.should_compact() {
14296 table.compact()?;
14297 }
14298 }
14299
14300 before_wal_reset()?;
14301
14302 let maintenance_epoch = self.epoch.visible();
14304 let min_active = self.snapshots.min_active(maintenance_epoch);
14305 for (table_id, table) in &mut tables {
14306 let backup_pinned: HashSet<u128> = self
14307 .backup_pins
14308 .lock()
14309 .keys()
14310 .filter_map(|(pinned_table, run_id)| {
14311 (*pinned_table == *table_id).then_some(*run_id)
14312 })
14313 .collect();
14314 table.reap_retiring(min_active, &backup_pinned)?;
14315 }
14316
14317 self.shared_wal.lock().reset_after_checkpoint()?;
14320
14321 let catalog_snapshot = self.catalog.read().clone();
14323 for entry in &catalog_snapshot.tables {
14324 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
14325 crate::durable_file::remove_directory_all(
14326 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
14327 )?;
14328 }
14329 if !matches!(entry.state, TableState::Live) {
14330 continue;
14331 }
14332 let transaction_dir = self
14333 .root
14334 .join(TABLES_DIR)
14335 .join(entry.table_id.to_string())
14336 .join("_txn");
14337 if transaction_dir.is_dir() {
14338 for child in std::fs::read_dir(&transaction_dir)? {
14339 let child = child?;
14340 let active = child
14341 .file_name()
14342 .to_str()
14343 .and_then(|name| name.parse::<u64>().ok())
14344 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
14345 if !active {
14346 crate::durable_file::remove_directory_all(&child.path())?;
14347 }
14348 }
14349 }
14350 }
14351 let external_names = catalog_snapshot
14352 .external_tables
14353 .iter()
14354 .map(|entry| entry.name.as_str())
14355 .collect::<HashSet<_>>();
14356 let external_root = self.root.join(VTAB_DIR);
14357 if external_root.is_dir() {
14358 for entry in std::fs::read_dir(&external_root)? {
14359 let entry = entry?;
14360 let name = entry.file_name();
14361 if name
14362 .to_str()
14363 .is_some_and(|name| external_names.contains(name))
14364 {
14365 continue;
14366 }
14367 if entry.file_type()?.is_dir() {
14368 crate::durable_file::remove_directory_all(&entry.path())?;
14369 } else {
14370 std::fs::remove_file(entry.path())?;
14371 crate::durable_file::sync_directory(&external_root)?;
14372 }
14373 }
14374 }
14375
14376 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
14379 let visible = self.epoch.visible();
14380 for (_, table) in &tables {
14381 table.persist_manifest(visible)?;
14382 }
14383
14384 Ok(())
14385 }
14386 fn alloc_txn_id(&self) -> Result<u64> {
14387 self.ensure_owner_process()?;
14388 crate::txn::allocate_txn_id(&self.next_txn_id)
14389 }
14390
14391 pub fn allocate_lock_txn_id(&self) -> Result<u64> {
14395 self.alloc_txn_id()
14396 }
14397
14398 pub fn set_spill_threshold(&self, bytes: u64) {
14402 self.spill_threshold
14403 .store(bytes, std::sync::atomic::Ordering::Relaxed);
14404 }
14405
14406 #[doc(hidden)]
14410 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14411 *self.spill_hook.lock() = Some(Box::new(f));
14412 }
14413
14414 #[doc(hidden)]
14417 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14418 *self.security_commit_hook.lock() = Some(Box::new(f));
14419 }
14420
14421 #[doc(hidden)]
14424 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14425 *self.catalog_commit_hook.lock() = Some(Box::new(f));
14426 }
14427
14428 #[doc(hidden)]
14431 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14432 *self.backup_hook.lock() = Some(Box::new(f));
14433 }
14434
14435 #[doc(hidden)]
14439 pub fn __set_fk_lock_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14440 *self.fk_lock_hook.lock() = Some(Arc::new(f));
14441 }
14442
14443 #[doc(hidden)]
14445 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
14446 *self.replication_hook.lock() = Some(Box::new(f));
14447 }
14448
14449 #[doc(hidden)]
14453 pub fn __wal_group_sync_count(&self) -> u64 {
14454 self.shared_wal.lock().group_sync_count()
14455 }
14456
14457 #[doc(hidden)]
14460 pub fn __poison(&self) {
14461 self.poisoned
14462 .store(true, std::sync::atomic::Ordering::Relaxed);
14463 }
14464
14465 pub fn check(&self) -> Vec<CheckIssue> {
14478 match self.check_inner(None) {
14479 Ok(issues) => issues,
14480 Err(error) => vec![CheckIssue {
14481 table_id: WAL_TABLE_ID,
14482 table_name: "shared WAL".into(),
14483 severity: "error".into(),
14484 description: error.to_string(),
14485 }],
14486 }
14487 }
14488
14489 #[doc(hidden)]
14491 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
14492 self.check_inner(Some(control))
14493 }
14494
14495 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
14496 let mut issues = Vec::new();
14497 let cat = self.catalog.read();
14498 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
14499 for (table_index, entry) in cat.tables.iter().enumerate() {
14500 if table_index % 256 == 0 {
14501 if let Some(control) = control {
14502 control.checkpoint()?;
14503 }
14504 }
14505 if !matches!(entry.state, TableState::Live) {
14506 continue;
14507 }
14508 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
14509 let mut err = |sev: &str, desc: String| {
14510 issues.push(CheckIssue {
14511 table_id: entry.table_id,
14512 table_name: entry.name.clone(),
14513 severity: sev.into(),
14514 description: desc,
14515 });
14516 };
14517 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
14518 Ok(m) => m,
14519 Err(e) => {
14520 err("error", format!("manifest read failed: {e}"));
14521 continue;
14522 }
14523 };
14524 if m.flushed_epoch > m.current_epoch {
14525 err(
14526 "error",
14527 format!(
14528 "flushed_epoch {} exceeds current_epoch {} (impossible)",
14529 m.flushed_epoch, m.current_epoch
14530 ),
14531 );
14532 }
14533
14534 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
14535 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
14536 for (run_index, rr) in m.runs.iter().enumerate() {
14537 if run_index % 256 == 0 {
14538 if let Some(control) = control {
14539 control.checkpoint()?;
14540 }
14541 }
14542 referenced.insert(rr.run_id);
14543 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
14544 if !run_path.exists() {
14545 err("error", format!("missing run file: r-{}.sr", rr.run_id));
14546 continue;
14547 }
14548 match crate::sorted_run::RunReader::open(
14549 &run_path,
14550 entry.schema.clone(),
14551 self.kek.clone(),
14552 ) {
14553 Ok(reader) => {
14554 if reader.row_count() as u64 != rr.row_count {
14555 err(
14556 "error",
14557 format!(
14558 "run r-{} row count mismatch: manifest {} vs run {}",
14559 rr.run_id,
14560 rr.row_count,
14561 reader.row_count()
14562 ),
14563 );
14564 }
14565 }
14566 Err(e) => {
14567 err(
14568 "error",
14569 format!("run r-{} integrity check failed: {e}", rr.run_id),
14570 );
14571 }
14572 }
14573 }
14574
14575 for r in &m.retiring {
14579 referenced.insert(r.run_id);
14580 }
14581
14582 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
14584 for (entry_index, ent) in rd.flatten().enumerate() {
14585 if entry_index % 256 == 0 {
14586 if let Some(control) = control {
14587 control.checkpoint()?;
14588 }
14589 }
14590 let p = ent.path();
14591 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
14592 continue;
14593 }
14594 let run_id = p
14595 .file_stem()
14596 .and_then(|s| s.to_str())
14597 .and_then(|s| s.strip_prefix("r-"))
14598 .and_then(|s| s.parse::<u128>().ok());
14599 if let Some(id) = run_id {
14600 if !referenced.contains(&id) {
14601 err(
14602 "warning",
14603 format!("orphan run file r-{id}.sr not referenced by the manifest"),
14604 );
14605 }
14606 }
14607 }
14608 }
14609 }
14610
14611 let external_names = cat
14612 .external_tables
14613 .iter()
14614 .map(|entry| entry.name.clone())
14615 .collect::<std::collections::HashSet<_>>();
14616 let vtab_dir = self.root.join(VTAB_DIR);
14617 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
14618 for (entry_index, entry) in entries.flatten().enumerate() {
14619 if entry_index % 256 == 0 {
14620 if let Some(control) = control {
14621 control.checkpoint()?;
14622 }
14623 }
14624 let name = entry.file_name();
14625 let Some(name) = name.to_str() else { continue };
14626 if !external_names.contains(name) {
14627 issues.push(CheckIssue {
14628 table_id: EXTERNAL_TABLE_ID,
14629 table_name: name.to_string(),
14630 severity: "warning".into(),
14631 description: format!(
14632 "orphan external table state entry {:?} not referenced by the catalog",
14633 entry.path()
14634 ),
14635 });
14636 }
14637 }
14638 }
14639
14640 if let Some(control) = control {
14647 control.checkpoint()?;
14648 }
14649 for (seg, msg) in self.shared_wal.lock().verify_segments() {
14650 issues.push(CheckIssue {
14651 table_id: WAL_TABLE_ID,
14652 table_name: "<wal>".into(),
14653 severity: "error".into(),
14654 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
14655 });
14656 }
14657 Ok(issues)
14658 }
14659
14660 pub fn doctor(&self) -> Result<Vec<u64>> {
14664 let control = crate::ExecutionControl::new(None);
14665 self.doctor_controlled(&control, || true)
14666 }
14667
14668 #[doc(hidden)]
14672 pub fn doctor_controlled<F>(
14673 &self,
14674 control: &crate::ExecutionControl,
14675 before_publish: F,
14676 ) -> Result<Vec<u64>>
14677 where
14678 F: FnOnce() -> bool,
14679 {
14680 self.doctor_controlled_with_receipt(control, before_publish)
14681 .map(|(quarantined, _)| quarantined)
14682 }
14683
14684 #[doc(hidden)]
14687 pub fn doctor_controlled_with_receipt<F>(
14688 &self,
14689 control: &crate::ExecutionControl,
14690 before_publish: F,
14691 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
14692 where
14693 F: FnOnce() -> bool,
14694 {
14695 let _ddl = self.ddl_lock.lock();
14698 let _security_write = self.security_write()?;
14699 let issues = self.check_inner(Some(control))?;
14700 let bad_tables: std::collections::HashSet<u64> = issues
14705 .iter()
14706 .filter(|i| {
14707 i.severity == "error"
14708 && i.table_id != WAL_TABLE_ID
14709 && i.table_id != EXTERNAL_TABLE_ID
14710 })
14711 .map(|i| i.table_id)
14712 .collect();
14713 if bad_tables.is_empty() {
14714 return Ok((Vec::new(), None));
14715 }
14716 let _commit = self.commit_lock.lock();
14717 control.checkpoint()?;
14718 if !before_publish() {
14719 return Err(MongrelError::Cancelled);
14720 }
14721 let maintenance_epoch = self.epoch.bump_assigned();
14722 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
14723
14724 let qdir = self.root.join("_quarantine");
14725 crate::durable_file::create_directory(&qdir)?;
14726 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
14727 bad_tables.sort_unstable();
14728
14729 let mut handles = self
14733 .tables
14734 .read()
14735 .iter()
14736 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
14737 .map(|(table_id, handle)| (*table_id, handle.clone()))
14738 .collect::<Vec<_>>();
14739 handles.sort_by_key(|(table_id, _)| *table_id);
14740 let mut table_guards = handles
14741 .iter()
14742 .map(|(table_id, handle)| (*table_id, handle.lock()))
14743 .collect::<Vec<_>>();
14744
14745 let mut next_catalog = self.catalog.read().clone();
14746 for table_id in &bad_tables {
14747 if let Some(entry) = next_catalog
14748 .tables
14749 .iter_mut()
14750 .find(|entry| entry.table_id == *table_id)
14751 {
14752 entry.state = TableState::Dropped {
14753 at_epoch: maintenance_epoch.0,
14754 };
14755 }
14756 }
14757 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
14758
14759 let txn_id = self.alloc_txn_id()?;
14760 let commit_seq = {
14761 let mut wal = self.shared_wal.lock();
14762 let append: Result<u64> = (|| {
14763 for table_id in &bad_tables {
14764 wal.append(
14765 txn_id,
14766 *table_id,
14767 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
14768 table_id: *table_id,
14769 }),
14770 )?;
14771 }
14772 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14773 wal.append_commit(txn_id, maintenance_epoch, &[])
14774 })();
14775 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
14776 };
14777 let receipt = self.await_durable_commit(txn_id, commit_seq, maintenance_epoch)?;
14778 for (_, table) in &mut table_guards {
14779 table.mark_unavailable_after_quarantine();
14780 }
14781 {
14782 let mut live_tables = self.tables.write();
14783 for table_id in &bad_tables {
14784 live_tables.remove(table_id);
14785 }
14786 }
14787 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14788 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, &receipt, checkpoint)?;
14789
14790 for table_id in &bad_tables {
14794 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
14795 if source.exists() {
14796 let destination = qdir.join(table_id.to_string());
14797 if let Err(error) = crate::durable_file::rename(&source, &destination) {
14798 return Err(MongrelError::DurableCommit {
14799 epoch: maintenance_epoch.0,
14800 message: format!(
14801 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
14802 ),
14803 });
14804 }
14805 }
14806 }
14807 Ok((
14808 bad_tables,
14809 Some(MaintenanceReceipt {
14810 epoch: maintenance_epoch,
14811 }),
14812 ))
14813 }
14814
14815 #[allow(dead_code)]
14817 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
14818 self.kek.as_ref()
14819 }
14820
14821 #[allow(dead_code)]
14823 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
14824 &self.epoch
14825 }
14826
14827 #[allow(dead_code)]
14829 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
14830 &self.snapshots
14831 }
14832}
14833
14834fn external_state_dir(root: &Path, name: &str) -> PathBuf {
14835 root.join(VTAB_DIR).join(name)
14836}
14837
14838fn append_catalog_snapshot(
14839 wal: &mut crate::wal::SharedWal,
14840 txn_id: u64,
14841 catalog: &Catalog,
14842) -> Result<()> {
14843 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
14844 wal.append(
14845 txn_id,
14846 WAL_TABLE_ID,
14847 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
14848 )?;
14849 Ok(())
14850}
14851
14852fn filter_ignored_staging(
14853 staging: Vec<(u64, crate::txn::Staged)>,
14854 ignored_indices: &std::collections::BTreeSet<usize>,
14855) -> Vec<(u64, crate::txn::Staged)> {
14856 if ignored_indices.is_empty() {
14857 return staging;
14858 }
14859 staging
14860 .into_iter()
14861 .enumerate()
14862 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
14863 .collect()
14864}
14865
14866fn external_state_file(root: &Path, name: &str) -> PathBuf {
14867 external_state_dir(root, name).join("state.json")
14868}
14869
14870fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
14871 let path = external_state_file(root, name);
14872 match std::fs::read(path) {
14873 Ok(bytes) => Ok(bytes),
14874 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
14875 Err(e) => Err(e.into()),
14876 }
14877}
14878
14879fn current_external_state_bytes(
14880 root: &Path,
14881 external_states: &[(String, Vec<u8>)],
14882 name: &str,
14883) -> Result<Vec<u8>> {
14884 for (table, state) in external_states.iter().rev() {
14885 if table == name {
14886 return Ok(state.clone());
14887 }
14888 }
14889 read_external_state_file(root, name)
14890}
14891
14892fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
14893 let mut out = external_states;
14894 dedup_external_states_in_place(&mut out);
14895 out
14896}
14897
14898fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
14899 let mut seen = std::collections::HashSet::new();
14900 let mut out = Vec::with_capacity(external_states.len());
14901 for (name, state) in std::mem::take(external_states).into_iter().rev() {
14902 if seen.insert(name.clone()) {
14903 out.push((name, state));
14904 }
14905 }
14906 out.reverse();
14907 *external_states = out;
14908}
14909
14910fn prepare_external_state_file(
14911 root: &Path,
14912 name: &str,
14913 state: &[u8],
14914 txn_id: u64,
14915) -> Result<PathBuf> {
14916 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
14917 let dir = external_state_dir(root, name);
14918 crate::durable_file::create_directory(&dir)?;
14919 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
14920 {
14921 let mut file = std::fs::OpenOptions::new()
14922 .create_new(true)
14923 .write(true)
14924 .open(&pending)?;
14925 file.write_all(state)?;
14926 file.sync_all()?;
14927 }
14928 Ok(pending)
14929}
14930
14931fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
14932 let path = external_state_file(root, name);
14933 crate::durable_file::replace(pending, &path)?;
14934 Ok(())
14935}
14936
14937fn write_external_state_file(
14938 durable: &crate::durable_file::DurableRoot,
14939 name: &str,
14940 state: &[u8],
14941) -> Result<()> {
14942 let directory = Path::new(VTAB_DIR).join(name);
14943 durable.create_directory_all(&directory)?;
14944 durable.write_atomic(directory.join("state.json"), state)?;
14945 Ok(())
14946}
14947
14948fn validate_recovered_data_table(
14949 catalog: &Catalog,
14950 tables: &HashMap<u64, TableHandle>,
14951 table_id: u64,
14952 commit_epoch: u64,
14953 offset: u64,
14954) -> Result<bool> {
14955 let entry = catalog
14956 .tables
14957 .iter()
14958 .find(|entry| entry.table_id == table_id)
14959 .ok_or_else(|| MongrelError::CorruptWal {
14960 offset,
14961 reason: format!("committed record references unknown table {table_id}"),
14962 })?;
14963 if commit_epoch < entry.created_epoch {
14964 return Err(MongrelError::CorruptWal {
14965 offset,
14966 reason: format!(
14967 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
14968 entry.created_epoch
14969 ),
14970 });
14971 }
14972 match entry.state {
14973 TableState::Dropped { at_epoch } => {
14974 let abandoned_build_boundary =
14979 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
14980 if commit_epoch >= at_epoch && !abandoned_build_boundary {
14981 Err(MongrelError::CorruptWal {
14982 offset,
14983 reason: format!(
14984 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
14985 ),
14986 })
14987 } else {
14988 Ok(false)
14989 }
14990 }
14991 TableState::Live | TableState::Building { .. } => {
14992 if tables.contains_key(&table_id) {
14993 Ok(true)
14994 } else {
14995 Err(MongrelError::CorruptWal {
14996 offset,
14997 reason: format!("live table {table_id} has no mounted recovery handle"),
14998 })
14999 }
15000 }
15001 }
15002}
15003
15004type RecoveryTableStage = (
15005 Vec<crate::memtable::Row>,
15006 Vec<(crate::rowid::RowId, Epoch)>,
15007 Option<Epoch>,
15008 Epoch,
15009);
15010
15011#[derive(Clone)]
15012struct RecoveryValidationTable {
15013 schema: Schema,
15014 flushed_epoch: u64,
15015}
15016
15017fn validate_shared_wal_recovery_plan(
15018 durable_root: &crate::durable_file::DurableRoot,
15019 catalog: &Catalog,
15020 recovered_table_ids: &HashSet<u64>,
15021 reconciled_table_ids: &HashSet<u64>,
15022 meta_dek: Option<&[u8; META_DEK_LEN]>,
15023 kek: Option<Arc<crate::encryption::Kek>>,
15024 records: &[crate::wal::Record],
15025) -> Result<()> {
15026 use crate::wal::{DdlOp, Op};
15027
15028 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
15029 for entry in &catalog.tables {
15030 if !matches!(entry.state, TableState::Live) {
15031 continue;
15032 }
15033 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15034 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
15035 Ok(manifest) => Some(manifest),
15036 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
15037 Err(error) => return Err(error),
15038 };
15039 let flushed_epoch = if let Some(manifest) = manifest {
15040 if manifest.table_id != entry.table_id {
15041 return Err(MongrelError::Conflict(format!(
15042 "catalog table {} storage identity mismatch",
15043 entry.table_id
15044 )));
15045 }
15046 if (manifest.schema_id != entry.schema.schema_id
15047 && !reconciled_table_ids.contains(&entry.table_id))
15048 || manifest.flushed_epoch > manifest.current_epoch
15049 || manifest.global_idx_epoch > manifest.current_epoch
15050 || manifest.next_row_id == u64::MAX
15051 || manifest.auto_inc_next < 0
15052 || manifest.auto_inc_next == i64::MAX
15053 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
15054 {
15055 return Err(MongrelError::InvalidArgument(format!(
15056 "table {} manifest counters or schema identity are invalid",
15057 entry.table_id
15058 )));
15059 }
15060 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
15061 crate::global_idx::read_durable_for(
15062 durable_root,
15063 &relative_dir,
15064 entry.table_id,
15065 &entry.schema,
15066 idx_dek.as_deref(),
15067 )?;
15068 let mut run_ids = HashSet::new();
15069 let mut maximum_row_id = None::<u64>;
15070 for run in &manifest.runs {
15071 if run.run_id >= u64::MAX as u128
15072 || run.epoch_created > manifest.current_epoch
15073 || !run_ids.insert(run.run_id)
15074 {
15075 return Err(MongrelError::InvalidArgument(format!(
15076 "table {} manifest contains an invalid or duplicate run id",
15077 entry.table_id
15078 )));
15079 }
15080 let relative = relative_dir
15081 .join(crate::engine::RUNS_DIR)
15082 .join(format!("r-{}.sr", run.run_id as u64));
15083 let file = durable_root.open_regular(&relative)?;
15084 let mut reader = crate::sorted_run::RunReader::open_file(
15085 file,
15086 entry.schema.clone(),
15087 kek.clone(),
15088 )?;
15089 let header = reader.header();
15090 if header.run_id != run.run_id
15091 || header.level != run.level
15092 || header.row_count != run.row_count
15093 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
15094 || header.is_uniform_epoch() && header.epoch_created != 0
15095 || header.schema_id > entry.schema.schema_id
15096 {
15097 return Err(MongrelError::InvalidArgument(format!(
15098 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
15099 entry.table_id,
15100 run.run_id,
15101 header.run_id,
15102 header.level,
15103 header.row_count,
15104 header.epoch_created,
15105 header.schema_id,
15106 run.run_id,
15107 run.level,
15108 run.row_count,
15109 run.epoch_created,
15110 entry.schema.schema_id,
15111 )));
15112 }
15113 if header.row_count != 0 {
15114 maximum_row_id = Some(
15115 maximum_row_id
15116 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
15117 );
15118 }
15119 reader.validate_all_pages()?;
15120 }
15121 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
15122 return Err(MongrelError::InvalidArgument(format!(
15123 "table {} next_row_id does not advance beyond persisted rows",
15124 entry.table_id
15125 )));
15126 }
15127 for run in &manifest.retiring {
15128 if run.run_id >= u64::MAX as u128
15129 || run.retire_epoch > manifest.current_epoch
15130 || !run_ids.insert(run.run_id)
15131 {
15132 return Err(MongrelError::InvalidArgument(format!(
15133 "table {} manifest contains an invalid or aliased retired run",
15134 entry.table_id
15135 )));
15136 }
15137 }
15138 manifest.flushed_epoch
15139 } else {
15140 if !recovered_table_ids.contains(&entry.table_id) {
15141 return Err(MongrelError::NotFound(format!(
15142 "live table {} manifest is missing",
15143 entry.table_id
15144 )));
15145 }
15146 0
15147 };
15148 tables.insert(
15149 entry.table_id,
15150 RecoveryValidationTable {
15151 schema: entry.schema.clone(),
15152 flushed_epoch,
15153 },
15154 );
15155 }
15156
15157 let committed = records
15158 .iter()
15159 .filter_map(|record| match record.op {
15160 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
15161 _ => None,
15162 })
15163 .collect::<HashMap<_, _>>();
15164 let mut run_ids = HashSet::new();
15165 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
15166 for record in records {
15167 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
15168 continue;
15169 };
15170 match &record.op {
15171 Op::Put { table_id, rows } => {
15172 let table = validate_recovery_data_table_plan(
15173 catalog,
15174 &tables,
15175 *table_id,
15176 commit_epoch,
15177 record.seq.0,
15178 )?;
15179 let decoded: Vec<crate::memtable::Row> =
15180 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
15181 offset: record.seq.0,
15182 reason: format!(
15183 "committed Put payload for transaction {} could not be decoded: {error}",
15184 record.txn_id
15185 ),
15186 })?;
15187 if let Some(table) = table {
15188 for row in &decoded {
15189 if !recovered_row_ids
15190 .entry(*table_id)
15191 .or_default()
15192 .insert(row.row_id.0)
15193 {
15194 return Err(MongrelError::CorruptWal {
15195 offset: record.seq.0,
15196 reason: format!(
15197 "committed WAL repeats recovered row id {} for table {table_id}",
15198 row.row_id.0
15199 ),
15200 });
15201 }
15202 validate_recovered_row(&table.schema, row)?;
15203 }
15204 }
15205 }
15206 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
15207 validate_recovery_data_table_plan(
15208 catalog,
15209 &tables,
15210 *table_id,
15211 commit_epoch,
15212 record.seq.0,
15213 )?;
15214 }
15215 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
15216 Op::Ddl(DdlOp::ResetExternalTableState {
15217 name,
15218 generation_epoch,
15219 }) => {
15220 if *generation_epoch != commit_epoch {
15221 return Err(MongrelError::CorruptWal {
15222 offset: record.seq.0,
15223 reason: format!(
15224 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
15225 ),
15226 });
15227 }
15228 validate_recovered_external_name(name)?;
15229 }
15230 Op::TxnCommit { added_runs, .. } => {
15231 for added in added_runs {
15232 let Some(table) = validate_recovery_data_table_plan(
15233 catalog,
15234 &tables,
15235 added.table_id,
15236 commit_epoch,
15237 record.seq.0,
15238 )?
15239 else {
15240 continue;
15241 };
15242 if added.run_id >= u64::MAX as u128
15243 || !run_ids.insert((added.table_id, added.run_id))
15244 {
15245 return Err(MongrelError::CorruptWal {
15246 offset: record.seq.0,
15247 reason: format!(
15248 "duplicate or invalid recovered run {} for table {}",
15249 added.run_id, added.table_id
15250 ),
15251 });
15252 }
15253 if commit_epoch <= table.flushed_epoch {
15254 continue;
15255 }
15256 validate_planned_spilled_run(
15257 durable_root,
15258 record.txn_id,
15259 commit_epoch,
15260 added,
15261 &table.schema,
15262 kek.clone(),
15263 )?;
15264 }
15265 }
15266 _ => {}
15267 }
15268 }
15269 Ok(())
15270}
15271
15272fn validate_recovery_data_table_plan<'a>(
15273 catalog: &Catalog,
15274 tables: &'a HashMap<u64, RecoveryValidationTable>,
15275 table_id: u64,
15276 commit_epoch: u64,
15277 offset: u64,
15278) -> Result<Option<&'a RecoveryValidationTable>> {
15279 let entry = catalog
15280 .tables
15281 .iter()
15282 .find(|entry| entry.table_id == table_id)
15283 .ok_or_else(|| MongrelError::CorruptWal {
15284 offset,
15285 reason: format!("committed record references unknown table {table_id}"),
15286 })?;
15287 if commit_epoch < entry.created_epoch {
15288 return Err(MongrelError::CorruptWal {
15289 offset,
15290 reason: format!(
15291 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
15292 entry.created_epoch
15293 ),
15294 });
15295 }
15296 match entry.state {
15297 TableState::Dropped { at_epoch } => {
15298 let abandoned =
15299 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
15300 if commit_epoch >= at_epoch && !abandoned {
15301 return Err(MongrelError::CorruptWal {
15302 offset,
15303 reason: format!(
15304 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
15305 ),
15306 });
15307 }
15308 Ok(None)
15309 }
15310 TableState::Live => {
15311 tables
15312 .get(&table_id)
15313 .map(Some)
15314 .ok_or_else(|| MongrelError::CorruptWal {
15315 offset,
15316 reason: format!("live table {table_id} has no recovery plan"),
15317 })
15318 }
15319 TableState::Building { .. } => Err(MongrelError::CorruptWal {
15320 offset,
15321 reason: format!("building table {table_id} was not normalized before recovery"),
15322 }),
15323 }
15324}
15325
15326fn validate_planned_spilled_run(
15327 root: &crate::durable_file::DurableRoot,
15328 txn_id: u64,
15329 commit_epoch: u64,
15330 added: &crate::wal::AddedRun,
15331 schema: &Schema,
15332 kek: Option<Arc<crate::encryption::Kek>>,
15333) -> Result<()> {
15334 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
15335 let destination = table
15336 .join(crate::engine::RUNS_DIR)
15337 .join(format!("r-{}.sr", added.run_id as u64));
15338 let pending = table
15339 .join("_txn")
15340 .join(txn_id.to_string())
15341 .join(format!("r-{}.sr", added.run_id as u64));
15342 let file = match root.open_regular(&destination) {
15343 Ok(file) => file,
15344 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
15345 root.open_regular(&pending).map_err(|pending_error| {
15346 if pending_error.kind() == std::io::ErrorKind::NotFound {
15347 MongrelError::CorruptWal {
15348 offset: commit_epoch,
15349 reason: format!(
15350 "committed spilled run {} for transaction {txn_id} is missing",
15351 added.run_id
15352 ),
15353 }
15354 } else {
15355 pending_error.into()
15356 }
15357 })?
15358 }
15359 Err(error) => return Err(error.into()),
15360 };
15361 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
15362 let header = reader.header();
15363 if header.run_id != added.run_id
15364 || header.content_hash != added.content_hash
15365 || header.row_count != added.row_count
15366 || header.level != added.level
15367 || header.min_row_id != added.min_row_id
15368 || header.max_row_id != added.max_row_id
15369 || header.schema_id != schema.schema_id
15370 || !header.is_uniform_epoch()
15371 || header.epoch_created != 0
15372 {
15373 return Err(MongrelError::CorruptWal {
15374 offset: commit_epoch,
15375 reason: format!(
15376 "committed spilled run {} metadata differs from WAL",
15377 added.run_id
15378 ),
15379 });
15380 }
15381 reader.validate_all_pages()?;
15382 Ok(())
15383}
15384
15385#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15405pub enum StagedTxnWrite {
15406 Put {
15411 table_id: u64,
15413 rows: Vec<u8>,
15415 },
15416 Delete {
15418 table_id: u64,
15420 row_ids: Vec<u64>,
15422 },
15423}
15424
15425impl StagedTxnWrite {
15426 pub fn encode(&self) -> Result<Vec<u8>> {
15428 Ok(bincode::serialize(self)?)
15429 }
15430
15431 pub fn decode(bytes: &[u8]) -> Result<Self> {
15433 Ok(bincode::deserialize(bytes)?)
15434 }
15435}
15436
15437pub fn translate_records_for_replication(
15474 records: &[crate::wal::Record],
15475) -> Result<Vec<crate::wal::Record>> {
15476 use crate::wal::Op;
15477
15478 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
15481 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
15482 })?;
15483 if records.iter().any(|record| record.txn_id != txn_id) {
15484 return Err(MongrelError::InvalidArgument(
15485 "replicated transaction payload mixes transaction ids".into(),
15486 ));
15487 }
15488 let commits = records
15489 .iter()
15490 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
15491 .count();
15492 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
15493 return Err(MongrelError::InvalidArgument(
15494 "replicated transaction payload must end in exactly one commit marker".into(),
15495 ));
15496 }
15497
15498 let mut spilled_rows: HashMap<u64, Vec<crate::memtable::Row>> = HashMap::new();
15502 for record in records {
15503 if let Op::SpilledRows { table_id, rows } = &record.op {
15504 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(rows).map_err(|error| {
15505 MongrelError::InvalidArgument(format!(
15506 "spilled row payload for table {table_id} cannot decode for replication: \
15507 {error}"
15508 ))
15509 })?;
15510 spilled_rows.entry(*table_id).or_default().extend(chunk);
15511 }
15512 }
15513
15514 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
15518 unreachable!("one trailing commit marker validated above");
15519 };
15520 for run in added_runs {
15521 let rows = spilled_rows.get(&run.table_id).ok_or_else(|| {
15522 MongrelError::InvalidArgument(format!(
15523 "commit links spilled run {} for table {} but carries no logical row records \
15524 for it",
15525 run.run_id, run.table_id
15526 ))
15527 })?;
15528 let covered = rows
15529 .iter()
15530 .filter(|row| row.row_id.0 >= run.min_row_id && row.row_id.0 <= run.max_row_id)
15531 .count() as u64;
15532 if covered != run.row_count {
15533 return Err(MongrelError::InvalidArgument(format!(
15534 "commit links spilled run {} for table {} ({} rows in [{}, {}]) but the logical \
15535 row records cover {} rows",
15536 run.run_id, run.table_id, run.row_count, run.min_row_id, run.max_row_id, covered
15537 )));
15538 }
15539 }
15540
15541 let translated = records
15544 .iter()
15545 .map(|record| {
15546 let op = match &record.op {
15547 Op::SpilledRows { table_id, rows } => Op::Put {
15548 table_id: *table_id,
15549 rows: rows.clone(),
15550 },
15551 Op::TxnCommit { epoch, .. } => Op::TxnCommit {
15552 epoch: *epoch,
15553 added_runs: Vec::new(),
15554 },
15555 op => op.clone(),
15556 };
15557 crate::wal::Record::new(record.seq, record.txn_id, op)
15558 })
15559 .collect();
15560 Ok(translated)
15561}
15562
15563fn recover_shared_wal(
15564 durable_root: &crate::durable_file::DurableRoot,
15565 tables: &HashMap<u64, TableHandle>,
15566 catalog: &Catalog,
15567 epoch: &EpochAuthority,
15568 records: &[crate::wal::Record],
15569) -> Result<()> {
15570 use crate::memtable::Row;
15571 use crate::wal::{DdlOp, Op};
15572
15573 let mut committed: HashMap<u64, u64> = HashMap::new();
15575 let mut spilled_to_link: Vec<(
15576 u64, u64, Vec<crate::wal::AddedRun>,
15579 )> = Vec::new();
15580 for r in records {
15581 if let Op::TxnCommit {
15582 epoch: ce,
15583 ref added_runs,
15584 } = r.op
15585 {
15586 committed.insert(r.txn_id, ce);
15587 if !added_runs.is_empty() {
15588 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
15589 }
15590 }
15591 }
15592 for record in records {
15593 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
15594 continue;
15595 };
15596 match &record.op {
15597 Op::Put { table_id, .. }
15598 | Op::Delete { table_id, .. }
15599 | Op::TruncateTable { table_id } => {
15600 validate_recovered_data_table(
15601 catalog,
15602 tables,
15603 *table_id,
15604 commit_epoch,
15605 record.seq.0,
15606 )?;
15607 }
15608 Op::TxnCommit { added_runs, .. } => {
15609 for run in added_runs {
15610 validate_recovered_data_table(
15611 catalog,
15612 tables,
15613 run.table_id,
15614 commit_epoch,
15615 record.seq.0,
15616 )?;
15617 }
15618 }
15619 _ => {}
15620 }
15621 }
15622 let truncated_transactions: HashSet<(u64, u64)> = records
15623 .iter()
15624 .filter_map(|record| {
15625 committed.get(&record.txn_id)?;
15626 match record.op {
15627 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
15628 _ => None,
15629 }
15630 })
15631 .collect();
15632
15633 enum ExternalRecoveryAction {
15635 Write { name: String, state: Vec<u8> },
15636 Reset { name: String },
15637 }
15638 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
15639 let mut external_actions = Vec::new();
15640 let mut max_epoch = epoch.visible().0;
15641 for r in records.iter().cloned() {
15642 let Some(&ce) = committed.get(&r.txn_id) else {
15643 continue; };
15645 let commit_epoch = Epoch(ce);
15646 max_epoch = max_epoch.max(ce);
15647 match r.op {
15648 Op::Put { table_id, rows } => {
15649 let skip = tables
15651 .get(&table_id)
15652 .map(|h| h.lock().flushed_epoch() >= ce)
15653 .unwrap_or(true);
15654 if skip {
15655 continue;
15656 }
15657 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
15658 MongrelError::CorruptWal {
15659 offset: r.seq.0,
15660 reason: format!(
15661 "committed Put payload for transaction {} could not be decoded: {error}",
15662 r.txn_id
15663 ),
15664 }
15665 })?;
15666 let rows: Vec<Row> = rows
15669 .into_iter()
15670 .map(|mut row| {
15671 row.committed_epoch = commit_epoch;
15672 row
15673 })
15674 .collect();
15675 let entry = stage
15676 .entry(table_id)
15677 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
15678 entry.0.extend(rows);
15679 entry.3 = commit_epoch;
15680 }
15681 Op::Delete { table_id, row_ids } => {
15682 let skip = tables
15683 .get(&table_id)
15684 .map(|h| h.lock().flushed_epoch() >= ce)
15685 .unwrap_or(true);
15686 if skip {
15687 continue;
15688 }
15689 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
15690 let entry = stage
15691 .entry(table_id)
15692 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
15693 entry.1.extend(dels);
15694 entry.3 = commit_epoch;
15695 }
15696 Op::TruncateTable { table_id } => {
15697 let skip = tables
15698 .get(&table_id)
15699 .map(|h| h.lock().flushed_epoch() >= ce)
15700 .unwrap_or(true);
15701 if skip {
15702 continue;
15703 }
15704 stage.insert(
15705 table_id,
15706 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
15707 );
15708 }
15709 Op::ExternalTableState { name, state } => {
15710 let current_generation = catalog
15711 .external_tables
15712 .iter()
15713 .find(|entry| entry.name == name)
15714 .map(|entry| entry.created_epoch);
15715 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
15716 validate_recovered_external_name(&name)?;
15717 external_actions.push(ExternalRecoveryAction::Write { name, state });
15718 }
15719 }
15720 Op::Ddl(DdlOp::ResetExternalTableState {
15721 name,
15722 generation_epoch,
15723 }) => {
15724 if generation_epoch != ce {
15725 return Err(MongrelError::CorruptWal {
15726 offset: r.seq.0,
15727 reason: format!(
15728 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
15729 ),
15730 });
15731 }
15732 validate_recovered_external_name(&name)?;
15733 external_actions.push(ExternalRecoveryAction::Reset { name });
15734 }
15735 Op::Flush { .. }
15736 | Op::TxnCommit { .. }
15737 | Op::TxnAbort
15738 | Op::Ddl(_)
15739 | Op::BeforeImage { .. }
15740 | Op::CommitTimestamp { .. }
15741 | Op::SpilledRows { .. } => {}
15742 }
15743 }
15744 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
15745 added_runs.retain(|added| {
15746 tables
15747 .get(&added.table_id)
15748 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
15749 });
15750 }
15751 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
15752 validate_recovery_table_stages(tables, &stage)?;
15753 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
15754
15755 for action in external_actions {
15759 match action {
15760 ExternalRecoveryAction::Write { name, state } => {
15761 write_external_state_file(durable_root, &name, &state)?;
15762 }
15763 ExternalRecoveryAction::Reset { name } => {
15764 durable_root.create_directory_all(VTAB_DIR)?;
15765 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
15766 }
15767 }
15768 }
15769 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
15770 let Some(handle) = tables.get(&table_id) else {
15771 continue;
15772 };
15773 let mut t = handle.lock();
15774 if let Some(epoch) = truncate_epoch {
15775 t.apply_truncate(epoch);
15776 }
15777 t.recover_apply(rows, deletes)?;
15778 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
15782 t.live_count = rows.len() as u64;
15783 t.persist_manifest(table_epoch.max(epoch.visible()))?;
15787 }
15788
15789 for (txn_id, ce, added_runs) in &spilled_to_link {
15793 for ar in added_runs {
15794 let Some(handle) = tables.get(&ar.table_id) else {
15795 continue;
15796 };
15797 let mut t = handle.lock();
15798 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
15799 let destination = table_dir
15800 .join(crate::engine::RUNS_DIR)
15801 .join(format!("r-{}.sr", ar.run_id));
15802 match durable_root.open_regular(&destination) {
15803 Ok(_) => {}
15804 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
15805 let pending = table_dir
15806 .join("_txn")
15807 .join(txn_id.to_string())
15808 .join(format!("r-{}.sr", ar.run_id));
15809 durable_root.rename_file_new(&pending, &destination)?;
15810 }
15811 Err(error) => return Err(error.into()),
15812 }
15813 let linked = t.recover_spilled_run(crate::manifest::RunRef {
15819 run_id: ar.run_id,
15820 level: ar.level,
15821 epoch_created: *ce,
15822 row_count: ar.row_count,
15823 });
15824 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
15825 if replaced {
15826 t.set_flushed_epoch(Epoch(*ce));
15827 }
15828 if linked || replaced {
15829 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
15830 }
15831 }
15832 }
15833
15834 epoch.advance_recovered(Epoch(max_epoch));
15835 Ok(())
15836}
15837
15838fn reconcile_recovered_table_metadata(
15839 tables: &HashMap<u64, TableHandle>,
15840 epoch: Epoch,
15841) -> Result<()> {
15842 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
15843 table_ids.sort_unstable();
15844 let mut plans = Vec::with_capacity(table_ids.len());
15845 for table_id in &table_ids {
15846 let handle = tables.get(table_id).ok_or_else(|| {
15847 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
15848 })?;
15849 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
15850 }
15851 for (table_id, plan) in plans {
15854 let handle = tables.get(&table_id).ok_or_else(|| {
15855 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
15856 })?;
15857 handle.lock().apply_recovered_metadata(plan, epoch)?;
15858 }
15859 Ok(())
15860}
15861
15862fn validate_recovered_external_name(name: &str) -> Result<()> {
15863 if name.is_empty()
15864 || !name.chars().all(|character| {
15865 character.is_ascii_alphanumeric() || character == '_' || character == '-'
15866 })
15867 {
15868 return Err(MongrelError::CorruptWal {
15869 offset: 0,
15870 reason: format!("unsafe recovered external-table name {name:?}"),
15871 });
15872 }
15873 Ok(())
15874}
15875
15876fn validate_recovery_table_stages(
15877 tables: &HashMap<u64, TableHandle>,
15878 stages: &HashMap<u64, RecoveryTableStage>,
15879) -> Result<()> {
15880 for (table_id, (rows, _, _, _)) in stages {
15881 let handle = tables
15882 .get(table_id)
15883 .ok_or_else(|| MongrelError::CorruptWal {
15884 offset: *table_id,
15885 reason: format!("recovery stage references unmounted table {table_id}"),
15886 })?;
15887 let table = handle.lock();
15888 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
15891 for row in rows {
15892 validate_recovered_row(table.schema(), row)?;
15893 }
15894 }
15895 Ok(())
15896}
15897
15898fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
15899 if row.deleted || row.row_id.0 == u64::MAX {
15900 return Err(MongrelError::CorruptWal {
15901 offset: row.row_id.0,
15902 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
15903 });
15904 }
15905 let cells = row
15906 .columns
15907 .iter()
15908 .map(|(column, value)| (*column, value.clone()))
15909 .collect::<Vec<_>>();
15910 schema
15911 .validate_persisted_values(&cells)
15912 .map_err(|error| MongrelError::CorruptWal {
15913 offset: row.row_id.0,
15914 reason: format!("recovered row violates table schema: {error}"),
15915 })?;
15916 if schema.auto_increment_column().is_some_and(|column| {
15917 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
15918 }) {
15919 return Err(MongrelError::CorruptWal {
15920 offset: row.row_id.0,
15921 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
15922 });
15923 }
15924 Ok(())
15925}
15926
15927fn validate_recovery_spilled_runs(
15928 root: &crate::durable_file::DurableRoot,
15929 tables: &HashMap<u64, TableHandle>,
15930 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
15931) -> Result<()> {
15932 let mut identities = HashSet::new();
15933 for (txn_id, commit_epoch, added_runs) in spilled {
15934 for added in added_runs {
15935 if added.run_id >= u64::MAX as u128 {
15936 return Err(MongrelError::CorruptWal {
15937 offset: *commit_epoch,
15938 reason: format!(
15939 "recovered run id {} exceeds the on-disk namespace",
15940 added.run_id
15941 ),
15942 });
15943 }
15944 let Some(handle) = tables.get(&added.table_id) else {
15945 continue;
15946 };
15947 if !identities.insert((added.table_id, added.run_id)) {
15948 return Err(MongrelError::CorruptWal {
15949 offset: *commit_epoch,
15950 reason: format!(
15951 "duplicate recovered run {} for table {}",
15952 added.run_id, added.table_id
15953 ),
15954 });
15955 }
15956 let table = handle.lock();
15957 validate_planned_spilled_run(
15958 root,
15959 *txn_id,
15960 *commit_epoch,
15961 added,
15962 table.schema(),
15963 table.kek(),
15964 )?;
15965 }
15966 }
15967 Ok(())
15968}
15969
15970fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
15971 match condition {
15972 ProcedureCondition::Pk { .. } => {
15973 if schema.primary_key().is_none() {
15974 return Err(MongrelError::InvalidArgument(
15975 "procedure condition Pk references a table without a primary key".into(),
15976 ));
15977 }
15978 }
15979 ProcedureCondition::BitmapEq { column_id, .. }
15980 | ProcedureCondition::BitmapIn { column_id, .. }
15981 | ProcedureCondition::Range { column_id, .. }
15982 | ProcedureCondition::RangeF64 { column_id, .. }
15983 | ProcedureCondition::IsNull { column_id }
15984 | ProcedureCondition::IsNotNull { column_id }
15985 | ProcedureCondition::FmContains { column_id, .. } => {
15986 validate_column_id(*column_id, schema)?;
15987 }
15988 }
15989 Ok(())
15990}
15991
15992fn bind_procedure_args(
15993 procedure: &StoredProcedure,
15994 mut args: HashMap<String, crate::Value>,
15995) -> Result<HashMap<String, crate::Value>> {
15996 let mut out = HashMap::new();
15997 for param in &procedure.params {
15998 let value = match args.remove(¶m.name) {
15999 Some(value) => value,
16000 None => param.default.clone().ok_or_else(|| {
16001 MongrelError::InvalidArgument(format!(
16002 "missing required procedure parameter {:?}",
16003 param.name
16004 ))
16005 })?,
16006 };
16007 if !param.nullable && matches!(value, crate::Value::Null) {
16008 return Err(MongrelError::InvalidArgument(format!(
16009 "procedure parameter {:?} must not be NULL",
16010 param.name
16011 )));
16012 }
16013 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
16014 return Err(MongrelError::InvalidArgument(format!(
16015 "procedure parameter {:?} has wrong type",
16016 param.name
16017 )));
16018 }
16019 out.insert(param.name.clone(), value);
16020 }
16021 if let Some(extra) = args.keys().next() {
16022 return Err(MongrelError::InvalidArgument(format!(
16023 "unknown procedure parameter {extra:?}"
16024 )));
16025 }
16026 Ok(out)
16027}
16028
16029fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
16030 matches!(
16031 (value, ty),
16032 (crate::Value::Bool(_), crate::TypeId::Bool)
16033 | (crate::Value::Int64(_), crate::TypeId::Int8)
16034 | (crate::Value::Int64(_), crate::TypeId::Int16)
16035 | (crate::Value::Int64(_), crate::TypeId::Int32)
16036 | (crate::Value::Int64(_), crate::TypeId::Int64)
16037 | (crate::Value::Int64(_), crate::TypeId::UInt8)
16038 | (crate::Value::Int64(_), crate::TypeId::UInt16)
16039 | (crate::Value::Int64(_), crate::TypeId::UInt32)
16040 | (crate::Value::Int64(_), crate::TypeId::UInt64)
16041 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
16042 | (crate::Value::Int64(_), crate::TypeId::Date32)
16043 | (crate::Value::Float64(_), crate::TypeId::Float32)
16044 | (crate::Value::Float64(_), crate::TypeId::Float64)
16045 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
16046 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
16047 )
16048}
16049
16050fn eval_cells(
16051 cells: &[crate::procedure::ProcedureCell],
16052 args: &HashMap<String, crate::Value>,
16053 outputs: &HashMap<String, ProcedureCallOutput>,
16054) -> Result<Vec<(u16, crate::Value)>> {
16055 cells
16056 .iter()
16057 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
16058 .collect()
16059}
16060
16061fn eval_condition(
16062 condition: &ProcedureCondition,
16063 args: &HashMap<String, crate::Value>,
16064 outputs: &HashMap<String, ProcedureCallOutput>,
16065) -> Result<crate::Condition> {
16066 Ok(match condition {
16067 ProcedureCondition::Pk { value } => {
16068 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
16069 }
16070 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
16071 column_id: *column_id,
16072 value: eval_value(value, args, outputs)?.encode_key(),
16073 },
16074 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
16075 column_id: *column_id,
16076 values: values
16077 .iter()
16078 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
16079 .collect::<Result<Vec<_>>>()?,
16080 },
16081 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
16082 column_id: *column_id,
16083 lo: expect_i64(eval_value(lo, args, outputs)?)?,
16084 hi: expect_i64(eval_value(hi, args, outputs)?)?,
16085 },
16086 ProcedureCondition::RangeF64 {
16087 column_id,
16088 lo,
16089 lo_inclusive,
16090 hi,
16091 hi_inclusive,
16092 } => crate::Condition::RangeF64 {
16093 column_id: *column_id,
16094 lo: expect_f64(eval_value(lo, args, outputs)?)?,
16095 lo_inclusive: *lo_inclusive,
16096 hi: expect_f64(eval_value(hi, args, outputs)?)?,
16097 hi_inclusive: *hi_inclusive,
16098 },
16099 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
16100 column_id: *column_id,
16101 },
16102 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
16103 column_id: *column_id,
16104 },
16105 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
16106 column_id: *column_id,
16107 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
16108 },
16109 })
16110}
16111
16112fn eval_value(
16113 value: &ProcedureValue,
16114 args: &HashMap<String, crate::Value>,
16115 outputs: &HashMap<String, ProcedureCallOutput>,
16116) -> Result<crate::Value> {
16117 match value {
16118 ProcedureValue::Literal(value) => Ok(value.clone()),
16119 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
16120 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
16121 }),
16122 ProcedureValue::StepScalar(id) => match outputs.get(id) {
16123 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
16124 _ => Err(MongrelError::InvalidArgument(format!(
16125 "procedure step {id:?} did not return a scalar"
16126 ))),
16127 },
16128 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
16129 Err(MongrelError::InvalidArgument(
16130 "row-valued procedure reference cannot be used as a scalar".into(),
16131 ))
16132 }
16133 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
16134 "structured procedure value cannot be used as a scalar cell".into(),
16135 )),
16136 }
16137}
16138
16139fn eval_return_output(
16140 value: &ProcedureValue,
16141 args: &HashMap<String, crate::Value>,
16142 outputs: &HashMap<String, ProcedureCallOutput>,
16143) -> Result<ProcedureCallOutput> {
16144 match value {
16145 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
16146 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
16147 args.get(name).cloned().ok_or_else(|| {
16148 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
16149 })?,
16150 )),
16151 ProcedureValue::StepRows(id)
16152 | ProcedureValue::StepRow(id)
16153 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
16154 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
16155 }),
16156 ProcedureValue::Object(fields) => {
16157 let mut out = Vec::with_capacity(fields.len());
16158 for (name, value) in fields {
16159 out.push((name.clone(), eval_return_output(value, args, outputs)?));
16160 }
16161 Ok(ProcedureCallOutput::Object(out))
16162 }
16163 ProcedureValue::Array(values) => {
16164 let mut out = Vec::with_capacity(values.len());
16165 for value in values {
16166 out.push(eval_return_output(value, args, outputs)?);
16167 }
16168 Ok(ProcedureCallOutput::Array(out))
16169 }
16170 }
16171}
16172
16173fn expect_i64(value: crate::Value) -> Result<i64> {
16174 match value {
16175 crate::Value::Int64(value) => Ok(value),
16176 _ => Err(MongrelError::InvalidArgument(
16177 "procedure value must be Int64".into(),
16178 )),
16179 }
16180}
16181
16182fn expect_f64(value: crate::Value) -> Result<f64> {
16183 match value {
16184 crate::Value::Float64(value) => Ok(value),
16185 _ => Err(MongrelError::InvalidArgument(
16186 "procedure value must be Float64".into(),
16187 )),
16188 }
16189}
16190
16191fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
16192 match value {
16193 crate::Value::Bytes(value) => Ok(value),
16194 _ => Err(MongrelError::InvalidArgument(
16195 "procedure value must be Bytes".into(),
16196 )),
16197 }
16198}
16199
16200fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
16201 if schema.columns.iter().any(|c| c.id == column_id) {
16202 Ok(())
16203 } else {
16204 Err(MongrelError::InvalidArgument(format!(
16205 "unknown column id {column_id}"
16206 )))
16207 }
16208}
16209
16210fn trigger_matches_event(
16211 trigger: &StoredTrigger,
16212 event: &WriteEvent,
16213 cat: &Catalog,
16214) -> Result<bool> {
16215 if trigger.event != event.kind {
16216 return Ok(false);
16217 }
16218 let TriggerTarget::Table(target) = &trigger.target else {
16219 return Ok(false);
16220 };
16221 if target != &event.table {
16222 return Ok(false);
16223 }
16224 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
16225 let schema = &cat
16226 .live(target)
16227 .ok_or_else(|| {
16228 MongrelError::InvalidArgument(format!(
16229 "trigger {:?} references unknown table {target:?}",
16230 trigger.name
16231 ))
16232 })?
16233 .schema;
16234 let mut watched = Vec::with_capacity(trigger.update_of.len());
16235 for name in &trigger.update_of {
16236 let col = schema.column(name).ok_or_else(|| {
16237 MongrelError::InvalidArgument(format!(
16238 "trigger {:?} references unknown UPDATE OF column {name:?}",
16239 trigger.name
16240 ))
16241 })?;
16242 watched.push(col.id);
16243 }
16244 if !event
16245 .changed_columns
16246 .iter()
16247 .any(|column_id| watched.contains(column_id))
16248 {
16249 return Ok(false);
16250 }
16251 }
16252 Ok(true)
16253}
16254
16255fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
16256 let mut ids = std::collections::BTreeSet::new();
16257 if let Some(old) = old {
16258 ids.extend(old.columns.keys().copied());
16259 }
16260 if let Some(new) = new {
16261 ids.extend(new.columns.keys().copied());
16262 }
16263 ids.into_iter()
16264 .filter(|id| {
16265 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
16266 })
16267 .collect()
16268}
16269
16270fn eval_trigger_cells(
16271 cells: &[crate::trigger::TriggerCell],
16272 event: &WriteEvent,
16273 selected: Option<&TriggerRowImage>,
16274) -> Result<Vec<(u16, Value)>> {
16275 cells
16276 .iter()
16277 .map(|cell| {
16278 Ok((
16279 cell.column_id,
16280 eval_trigger_value(&cell.value, event, selected)?,
16281 ))
16282 })
16283 .collect()
16284}
16285
16286fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
16287 match expr {
16288 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
16289 Value::Bool(value) => Ok(value),
16290 Value::Null => Ok(false),
16291 other => Err(MongrelError::InvalidArgument(format!(
16292 "trigger WHEN value must be boolean, got {other:?}"
16293 ))),
16294 },
16295 TriggerExpr::Eq { left, right } => Ok(values_equal(
16296 &eval_trigger_value(left, event, None)?,
16297 &eval_trigger_value(right, event, None)?,
16298 )),
16299 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
16300 &eval_trigger_value(left, event, None)?,
16301 &eval_trigger_value(right, event, None)?,
16302 )),
16303 TriggerExpr::Lt { left, right } => match value_order(
16304 &eval_trigger_value(left, event, None)?,
16305 &eval_trigger_value(right, event, None)?,
16306 ) {
16307 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
16308 None => Ok(false),
16309 },
16310 TriggerExpr::Lte { left, right } => match value_order(
16311 &eval_trigger_value(left, event, None)?,
16312 &eval_trigger_value(right, event, None)?,
16313 ) {
16314 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
16315 None => Ok(false),
16316 },
16317 TriggerExpr::Gt { left, right } => match value_order(
16318 &eval_trigger_value(left, event, None)?,
16319 &eval_trigger_value(right, event, None)?,
16320 ) {
16321 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
16322 None => Ok(false),
16323 },
16324 TriggerExpr::Gte { left, right } => match value_order(
16325 &eval_trigger_value(left, event, None)?,
16326 &eval_trigger_value(right, event, None)?,
16327 ) {
16328 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
16329 None => Ok(false),
16330 },
16331 TriggerExpr::IsNull(value) => Ok(matches!(
16332 eval_trigger_value(value, event, None)?,
16333 Value::Null
16334 )),
16335 TriggerExpr::IsNotNull(value) => Ok(!matches!(
16336 eval_trigger_value(value, event, None)?,
16337 Value::Null
16338 )),
16339 TriggerExpr::And { left, right } => {
16340 if !eval_trigger_expr(left, event)? {
16341 Ok(false)
16342 } else {
16343 Ok(eval_trigger_expr(right, event)?)
16344 }
16345 }
16346 TriggerExpr::Or { left, right } => {
16347 if eval_trigger_expr(left, event)? {
16348 Ok(true)
16349 } else {
16350 Ok(eval_trigger_expr(right, event)?)
16351 }
16352 }
16353 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
16354 }
16355}
16356
16357fn eval_trigger_condition(
16358 condition: &TriggerCondition,
16359 event: &WriteEvent,
16360 selected: &TriggerRowImage,
16361 schema: &Schema,
16362) -> Result<bool> {
16363 match condition {
16364 TriggerCondition::Pk { value } => {
16365 let pk = schema.primary_key().ok_or_else(|| {
16366 MongrelError::InvalidArgument(
16367 "trigger condition Pk references a table without a primary key".into(),
16368 )
16369 })?;
16370 let lhs = eval_trigger_value(value, event, Some(selected))?;
16371 Ok(values_equal(
16372 &lhs,
16373 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
16374 ))
16375 }
16376 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
16377 selected.columns.get(column_id).unwrap_or(&Value::Null),
16378 &eval_trigger_value(value, event, Some(selected))?,
16379 )),
16380 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
16381 selected.columns.get(column_id).unwrap_or(&Value::Null),
16382 &eval_trigger_value(value, event, Some(selected))?,
16383 )),
16384 TriggerCondition::Lt { column_id, value } => match value_order(
16385 selected.columns.get(column_id).unwrap_or(&Value::Null),
16386 &eval_trigger_value(value, event, Some(selected))?,
16387 ) {
16388 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
16389 None => Ok(false),
16390 },
16391 TriggerCondition::Lte { column_id, value } => match value_order(
16392 selected.columns.get(column_id).unwrap_or(&Value::Null),
16393 &eval_trigger_value(value, event, Some(selected))?,
16394 ) {
16395 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
16396 None => Ok(false),
16397 },
16398 TriggerCondition::Gt { column_id, value } => match value_order(
16399 selected.columns.get(column_id).unwrap_or(&Value::Null),
16400 &eval_trigger_value(value, event, Some(selected))?,
16401 ) {
16402 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
16403 None => Ok(false),
16404 },
16405 TriggerCondition::Gte { column_id, value } => match value_order(
16406 selected.columns.get(column_id).unwrap_or(&Value::Null),
16407 &eval_trigger_value(value, event, Some(selected))?,
16408 ) {
16409 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
16410 None => Ok(false),
16411 },
16412 TriggerCondition::IsNull { column_id } => Ok(matches!(
16413 selected.columns.get(column_id),
16414 None | Some(Value::Null)
16415 )),
16416 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
16417 selected.columns.get(column_id),
16418 None | Some(Value::Null)
16419 )),
16420 TriggerCondition::And { left, right } => {
16421 if !eval_trigger_condition(left, event, selected, schema)? {
16422 Ok(false)
16423 } else {
16424 Ok(eval_trigger_condition(right, event, selected, schema)?)
16425 }
16426 }
16427 TriggerCondition::Or { left, right } => {
16428 if eval_trigger_condition(left, event, selected, schema)? {
16429 Ok(true)
16430 } else {
16431 Ok(eval_trigger_condition(right, event, selected, schema)?)
16432 }
16433 }
16434 TriggerCondition::Not(condition) => {
16435 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
16436 }
16437 }
16438}
16439
16440fn eval_trigger_value(
16441 value: &TriggerValue,
16442 event: &WriteEvent,
16443 selected: Option<&TriggerRowImage>,
16444) -> Result<Value> {
16445 match value {
16446 TriggerValue::Literal(value) => Ok(value.clone()),
16447 TriggerValue::NewColumn(column_id) => event
16448 .new
16449 .as_ref()
16450 .and_then(|row| row.columns.get(column_id))
16451 .cloned()
16452 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
16453 TriggerValue::OldColumn(column_id) => event
16454 .old
16455 .as_ref()
16456 .and_then(|row| row.columns.get(column_id))
16457 .cloned()
16458 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
16459 TriggerValue::SelectedColumn(column_id) => selected
16460 .and_then(|row| row.columns.get(column_id))
16461 .cloned()
16462 .ok_or_else(|| {
16463 MongrelError::InvalidArgument("SELECTED column is not available".into())
16464 }),
16465 }
16466}
16467
16468fn values_equal(left: &Value, right: &Value) -> bool {
16469 match (left, right) {
16470 (Value::Null, Value::Null) => true,
16471 (Value::Bool(a), Value::Bool(b)) => a == b,
16472 (Value::Int64(a), Value::Int64(b)) => a == b,
16473 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
16474 (Value::Bytes(a), Value::Bytes(b)) => a == b,
16475 (Value::Embedding(a), Value::Embedding(b)) => {
16476 a.len() == b.len()
16477 && a.iter()
16478 .zip(b.iter())
16479 .all(|(a, b)| a.to_bits() == b.to_bits())
16480 }
16481 _ => false,
16482 }
16483}
16484
16485fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
16486 match (left, right) {
16487 (Value::Null, _) | (_, Value::Null) => None,
16488 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
16489 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
16490 (Value::Int64(a), Value::Float64(b)) => {
16493 let af = *a as f64;
16494 Some(af.total_cmp(b))
16495 }
16496 (Value::Float64(a), Value::Int64(b)) => {
16499 let bf = *b as f64;
16500 Some(a.total_cmp(&bf))
16501 }
16502 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
16503 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
16504 (Value::Embedding(_), Value::Embedding(_)) => None,
16505 _ => None,
16506 }
16507}
16508
16509fn trigger_message(value: Value) -> String {
16510 match value {
16511 Value::Null => "NULL".into(),
16512 Value::Bool(value) => value.to_string(),
16513 Value::Int64(value) => value.to_string(),
16514 Value::Float64(value) => value.to_string(),
16515 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
16516 Value::Embedding(value) => format!("{value:?}"),
16517 Value::Decimal(value) => value.to_string(),
16518 Value::Interval {
16519 months,
16520 days,
16521 nanos,
16522 } => format!("{months}m {days}d {nanos}ns"),
16523 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
16524 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
16525 }
16526}
16527
16528fn validate_trigger_step<'a>(
16529 step: &TriggerStep,
16530 cat: &'a Catalog,
16531 target_schema: &Schema,
16532 event: TriggerEvent,
16533 select_schemas: &mut HashMap<String, &'a Schema>,
16534) -> Result<()> {
16535 match step {
16536 TriggerStep::SetNew { cells } => {
16537 if event == TriggerEvent::Delete {
16538 return Err(MongrelError::InvalidArgument(
16539 "SetNew trigger step is not valid for DELETE triggers".into(),
16540 ));
16541 }
16542 for cell in cells {
16543 validate_column_id(cell.column_id, target_schema)?;
16544 validate_trigger_value(&cell.value, target_schema, event)?;
16545 }
16546 }
16547 TriggerStep::Insert { table, cells } => {
16548 let schema = trigger_write_schema(cat, table, "insert")?;
16549 for cell in cells {
16550 validate_column_id(cell.column_id, schema)?;
16551 validate_trigger_value(&cell.value, target_schema, event)?;
16552 }
16553 }
16554 TriggerStep::UpdateByPk { table, pk, cells } => {
16555 let schema = trigger_write_schema(cat, table, "update")?;
16556 if schema.primary_key().is_none() {
16557 return Err(MongrelError::InvalidArgument(format!(
16558 "trigger update_by_pk references table {table:?} without a primary key"
16559 )));
16560 }
16561 validate_trigger_value(pk, target_schema, event)?;
16562 for cell in cells {
16563 validate_column_id(cell.column_id, schema)?;
16564 validate_trigger_value(&cell.value, target_schema, event)?;
16565 }
16566 }
16567 TriggerStep::DeleteByPk { table, pk } => {
16568 let schema = trigger_write_schema(cat, table, "delete")?;
16569 if schema.primary_key().is_none() {
16570 return Err(MongrelError::InvalidArgument(format!(
16571 "trigger delete_by_pk references table {table:?} without a primary key"
16572 )));
16573 }
16574 validate_trigger_value(pk, target_schema, event)?;
16575 }
16576 TriggerStep::Select {
16577 id,
16578 table,
16579 conditions,
16580 } => {
16581 let schema = trigger_read_schema(cat, table)?;
16582 for condition in conditions {
16583 validate_trigger_condition(condition, schema, target_schema, event)?;
16584 }
16585 if select_schemas.contains_key(id) {
16586 return Err(MongrelError::InvalidArgument(format!(
16587 "duplicate select id {id:?} in trigger program"
16588 )));
16589 }
16590 select_schemas.insert(id.clone(), schema);
16591 }
16592 TriggerStep::Foreach { id, steps } => {
16593 if !select_schemas.contains_key(id) {
16594 return Err(MongrelError::InvalidArgument(format!(
16595 "foreach references unknown select id {id:?}"
16596 )));
16597 }
16598 let mut inner_select_schemas = select_schemas.clone();
16599 for step in steps {
16600 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
16601 }
16602 }
16603 TriggerStep::DeleteWhere { table, conditions } => {
16604 let schema = trigger_write_schema(cat, table, "delete")?;
16605 for condition in conditions {
16606 validate_trigger_condition(condition, schema, target_schema, event)?;
16607 }
16608 }
16609 TriggerStep::UpdateWhere {
16610 table,
16611 conditions,
16612 cells,
16613 } => {
16614 let schema = trigger_write_schema(cat, table, "update")?;
16615 for condition in conditions {
16616 validate_trigger_condition(condition, schema, target_schema, event)?;
16617 }
16618 for cell in cells {
16619 validate_column_id(cell.column_id, schema)?;
16620 validate_trigger_value(&cell.value, target_schema, event)?;
16621 }
16622 }
16623 TriggerStep::Raise { message, .. } => {
16624 validate_trigger_value(message, target_schema, event)?
16625 }
16626 }
16627 Ok(())
16628}
16629
16630fn trigger_validation_error(error: MongrelError) -> MongrelError {
16631 match error {
16632 MongrelError::TriggerValidation(_) => error,
16633 MongrelError::InvalidArgument(message)
16634 | MongrelError::Conflict(message)
16635 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
16636 error => error,
16637 }
16638}
16639
16640fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
16641 if let Some(entry) = cat.live(table) {
16642 return Ok(&entry.schema);
16643 }
16644 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
16645 let allowed = match op {
16646 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
16647 "update" | "delete" => entry.capabilities.writable,
16648 _ => false,
16649 };
16650 if !allowed {
16651 return Err(MongrelError::InvalidArgument(format!(
16652 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
16653 entry.module
16654 )));
16655 }
16656 if !entry.capabilities.transaction_safe {
16657 return Err(MongrelError::InvalidArgument(format!(
16658 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
16659 entry.module
16660 )));
16661 }
16662 return Ok(&entry.declared_schema);
16663 }
16664 Err(MongrelError::InvalidArgument(format!(
16665 "trigger references unknown table {table:?}"
16666 )))
16667}
16668
16669fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
16670 if let Some(entry) = cat.live(table) {
16671 return Ok(&entry.schema);
16672 }
16673 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
16674 if entry.capabilities.trigger_safe {
16675 return Ok(&entry.declared_schema);
16676 }
16677 return Err(MongrelError::InvalidArgument(format!(
16678 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
16679 entry.module
16680 )));
16681 }
16682 Err(MongrelError::InvalidArgument(format!(
16683 "trigger references unknown table {table:?}"
16684 )))
16685}
16686
16687fn validate_trigger_condition(
16688 condition: &TriggerCondition,
16689 schema: &Schema,
16690 target_schema: &Schema,
16691 event: TriggerEvent,
16692) -> Result<()> {
16693 match condition {
16694 TriggerCondition::Pk { value } => {
16695 if schema.primary_key().is_none() {
16696 return Err(MongrelError::InvalidArgument(
16697 "trigger condition Pk references a table without a primary key".into(),
16698 ));
16699 }
16700 validate_trigger_value(value, target_schema, event)
16701 }
16702 TriggerCondition::Eq { column_id, value }
16703 | TriggerCondition::NotEq { column_id, value }
16704 | TriggerCondition::Lt { column_id, value }
16705 | TriggerCondition::Lte { column_id, value }
16706 | TriggerCondition::Gt { column_id, value }
16707 | TriggerCondition::Gte { column_id, value } => {
16708 validate_column_id(*column_id, schema)?;
16709 validate_trigger_value(value, target_schema, event)
16710 }
16711 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
16712 validate_column_id(*column_id, schema)
16713 }
16714 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
16715 validate_trigger_condition(left, schema, target_schema, event)?;
16716 validate_trigger_condition(right, schema, target_schema, event)
16717 }
16718 TriggerCondition::Not(condition) => {
16719 validate_trigger_condition(condition, schema, target_schema, event)
16720 }
16721 }
16722}
16723
16724fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
16725 match expr {
16726 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
16727 validate_trigger_value(value, schema, event)
16728 }
16729 TriggerExpr::Eq { left, right }
16730 | TriggerExpr::NotEq { left, right }
16731 | TriggerExpr::Lt { left, right }
16732 | TriggerExpr::Lte { left, right }
16733 | TriggerExpr::Gt { left, right }
16734 | TriggerExpr::Gte { left, right } => {
16735 validate_trigger_value(left, schema, event)?;
16736 validate_trigger_value(right, schema, event)
16737 }
16738 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
16739 validate_trigger_expr(left, schema, event)?;
16740 validate_trigger_expr(right, schema, event)
16741 }
16742 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
16743 }
16744}
16745
16746fn validate_trigger_value(
16747 value: &TriggerValue,
16748 schema: &Schema,
16749 event: TriggerEvent,
16750) -> Result<()> {
16751 match value {
16752 TriggerValue::Literal(_) => Ok(()),
16753 TriggerValue::NewColumn(id) => {
16754 if event == TriggerEvent::Delete {
16755 return Err(MongrelError::InvalidArgument(
16756 "DELETE triggers cannot reference NEW".into(),
16757 ));
16758 }
16759 validate_column_id(*id, schema)
16760 }
16761 TriggerValue::OldColumn(id) => {
16762 if event == TriggerEvent::Insert {
16763 return Err(MongrelError::InvalidArgument(
16764 "INSERT triggers cannot reference OLD".into(),
16765 ));
16766 }
16767 validate_column_id(*id, schema)
16768 }
16769 TriggerValue::SelectedColumn(_) => Ok(()),
16773 }
16774}
16775
16776const COMMIT_TS_LEDGER_CAP: usize = 10_000;
16781
16782fn commit_ts_ledger_from_recovery(
16790 records: &[crate::wal::Record],
16791) -> std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp> {
16792 use crate::wal::Op;
16793 let mut commits = HashMap::new();
16794 let mut timestamps = HashMap::new();
16795 for record in records {
16796 match &record.op {
16797 Op::TxnCommit { epoch, .. } => {
16798 commits.insert(record.txn_id, *epoch);
16799 }
16800 Op::CommitTimestamp { unix_nanos } => {
16801 timestamps.insert(record.txn_id, *unix_nanos);
16802 }
16803 _ => {}
16804 }
16805 }
16806 let mut ledger = std::collections::BTreeMap::new();
16807 for (txn_id, epoch) in commits {
16808 let Some(unix_nanos) = timestamps.get(&txn_id) else {
16809 continue;
16810 };
16811 ledger.insert(
16812 epoch,
16813 mongreldb_types::hlc::HlcTimestamp {
16814 physical_micros: unix_nanos / 1_000,
16815 logical: 0,
16816 node_tiebreaker: 0,
16817 },
16818 );
16819 }
16820 while ledger.len() > COMMIT_TS_LEDGER_CAP {
16821 ledger.pop_first();
16822 }
16823 ledger
16824}
16825
16826fn recover_ddl_from_wal(
16832 root: &Path,
16833 durable_root: Option<&crate::durable_file::DurableRoot>,
16834 target_catalog: &mut Catalog,
16835 meta_dek: Option<&[u8; META_DEK_LEN]>,
16836 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
16837 apply: bool,
16838 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
16839) -> Result<()> {
16840 use crate::wal::SharedWal;
16841 let records = match durable_root {
16842 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
16843 None => SharedWal::replay_with_dek(root, wal_dek)?,
16844 };
16845 recover_ddl_from_records(
16846 root,
16847 durable_root,
16848 target_catalog,
16849 meta_dek,
16850 apply,
16851 table_roots,
16852 &records,
16853 )
16854}
16855
16856fn recover_ddl_from_records(
16857 root: &Path,
16858 durable_root: Option<&crate::durable_file::DurableRoot>,
16859 target_catalog: &mut Catalog,
16860 meta_dek: Option<&[u8; META_DEK_LEN]>,
16861 apply: bool,
16862 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
16863 records: &[crate::wal::Record],
16864) -> Result<()> {
16865 use crate::wal::{DdlOp, Op};
16866
16867 let original_catalog = target_catalog.clone();
16868 let mut recovered_catalog = original_catalog.clone();
16869 let cat = &mut recovered_catalog;
16870 let mut created_table_ids = HashSet::<u64>::new();
16871 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
16872
16873 let mut committed: HashMap<u64, u64> = HashMap::new();
16874 for r in records {
16875 if let Op::TxnCommit { epoch: ce, .. } = r.op {
16876 committed.insert(r.txn_id, ce);
16877 }
16878 }
16879 let catalog_snapshot_txns = records
16880 .iter()
16881 .filter_map(|record| {
16882 (committed.contains_key(&record.txn_id)
16883 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
16884 .then_some(record.txn_id)
16885 })
16886 .collect::<HashSet<_>>();
16887
16888 let mut changed = false;
16889 let mut applied_catalog_epoch = cat.db_epoch;
16890 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
16891 for r in records.iter().cloned() {
16892 let Some(&ce) = committed.get(&r.txn_id) else {
16893 continue;
16894 };
16895 let txn_id = r.txn_id;
16896 match r.op {
16897 Op::Ddl(DdlOp::CreateTable {
16898 table_id,
16899 ref name,
16900 ref schema_json,
16901 }) => {
16902 if cat.tables.iter().any(|t| t.table_id == table_id) {
16903 continue;
16904 }
16905 let schema = DdlOp::decode_schema(schema_json)?;
16906 validate_recovered_schema(&schema)?;
16907 created_table_ids.insert(table_id);
16908 cat.tables.push(CatalogEntry {
16909 table_id,
16910 name: name.clone(),
16911 schema,
16912 state: TableState::Live,
16913 created_epoch: ce,
16914 });
16915 cat.next_table_id =
16916 cat.next_table_id
16917 .max(table_id.checked_add(1).ok_or_else(|| {
16918 MongrelError::Full("table id namespace exhausted".into())
16919 })?);
16920 changed = true;
16921 }
16922 Op::Ddl(DdlOp::CreateBuildingTable {
16923 table_id,
16924 ref build_name,
16925 ref intended_name,
16926 ref query_id,
16927 created_at_unix_nanos,
16928 ref schema_json,
16929 }) => {
16930 if cat.tables.iter().any(|table| table.table_id == table_id) {
16931 continue;
16932 }
16933 let schema = DdlOp::decode_schema(schema_json)?;
16934 validate_recovered_schema(&schema)?;
16935 created_table_ids.insert(table_id);
16936 cat.tables.push(CatalogEntry {
16937 table_id,
16938 name: build_name.clone(),
16939 schema,
16940 state: TableState::Building {
16941 intended_name: intended_name.clone(),
16942 query_id: query_id.clone(),
16943 created_at_unix_nanos,
16944 replaces_table_id: None,
16945 },
16946 created_epoch: ce,
16947 });
16948 cat.next_table_id =
16949 cat.next_table_id
16950 .max(table_id.checked_add(1).ok_or_else(|| {
16951 MongrelError::Full("table id namespace exhausted".into())
16952 })?);
16953 changed = true;
16954 }
16955 Op::Ddl(DdlOp::CreateRebuildingTable {
16956 table_id,
16957 ref build_name,
16958 ref intended_name,
16959 ref query_id,
16960 created_at_unix_nanos,
16961 replaces_table_id,
16962 ref schema_json,
16963 }) => {
16964 if cat.tables.iter().any(|table| table.table_id == table_id) {
16965 continue;
16966 }
16967 let schema = DdlOp::decode_schema(schema_json)?;
16968 validate_recovered_schema(&schema)?;
16969 created_table_ids.insert(table_id);
16970 cat.tables.push(CatalogEntry {
16971 table_id,
16972 name: build_name.clone(),
16973 schema,
16974 state: TableState::Building {
16975 intended_name: intended_name.clone(),
16976 query_id: query_id.clone(),
16977 created_at_unix_nanos,
16978 replaces_table_id: Some(replaces_table_id),
16979 },
16980 created_epoch: ce,
16981 });
16982 cat.next_table_id =
16983 cat.next_table_id
16984 .max(table_id.checked_add(1).ok_or_else(|| {
16985 MongrelError::Full("table id namespace exhausted".into())
16986 })?);
16987 changed = true;
16988 }
16989 Op::Ddl(DdlOp::DropTable { table_id }) => {
16990 let mut dropped_name = None;
16991 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
16992 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
16993 dropped_name = Some(entry.name.clone());
16994 entry.state = TableState::Dropped { at_epoch: ce };
16995 changed = true;
16996 }
16997 }
16998 if let Some(name) = dropped_name {
16999 let before = cat.materialized_views.len();
17000 cat.materialized_views
17001 .retain(|definition| definition.name != name);
17002 changed |= before != cat.materialized_views.len();
17003 cat.security.rls_tables.retain(|table| table != &name);
17004 cat.security.policies.retain(|policy| policy.table != name);
17005 cat.security.masks.retain(|mask| mask.table != name);
17006 for role in &mut cat.roles {
17007 role.permissions
17008 .retain(|permission| permission_table(permission) != Some(&name));
17009 }
17010 if !catalog_snapshot_txns.contains(&txn_id) {
17011 advance_security_version(cat)?;
17012 }
17013 }
17014 }
17015 Op::Ddl(DdlOp::PublishBuildingTable {
17016 table_id,
17017 ref new_name,
17018 }) => {
17019 if let Some(entry) = cat
17020 .tables
17021 .iter_mut()
17022 .find(|table| table.table_id == table_id)
17023 {
17024 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
17025 entry.name = new_name.clone();
17026 entry.state = TableState::Live;
17027 changed = true;
17028 }
17029 }
17030 }
17031 Op::Ddl(DdlOp::ReplaceBuildingTable {
17032 table_id,
17033 replaced_table_id,
17034 ref new_name,
17035 }) => {
17036 changed |=
17037 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
17038 }
17039 Op::Ddl(DdlOp::RenameTable {
17040 table_id,
17041 ref new_name,
17042 }) => {
17043 let mut old_name = None;
17044 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
17045 if entry.name != *new_name {
17046 old_name = Some(entry.name.clone());
17047 entry.name = new_name.clone();
17048 changed = true;
17049 }
17050 }
17051 if let Some(old_name) = old_name {
17052 if let Some(definition) = cat
17053 .materialized_views
17054 .iter_mut()
17055 .find(|definition| definition.name == old_name)
17056 {
17057 definition.name = new_name.clone();
17058 }
17059 for table in &mut cat.security.rls_tables {
17060 if *table == old_name {
17061 *table = new_name.clone();
17062 }
17063 }
17064 for policy in &mut cat.security.policies {
17065 if policy.table == old_name {
17066 policy.table = new_name.clone();
17067 }
17068 }
17069 for mask in &mut cat.security.masks {
17070 if mask.table == old_name {
17071 mask.table = new_name.clone();
17072 }
17073 }
17074 for role in &mut cat.roles {
17075 for permission in &mut role.permissions {
17076 rename_permission_table(permission, &old_name, new_name);
17077 }
17078 }
17079 if !catalog_snapshot_txns.contains(&txn_id) {
17080 advance_security_version(cat)?;
17081 }
17082 }
17083 }
17087 Op::Ddl(DdlOp::AlterTable {
17088 table_id,
17089 ref column_json,
17090 }) => {
17091 let column = DdlOp::decode_column(column_json)?;
17092 let mut renamed = None;
17093 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
17094 renamed = entry
17095 .schema
17096 .columns
17097 .iter()
17098 .find(|existing| existing.id == column.id && existing.name != column.name)
17099 .map(|existing| {
17100 (
17101 entry.name.clone(),
17102 existing.name.clone(),
17103 column.name.clone(),
17104 )
17105 });
17106 if apply_recovered_column_def(&mut entry.schema, column)? {
17107 validate_recovered_schema(&entry.schema)?;
17108 changed = true;
17109 }
17110 }
17111 if let Some((table, old_name, new_name)) = renamed {
17112 for role in &mut cat.roles {
17113 for permission in &mut role.permissions {
17114 rename_permission_column(permission, &table, &old_name, &new_name);
17115 }
17116 }
17117 if !catalog_snapshot_txns.contains(&txn_id) {
17118 advance_security_version(cat)?;
17119 }
17120 }
17121 }
17122 Op::Ddl(DdlOp::SetTtl {
17123 table_id,
17124 ref policy_json,
17125 }) => {
17126 let policy = DdlOp::decode_ttl(policy_json)?;
17127 let entry = cat
17128 .tables
17129 .iter()
17130 .find(|entry| entry.table_id == table_id)
17131 .ok_or_else(|| {
17132 MongrelError::Schema(format!(
17133 "recovered TTL references unknown table id {table_id}"
17134 ))
17135 })?;
17136 if let Some(policy) = policy {
17137 let valid = entry
17138 .schema
17139 .columns
17140 .iter()
17141 .find(|column| column.id == policy.column_id)
17142 .is_some_and(|column| {
17143 column.ty == TypeId::TimestampNanos
17144 && policy.duration_nanos > 0
17145 && policy.duration_nanos <= i64::MAX as u64
17146 });
17147 if !valid {
17148 return Err(MongrelError::Schema(format!(
17149 "invalid recovered TTL policy for table id {table_id}"
17150 )));
17151 }
17152 }
17153 ttl_updates.insert(table_id, (policy, ce));
17154 }
17155 Op::Ddl(DdlOp::SetMaterializedView {
17156 ref name,
17157 ref definition_json,
17158 }) => {
17159 let definition = DdlOp::decode_materialized_view(definition_json)?;
17160 if definition.name != *name {
17161 return Err(MongrelError::Schema(format!(
17162 "materialized view WAL name mismatch: {name:?}"
17163 )));
17164 }
17165 if cat.live(name).is_some() {
17166 if let Some(existing) = cat
17167 .materialized_views
17168 .iter_mut()
17169 .find(|existing| existing.name == *name)
17170 {
17171 if *existing != definition {
17172 *existing = definition;
17173 changed = true;
17174 }
17175 } else {
17176 cat.materialized_views.push(definition);
17177 changed = true;
17178 }
17179 }
17180 }
17181 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
17182 let security = DdlOp::decode_security(security_json)?;
17183 validate_security_catalog(cat, &security)?;
17184 if cat.security != security {
17185 cat.security = security;
17186 if !catalog_snapshot_txns.contains(&txn_id) {
17187 advance_security_version(cat)?;
17188 }
17189 changed = true;
17190 }
17191 }
17192 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
17193 let target = match key.as_str() {
17194 "user_version" => &mut cat.user_version,
17195 "application_id" => &mut cat.application_id,
17196 _ => {
17197 return Err(MongrelError::InvalidArgument(format!(
17198 "unsupported recovered SQL pragma {key:?}"
17199 )))
17200 }
17201 };
17202 if *target != Some(value) {
17203 *target = Some(value);
17204 cat.db_epoch = cat.db_epoch.max(ce);
17205 changed = true;
17206 }
17207 }
17208 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
17209 if ce <= applied_catalog_epoch {
17210 continue;
17211 }
17212 let snapshot = DdlOp::decode_catalog(catalog_json)?;
17213 if snapshot.db_epoch != ce {
17214 return Err(MongrelError::Schema(format!(
17215 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
17216 snapshot.db_epoch
17217 )));
17218 }
17219 validate_recovered_catalog(&snapshot)?;
17220 validate_catalog_transition(cat, &snapshot)?;
17221 *cat = snapshot;
17222 applied_catalog_epoch = ce;
17223 changed = true;
17224 }
17225 _ => {}
17226 }
17227 }
17228
17229 if cat.db_epoch < max_committed_epoch {
17230 cat.db_epoch = max_committed_epoch;
17231 changed = true;
17232 }
17233 changed |= repair_catalog_allocator_counters(cat)?;
17234
17235 validate_recovered_catalog(cat)?;
17236 let storage_reconciliation = validate_recovered_storage_plan(
17237 root,
17238 durable_root,
17239 cat,
17240 &created_table_ids,
17241 &ttl_updates,
17242 meta_dek,
17243 )?;
17244
17245 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
17246 if apply && (changed || needs_storage_apply) {
17247 for table_id in storage_reconciliation {
17248 let entry = cat
17249 .tables
17250 .iter()
17251 .find(|entry| entry.table_id == table_id)
17252 .ok_or_else(|| MongrelError::CorruptWal {
17253 offset: table_id,
17254 reason: "recovery storage plan lost its catalog table".into(),
17255 })?;
17256 ensure_recovered_table_storage(
17257 table_roots
17258 .and_then(|roots| roots.get(&table_id))
17259 .map(Arc::as_ref),
17260 durable_root,
17261 &root.join(TABLES_DIR).join(table_id.to_string()),
17262 table_id,
17263 &entry.schema,
17264 meta_dek,
17265 )?;
17266 }
17267 for (table_id, (policy, ttl_epoch)) in ttl_updates {
17268 let Some(entry) = cat.tables.iter().find(|entry| {
17269 entry.table_id == table_id
17270 && matches!(entry.state, TableState::Live | TableState::Building { .. })
17271 }) else {
17272 continue;
17273 };
17274 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
17275 {
17276 root.try_clone()?
17277 } else if let Some(root) = durable_root {
17278 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
17279 } else {
17280 crate::durable_file::DurableRoot::open(
17281 root.join(TABLES_DIR).join(table_id.to_string()),
17282 )?
17283 };
17284 let table_dir = table_root.io_path()?;
17285 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
17286 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
17287 manifest.ttl = policy;
17288 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
17289 manifest.schema_id = entry.schema.schema_id;
17290 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
17291 }
17292 }
17293 if changed {
17294 match durable_root {
17295 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
17296 None => catalog::write_atomic(root, cat, meta_dek)?,
17297 }
17298 }
17299 }
17300 *target_catalog = recovered_catalog;
17301 Ok(())
17302}
17303
17304fn ensure_recovered_table_storage(
17305 pinned_table: Option<&crate::durable_file::DurableRoot>,
17306 durable_root: Option<&crate::durable_file::DurableRoot>,
17307 fallback_table_dir: &Path,
17308 table_id: u64,
17309 schema: &Schema,
17310 meta_dek: Option<&[u8; META_DEK_LEN]>,
17311) -> Result<()> {
17312 let table_root = if let Some(root) = pinned_table {
17313 root.try_clone()?
17314 } else if let Some(root) = durable_root {
17315 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
17316 match root.open_directory(&relative) {
17317 Ok(table) => table,
17318 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
17319 root.create_directory_all_pinned(relative)?
17320 }
17321 Err(error) => return Err(error.into()),
17322 }
17323 } else {
17324 crate::durable_file::create_directory_all(fallback_table_dir)?;
17325 crate::durable_file::DurableRoot::open(fallback_table_dir)?
17326 };
17327 let table_dir = table_root.io_path()?;
17328 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
17329 Ok(manifest) => {
17330 if manifest.table_id != table_id {
17331 return Err(MongrelError::Conflict(format!(
17332 "recovered table directory id mismatch: expected {table_id}, found {}",
17333 manifest.table_id
17334 )));
17335 }
17336 Some(manifest)
17337 }
17338 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
17339 Err(error) => return Err(error),
17340 };
17341
17342 table_root.create_directory_all(crate::engine::WAL_DIR)?;
17343 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
17344 crate::engine::write_schema(&table_dir, schema)?;
17345
17346 if let Some(mut manifest) = existing_manifest.take() {
17347 if manifest.schema_id != schema.schema_id {
17348 manifest.schema_id = schema.schema_id;
17349 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
17350 }
17351 } else {
17352 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
17354 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
17355 }
17356 Ok(())
17357}
17358
17359fn validate_recovered_schema(schema: &Schema) -> Result<()> {
17360 schema.validate_auto_increment()?;
17361 schema.validate_defaults()?;
17362 schema.validate_ai()?;
17363 let mut column_ids = HashSet::new();
17364 let mut column_names = HashSet::new();
17365 for column in &schema.columns {
17366 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
17367 return Err(MongrelError::Schema(
17368 "recovered schema contains duplicate columns".into(),
17369 ));
17370 }
17371 match &column.ty {
17372 TypeId::Decimal128 { precision, scale }
17373 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
17374 {
17375 return Err(MongrelError::Schema(format!(
17376 "column {:?} has invalid decimal precision or scale",
17377 column.name
17378 )));
17379 }
17380 TypeId::Enum { variants }
17381 if variants.is_empty()
17382 || variants.iter().any(String::is_empty)
17383 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
17384 {
17385 return Err(MongrelError::Schema(format!(
17386 "column {:?} has invalid enum variants",
17387 column.name
17388 )));
17389 }
17390 _ => {}
17391 }
17392 }
17393 let mut index_names = HashSet::new();
17394 for index in &schema.indexes {
17395 index.validate_options()?;
17396 if index.name.is_empty()
17397 || !index_names.insert(index.name.as_str())
17398 || schema
17399 .columns
17400 .iter()
17401 .all(|column| column.id != index.column_id)
17402 {
17403 return Err(MongrelError::Schema(format!(
17404 "recovered index {:?} references missing column {}",
17405 index.name, index.column_id
17406 )));
17407 }
17408 }
17409 let mut colocated = HashSet::new();
17410 for group in &schema.colocation {
17411 if group.is_empty()
17412 || group.iter().any(|id| !column_ids.contains(id))
17413 || group.iter().any(|id| !colocated.insert(*id))
17414 {
17415 return Err(MongrelError::Schema(
17416 "recovered schema contains invalid column co-location groups".into(),
17417 ));
17418 }
17419 }
17420
17421 let mut constraint_ids = HashSet::new();
17422 let mut constraint_names = HashSet::<String>::new();
17423 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
17424 if name.is_empty()
17425 || !constraint_ids.insert(id)
17426 || !constraint_names.insert(name.to_owned())
17427 {
17428 return Err(MongrelError::Schema(
17429 "recovered schema contains duplicate or empty constraint identities".into(),
17430 ));
17431 }
17432 Ok(())
17433 };
17434 for unique in &schema.constraints.uniques {
17435 validate_constraint_identity(unique.id, &unique.name)?;
17436 if unique.columns.is_empty()
17437 || unique.columns.iter().any(|id| !column_ids.contains(id))
17438 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
17439 {
17440 return Err(MongrelError::Schema(format!(
17441 "unique constraint {:?} has invalid columns",
17442 unique.name
17443 )));
17444 }
17445 }
17446 for foreign_key in &schema.constraints.foreign_keys {
17447 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
17448 if foreign_key.ref_table.is_empty()
17449 || foreign_key.columns.is_empty()
17450 || foreign_key.columns.len() != foreign_key.ref_columns.len()
17451 || foreign_key
17452 .columns
17453 .iter()
17454 .any(|id| !column_ids.contains(id))
17455 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
17456 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
17457 != foreign_key.ref_columns.len()
17458 {
17459 return Err(MongrelError::Schema(format!(
17460 "foreign key {:?} has invalid columns",
17461 foreign_key.name
17462 )));
17463 }
17464 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
17465 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
17466 && foreign_key.columns.iter().any(|id| {
17467 schema
17468 .columns
17469 .iter()
17470 .find(|column| column.id == *id)
17471 .is_none_or(|column| {
17472 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
17473 })
17474 })
17475 {
17476 return Err(MongrelError::Schema(format!(
17477 "foreign key {:?} uses SET NULL on a non-nullable column",
17478 foreign_key.name
17479 )));
17480 }
17481 }
17482 for check in &schema.constraints.checks {
17483 validate_constraint_identity(check.id, &check.name)?;
17484 check.expr.validate()?;
17485 validate_check_columns(&check.expr, &column_ids)?;
17486 }
17487 Ok(())
17488}
17489
17490fn validate_check_columns(
17491 expression: &crate::constraint::CheckExpr,
17492 column_ids: &HashSet<u16>,
17493) -> Result<()> {
17494 use crate::constraint::CheckExpr;
17495 match expression {
17496 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
17497 if column_ids.contains(id) {
17498 Ok(())
17499 } else {
17500 Err(MongrelError::Schema(format!(
17501 "check constraint references unknown column {id}"
17502 )))
17503 }
17504 }
17505 CheckExpr::Regex { col, .. } => {
17506 if column_ids.contains(col) {
17507 Ok(())
17508 } else {
17509 Err(MongrelError::Schema(format!(
17510 "check constraint references unknown column {col}"
17511 )))
17512 }
17513 }
17514 CheckExpr::Add(left, right)
17515 | CheckExpr::Sub(left, right)
17516 | CheckExpr::Mul(left, right)
17517 | CheckExpr::Div(left, right)
17518 | CheckExpr::Mod(left, right)
17519 | CheckExpr::Eq(left, right)
17520 | CheckExpr::Ne(left, right)
17521 | CheckExpr::Lt(left, right)
17522 | CheckExpr::Le(left, right)
17523 | CheckExpr::Gt(left, right)
17524 | CheckExpr::Ge(left, right)
17525 | CheckExpr::And(left, right)
17526 | CheckExpr::Or(left, right) => {
17527 validate_check_columns(left, column_ids)?;
17528 validate_check_columns(right, column_ids)
17529 }
17530 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
17531 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
17532 }
17533}
17534
17535fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
17536 for (name, prior, candidate) in [
17537 ("db_epoch", current.db_epoch, next.db_epoch),
17538 ("next_table_id", current.next_table_id, next.next_table_id),
17539 (
17540 "next_segment_no",
17541 current.next_segment_no,
17542 next.next_segment_no,
17543 ),
17544 ("next_user_id", current.next_user_id, next.next_user_id),
17545 (
17546 "security_version",
17547 current.security_version,
17548 next.security_version,
17549 ),
17550 ] {
17551 if candidate < prior {
17552 return Err(MongrelError::Schema(format!(
17553 "catalog snapshot rolls back {name} from {prior} to {candidate}"
17554 )));
17555 }
17556 }
17557 for prior in ¤t.tables {
17558 let Some(candidate) = next
17559 .tables
17560 .iter()
17561 .find(|entry| entry.table_id == prior.table_id)
17562 else {
17563 return Err(MongrelError::Schema(format!(
17564 "catalog snapshot removes table identity {}",
17565 prior.table_id
17566 )));
17567 };
17568 if candidate.created_epoch != prior.created_epoch
17569 || candidate.schema.schema_id < prior.schema.schema_id
17570 || matches!(prior.state, TableState::Dropped { .. })
17571 && !matches!(candidate.state, TableState::Dropped { .. })
17572 {
17573 return Err(MongrelError::Schema(format!(
17574 "catalog snapshot rolls back table identity {}",
17575 prior.table_id
17576 )));
17577 }
17578 }
17579 for prior in ¤t.users {
17580 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
17581 if candidate.username != prior.username
17582 || candidate.created_epoch != prior.created_epoch
17583 {
17584 return Err(MongrelError::Schema(format!(
17585 "catalog snapshot reuses user identity {}",
17586 prior.id
17587 )));
17588 }
17589 }
17590 }
17591 Ok(())
17592}
17593
17594fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
17595 let mut table_ids = HashSet::new();
17596 let mut active_names = HashSet::new();
17597 let mut max_table_id = None::<u64>;
17598 for entry in &catalog.tables {
17599 if !table_ids.insert(entry.table_id) {
17600 return Err(MongrelError::Schema(format!(
17601 "catalog contains duplicate table id {}",
17602 entry.table_id
17603 )));
17604 }
17605 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
17606 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
17607 return Err(MongrelError::Schema(format!(
17608 "catalog table {} has invalid name or creation epoch",
17609 entry.table_id
17610 )));
17611 }
17612 validate_recovered_schema(&entry.schema)?;
17613 match &entry.state {
17614 TableState::Live => {
17615 if !active_names.insert(entry.name.as_str()) {
17616 return Err(MongrelError::Schema(format!(
17617 "catalog contains duplicate active table name {:?}",
17618 entry.name
17619 )));
17620 }
17621 }
17622 TableState::Dropped { at_epoch } => {
17623 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
17624 return Err(MongrelError::Schema(format!(
17625 "catalog table {} has invalid drop epoch {at_epoch}",
17626 entry.table_id
17627 )));
17628 }
17629 }
17630 TableState::Building {
17631 intended_name,
17632 query_id,
17633 replaces_table_id,
17634 ..
17635 } => {
17636 if intended_name.is_empty() || query_id.is_empty() {
17637 return Err(MongrelError::Schema(format!(
17638 "building table {} has empty identity fields",
17639 entry.table_id
17640 )));
17641 }
17642 if !active_names.insert(entry.name.as_str()) {
17643 return Err(MongrelError::Schema(format!(
17644 "catalog contains duplicate active/building table name {:?}",
17645 entry.name
17646 )));
17647 }
17648 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
17649 return Err(MongrelError::Schema(
17650 "building table cannot replace itself".into(),
17651 ));
17652 }
17653 }
17654 }
17655 }
17656 if let Some(maximum) = max_table_id {
17657 let required = maximum
17658 .checked_add(1)
17659 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
17660 if catalog.next_table_id < required {
17661 return Err(MongrelError::Schema(format!(
17662 "catalog next_table_id {} precedes required {required}",
17663 catalog.next_table_id
17664 )));
17665 }
17666 }
17667 for entry in &catalog.tables {
17668 if let TableState::Building {
17669 replaces_table_id: Some(replaced),
17670 ..
17671 } = entry.state
17672 {
17673 if !table_ids.contains(&replaced) {
17674 return Err(MongrelError::Schema(format!(
17675 "building table {} replaces unknown table {replaced}",
17676 entry.table_id
17677 )));
17678 }
17679 }
17680 }
17681 for entry in &catalog.tables {
17682 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
17683 validate_foreign_key_targets(catalog, &entry.schema)?;
17684 }
17685 }
17686
17687 let mut external_names = HashSet::new();
17688 for entry in &catalog.external_tables {
17689 entry.validate()?;
17690 validate_recovered_schema(&entry.declared_schema)?;
17691 if !entry.declared_schema.constraints.is_empty() {
17692 return Err(MongrelError::Schema(format!(
17693 "external table {:?} cannot carry engine-enforced constraints",
17694 entry.name
17695 )));
17696 }
17697 if entry.created_epoch > catalog.db_epoch
17698 || !external_names.insert(entry.name.as_str())
17699 || active_names.contains(entry.name.as_str())
17700 {
17701 return Err(MongrelError::Schema(format!(
17702 "invalid or duplicate external table {:?}",
17703 entry.name
17704 )));
17705 }
17706 }
17707
17708 let mut procedure_names = HashSet::new();
17709 for entry in &catalog.procedures {
17710 entry.procedure.validate()?;
17711 if entry.procedure.created_epoch > entry.procedure.updated_epoch
17712 || entry.procedure.updated_epoch > catalog.db_epoch
17713 || !procedure_names.insert(entry.procedure.name.as_str())
17714 {
17715 return Err(MongrelError::Schema(format!(
17716 "invalid or duplicate procedure {:?}",
17717 entry.procedure.name
17718 )));
17719 }
17720 validate_recovered_procedure_references(catalog, &entry.procedure)?;
17721 }
17722
17723 let mut trigger_names = HashSet::new();
17724 for entry in &catalog.triggers {
17725 entry.trigger.validate()?;
17726 if entry.trigger.created_epoch > entry.trigger.updated_epoch
17727 || entry.trigger.updated_epoch > catalog.db_epoch
17728 || !trigger_names.insert(entry.trigger.name.as_str())
17729 {
17730 return Err(MongrelError::Schema(format!(
17731 "invalid or duplicate trigger {:?}",
17732 entry.trigger.name
17733 )));
17734 }
17735 validate_recovered_trigger_references(catalog, &entry.trigger)?;
17736 }
17737
17738 let mut views = HashSet::new();
17739 for view in &catalog.materialized_views {
17740 let target = catalog.live(&view.name).ok_or_else(|| {
17741 MongrelError::Schema(format!(
17742 "materialized view {:?} has no live table",
17743 view.name
17744 ))
17745 })?;
17746 if view.name.is_empty()
17747 || view.query.trim().is_empty()
17748 || view.last_refresh_epoch > catalog.db_epoch
17749 || !views.insert(view.name.as_str())
17750 {
17751 return Err(MongrelError::Schema(format!(
17752 "materialized view {:?} has no unique live table",
17753 view.name
17754 )));
17755 }
17756 if let Some(incremental) = &view.incremental {
17757 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
17758 MongrelError::Schema(format!(
17759 "materialized view {:?} references missing source {:?}",
17760 view.name, incremental.source_table
17761 ))
17762 })?;
17763 if source.table_id != incremental.source_table_id
17764 || source
17765 .schema
17766 .columns
17767 .iter()
17768 .all(|column| column.id != incremental.group_column)
17769 {
17770 return Err(MongrelError::Schema(format!(
17771 "materialized view {:?} has invalid incremental source",
17772 view.name
17773 )));
17774 }
17775 let target_ids = target
17776 .schema
17777 .columns
17778 .iter()
17779 .map(|column| column.id)
17780 .collect::<HashSet<_>>();
17781 let mut output_ids = HashSet::new();
17782 let count_outputs = incremental
17783 .outputs
17784 .iter()
17785 .filter(|output| {
17786 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
17787 })
17788 .count();
17789 if incremental.checkpoint_event_id.is_empty()
17790 || !target_ids.contains(&incremental.group_output_column)
17791 || !target_ids.contains(&incremental.count_output_column)
17792 || incremental.outputs.is_empty()
17793 || count_outputs != 1
17794 || incremental.outputs.iter().any(|output| {
17795 !target_ids.contains(&output.output_column)
17796 || output.output_column == incremental.group_output_column
17797 || !output_ids.insert(output.output_column)
17798 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
17799 && output.output_column != incremental.count_output_column
17800 || match output.kind {
17801 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
17802 source
17803 .schema
17804 .columns
17805 .iter()
17806 .all(|column| column.id != source_column)
17807 }
17808 crate::catalog::IncrementalAggregateKind::Count => false,
17809 }
17810 })
17811 {
17812 return Err(MongrelError::Schema(format!(
17813 "materialized view {:?} has invalid incremental outputs",
17814 view.name
17815 )));
17816 }
17817 }
17818 }
17819
17820 validate_security_catalog(catalog, &catalog.security)?;
17821 validate_recovered_auth_catalog(catalog)?;
17822 Ok(())
17823}
17824
17825fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
17826 let mut changed = false;
17827 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
17828 let required = maximum
17829 .checked_add(1)
17830 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
17831 if catalog.next_table_id < required {
17832 catalog.next_table_id = required;
17833 changed = true;
17834 }
17835 }
17836 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
17837 let required = maximum
17838 .checked_add(1)
17839 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
17840 if catalog.next_user_id < required {
17841 catalog.next_user_id = required;
17842 changed = true;
17843 }
17844 }
17845 Ok(changed)
17846}
17847
17848fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
17849 for foreign_key in &schema.constraints.foreign_keys {
17850 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
17851 MongrelError::Schema(format!(
17852 "foreign key {:?} references unknown live table {:?}",
17853 foreign_key.name, foreign_key.ref_table
17854 ))
17855 })?;
17856 let referenced_unique = parent
17857 .schema
17858 .constraints
17859 .uniques
17860 .iter()
17861 .any(|unique| unique.columns == foreign_key.ref_columns)
17862 || foreign_key.ref_columns.len() == 1
17863 && parent
17864 .schema
17865 .primary_key()
17866 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
17867 if !referenced_unique {
17868 return Err(MongrelError::Schema(format!(
17869 "foreign key {:?} does not reference a unique key",
17870 foreign_key.name
17871 )));
17872 }
17873 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
17874 let local = schema.columns.iter().find(|column| column.id == *local_id);
17875 let referenced = parent
17876 .schema
17877 .columns
17878 .iter()
17879 .find(|column| column.id == *parent_id);
17880 if local
17881 .zip(referenced)
17882 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
17883 {
17884 return Err(MongrelError::Schema(format!(
17885 "foreign key {:?} has missing or incompatible columns",
17886 foreign_key.name
17887 )));
17888 }
17889 }
17890 }
17891 Ok(())
17892}
17893
17894fn validate_recovered_procedure_references(
17895 catalog: &Catalog,
17896 procedure: &StoredProcedure,
17897) -> Result<()> {
17898 for step in &procedure.body.steps {
17899 let Some(table_name) = step.table() else {
17900 continue;
17901 };
17902 let schema = &catalog
17903 .live(table_name)
17904 .ok_or_else(|| {
17905 MongrelError::Schema(format!(
17906 "procedure {:?} references unknown table {table_name:?}",
17907 procedure.name
17908 ))
17909 })?
17910 .schema;
17911 match step {
17912 ProcedureStep::NativeQuery {
17913 conditions,
17914 projection,
17915 ..
17916 } => {
17917 for condition in conditions {
17918 validate_condition_columns(condition, schema)?;
17919 }
17920 for id in projection.iter().flatten() {
17921 validate_column_id(*id, schema)?;
17922 }
17923 }
17924 ProcedureStep::Put { cells, .. } => {
17925 for cell in cells {
17926 validate_column_id(cell.column_id, schema)?;
17927 }
17928 }
17929 ProcedureStep::Upsert {
17930 cells,
17931 update_cells,
17932 ..
17933 } => {
17934 for cell in cells.iter().chain(update_cells.iter().flatten()) {
17935 validate_column_id(cell.column_id, schema)?;
17936 }
17937 }
17938 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
17939 return Err(MongrelError::Schema(format!(
17940 "procedure {:?} deletes by primary key on table without one",
17941 procedure.name
17942 )));
17943 }
17944 ProcedureStep::DeleteByPk { .. }
17945 | ProcedureStep::DeleteRows { .. }
17946 | ProcedureStep::SqlQuery { .. } => {}
17947 }
17948 }
17949 Ok(())
17950}
17951
17952fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
17953 let target_schema = match &trigger.target {
17954 TriggerTarget::Table(name) => catalog
17955 .live(name)
17956 .ok_or_else(|| {
17957 MongrelError::Schema(format!(
17958 "trigger {:?} references unknown table {name:?}",
17959 trigger.name
17960 ))
17961 })?
17962 .schema
17963 .clone(),
17964 TriggerTarget::View(_) => Schema {
17965 columns: trigger.target_columns.clone(),
17966 ..Schema::default()
17967 },
17968 };
17969 for column in &trigger.update_of {
17970 if target_schema.column(column).is_none() {
17971 return Err(MongrelError::Schema(format!(
17972 "trigger {:?} references unknown UPDATE OF column {column:?}",
17973 trigger.name
17974 )));
17975 }
17976 }
17977 if let Some(expr) = &trigger.when {
17978 validate_trigger_expr(expr, &target_schema, trigger.event)?;
17979 }
17980 let mut selects = HashMap::new();
17981 for step in &trigger.program.steps {
17982 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
17983 return Err(MongrelError::Schema(
17984 "SetNew is only valid in BEFORE triggers".into(),
17985 ));
17986 }
17987 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
17988 }
17989 Ok(())
17990}
17991
17992fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
17993 let mut role_names = HashSet::new();
17994 for role in &catalog.roles {
17995 if role.name.is_empty()
17996 || role.created_epoch > catalog.db_epoch
17997 || !role_names.insert(role.name.as_str())
17998 {
17999 return Err(MongrelError::Schema(format!(
18000 "invalid or duplicate role {:?}",
18001 role.name
18002 )));
18003 }
18004 for permission in &role.permissions {
18005 if let Some(table) = permission_table(permission) {
18006 let schema = catalog
18007 .live(table)
18008 .map(|entry| &entry.schema)
18009 .or_else(|| {
18010 catalog
18011 .external_tables
18012 .iter()
18013 .find(|entry| entry.name == table)
18014 .map(|entry| &entry.declared_schema)
18015 })
18016 .ok_or_else(|| {
18017 MongrelError::Schema(format!(
18018 "role {:?} references unknown table {table:?}",
18019 role.name
18020 ))
18021 })?;
18022 let columns = match permission {
18023 crate::auth::Permission::SelectColumns { columns, .. }
18024 | crate::auth::Permission::InsertColumns { columns, .. }
18025 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
18026 _ => None,
18027 };
18028 if columns.is_some_and(|columns| {
18029 columns.is_empty()
18030 || columns.iter().any(|column| schema.column(column).is_none())
18031 }) {
18032 return Err(MongrelError::Schema(format!(
18033 "role {:?} contains invalid column permissions",
18034 role.name
18035 )));
18036 }
18037 }
18038 }
18039 }
18040 let mut user_ids = HashSet::new();
18041 let mut usernames = HashSet::new();
18042 let mut maximum_user_id = 0;
18043 for user in &catalog.users {
18044 maximum_user_id = maximum_user_id.max(user.id);
18045 if user.id == 0
18046 || user.username.is_empty()
18047 || user.password_hash.is_empty()
18048 || user.created_epoch > catalog.db_epoch
18049 || !user_ids.insert(user.id)
18050 || !usernames.insert(user.username.as_str())
18051 || user
18052 .roles
18053 .iter()
18054 .any(|role| !role_names.contains(role.as_str()))
18055 {
18056 return Err(MongrelError::Schema(format!(
18057 "invalid or duplicate user {:?}",
18058 user.username
18059 )));
18060 }
18061 }
18062 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
18063 return Err(MongrelError::Schema(
18064 "catalog next_user_id does not advance beyond existing user ids".into(),
18065 ));
18066 }
18067 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
18068 return Err(MongrelError::Schema(
18069 "authenticated catalog has no administrator".into(),
18070 ));
18071 }
18072 Ok(())
18073}
18074
18075fn validate_recovered_storage_plan(
18076 root: &Path,
18077 durable_root: Option<&crate::durable_file::DurableRoot>,
18078 catalog: &Catalog,
18079 created_table_ids: &HashSet<u64>,
18080 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
18081 meta_dek: Option<&[u8; META_DEK_LEN]>,
18082) -> Result<Vec<u64>> {
18083 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
18084 let mut reconcile = Vec::new();
18085 for entry in &catalog.tables {
18086 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18087 continue;
18088 }
18089 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
18090 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
18091 let table_exists = match durable_root {
18092 Some(root) => match root.open_directory(&relative_dir) {
18093 Ok(_) => true,
18094 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
18095 Err(error) => return Err(error.into()),
18096 },
18097 None => table_dir.is_dir(),
18098 };
18099 if !table_exists {
18100 if created_table_ids.contains(&entry.table_id) {
18101 reconcile.push(entry.table_id);
18102 continue;
18103 }
18104 return Err(MongrelError::NotFound(format!(
18105 "catalog table {} storage is missing",
18106 entry.table_id
18107 )));
18108 }
18109 let manifest_result = match durable_root {
18110 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
18111 None => crate::manifest::read(&table_dir, meta_dek),
18112 };
18113 let manifest = match manifest_result {
18114 Ok(manifest) => manifest,
18115 Err(MongrelError::Io(error))
18116 if created_table_ids.contains(&entry.table_id)
18117 && error.kind() == std::io::ErrorKind::NotFound =>
18118 {
18119 reconcile.push(entry.table_id);
18120 continue;
18121 }
18122 Err(error) => return Err(error),
18123 };
18124 if manifest.table_id != entry.table_id {
18125 return Err(MongrelError::Conflict(format!(
18126 "catalog table {} storage identity mismatch",
18127 entry.table_id
18128 )));
18129 }
18130 let schema_result = match durable_root {
18131 Some(root) => root
18132 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
18133 .map_err(MongrelError::from),
18134 None => crate::durable_file::open_regular_nofollow(
18135 &table_dir.join(crate::engine::SCHEMA_FILENAME),
18136 ),
18137 };
18138 let file = match schema_result {
18139 Ok(file) => file,
18140 Err(MongrelError::Io(error))
18141 if created_table_ids.contains(&entry.table_id)
18142 && error.kind() == std::io::ErrorKind::NotFound =>
18143 {
18144 reconcile.push(entry.table_id);
18145 continue;
18146 }
18147 Err(error) => return Err(error),
18148 };
18149 let length = file.metadata()?.len();
18150 if length > MAX_SCHEMA_BYTES {
18151 return Err(MongrelError::ResourceLimitExceeded {
18152 resource: "recovered schema bytes",
18153 requested: usize::try_from(length).unwrap_or(usize::MAX),
18154 limit: MAX_SCHEMA_BYTES as usize,
18155 });
18156 }
18157 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
18158 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
18159 if manifest.schema_id != entry.schema.schema_id
18160 || crate::wal::DdlOp::encode_schema(&disk_schema)?
18161 != crate::wal::DdlOp::encode_schema(&entry.schema)?
18162 {
18163 reconcile.push(entry.table_id);
18164 }
18165 }
18166 for table_id in ttl_updates.keys() {
18167 if !catalog.tables.iter().any(|entry| {
18168 entry.table_id == *table_id
18169 && matches!(entry.state, TableState::Live | TableState::Building { .. })
18170 }) {
18171 continue;
18172 }
18173 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
18174 let table_exists = match durable_root {
18175 Some(root) => match root.open_directory(&relative_dir) {
18176 Ok(_) => true,
18177 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
18178 Err(error) => return Err(error.into()),
18179 },
18180 None => root.join(&relative_dir).is_dir(),
18181 };
18182 if !table_exists && !created_table_ids.contains(table_id) {
18183 return Err(MongrelError::NotFound(format!(
18184 "TTL recovery table {table_id} storage is missing"
18185 )));
18186 }
18187 }
18188 reconcile.sort_unstable();
18189 reconcile.dedup();
18190 Ok(reconcile)
18191}
18192
18193fn validate_catalog_table_storage(
18194 root: &crate::durable_file::DurableRoot,
18195 catalog: &Catalog,
18196 meta_dek: Option<&[u8; META_DEK_LEN]>,
18197) -> Result<()> {
18198 for entry in &catalog.tables {
18199 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18200 continue;
18201 }
18202 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
18203 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
18204 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
18205 return Err(MongrelError::Conflict(format!(
18206 "catalog table {} storage identity mismatch",
18207 entry.table_id
18208 )));
18209 }
18210 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
18211 }
18212 Ok(())
18213}
18214
18215fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
18216 match schema.columns.iter_mut().find(|c| c.id == column.id) {
18217 Some(existing) if *existing == column => Ok(false),
18218 Some(existing) => {
18219 *existing = column;
18220 schema.schema_id = schema
18221 .schema_id
18222 .checked_add(1)
18223 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
18224 Ok(true)
18225 }
18226 None => {
18227 schema.columns.push(column);
18228 schema.schema_id = schema
18229 .schema_id
18230 .checked_add(1)
18231 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
18232 Ok(true)
18233 }
18234 }
18235}
18236
18237fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
18238 use crate::auth::Permission;
18239 match permission {
18240 Permission::Select { table }
18241 | Permission::Insert { table }
18242 | Permission::Update { table }
18243 | Permission::Delete { table }
18244 | Permission::SelectColumns { table, .. }
18245 | Permission::InsertColumns { table, .. }
18246 | Permission::UpdateColumns { table, .. } => Some(table),
18247 Permission::All | Permission::Ddl | Permission::Admin => None,
18248 }
18249}
18250
18251fn apply_rebuilding_publish(
18252 catalog: &mut Catalog,
18253 table_id: u64,
18254 replaced_table_id: u64,
18255 new_name: &str,
18256 epoch: u64,
18257) -> Result<bool> {
18258 let already_published = catalog.tables.iter().any(|entry| {
18259 entry.table_id == table_id
18260 && entry.name == new_name
18261 && matches!(entry.state, TableState::Live)
18262 }) && catalog.tables.iter().any(|entry| {
18263 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
18264 });
18265 if already_published {
18266 return Ok(false);
18267 }
18268 let schema = catalog
18269 .tables
18270 .iter()
18271 .find(|entry| entry.table_id == table_id)
18272 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
18273 .schema
18274 .clone();
18275 let replaced = catalog
18276 .tables
18277 .iter_mut()
18278 .find(|entry| entry.table_id == replaced_table_id)
18279 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
18280 replaced.state = TableState::Dropped { at_epoch: epoch };
18281 let replacement = catalog
18282 .tables
18283 .iter_mut()
18284 .find(|entry| entry.table_id == table_id)
18285 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
18286 replacement.name = new_name.to_string();
18287 replacement.state = TableState::Live;
18288
18289 for role in &mut catalog.roles {
18290 role.permissions.retain_mut(|permission| {
18291 retain_rebuilt_permission_columns(permission, new_name, &schema)
18292 });
18293 }
18294 for definition in &mut catalog.materialized_views {
18295 if let Some(incremental) = definition.incremental.as_mut() {
18296 if incremental.source_table == new_name
18297 && incremental.source_table_id == replaced_table_id
18298 {
18299 incremental.source_table_id = table_id;
18300 }
18301 }
18302 }
18303 advance_security_version(catalog)?;
18304 Ok(true)
18305}
18306
18307fn retain_rebuilt_permission_columns(
18308 permission: &mut crate::auth::Permission,
18309 target_table: &str,
18310 schema: &Schema,
18311) -> bool {
18312 use crate::auth::Permission;
18313 let columns = match permission {
18314 Permission::SelectColumns { table, columns }
18315 | Permission::InsertColumns { table, columns }
18316 | Permission::UpdateColumns { table, columns }
18317 if table == target_table =>
18318 {
18319 Some(columns)
18320 }
18321 _ => None,
18322 };
18323 if let Some(columns) = columns {
18324 columns.retain(|column| schema.column(column).is_some());
18325 !columns.is_empty()
18326 } else {
18327 true
18328 }
18329}
18330
18331fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
18332 use crate::auth::Permission;
18333 let table = match permission {
18334 Permission::Select { table }
18335 | Permission::Insert { table }
18336 | Permission::Update { table }
18337 | Permission::Delete { table }
18338 | Permission::SelectColumns { table, .. }
18339 | Permission::InsertColumns { table, .. }
18340 | Permission::UpdateColumns { table, .. } => Some(table),
18341 Permission::All | Permission::Ddl | Permission::Admin => None,
18342 };
18343 if let Some(table) = table.filter(|table| table.as_str() == old) {
18344 *table = new.to_string();
18345 }
18346}
18347
18348fn rename_permission_column(
18349 permission: &mut crate::auth::Permission,
18350 target_table: &str,
18351 old: &str,
18352 new: &str,
18353) {
18354 use crate::auth::Permission;
18355 let columns = match permission {
18356 Permission::SelectColumns { table, columns }
18357 | Permission::InsertColumns { table, columns }
18358 | Permission::UpdateColumns { table, columns }
18359 if table == target_table =>
18360 {
18361 Some(columns)
18362 }
18363 _ => None,
18364 };
18365 if let Some(column) = columns
18366 .into_iter()
18367 .flatten()
18368 .find(|column| column.as_str() == old)
18369 {
18370 *column = new.to_string();
18371 }
18372}
18373
18374pub(crate) fn validate_security_catalog(
18375 catalog: &Catalog,
18376 security: &crate::security::SecurityCatalog,
18377) -> Result<()> {
18378 let mut policy_names = HashSet::new();
18379 for table in &security.rls_tables {
18380 if catalog.live(table).is_none() {
18381 return Err(MongrelError::NotFound(format!(
18382 "RLS table {table:?} not found"
18383 )));
18384 }
18385 }
18386 for policy in &security.policies {
18387 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
18388 return Err(MongrelError::InvalidArgument(format!(
18389 "duplicate policy {:?} on {:?}",
18390 policy.name, policy.table
18391 )));
18392 }
18393 let schema = &catalog
18394 .live(&policy.table)
18395 .ok_or_else(|| {
18396 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
18397 })?
18398 .schema;
18399 if let Some(expression) = &policy.using {
18400 validate_security_expression(expression, schema)?;
18401 }
18402 if let Some(expression) = &policy.with_check {
18403 validate_security_expression(expression, schema)?;
18404 }
18405 }
18406 let mut mask_names = HashSet::new();
18407 for mask in &security.masks {
18408 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
18409 return Err(MongrelError::InvalidArgument(format!(
18410 "duplicate mask {:?} on {:?}",
18411 mask.name, mask.table
18412 )));
18413 }
18414 let column = catalog
18415 .live(&mask.table)
18416 .and_then(|entry| {
18417 entry
18418 .schema
18419 .columns
18420 .iter()
18421 .find(|column| column.id == mask.column)
18422 })
18423 .ok_or_else(|| {
18424 MongrelError::NotFound(format!(
18425 "mask column {} on {:?} not found",
18426 mask.column, mask.table
18427 ))
18428 })?;
18429 if matches!(
18430 mask.strategy,
18431 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
18432 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
18433 {
18434 return Err(MongrelError::InvalidArgument(format!(
18435 "mask {:?} requires a string/bytes column",
18436 mask.name
18437 )));
18438 }
18439 }
18440 Ok(())
18441}
18442
18443fn validate_security_expression(
18444 expression: &crate::security::SecurityExpr,
18445 schema: &Schema,
18446) -> Result<()> {
18447 use crate::security::SecurityExpr;
18448 match expression {
18449 SecurityExpr::True => Ok(()),
18450 SecurityExpr::ColumnEqCurrentUser { column }
18451 | SecurityExpr::ColumnEqValue { column, .. } => {
18452 if schema
18453 .columns
18454 .iter()
18455 .any(|candidate| candidate.id == *column)
18456 {
18457 Ok(())
18458 } else {
18459 Err(MongrelError::InvalidArgument(format!(
18460 "security expression references unknown column id {column}"
18461 )))
18462 }
18463 }
18464 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
18465 validate_security_expression(left, schema)?;
18466 validate_security_expression(right, schema)
18467 }
18468 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
18469 }
18470}
18471
18472fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
18474 let referenced = cat
18475 .tables
18476 .iter()
18477 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
18478 .map(|entry| entry.table_id)
18479 .collect::<HashSet<_>>();
18480 let tables_dir = root.join(TABLES_DIR);
18481 let entries = match std::fs::read_dir(&tables_dir) {
18482 Ok(entries) => entries,
18483 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
18484 Err(error) => return Err(error.into()),
18485 };
18486 for entry in entries {
18487 let entry = entry?;
18488 if !entry.file_type()?.is_dir() {
18489 continue;
18490 }
18491 let file_name = entry.file_name();
18492 let Some(name) = file_name.to_str() else {
18493 continue;
18494 };
18495 let Ok(table_id) = name.parse::<u64>() else {
18496 continue;
18497 };
18498 if name != table_id.to_string() {
18499 continue;
18500 }
18501 if !referenced.contains(&table_id) {
18502 crate::durable_file::remove_directory_all(&entry.path())?;
18503 }
18504 }
18505 Ok(())
18506}
18507
18508fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
18513 for entry in &cat.tables {
18514 let txn_dir = root
18515 .join(TABLES_DIR)
18516 .join(entry.table_id.to_string())
18517 .join("_txn");
18518 if txn_dir.exists() {
18519 let _ = std::fs::remove_dir_all(&txn_dir);
18520 }
18521 }
18522}
18523
18524#[cfg(test)]
18525mod write_permission_tests {
18526 use super::*;
18527 use crate::txn::Staged;
18528
18529 struct NoopExternalBridge;
18530
18531 impl ExternalTriggerBridge for NoopExternalBridge {
18532 fn apply_trigger_external_write(
18533 &self,
18534 _entry: &ExternalTableEntry,
18535 base_state: Vec<u8>,
18536 _op: ExternalTriggerWrite,
18537 ) -> Result<ExternalTriggerWriteResult> {
18538 Ok(ExternalTriggerWriteResult::new(base_state))
18539 }
18540 }
18541
18542 fn assert_txn_namespace_full<T>(result: Result<T>) {
18543 assert!(matches!(result, Err(MongrelError::Full(_))));
18544 }
18545
18546 #[test]
18547 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
18548 let directory = tempfile::tempdir().unwrap();
18549 let database = Database::create(directory.path()).unwrap();
18550 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
18551 *database.next_txn_id.lock() = generation << 32;
18552 let before = crate::wal::SharedWal::replay(directory.path())
18553 .unwrap()
18554 .len();
18555 let bridge = NoopExternalBridge;
18556
18557 assert_txn_namespace_full(database.begin().commit());
18558 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
18559 assert_txn_namespace_full(
18560 database
18561 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
18562 .commit(),
18563 );
18564 assert_txn_namespace_full(
18565 database
18566 .begin_with_external_trigger_bridge(&bridge)
18567 .commit(),
18568 );
18569 assert_txn_namespace_full(
18570 database
18571 .begin_with_external_trigger_bridge_as(&bridge, None)
18572 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
18573 );
18574
18575 assert_eq!(
18576 crate::wal::SharedWal::replay(directory.path())
18577 .unwrap()
18578 .len(),
18579 before
18580 );
18581 drop(database);
18582 Database::open(directory.path()).unwrap();
18583 }
18584
18585 #[test]
18586 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
18587 let directory = tempfile::tempdir().unwrap();
18588 let table_dir = directory.path().join("7");
18589 crate::durable_file::create_directory_all(&table_dir).unwrap();
18590 let original_schema = test_schema();
18591 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
18592 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
18593 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
18594 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
18595 let original_bytes = std::fs::read(&schema_path).unwrap();
18596
18597 let mut replacement_schema = original_schema;
18598 replacement_schema.schema_id += 1;
18599 assert!(matches!(
18600 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
18601 Err(MongrelError::Conflict(_))
18602 ));
18603
18604 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
18605 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
18606 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
18607 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
18608 }
18609
18610 #[test]
18611 fn catalog_table_missing_storage_fails_without_recreating_it() {
18612 let directory = tempfile::tempdir().unwrap();
18613 let table_dir = {
18614 let database = Database::create(directory.path()).unwrap();
18615 database.create_table("docs", test_schema()).unwrap();
18616 directory
18617 .path()
18618 .join(TABLES_DIR)
18619 .join(database.table_id("docs").unwrap().to_string())
18620 };
18621 std::fs::remove_dir_all(&table_dir).unwrap();
18622
18623 assert!(matches!(
18624 Database::open(directory.path()),
18625 Err(MongrelError::NotFound(_))
18626 ));
18627 assert!(!table_dir.exists());
18628 }
18629
18630 #[test]
18631 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
18632 let directory = tempfile::tempdir().unwrap();
18633 let database = std::sync::Arc::new(
18634 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
18635 );
18636 database.create_user("alice", "old-password").unwrap();
18637 let old_identity = database.user_identity("alice").unwrap();
18638 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
18639 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
18640 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
18641 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
18642
18643 std::thread::scope(|scope| {
18644 let authenticate = {
18645 let database = std::sync::Arc::clone(&database);
18646 scope.spawn(move || {
18647 database.authenticate_principal_inner("alice", "old-password", || {
18648 verified_tx.send(()).unwrap();
18649 resume_rx.recv().unwrap();
18650 })
18651 })
18652 };
18653 verified_rx.recv().unwrap();
18654 let mutate = {
18655 let database = std::sync::Arc::clone(&database);
18656 scope.spawn(move || {
18657 mutation_started_tx.send(()).unwrap();
18658 database.drop_user("alice").unwrap();
18659 database.create_user("alice", "new-password").unwrap();
18660 mutation_done_tx.send(()).unwrap();
18661 })
18662 };
18663 mutation_started_rx.recv().unwrap();
18664 assert!(mutation_done_rx
18665 .recv_timeout(std::time::Duration::from_millis(50))
18666 .is_err());
18667 resume_tx.send(()).unwrap();
18668 let principal = authenticate.join().unwrap().unwrap().unwrap();
18669 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
18670 mutate.join().unwrap();
18671 });
18672
18673 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
18674 assert!(database
18675 .authenticate_principal("alice", "old-password")
18676 .unwrap()
18677 .is_none());
18678 assert!(database
18679 .authenticate_principal("alice", "new-password")
18680 .unwrap()
18681 .is_some());
18682 }
18683
18684 #[test]
18685 fn homogeneous_batch_summarizes_to_one_permission_decision() {
18686 let staging = (0..10_050)
18687 .map(|_| {
18688 (
18689 7,
18690 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
18691 )
18692 })
18693 .collect::<Vec<_>>();
18694
18695 let needs = summarize_write_permissions(&staging);
18696 let table = needs.get(&7).unwrap();
18697 assert_eq!(needs.len(), 1);
18698 assert!(table.insert);
18699 assert_eq!(table.insert_columns, [1, 2]);
18700 assert!(!table.update);
18701 assert!(!table.delete);
18702 assert!(!table.truncate);
18703 }
18704
18705 #[test]
18706 fn mixed_writes_union_columns_and_preserve_empty_operations() {
18707 let staging = vec![
18708 (7, Staged::Put(vec![(2, Value::Int64(2))])),
18709 (7, Staged::Put(vec![(1, Value::Int64(1))])),
18710 (
18711 7,
18712 Staged::Update {
18713 row_id: RowId(1),
18714 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
18715 changed_columns: vec![2],
18716 },
18717 ),
18718 (7, Staged::Delete(RowId(2))),
18719 (8, Staged::Truncate),
18720 ];
18721
18722 let needs = summarize_write_permissions(&staging);
18723 let table = needs.get(&7).unwrap();
18724 assert_eq!(table.insert_columns, [1, 2]);
18725 assert!(table.update);
18726 assert_eq!(table.update_columns, [2]);
18727 assert!(table.delete);
18728 assert!(needs.get(&8).unwrap().truncate);
18729 }
18730
18731 #[test]
18732 fn final_permission_decisions_do_not_scale_with_rows() {
18733 let credentialless_dir = tempfile::tempdir().unwrap();
18734 let credentialless = Database::create(credentialless_dir.path()).unwrap();
18735 credentialless.create_table("docs", test_schema()).unwrap();
18736 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18737 credentialless
18738 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
18739 .unwrap();
18740 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
18741
18742 let authenticated_dir = tempfile::tempdir().unwrap();
18743 let authenticated =
18744 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
18745 .unwrap();
18746 authenticated.create_table("docs", test_schema()).unwrap();
18747 let admin = authenticated.resolve_principal("admin").unwrap();
18748 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18749 authenticated
18750 .validate_write_permissions(
18751 &puts(authenticated.table_id("docs").unwrap()),
18752 Some(&admin),
18753 None,
18754 )
18755 .unwrap();
18756 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
18757 }
18758
18759 #[test]
18760 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
18761 let dir = tempfile::tempdir().unwrap();
18762 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
18763 db.create_table("docs", test_schema()).unwrap();
18764 let admin = db.resolve_principal("admin").unwrap();
18765 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18766
18767 let mut transaction = db.begin_as(Some(admin));
18768 transaction
18769 .delete_batch("docs", (0..100).map(RowId).collect())
18770 .unwrap();
18771 transaction.commit().unwrap();
18772
18773 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
18774 }
18775
18776 #[test]
18777 fn truncate_validation_checks_admin_once_for_all_tables() {
18778 let dir = tempfile::tempdir().unwrap();
18779 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
18780 db.create_table("first", test_schema()).unwrap();
18781 db.create_table("second", test_schema()).unwrap();
18782 let admin = db.resolve_principal("admin").unwrap();
18783 let staging = vec![
18784 (db.table_id("first").unwrap(), Staged::Truncate),
18785 (db.table_id("second").unwrap(), Staged::Truncate),
18786 ];
18787
18788 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
18789 db.validate_write_permissions(&staging, Some(&admin), None)
18790 .unwrap();
18791 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
18792 }
18793
18794 #[test]
18795 fn one_table_commit_batches_structural_work() {
18796 let dir = tempfile::tempdir().unwrap();
18797 let db = Database::create(dir.path()).unwrap();
18798 db.create_table("docs", test_schema()).unwrap();
18799 let table_id = db.table_id("docs").unwrap();
18800
18801 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
18802 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
18803 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
18804 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
18805 db.transaction(|transaction| {
18806 for id in 0..100 {
18807 transaction.put("docs", vec![(1, Value::Int64(id))])?;
18808 }
18809 Ok(())
18810 })
18811 .unwrap();
18812
18813 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
18814 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18815 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18816 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
18817
18818 let puts = crate::wal::SharedWal::replay(dir.path())
18819 .unwrap()
18820 .into_iter()
18821 .filter_map(|record| match record.op {
18822 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
18823 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
18824 .unwrap()
18825 .len(),
18826 ),
18827 _ => None,
18828 })
18829 .collect::<Vec<_>>();
18830 assert_eq!(puts, [100]);
18831
18832 let row_ids = db
18833 .table("docs")
18834 .unwrap()
18835 .lock()
18836 .visible_rows(db.snapshot().0)
18837 .unwrap()
18838 .into_iter()
18839 .take(2)
18840 .map(|row| row.row_id)
18841 .collect::<Vec<_>>();
18842 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
18843 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
18844 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
18845 db.transaction(|transaction| {
18846 for row_id in row_ids {
18847 transaction.delete("docs", row_id)?;
18848 }
18849 Ok(())
18850 })
18851 .unwrap();
18852 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18853 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
18854 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
18855
18856 let deletes = crate::wal::SharedWal::replay(dir.path())
18857 .unwrap()
18858 .into_iter()
18859 .filter_map(|record| match record.op {
18860 crate::wal::Op::Delete {
18861 table_id: id,
18862 row_ids,
18863 } if id == table_id => Some(row_ids.len()),
18864 _ => None,
18865 })
18866 .collect::<Vec<_>>();
18867 assert_eq!(deletes, [2]);
18868 }
18869
18870 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
18871 (0..10_050)
18872 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
18873 .collect()
18874 }
18875
18876 fn test_schema() -> Schema {
18877 Schema {
18878 columns: vec![ColumnDef {
18879 id: 1,
18880 name: "id".into(),
18881 ty: TypeId::Int64,
18882 flags: crate::schema::ColumnFlags::empty()
18883 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
18884 default_value: None,
18885 embedding_source: None,
18886 }],
18887 ..Schema::default()
18888 }
18889 }
18890}
18891
18892#[cfg(test)]
18893mod cdc_bounds_tests {
18894 use super::*;
18895
18896 #[test]
18897 fn retained_byte_limit_rejects_without_allocating_payload() {
18898 let mut retained = 0;
18899 let error = charge_cdc_bytes(
18900 &mut retained,
18901 CDC_MAX_RETAINED_BYTES.saturating_add(1),
18902 "CDC retained bytes",
18903 )
18904 .unwrap_err();
18905 assert!(matches!(
18906 error,
18907 MongrelError::ResourceLimitExceeded {
18908 resource: "CDC retained bytes",
18909 ..
18910 }
18911 ));
18912 }
18913
18914 #[test]
18915 fn row_json_estimate_accounts_for_byte_array_expansion() {
18916 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
18917 .with_column(1, Value::Bytes(vec![0; 1024]));
18918 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
18919 }
18920}
18921
18922#[cfg(test)]
18923mod generation_metrics_tests {
18924 use super::*;
18925 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
18926
18927 #[test]
18928 fn legacy_cow_fallback_is_measured() {
18929 let dir = tempfile::tempdir().unwrap();
18930 let table = Table::create(
18931 dir.path(),
18932 Schema {
18933 columns: vec![ColumnDef {
18934 id: 1,
18935 name: "id".into(),
18936 ty: TypeId::Int64,
18937 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
18938 default_value: None,
18939 embedding_source: None,
18940 }],
18941 ..Schema::default()
18942 },
18943 1,
18944 )
18945 .unwrap();
18946 let handle = TableHandle::from_table(table);
18947 let held = match &handle.inner {
18948 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
18949 TableHandleInner::Direct(_) => unreachable!(),
18950 };
18951
18952 handle.lock().set_sync_byte_threshold(1);
18953
18954 let stats = handle.generation_stats();
18955 assert_eq!(stats.cow_clone_count, 1);
18956 assert!(stats.estimated_cow_clone_bytes > 0);
18957 drop(held);
18958 }
18959}
18960
18961#[cfg(test)]
18962mod trigger_engine_tests {
18963 use super::*;
18964
18965 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
18966 WriteEvent {
18967 table: "test".into(),
18968 kind: TriggerEvent::Insert,
18969 new: Some(TriggerRowImage {
18970 columns: new_cells.iter().cloned().collect(),
18971 }),
18972 old: Some(TriggerRowImage {
18973 columns: old_cells.iter().cloned().collect(),
18974 }),
18975 changed_columns: Vec::new(),
18976 op_indices: Vec::new(),
18977 put_idx: None,
18978 trigger_stack: Vec::new(),
18979 }
18980 }
18981
18982 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
18983 WriteEvent {
18984 table: "test".into(),
18985 kind: TriggerEvent::Insert,
18986 new: Some(TriggerRowImage {
18987 columns: new_cells.iter().cloned().collect(),
18988 }),
18989 old: None,
18990 changed_columns: Vec::new(),
18991 op_indices: Vec::new(),
18992 put_idx: None,
18993 trigger_stack: Vec::new(),
18994 }
18995 }
18996
18997 #[test]
18998 fn value_order_int64_vs_float64() {
18999 assert_eq!(
19000 value_order(&Value::Int64(5), &Value::Float64(5.0)),
19001 Some(std::cmp::Ordering::Equal)
19002 );
19003 assert_eq!(
19004 value_order(&Value::Int64(5), &Value::Float64(3.0)),
19005 Some(std::cmp::Ordering::Greater)
19006 );
19007 assert_eq!(
19008 value_order(&Value::Int64(2), &Value::Float64(3.0)),
19009 Some(std::cmp::Ordering::Less)
19010 );
19011 }
19012
19013 #[test]
19014 fn value_order_null_returns_none() {
19015 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
19016 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
19017 assert_eq!(value_order(&Value::Null, &Value::Null), None);
19018 }
19019
19020 #[test]
19021 fn value_order_cross_group_returns_none() {
19022 assert_eq!(
19023 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
19024 None
19025 );
19026 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
19027 assert_eq!(
19028 value_order(
19029 &Value::Embedding(vec![1.0, 2.0]),
19030 &Value::Embedding(vec![1.0, 2.0])
19031 ),
19032 None
19033 );
19034 }
19035
19036 #[test]
19037 fn eval_trigger_expr_ranges_and_booleans() {
19038 let expr = TriggerExpr::And {
19039 left: Box::new(TriggerExpr::Gt {
19040 left: TriggerValue::NewColumn(1),
19041 right: TriggerValue::Literal(Value::Int64(0)),
19042 }),
19043 right: Box::new(TriggerExpr::Lte {
19044 left: TriggerValue::NewColumn(1),
19045 right: TriggerValue::Literal(Value::Int64(100)),
19046 }),
19047 };
19048 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
19049 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
19050 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
19051
19052 let or_expr = TriggerExpr::Or {
19053 left: Box::new(TriggerExpr::Lt {
19054 left: TriggerValue::NewColumn(1),
19055 right: TriggerValue::Literal(Value::Int64(0)),
19056 }),
19057 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
19058 TriggerValue::OldColumn(2),
19059 )))),
19060 };
19061 assert!(eval_trigger_expr(
19062 &or_expr,
19063 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
19064 )
19065 .unwrap());
19066 assert!(!eval_trigger_expr(
19067 &or_expr,
19068 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
19069 )
19070 .unwrap());
19071
19072 assert!(eval_trigger_expr(
19073 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
19074 &event_insert(&[])
19075 )
19076 .unwrap());
19077 assert!(!eval_trigger_expr(
19078 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
19079 &event_insert(&[])
19080 )
19081 .unwrap());
19082 assert!(!eval_trigger_expr(
19083 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
19084 &event_insert(&[])
19085 )
19086 .unwrap());
19087 }
19088}
19089
19090#[cfg(test)]
19091mod core_resource_tests {
19092 use super::*;
19093
19094 fn int_pk_schema() -> Schema {
19095 Schema {
19096 columns: vec![ColumnDef {
19097 id: 1,
19098 name: "id".into(),
19099 ty: TypeId::Int64,
19100 flags: crate::schema::ColumnFlags::empty()
19101 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19102 default_value: None,
19103 embedding_source: None,
19104 }],
19105 ..Schema::default()
19106 }
19107 }
19108
19109 #[test]
19110 fn open_constructs_governor_spill_and_jobs_with_documented_defaults() {
19111 let dir = tempfile::tempdir().unwrap();
19112 let db = Database::create(dir.path()).unwrap();
19113 assert_eq!(
19114 db.memory_governor().max_bytes(),
19115 DEFAULT_MEMORY_BUDGET_BYTES
19116 );
19117 assert_eq!(
19118 db.spill_manager().config().global_bytes,
19119 DEFAULT_TEMP_DISK_BUDGET_BYTES
19120 );
19121 assert!(db.job_registry().list().is_empty());
19122 assert_eq!(
19124 db.resource_groups().len(),
19125 crate::resource::WorkloadClass::ALL.len()
19126 );
19127 assert!(db.resource_groups().get("control").is_some());
19128 assert!(db.embedding_providers().list_ids().is_empty());
19129 let err = db
19131 .embedding_providers()
19132 .embed(
19133 &crate::embedding::EmbeddingSource::SuppliedByApplication,
19134 &["text"],
19135 4,
19136 )
19137 .unwrap_err();
19138 assert!(matches!(
19139 err,
19140 crate::embedding::EmbeddingError::SuppliedByApplication
19141 ));
19142 }
19143
19144 #[test]
19145 fn lock_rows_for_update_acquires_exclusive_row_locks() {
19146 use crate::locks::LockKey;
19147 use crate::rowid::RowId;
19148
19149 let dir = tempfile::tempdir().unwrap();
19150 let db = Database::create(dir.path()).unwrap();
19151 let txn_id = db.allocate_lock_txn_id().unwrap();
19152 let rid = RowId(42);
19153 db.lock_rows_for_update(txn_id, 7, &[rid], None).unwrap();
19154 assert!(db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
19155 db.release_txn_locks(txn_id);
19156 assert!(!db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
19157 }
19158
19159 #[test]
19160 fn open_with_options_sizes_the_core_budgets() {
19161 let dir = tempfile::tempdir().unwrap();
19162 let db = Database::create(dir.path()).unwrap();
19163 drop(db);
19164 let db = Database::open_with_options(
19165 dir.path(),
19166 OpenOptions::default()
19167 .with_memory_budget_bytes(256 * 1024 * 1024)
19168 .with_temp_disk_budget_bytes(16 * 1024 * 1024),
19169 )
19170 .unwrap();
19171 assert_eq!(db.memory_governor().max_bytes(), 256 * 1024 * 1024);
19172 assert_eq!(db.spill_manager().config().global_bytes, 16 * 1024 * 1024);
19173 }
19174
19175 #[test]
19176 fn zero_budgets_are_rejected() {
19177 let dir = tempfile::tempdir().unwrap();
19178 let db = Database::create(dir.path()).unwrap();
19179 drop(db);
19180 let result = Database::open_with_options(
19181 dir.path(),
19182 OpenOptions::default().with_memory_budget_bytes(0),
19183 );
19184 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
19185 let result = Database::open_with_options(
19186 dir.path(),
19187 OpenOptions::default().with_temp_disk_budget_bytes(0),
19188 );
19189 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
19190 }
19191
19192 #[test]
19193 fn page_caches_reserve_under_the_governor() {
19194 let dir = tempfile::tempdir().unwrap();
19195 let db = Database::create(dir.path()).unwrap();
19196 db.create_table("t", int_pk_schema()).unwrap();
19197 let mut txn = db.begin();
19198 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
19199 txn.commit().unwrap();
19200 let stats = db.memory_governor().stats();
19204 assert_eq!(
19205 stats.usage_for(crate::memory::MemoryClass::PageCache),
19206 db.page_cache.used_bytes()
19207 );
19208 assert_eq!(
19209 stats.usage_for(crate::memory::MemoryClass::DecodedCache),
19210 db.decoded_cache.used_bytes()
19211 );
19212 assert_eq!(
19213 db.memory_governor().reclaimable_bytes(),
19214 db.page_cache.used_bytes() + db.decoded_cache.used_bytes()
19215 );
19216 let _ = db.memory_governor().evict_reclaimable(1024 * 1024);
19218 }
19219
19220 #[test]
19221 fn job_registry_persists_across_reopen() {
19222 let dir = tempfile::tempdir().unwrap();
19223 let db = Database::create(dir.path()).unwrap();
19224 db.create_table("t", int_pk_schema()).unwrap();
19225 let job_id = db
19226 .job_registry()
19227 .submit(
19228 crate::jobs::JobKind::IndexBuild,
19229 crate::jobs::JobTarget {
19230 table: "t".to_string(),
19231 index: Some("idx".to_string()),
19232 },
19233 )
19234 .unwrap();
19235 drop(db);
19236 let db = Database::open(dir.path()).unwrap();
19237 let record = db.job_registry().get(job_id).expect("job survives reopen");
19238 assert_eq!(record.state, crate::jobs::JobState::Pending);
19239 }
19240
19241 #[test]
19242 fn spill_manager_open_sweeps_stale_temp_tree() {
19243 let dir = tempfile::tempdir().unwrap();
19244 let db = Database::create(dir.path()).unwrap();
19245 let stale = dir.path().join("temp").join("spill").join("q-deadbeef");
19246 std::fs::create_dir_all(&stale).unwrap();
19247 std::fs::write(stale.join("chunk-0"), b"stale").unwrap();
19248 drop(db);
19249 let db = Database::open(dir.path()).unwrap();
19250 assert!(
19251 !stale.exists(),
19252 "the startup sweep removes stale spill files (S1E-004)"
19253 );
19254 let session = db
19256 .spill_manager()
19257 .begin_query(
19258 mongreldb_types::ids::QueryId::from_bytes([7u8; 16]),
19259 1024 * 1024,
19260 )
19261 .unwrap();
19262 assert_eq!(session.used(), 0);
19263 }
19264}
19265
19266#[cfg(test)]
19267mod version_pin_tests {
19268 use super::*;
19269
19270 fn int_pk_schema() -> Schema {
19271 Schema {
19272 columns: vec![ColumnDef {
19273 id: 1,
19274 name: "id".into(),
19275 ty: TypeId::Int64,
19276 flags: crate::schema::ColumnFlags::empty()
19277 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19278 default_value: None,
19279 embedding_source: None,
19280 }],
19281 ..Schema::default()
19282 }
19283 }
19284
19285 fn pins_for(
19286 report: &[TablePinsReport],
19287 table: &str,
19288 source: crate::retention::PinSource,
19289 ) -> Option<crate::retention::PinInfo> {
19290 report
19291 .iter()
19292 .find(|entry| entry.table == table)
19293 .and_then(|entry| entry.pins.get(source).cloned())
19294 }
19295
19296 #[test]
19297 fn backup_boundary_registers_backup_pitr_pin() {
19298 let source = tempfile::tempdir().unwrap();
19299 let destination_parent = tempfile::tempdir().unwrap();
19300 let destination = destination_parent.path().join("backup");
19301 let db = Arc::new(Database::create(source.path()).unwrap());
19302 db.create_table("t", int_pk_schema()).unwrap();
19303 let mut txn = db.begin();
19304 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
19305 let boundary_epoch = txn.commit().unwrap();
19306
19307 let hold = Arc::new(std::sync::Barrier::new(2));
19308 let resume = Arc::new(std::sync::Barrier::new(2));
19309 db.__set_backup_hook({
19310 let hold = Arc::clone(&hold);
19311 let resume = Arc::clone(&resume);
19312 move || {
19313 hold.wait();
19314 resume.wait();
19315 }
19316 });
19317
19318 let backup = {
19319 let db = Arc::clone(&db);
19320 let destination = destination.clone();
19321 std::thread::spawn(move || db.hot_backup(destination))
19322 };
19323 hold.wait();
19324 let report = db.version_pins_report();
19327 let pin = pins_for(&report, "t", crate::retention::PinSource::BackupPitr)
19328 .expect("backup boundary must register a BackupPitr pin");
19329 assert_eq!(pin.oldest_epoch, boundary_epoch);
19330 assert!(pin.pin_count >= 1);
19331 resume.wait();
19332 backup.join().unwrap().unwrap();
19333
19334 let report = db.version_pins_report();
19335 assert!(
19336 pins_for(&report, "t", crate::retention::PinSource::BackupPitr).is_none(),
19337 "the BackupPitr pin releases when the backup finishes"
19338 );
19339 }
19340
19341 #[test]
19342 fn snapshot_and_read_generation_pins_surface_in_report() {
19343 let dir = tempfile::tempdir().unwrap();
19344 let db = Database::create(dir.path()).unwrap();
19345 db.create_table("t", int_pk_schema()).unwrap();
19346 let mut txn = db.begin();
19347 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
19348 txn.commit().unwrap();
19349
19350 let (_snapshot, guard) = db.snapshot();
19351 let report = db.version_pins_report();
19352 assert!(
19353 pins_for(
19354 &report,
19355 "t",
19356 crate::retention::PinSource::TransactionSnapshot
19357 )
19358 .is_some(),
19359 "a database snapshot projects the TransactionSnapshot source"
19360 );
19361 drop(guard);
19362
19363 let handle = db.table("t").unwrap();
19364 let (generation, _snapshot) = handle.read_generation_with_context(None).unwrap();
19365 let report = db.version_pins_report();
19366 assert!(
19367 pins_for(&report, "t", crate::retention::PinSource::ReadGeneration).is_some(),
19368 "a cloned read generation registers a ReadGeneration pin"
19369 );
19370 drop(generation);
19371
19372 let report = db.version_pins_report();
19373 let entry = report.iter().find(|entry| entry.table == "t").unwrap();
19374 assert!(
19375 entry
19376 .pins
19377 .get(crate::retention::PinSource::BackupPitr)
19378 .is_none()
19379 && entry
19380 .pins
19381 .get(crate::retention::PinSource::Replication)
19382 .is_none()
19383 && entry
19384 .pins
19385 .get(crate::retention::PinSource::OnlineIndexBuild)
19386 .is_none(),
19387 "untaken sources stay absent from the report"
19388 );
19389 }
19390}
19391
19392#[cfg(test)]
19393mod lock_manager_tests {
19394 use super::*;
19395 use crate::locks::LockKey;
19396
19397 fn col(id: u16, name: &str, ty: TypeId, flags: crate::schema::ColumnFlags) -> ColumnDef {
19398 ColumnDef {
19399 id,
19400 name: name.into(),
19401 ty,
19402 flags,
19403 default_value: None,
19404 embedding_source: None,
19405 }
19406 }
19407
19408 fn unique_schema() -> Schema {
19409 let mut constraints = crate::constraint::TableConstraints::default();
19410 constraints
19411 .uniques
19412 .push(crate::constraint::UniqueConstraint {
19413 id: 1,
19414 name: "users_email_unique".into(),
19415 columns: vec![1],
19416 });
19417 Schema {
19418 columns: vec![
19419 col(
19420 0,
19421 "id",
19422 TypeId::Int64,
19423 crate::schema::ColumnFlags::empty()
19424 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19425 ),
19426 col(
19427 1,
19428 "email",
19429 TypeId::Bytes,
19430 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
19431 ),
19432 ],
19433 constraints,
19434 ..Schema::default()
19435 }
19436 }
19437
19438 fn parent_schema() -> Schema {
19439 Schema {
19440 columns: vec![col(
19441 0,
19442 "id",
19443 TypeId::Int64,
19444 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::PRIMARY_KEY),
19445 )],
19446 ..Schema::default()
19447 }
19448 }
19449
19450 fn child_schema() -> Schema {
19451 let mut constraints = crate::constraint::TableConstraints::default();
19452 constraints
19453 .foreign_keys
19454 .push(crate::constraint::ForeignKey {
19455 id: 1,
19456 name: "child_parent_fk".into(),
19457 columns: vec![1],
19458 ref_table: "parent".into(),
19459 ref_columns: vec![0],
19460 on_delete: crate::constraint::FkAction::Restrict,
19461 on_update: crate::constraint::FkAction::Restrict,
19462 });
19463 Schema {
19464 columns: vec![
19465 col(
19466 0,
19467 "id",
19468 TypeId::Int64,
19469 crate::schema::ColumnFlags::empty()
19470 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19471 ),
19472 col(
19473 1,
19474 "pid",
19475 TypeId::Int64,
19476 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
19477 ),
19478 ],
19479 constraints,
19480 ..Schema::default()
19481 }
19482 }
19483
19484 fn auto_inc_schema() -> Schema {
19485 Schema {
19486 columns: vec![col(
19487 0,
19488 "id",
19489 TypeId::Int64,
19490 crate::schema::ColumnFlags::empty()
19491 .with(crate::schema::ColumnFlags::PRIMARY_KEY)
19492 .with(crate::schema::ColumnFlags::AUTO_INCREMENT),
19493 )],
19494 ..Schema::default()
19495 }
19496 }
19497
19498 fn pk_lock_key(table_id: u64, value: i64) -> LockKey {
19499 let mut key = b"pk:".to_vec();
19500 key.extend_from_slice(&Value::Int64(value).encode_key());
19501 LockKey::key(table_id, key)
19502 }
19503
19504 #[test]
19505 fn unique_claims_serialize_concurrent_commits() {
19506 let dir = tempfile::tempdir().unwrap();
19507 let db = Arc::new(Database::create(dir.path()).unwrap());
19508 let table_id = db.create_table("users", unique_schema()).unwrap();
19509 let pk_key = pk_lock_key(table_id, 1);
19510 let entered = Arc::new(std::sync::Barrier::new(2));
19511 let resume = Arc::new(std::sync::Barrier::new(2));
19512 let parked = Arc::new(AtomicBool::new(false));
19513 db.__set_catalog_commit_hook({
19514 let entered = Arc::clone(&entered);
19515 let resume = Arc::clone(&resume);
19516 let parked = Arc::clone(&parked);
19517 move || {
19518 if !parked.swap(true, Ordering::SeqCst) {
19521 entered.wait();
19522 resume.wait();
19523 }
19524 }
19525 });
19526
19527 let mut txn_a = db.begin();
19528 txn_a
19529 .put(
19530 "users",
19531 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
19532 )
19533 .unwrap();
19534 let a_id = txn_a.txn_id();
19535 let (a_tx, a_rx) = std::sync::mpsc::channel();
19536 let (b_tx, b_rx) = std::sync::mpsc::channel();
19537 std::thread::scope(|scope| {
19538 scope.spawn(|| {
19539 a_tx.send(txn_a.commit()).unwrap();
19540 });
19541 entered.wait();
19542 assert!(
19544 db.lock_manager().holds(a_id, &pk_key),
19545 "primary-key claim must be held until the commit ends"
19546 );
19547 let mut uq_key = format!("uq{}:", 1).into_bytes();
19548 let cells_map: HashMap<u16, Value> = [(1u16, Value::Bytes(b"a@x".to_vec()))]
19549 .into_iter()
19550 .collect();
19551 uq_key.extend_from_slice(
19552 &crate::constraint::encode_composite_key(&[1], &cells_map).unwrap(),
19553 );
19554 assert!(
19555 db.lock_manager()
19556 .holds(a_id, &LockKey::key(table_id, uq_key)),
19557 "declared-unique claim must be held until the commit ends"
19558 );
19559
19560 let mut txn_b = db.begin();
19561 txn_b
19562 .put(
19563 "users",
19564 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"b@x".to_vec()))],
19565 )
19566 .unwrap();
19567 scope.spawn(|| {
19568 b_tx.send(txn_b.commit()).unwrap();
19569 });
19570 std::thread::sleep(std::time::Duration::from_millis(100));
19571 assert!(
19572 b_rx.try_recv().is_err(),
19573 "the concurrent claim must block until A ends its transaction"
19574 );
19575 resume.wait();
19576 assert!(a_rx.recv().unwrap().is_ok());
19577 let b_result = b_rx.recv().unwrap();
19578 assert!(
19579 matches!(b_result, Err(MongrelError::Conflict(_))),
19580 "the loser surfaces a conflict after serializing: {b_result:?}"
19581 );
19582 });
19583 assert!(
19584 !db.lock_manager().holds(a_id, &pk_key),
19585 "no phantom holds remain after the commit"
19586 );
19587 }
19588
19589 #[test]
19590 fn ddl_waits_for_inflight_dml_commit_on_schema_barrier() {
19591 let dir = tempfile::tempdir().unwrap();
19592 let db = Arc::new(Database::create(dir.path()).unwrap());
19593 db.create_table("parent", parent_schema()).unwrap();
19594 db.create_table("child", child_schema()).unwrap();
19595 let mut seed = db.begin();
19596 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
19597 seed.commit().unwrap();
19598
19599 let entered = Arc::new(std::sync::Barrier::new(2));
19600 let resume = Arc::new(std::sync::Barrier::new(2));
19601 db.__set_fk_lock_hook({
19602 let entered = Arc::clone(&entered);
19603 let resume = Arc::clone(&resume);
19604 move || {
19605 entered.wait();
19606 resume.wait();
19607 }
19608 });
19609
19610 let mut txn_a = db.begin();
19611 txn_a
19612 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(1))])
19613 .unwrap();
19614 let a_id = txn_a.txn_id();
19615 let (a_tx, a_rx) = std::sync::mpsc::channel();
19616 let (ddl_tx, ddl_rx) = std::sync::mpsc::channel();
19617 std::thread::scope(|scope| {
19618 scope.spawn(|| {
19619 a_tx.send(txn_a.commit()).unwrap();
19620 });
19621 entered.wait();
19622 assert!(
19624 db.lock_manager().holds(a_id, &LockKey::schema_barrier()),
19625 "DML holds the schema barrier Shared for its commit"
19626 );
19627 let db = Arc::clone(&db);
19628 scope.spawn(move || {
19629 ddl_tx.send(db.drop_table("parent")).unwrap();
19630 });
19631 std::thread::sleep(std::time::Duration::from_millis(100));
19632 assert!(
19633 ddl_rx.try_recv().is_err(),
19634 "DDL must wait on the Exclusive schema barrier while DML is in flight"
19635 );
19636 resume.wait();
19637 let a_result = a_rx.recv().unwrap();
19643 match &a_result {
19644 Ok(_) => {}
19645 Err(MongrelError::Conflict(message)) => {
19646 assert!(
19647 message.contains("security policy changed during write"),
19648 "unexpected commit conflict: {message}"
19649 );
19650 }
19651 other => panic!("unexpected commit outcome: {other:?}"),
19652 }
19653 assert!(ddl_rx.recv().unwrap().is_ok());
19654 });
19655 assert!(!db.lock_manager().holds(a_id, &LockKey::schema_barrier()));
19656 }
19657
19658 #[test]
19659 fn auto_increment_sequence_barrier_held_until_commit() {
19660 let dir = tempfile::tempdir().unwrap();
19661 let db = Database::create(dir.path()).unwrap();
19662 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
19663 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
19664 let entered = Arc::new(std::sync::Barrier::new(2));
19665 let resume = Arc::new(std::sync::Barrier::new(2));
19666 let parked = Arc::new(AtomicBool::new(false));
19667 db.__set_catalog_commit_hook({
19668 let entered = Arc::clone(&entered);
19669 let resume = Arc::clone(&resume);
19670 let parked = Arc::clone(&parked);
19671 move || {
19672 if !parked.swap(true, Ordering::SeqCst) {
19673 entered.wait();
19674 resume.wait();
19675 }
19676 }
19677 });
19678
19679 let mut txn_a = db.begin();
19680 txn_a.put("seq_t", vec![(0, Value::Null)]).unwrap();
19681 let a_id = txn_a.txn_id();
19682 assert!(
19684 db.lock_manager().holds(a_id, &barrier_key),
19685 "sequence allocation takes the barrier at stage time"
19686 );
19687 let (a_tx, a_rx) = std::sync::mpsc::channel();
19688 std::thread::scope(|scope| {
19689 scope.spawn(|| {
19690 a_tx.send(txn_a.commit()).unwrap();
19691 });
19692 entered.wait();
19693 assert!(
19694 db.lock_manager().holds(a_id, &barrier_key),
19695 "the barrier is held through the commit"
19696 );
19697 resume.wait();
19698 assert!(a_rx.recv().unwrap().is_ok());
19699 });
19700 assert!(
19701 !db.lock_manager().holds(a_id, &barrier_key),
19702 "the barrier releases when the commit ends"
19703 );
19704 }
19705
19706 #[test]
19707 fn fk_wait_for_cycle_surfaces_deadlock_victim() {
19708 let dir = tempfile::tempdir().unwrap();
19709 let db = Database::create(dir.path()).unwrap();
19710 db.create_table("parent", parent_schema()).unwrap();
19711 db.create_table("child", child_schema()).unwrap();
19712 let mut seed = db.begin();
19713 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
19714 seed.put("parent", vec![(0, Value::Int64(2))]).unwrap();
19715 seed.commit().unwrap();
19716 let (rid1, rid2) = {
19717 let handle = db.table("parent").unwrap();
19718 let table = handle.lock();
19719 let rid = |pk: i64| {
19720 table
19721 .lookup_pk(&Value::Int64(pk).encode_key())
19722 .expect("seeded parent row")
19723 };
19724 (rid(1), rid(2))
19725 };
19726
19727 let rendezvous = Arc::new(std::sync::Barrier::new(2));
19732 let calls = Arc::new(AtomicUsize::new(0));
19733 db.__set_fk_lock_hook({
19734 let rendezvous = Arc::clone(&rendezvous);
19735 let calls = Arc::clone(&calls);
19736 move || {
19737 if calls.fetch_add(1, Ordering::SeqCst) < 2 {
19738 rendezvous.wait();
19739 }
19740 }
19741 });
19742
19743 let mut txn_a = db.begin();
19746 txn_a.delete("parent", rid1).unwrap();
19747 txn_a
19748 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(2))])
19749 .unwrap();
19750 let mut txn_b = db.begin();
19751 txn_b.delete("parent", rid2).unwrap();
19752 txn_b
19753 .put("child", vec![(0, Value::Int64(101)), (1, Value::Int64(1))])
19754 .unwrap();
19755 let b_id = txn_b.txn_id();
19756
19757 let (a_tx, a_rx) = std::sync::mpsc::channel();
19758 let (b_tx, b_rx) = std::sync::mpsc::channel();
19759 std::thread::scope(|scope| {
19760 scope.spawn(|| {
19761 a_tx.send(txn_a.commit()).unwrap();
19762 });
19763 scope.spawn(|| {
19764 b_tx.send(txn_b.commit()).unwrap();
19765 });
19766 let a_result = a_rx.recv().unwrap();
19767 let b_result = b_rx.recv().unwrap();
19768 assert!(
19769 a_result.is_ok(),
19770 "the survivor commits once the victim releases: {a_result:?}"
19771 );
19772 match b_result {
19773 Err(MongrelError::Deadlock { victim, .. }) => {
19774 assert_eq!(victim, b_id, "the youngest transaction is the victim");
19775 }
19776 other => panic!("the victim must surface a deadlock, got {other:?}"),
19777 }
19778 });
19779 let fk_key = |table: &str, pk: i64| {
19781 let table_id = db.table_id(table).unwrap();
19782 let mut key = b"fk:".to_vec();
19783 key.extend_from_slice(&Value::Int64(pk).encode_key());
19784 LockKey::key(table_id, key)
19785 };
19786 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 2)));
19787 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 1)));
19788 }
19789
19790 #[test]
19791 fn locks_release_after_commit_rollback_and_failed_commit() {
19792 let dir = tempfile::tempdir().unwrap();
19793 let db = Database::create(dir.path()).unwrap();
19794 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
19795 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
19796
19797 let mut txn = db.begin();
19799 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
19800 let committed_id = txn.txn_id();
19801 txn.commit().unwrap();
19802 assert!(!db.lock_manager().holds(committed_id, &barrier_key));
19803
19804 let mut txn = db.begin();
19806 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
19807 let rolled_back_id = txn.txn_id();
19808 assert!(db.lock_manager().holds(rolled_back_id, &barrier_key));
19809 txn.rollback();
19810 assert!(
19811 !db.lock_manager().holds(rolled_back_id, &barrier_key),
19812 "rollback must not leave phantom holds"
19813 );
19814
19815 db.create_table("users", unique_schema()).unwrap();
19818 let users_id = db.table_id("users").unwrap();
19819 let mut seed = db.begin();
19820 seed.put(
19821 "users",
19822 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
19823 )
19824 .unwrap();
19825 seed.commit().unwrap();
19826 let mut txn = db.begin();
19827 txn.put(
19830 "users",
19831 vec![(0, Value::Int64(2)), (1, Value::Bytes(b"a@x".to_vec()))],
19832 )
19833 .unwrap();
19834 let failed_id = txn.txn_id();
19835 let result = txn.commit();
19836 assert!(matches!(result, Err(MongrelError::Conflict(_))));
19837 assert!(
19838 !db.lock_manager()
19839 .holds(failed_id, &pk_lock_key(users_id, 2)),
19840 "a failed commit must not leave phantom holds"
19841 );
19842 }
19843}
19844
19845#[cfg(test)]
19846mod lifecycle_tests {
19847 use super::*;
19848
19849 fn int_pk_schema() -> Schema {
19850 Schema {
19851 columns: vec![ColumnDef {
19852 id: 1,
19853 name: "id".into(),
19854 ty: TypeId::Int64,
19855 flags: crate::schema::ColumnFlags::empty()
19856 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19857 default_value: None,
19858 embedding_source: None,
19859 }],
19860 ..Schema::default()
19861 }
19862 }
19863
19864 #[test]
19865 fn poisoned_core_rejects_operations_with_typed_errors() {
19866 let dir = tempfile::tempdir().unwrap();
19867 let db = Database::create(dir.path()).unwrap();
19868 db.create_table("t", int_pk_schema()).unwrap();
19869 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Open);
19870
19871 db.poisoned.store(true, Ordering::Relaxed);
19876 db.lifecycle.poison();
19877 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Poisoned);
19878
19879 let error = db.gc().unwrap_err();
19882 assert!(
19883 matches!(error, MongrelError::Conflict(_)),
19884 "gc must reject on a poisoned core: {error:?}"
19885 );
19886 let error = db.compact().unwrap_err();
19887 assert!(
19888 matches!(error, MongrelError::Conflict(_)),
19889 "compact must reject on a poisoned core: {error:?}"
19890 );
19891 assert!(db.operation_guard().is_err());
19892 let error = db.create_table("t2", int_pk_schema()).unwrap_err();
19895 assert!(
19896 error.to_string().contains("database poisoned"),
19897 "the legacy poison error still wins where it existed: {error:?}"
19898 );
19899 let mut txn = db.begin();
19900 txn.put("t", vec![(1, Value::Int64(2))]).unwrap();
19901 assert!(txn
19902 .commit()
19903 .unwrap_err()
19904 .to_string()
19905 .contains("database poisoned"));
19906 }
19907
19908 #[test]
19909 fn shutdown_waits_for_operation_guards_to_drain() {
19910 let dir = tempfile::tempdir().unwrap();
19911 let db = Arc::new(Database::create(dir.path()).unwrap());
19912 db.create_table("t", int_pk_schema()).unwrap();
19913 let guard = db.operation_guard().unwrap();
19916 let (started_tx, started_rx) = std::sync::mpsc::channel();
19917 let (done_tx, done_rx) = std::sync::mpsc::channel();
19918 let shutdown_thread = std::thread::spawn(move || {
19919 started_tx.send(()).unwrap();
19920 let result = db.shutdown();
19921 let _ = done_tx.send(result);
19922 });
19923 started_rx.recv().unwrap();
19924 std::thread::sleep(std::time::Duration::from_millis(100));
19925 assert!(
19926 done_rx.try_recv().is_err(),
19927 "shutdown must wait for the outstanding guard to drain"
19928 );
19929 drop(guard);
19930 shutdown_thread.join().unwrap();
19931 assert!(
19932 done_rx.recv().unwrap().is_ok(),
19933 "shutdown completes once the guard drops"
19934 );
19935 }
19936}
19937
19938#[cfg(test)]
19939mod commit_ts_ledger_tests {
19940 use super::*;
19941
19942 fn int_pk_schema() -> Schema {
19943 Schema {
19944 columns: vec![ColumnDef {
19945 id: 1,
19946 name: "id".into(),
19947 ty: TypeId::Int64,
19948 flags: crate::schema::ColumnFlags::empty()
19949 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19950 default_value: None,
19951 embedding_source: None,
19952 }],
19953 ..Schema::default()
19954 }
19955 }
19956
19957 fn commit_one(db: &Database) -> (Epoch, mongreldb_types::hlc::HlcTimestamp) {
19958 let mut txn = db.begin();
19959 let handle = txn.state_handle();
19960 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
19961 let epoch = txn.commit().unwrap();
19962 let crate::txn::TransactionState::Committed(receipt) = handle.state() else {
19963 panic!("expected Committed, got {:?}", handle.state());
19964 };
19965 (epoch, receipt.commit_ts)
19966 }
19967
19968 #[test]
19969 fn commit_ts_for_epoch_returns_the_exact_receipt_within_one_open() {
19970 let dir = tempfile::tempdir().unwrap();
19971 let db = Database::create(dir.path()).unwrap();
19972 db.create_table("t", int_pk_schema()).unwrap();
19973
19974 let (epoch, commit_ts) = commit_one(&db);
19975 assert_eq!(db.commit_ts_for_epoch(epoch), Some(commit_ts));
19976 assert_eq!(db.commit_ts_for_epoch(Epoch(epoch.0 + 100)), None);
19978 }
19979
19980 #[test]
19981 fn commit_ts_for_epoch_survives_reopen_with_the_physical_component() {
19982 let dir = tempfile::tempdir().unwrap();
19983 let (epoch, commit_ts) = {
19984 let db = Database::create(dir.path()).unwrap();
19985 db.create_table("t", int_pk_schema()).unwrap();
19986 commit_one(&db)
19987 };
19988
19989 let db = Database::open(dir.path()).unwrap();
19990 let reconstructed = db
19991 .commit_ts_for_epoch(epoch)
19992 .expect("the durable WAL CommitTimestamp ledger reconstructs the epoch");
19993 assert_eq!(reconstructed.physical_micros, commit_ts.physical_micros);
19994 assert_eq!(reconstructed.logical, 0);
19997 assert_eq!(reconstructed.node_tiebreaker, 0);
19998 }
19999
20000 #[test]
20001 fn recovery_ledger_keeps_only_newest_epochs_and_ignores_aborted_txns() {
20002 use crate::wal::Op;
20003 let records = vec![
20004 crate::wal::Record::new(Epoch(1), 7, Op::CommitTimestamp { unix_nanos: 1_000 }),
20005 crate::wal::Record::new(
20006 Epoch(2),
20007 7,
20008 Op::TxnCommit {
20009 epoch: 41,
20010 added_runs: vec![],
20011 },
20012 ),
20013 crate::wal::Record::new(
20015 Epoch(3),
20016 8,
20017 Op::TxnCommit {
20018 epoch: 42,
20019 added_runs: vec![],
20020 },
20021 ),
20022 crate::wal::Record::new(Epoch(4), 9, Op::CommitTimestamp { unix_nanos: 9_000 }),
20024 ];
20025 let ledger = commit_ts_ledger_from_recovery(&records);
20026 assert_eq!(ledger.len(), 1);
20027 assert_eq!(
20028 ledger.get(&41),
20029 Some(&mongreldb_types::hlc::HlcTimestamp {
20030 physical_micros: 1,
20031 logical: 0,
20032 node_tiebreaker: 0,
20033 })
20034 );
20035 }
20036}
20037
20038#[cfg(test)]
20039mod stage2e_storage_mode_tests {
20040 use super::*;
20041 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20042 use crate::storage_mode::{StorageMode, STORAGE_MODE_FILENAME};
20043 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
20044
20045 fn identity(seed: u8) -> (ClusterId, NodeId, DatabaseId) {
20046 (
20047 ClusterId::from_bytes([seed; 16]),
20048 NodeId::from_bytes([seed + 1; 16]),
20049 DatabaseId::from_bytes([seed + 2; 16]),
20050 )
20051 }
20052
20053 fn marker(root: &Path) -> Option<StorageMode> {
20054 let durable = crate::durable_file::DurableRoot::open(root).unwrap();
20055 crate::storage_mode::read(&durable).unwrap()
20056 }
20057
20058 fn simple_schema() -> Schema {
20059 Schema {
20060 columns: vec![ColumnDef {
20061 id: 1,
20062 name: "id".into(),
20063 ty: TypeId::Int64,
20064 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20065 default_value: None,
20066 embedding_source: None,
20067 }],
20068 ..Schema::default()
20069 }
20070 }
20071
20072 #[test]
20073 fn standalone_create_writes_marker_and_reopens() {
20074 let dir = tempfile::tempdir().unwrap();
20075 let root = dir.path().join("db");
20076 let db = Database::create(&root).unwrap();
20077 assert_eq!(marker(&root), Some(StorageMode::Standalone));
20078 assert_eq!(db.storage_mode().unwrap(), Some(StorageMode::Standalone));
20079 drop(db);
20080 let db = Database::open(&root).unwrap();
20081 assert_eq!(marker(&root), Some(StorageMode::Standalone));
20082 drop(db);
20083 }
20084
20085 #[test]
20086 fn legacy_database_without_marker_opens_and_gains_marker() {
20087 let dir = tempfile::tempdir().unwrap();
20088 let root = dir.path().join("db");
20089 let db = Database::create(&root).unwrap();
20090 drop(db);
20091 std::fs::remove_file(root.join(META_DIR).join(STORAGE_MODE_FILENAME)).unwrap();
20093 assert_eq!(marker(&root), None);
20094 let db = Database::open(&root).unwrap();
20095 assert_eq!(marker(&root), Some(StorageMode::Standalone));
20096 drop(db);
20097 }
20098
20099 #[test]
20100 fn server_owned_standalone_opens_embedded() {
20101 let dir = tempfile::tempdir().unwrap();
20102 let root = dir.path().join("db");
20103 let db = Database::create(&root).unwrap();
20104 drop(db);
20105 let durable = crate::durable_file::DurableRoot::open(&root).unwrap();
20106 crate::storage_mode::rewrite(&durable, &StorageMode::ServerOwnedStandalone).unwrap();
20107 let db = Database::open(&root).unwrap();
20108 assert_eq!(marker(&root), Some(StorageMode::ServerOwnedStandalone));
20109 drop(db);
20110 }
20111
20112 #[test]
20113 fn cluster_replica_is_rejected_by_normal_opens() {
20114 let dir = tempfile::tempdir().unwrap();
20115 let root = dir.path().join("db");
20116 let (cluster_id, node_id, database_id) = identity(10);
20117 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
20118 assert_eq!(
20119 marker(&root),
20120 Some(StorageMode::ClusterReplica {
20121 cluster_id,
20122 node_id,
20123 database_id,
20124 })
20125 );
20126 drop(db);
20127
20128 let error = Database::open(&root).unwrap_err();
20129 let message = error.to_string();
20130 assert!(
20131 matches!(error, MongrelError::InvalidArgument(_)),
20132 "unexpected error: {message}"
20133 );
20134 assert!(message.contains("cluster node runtime"), "{message}");
20135 assert!(message.contains(&cluster_id.to_hex()), "{message}");
20136 assert!(message.contains(&node_id.to_hex()), "{message}");
20137 assert!(message.contains(&database_id.to_hex()), "{message}");
20138
20139 let error = Database::open_with_options(&root, OpenOptions::default()).unwrap_err();
20140 assert!(error.to_string().contains("cluster node runtime"));
20141 assert_eq!(
20143 marker(&root),
20144 Some(StorageMode::ClusterReplica {
20145 cluster_id,
20146 node_id,
20147 database_id,
20148 })
20149 );
20150 }
20151
20152 #[test]
20153 fn offline_validation_opens_cluster_replica_read_only() {
20154 let dir = tempfile::tempdir().unwrap();
20155 let root = dir.path().join("db");
20156 let (cluster_id, node_id, database_id) = identity(20);
20157 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
20158 drop(db);
20159
20160 let options = OpenOptions::default().with_offline_validation(true);
20161 let db = Database::open_with_options(&root, options).unwrap();
20162 assert!(db.is_read_only_replica());
20163 let error = db.create_table("t", simple_schema()).unwrap_err();
20164 assert!(matches!(error, MongrelError::ReadOnlyReplica));
20165 drop(db);
20166 assert_eq!(
20168 marker(&root),
20169 Some(StorageMode::ClusterReplica {
20170 cluster_id,
20171 node_id,
20172 database_id,
20173 })
20174 );
20175 }
20176
20177 #[test]
20178 fn cluster_runtime_open_requires_exact_identity() {
20179 let dir = tempfile::tempdir().unwrap();
20180 let root = dir.path().join("db");
20181 let (cluster_id, node_id, database_id) = identity(30);
20182 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
20183 drop(db);
20184
20185 let error = Database::open_cluster_replica(&root, &StorageMode::Standalone).unwrap_err();
20187 assert!(matches!(error, MongrelError::InvalidArgument(_)));
20188 let wrong = StorageMode::ClusterReplica {
20190 cluster_id,
20191 node_id,
20192 database_id: DatabaseId::from_bytes([99; 16]),
20193 };
20194 let error = Database::open_cluster_replica(&root, &wrong).unwrap_err();
20195 assert!(error.to_string().contains("identity mismatch"), "{error}");
20196 let legacy = dir.path().join("legacy");
20198 let legacy_db = Database::create(&legacy).unwrap();
20199 drop(legacy_db);
20200 let expected = StorageMode::ClusterReplica {
20201 cluster_id,
20202 node_id,
20203 database_id,
20204 };
20205 let error = Database::open_cluster_replica(&legacy, &expected).unwrap_err();
20206 assert!(error.to_string().contains("identity mismatch"), "{error}");
20207
20208 let db = Database::open_cluster_replica(&root, &expected).unwrap();
20211 assert!(db.is_read_only_replica());
20212 let error = db.create_table("t", simple_schema()).unwrap_err();
20213 assert!(matches!(error, MongrelError::ReadOnlyReplica));
20214 drop(db);
20215 }
20216}
20217
20218#[cfg(test)]
20219mod stage2e_replicated_apply_tests {
20220 use super::*;
20221 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord, CatalogDelta};
20222 use crate::memtable::{Row, Value};
20223 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20224 use crate::wal::{Op, Record};
20225 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
20226 use std::sync::Arc;
20227
20228 fn ids() -> (ClusterId, NodeId, DatabaseId) {
20229 (
20230 ClusterId::from_bytes([1; 16]),
20231 NodeId::from_bytes([2; 16]),
20232 DatabaseId::from_bytes([3; 16]),
20233 )
20234 }
20235
20236 fn expected_mode() -> crate::storage_mode::StorageMode {
20237 let (cluster_id, node_id, database_id) = ids();
20238 crate::storage_mode::StorageMode::ClusterReplica {
20239 cluster_id,
20240 node_id,
20241 database_id,
20242 }
20243 }
20244
20245 fn simple_schema() -> Schema {
20246 Schema {
20247 columns: vec![ColumnDef {
20248 id: 1,
20249 name: "id".into(),
20250 ty: TypeId::Int64,
20251 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20252 default_value: None,
20253 embedding_source: None,
20254 }],
20255 ..Schema::default()
20256 }
20257 }
20258
20259 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
20260 CatalogCommandRecord {
20261 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
20262 catalog_version,
20263 command: CatalogCommand::CreateTable {
20264 name: name.to_string(),
20265 schema: simple_schema(),
20266 created_epoch: 1,
20267 },
20268 }
20269 }
20270
20271 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
20272 let rows: Vec<Row> = values
20273 .iter()
20274 .map(|value| {
20275 Row::new(crate::RowId(*value as u64), Epoch(epoch))
20278 .with_column(1, Value::Int64(*value))
20279 })
20280 .collect();
20281 vec![
20282 Record::new(
20283 Epoch(0),
20284 txn_id,
20285 Op::Put {
20286 table_id,
20287 rows: bincode::serialize(&rows).unwrap(),
20288 },
20289 ),
20290 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
20291 Record::new(
20292 Epoch(0),
20293 txn_id,
20294 Op::TxnCommit {
20295 epoch,
20296 added_runs: Vec::new(),
20297 },
20298 ),
20299 ]
20300 }
20301
20302 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
20303 let handle = db.table(table).unwrap();
20304 let rows = handle
20305 .lock()
20306 .visible_rows(crate::epoch::Snapshot::at(Epoch(u64::MAX)))
20307 .unwrap();
20308 let mut values: Vec<i64> = rows
20309 .iter()
20310 .map(|row| match row.columns.get(&1) {
20311 Some(Value::Int64(value)) => *value,
20312 other => panic!("unexpected column: {other:?}"),
20313 })
20314 .collect();
20315 values.sort_unstable();
20316 values
20317 }
20318
20319 #[test]
20320 fn catalog_command_mounts_table_and_replays_as_noop() {
20321 let dir = tempfile::tempdir().unwrap();
20322 let (cluster_id, node_id, database_id) = ids();
20323 let db =
20324 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20325
20326 let record = create_table_record("items", 1);
20327 let delta = db.apply_replicated_catalog_command(&record).unwrap();
20328 assert!(matches!(delta, CatalogDelta::TableCreated { .. }));
20329 assert_eq!(db.table_names(), vec!["items".to_string()]);
20330 assert_eq!(db.catalog_version(), 1);
20331
20332 let delta = db.apply_replicated_catalog_command(&record).unwrap();
20334 assert!(matches!(delta, CatalogDelta::NoOp));
20335 assert_eq!(db.table_names().len(), 1);
20336 drop(db);
20337
20338 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
20340 assert_eq!(db.table_names(), vec!["items".to_string()]);
20341 assert_eq!(db.catalog_version(), 1);
20342 }
20343
20344 #[test]
20345 fn records_apply_rows_and_skip_replays_across_restart() {
20346 let dir = tempfile::tempdir().unwrap();
20347 let (cluster_id, node_id, database_id) = ids();
20348 let db =
20349 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20350 db.apply_replicated_catalog_command(&create_table_record("items", 1))
20351 .unwrap();
20352
20353 let records = put_records(1, 0, 2, &[10, 20, 30]);
20354 assert!(db.apply_replicated_records(&records).unwrap());
20355 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
20356 assert_eq!(db.visible_epoch(), Epoch(2));
20357
20358 assert!(!db.apply_replicated_records(&records).unwrap());
20361 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
20362
20363 let later = put_records(2, 0, 3, &[40]);
20365 assert!(db.apply_replicated_records(&later).unwrap());
20366 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
20367 let db = Arc::new(db);
20368 db.shutdown().unwrap();
20369
20370 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
20373 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
20374 assert!(!db.apply_replicated_records(&later).unwrap());
20375 assert!(!db.apply_replicated_records(&records).unwrap());
20376 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
20377 }
20378
20379 #[test]
20380 fn spilled_run_commits_fail_closed_this_wave() {
20381 let dir = tempfile::tempdir().unwrap();
20382 let (cluster_id, node_id, database_id) = ids();
20383 let db =
20384 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20385 db.apply_replicated_catalog_command(&create_table_record("items", 1))
20386 .unwrap();
20387 let mut records = put_records(1, 0, 2, &[10]);
20388 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20389 panic!("put_records ends in TxnCommit");
20390 };
20391 added_runs.push(crate::wal::AddedRun {
20392 table_id: 0,
20393 run_id: 7,
20394 row_count: 1,
20395 level: 0,
20396 min_row_id: 1,
20397 max_row_id: 1,
20398 content_hash: [0; 32],
20399 });
20400 let error = db.apply_replicated_records(&records).unwrap_err();
20401 assert!(
20402 error.to_string().contains("spilled-run"),
20403 "unexpected error: {error}"
20404 );
20405 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
20406 }
20407
20408 #[test]
20409 fn records_without_commit_marker_fail_closed() {
20410 let dir = tempfile::tempdir().unwrap();
20411 let (cluster_id, node_id, database_id) = ids();
20412 let db =
20413 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
20414 db.apply_replicated_catalog_command(&create_table_record("items", 1))
20415 .unwrap();
20416 let mut records = put_records(1, 0, 2, &[10]);
20417 records.pop(); let error = db.apply_replicated_records(&records).unwrap_err();
20419 assert!(matches!(error, MongrelError::InvalidArgument(_)));
20420 assert!(db.apply_replicated_records(&[]).is_err());
20421 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
20422 }
20423}
20424
20425#[cfg(test)]
20426mod stage2c_spill_translation_tests {
20427 use super::*;
20428 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord};
20429 use crate::memtable::{Row, Value};
20430 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20431 use crate::wal::{Op, Record};
20432 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
20433
20434 fn simple_schema() -> Schema {
20435 Schema {
20436 columns: vec![ColumnDef {
20437 id: 1,
20438 name: "id".into(),
20439 ty: TypeId::Int64,
20440 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20441 default_value: None,
20442 embedding_source: None,
20443 }],
20444 ..Schema::default()
20445 }
20446 }
20447
20448 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
20449 CatalogCommandRecord {
20450 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
20451 catalog_version,
20452 command: CatalogCommand::CreateTable {
20453 name: name.to_string(),
20454 schema: simple_schema(),
20455 created_epoch: 1,
20456 },
20457 }
20458 }
20459
20460 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
20461 let handle = db.table(table).unwrap();
20462 let rows = handle
20463 .lock()
20464 .visible_rows(crate::epoch::Snapshot::at(Epoch(u64::MAX)))
20465 .unwrap();
20466 let mut values: Vec<i64> = rows
20467 .iter()
20468 .map(|row| match row.columns.get(&1) {
20469 Some(Value::Int64(value)) => *value,
20470 other => panic!("unexpected column: {other:?}"),
20471 })
20472 .collect();
20473 values.sort_unstable();
20474 values
20475 }
20476
20477 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
20478 let rows: Vec<Row> = values
20479 .iter()
20480 .map(|value| {
20481 Row::new(crate::RowId(*value as u64), Epoch(epoch))
20482 .with_column(1, Value::Int64(*value))
20483 })
20484 .collect();
20485 vec![
20486 Record::new(
20487 Epoch(0),
20488 txn_id,
20489 Op::Put {
20490 table_id,
20491 rows: bincode::serialize(&rows).unwrap(),
20492 },
20493 ),
20494 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
20495 Record::new(
20496 Epoch(0),
20497 txn_id,
20498 Op::TxnCommit {
20499 epoch,
20500 added_runs: Vec::new(),
20501 },
20502 ),
20503 ]
20504 }
20505
20506 fn added_run(
20507 table_id: u64,
20508 row_count: u64,
20509 min_row_id: u64,
20510 max_row_id: u64,
20511 ) -> crate::wal::AddedRun {
20512 crate::wal::AddedRun {
20513 table_id,
20514 run_id: 7,
20515 row_count,
20516 level: 0,
20517 min_row_id,
20518 max_row_id,
20519 content_hash: [0; 32],
20520 }
20521 }
20522
20523 fn spilled_commit_records(db: &Database) -> Vec<Record> {
20526 let records = crate::wal::SharedWal::replay_with_dek(&db.root, None).unwrap();
20527 let txn_id = records
20528 .iter()
20529 .find_map(|record| match &record.op {
20530 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty() => Some(record.txn_id),
20531 _ => None,
20532 })
20533 .expect("a spilled commit is present in the WAL");
20534 records
20535 .into_iter()
20536 .filter(|record| record.txn_id == txn_id)
20537 .collect()
20538 }
20539
20540 #[test]
20541 fn non_spilled_records_translate_byte_identical() {
20542 let records = put_records(1, 0, 2, &[10, 20, 30]);
20543 let translated = translate_records_for_replication(&records).unwrap();
20544 assert_eq!(
20545 bincode::serialize(&translated).unwrap(),
20546 bincode::serialize(&records).unwrap(),
20547 "a commit without spill links must pass through byte-identical"
20548 );
20549 }
20550
20551 #[test]
20552 fn translation_rejects_uncovered_or_malformed_spills() {
20553 let mut records = put_records(1, 0, 2, &[10]);
20555 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20556 panic!("put_records ends in TxnCommit");
20557 };
20558 added_runs.push(added_run(0, 1, 10, 10));
20559 let error = translate_records_for_replication(&records).unwrap_err();
20560 assert!(
20561 error.to_string().contains("no logical row records"),
20562 "unexpected error: {error}"
20563 );
20564
20565 let mut records = put_records(1, 0, 2, &[10]);
20567 let spilled: Vec<Row> = (0..3_u64)
20568 .map(|value| {
20569 Row::new(crate::RowId(value), Epoch(2)).with_column(1, Value::Int64(value as i64))
20570 })
20571 .collect();
20572 records.insert(
20573 0,
20574 Record::new(
20575 Epoch(0),
20576 1,
20577 Op::SpilledRows {
20578 table_id: 0,
20579 rows: bincode::serialize(&spilled).unwrap(),
20580 },
20581 ),
20582 );
20583 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20584 panic!("put_records ends in TxnCommit");
20585 };
20586 added_runs.push(added_run(0, 4, 0, 3));
20587 let error = translate_records_for_replication(&records).unwrap_err();
20588 assert!(
20589 error.to_string().contains("cover 3 rows"),
20590 "unexpected error: {error}"
20591 );
20592
20593 let mut records = put_records(1, 0, 2, &[10]);
20595 records.insert(
20596 0,
20597 Record::new(
20598 Epoch(0),
20599 1,
20600 Op::SpilledRows {
20601 table_id: 0,
20602 rows: vec![0xFF, 0x01, 0x02],
20603 },
20604 ),
20605 );
20606 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
20607 panic!("put_records ends in TxnCommit");
20608 };
20609 added_runs.push(added_run(0, 1, 0, 0));
20610 assert!(translate_records_for_replication(&records).is_err());
20611
20612 assert!(translate_records_for_replication(&[]).is_err());
20614 let mut mixed = put_records(1, 0, 2, &[10]);
20615 mixed[0].txn_id = 99;
20616 assert!(translate_records_for_replication(&mixed).is_err());
20617 let mut no_commit = put_records(1, 0, 2, &[10]);
20618 no_commit.pop();
20619 assert!(translate_records_for_replication(&no_commit).is_err());
20620 }
20621
20622 #[test]
20623 fn spilled_commit_translates_to_logical_rows_and_applies_on_replica() {
20624 let leader_dir = tempfile::tempdir().unwrap();
20626 let leader = Database::create(leader_dir.path()).unwrap();
20627 leader.create_table("t", simple_schema()).unwrap();
20628 leader.set_spill_threshold(1);
20629 let table_id = leader.table_id("t").unwrap();
20630 let values: Vec<i64> = (0..60).collect();
20631 leader
20632 .transaction(|txn| {
20633 for value in &values {
20634 txn.put("t", vec![(1, Value::Int64(*value))])?;
20635 }
20636 Ok(())
20637 })
20638 .unwrap();
20639 assert_eq!(visible_ids(&leader, "t"), values);
20640
20641 let records = spilled_commit_records(&leader);
20644 assert!(records
20645 .iter()
20646 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
20647 let Some(Op::TxnCommit { added_runs, epoch }) = records.last().map(|r| &r.op) else {
20648 panic!("a commit sequence ends in TxnCommit");
20649 };
20650 assert!(!added_runs.is_empty());
20651 let commit_epoch = *epoch;
20652
20653 let translated = translate_records_for_replication(&records).unwrap();
20656 assert!(records
20657 .iter()
20658 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
20659 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
20660 panic!("a commit sequence ends in TxnCommit");
20661 };
20662 assert!(!added_runs.is_empty(), "input records must be unchanged");
20663 assert_eq!(translated.len(), records.len());
20664 assert!(translated
20665 .iter()
20666 .all(|record| !matches!(record.op, Op::SpilledRows { .. })));
20667 assert!(translated
20668 .iter()
20669 .any(|record| matches!(record.op, Op::Put { .. })));
20670 let Some(Op::TxnCommit { added_runs, epoch }) = translated.last().map(|r| &r.op) else {
20671 panic!("a commit sequence ends in TxnCommit");
20672 };
20673 assert!(added_runs.is_empty(), "no added_runs may reach a replica");
20674 assert_eq!(*epoch, commit_epoch);
20675
20676 let replica_dir = tempfile::tempdir().unwrap();
20678 let replica = Database::create_cluster_replica(
20679 replica_dir.path(),
20680 ClusterId::from_bytes([1; 16]),
20681 NodeId::from_bytes([2; 16]),
20682 DatabaseId::from_bytes([3; 16]),
20683 )
20684 .unwrap();
20685 replica
20686 .apply_replicated_catalog_command(&create_table_record("t", 1))
20687 .unwrap();
20688 assert_eq!(replica.table_id("t").unwrap(), table_id);
20689 assert!(replica.apply_replicated_records(&translated).unwrap());
20690 assert_eq!(visible_ids(&replica, "t"), values);
20691 assert_eq!(visible_ids(&replica, "t"), visible_ids(&leader, "t"));
20692
20693 drop(leader);
20696 let leader = Database::open(leader_dir.path()).unwrap();
20697 assert_eq!(visible_ids(&leader, "t"), values);
20698 }
20699
20700 #[test]
20701 fn staged_txn_writes_validate_and_apply() {
20702 let dir = tempfile::tempdir().unwrap();
20703 let db = Database::create_cluster_replica(
20704 dir.path(),
20705 ClusterId::from_bytes([1; 16]),
20706 NodeId::from_bytes([2; 16]),
20707 DatabaseId::from_bytes([3; 16]),
20708 )
20709 .unwrap();
20710 db.apply_replicated_catalog_command(&create_table_record("t", 1))
20711 .unwrap();
20712 let table_id = db.table_id("t").unwrap();
20713
20714 assert!(db.validate_staged_txn_writes(&[vec![0xFF]]).is_err());
20716 let unknown_table = StagedTxnWrite::Put {
20717 table_id: 99,
20718 rows: bincode::serialize(&Vec::<Row>::new()).unwrap(),
20719 }
20720 .encode()
20721 .unwrap();
20722 assert!(db.validate_staged_txn_writes(&[unknown_table]).is_err());
20723 let good: Vec<Vec<u8>> = [10_i64, 20, 30]
20724 .iter()
20725 .map(|value| {
20726 let rows = vec![Row::new(crate::RowId(*value as u64), Epoch(0))
20727 .with_column(1, Value::Int64(*value))];
20728 StagedTxnWrite::Put {
20729 table_id,
20730 rows: bincode::serialize(&rows).unwrap(),
20731 }
20732 .encode()
20733 .unwrap()
20734 })
20735 .collect();
20736 db.validate_staged_txn_writes(&good).unwrap();
20737
20738 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
20741 physical_micros: 5_000,
20742 logical: 0,
20743 node_tiebreaker: 0,
20744 };
20745 assert!(db
20746 .apply_staged_txn_writes(1 << 63, &good, commit_ts)
20747 .unwrap());
20748 assert_eq!(visible_ids(&db, "t"), vec![10, 20, 30]);
20749 let delete = StagedTxnWrite::Delete {
20750 table_id,
20751 row_ids: vec![20],
20752 }
20753 .encode()
20754 .unwrap();
20755 assert!(db
20756 .apply_staged_txn_writes((1 << 63) + 1, &[delete], commit_ts)
20757 .unwrap());
20758 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
20759
20760 let db = Arc::new(db);
20763 db.shutdown().unwrap();
20764 let expected = crate::storage_mode::StorageMode::ClusterReplica {
20765 cluster_id: ClusterId::from_bytes([1; 16]),
20766 node_id: NodeId::from_bytes([2; 16]),
20767 database_id: DatabaseId::from_bytes([3; 16]),
20768 };
20769 let db = Database::open_cluster_replica(dir.path(), &expected).unwrap();
20770 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
20771 }
20772}