1#[path = "index_ddl.rs"]
13mod index_ddl;
14
15use crate::catalog::{self, Catalog, CatalogEntry, TableState, META_DEK_LEN};
16use crate::engine::{SharedCtx, Table};
17use crate::epoch::{Epoch, EpochAuthority, EpochGuard, MaintenanceReceipt, Snapshot};
18use crate::error::{MongrelError, Result};
19use crate::external_table::ExternalTableEntry;
20use crate::memtable::Value;
21use crate::procedure::{
22 ProcedureCallOutput, ProcedureCallResult, ProcedureCallRow, ProcedureCondition, ProcedureStep,
23 ProcedureValue, StoredProcedure,
24};
25use crate::retention::{OwnedSnapshotGuard, SnapshotGuard, SnapshotRegistry};
26use crate::rowid::RowId;
27use crate::schema::{AlterColumn, ColumnDef, ColumnFlags, Schema, TypeId};
28use crate::trigger::{
29 StoredTrigger, TriggerCondition, TriggerConfig, TriggerEntry, TriggerEvent, TriggerExpr,
30 TriggerRaiseAction, TriggerStep, TriggerTarget, TriggerTiming, TriggerValue,
31};
32use parking_lot::{Mutex, RwLock};
33use sha2::{Digest, Sha256};
34use std::collections::{HashMap, HashSet, VecDeque};
35use std::io::{Read, Write};
36use std::path::{Path, PathBuf};
37use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
38use std::sync::Arc;
39use subtle::ConstantTimeEq;
40
41pub const TABLES_DIR: &str = "tables";
42pub const VTAB_DIR: &str = "_vtab";
43pub const META_DIR: &str = "_meta";
44pub const KEYS_FILENAME: &str = "keys";
45pub const KMS_KEY_FILENAME: &str = "kms_key.json";
46const MAX_KMS_KEY_ENVELOPE_BYTES: usize = 1024 * 1024;
47pub const HISTORY_RETENTION_FILENAME: &str = "history_retention";
48pub const CTAS_BUILD_TABLE_PREFIX: &str = "__mongreldb_ctas_build_";
49
50pub const WAL_TABLE_ID: u64 = u64::MAX;
54pub const EXTERNAL_TABLE_ID: u64 = u64::MAX - 1;
57
58fn advance_security_version(catalog: &mut Catalog) -> Result<()> {
59 catalog.security_version = catalog.security_version.checked_add(1).ok_or_else(|| {
60 MongrelError::Conflict("security catalog version space is exhausted".into())
61 })?;
62 Ok(())
63}
64
65type OpenLeaseId = u64;
66
67static DATABASE_OPEN_WAIT_COUNT: AtomicU64 = AtomicU64::new(0);
68static DATABASE_OPEN_FAILURE_COUNT: AtomicU64 = AtomicU64::new(0);
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct DatabaseOpenMetrics {
72 pub lock_waits: u64,
73 pub failures: u64,
74}
75
76#[derive(Clone, Debug, Eq, Hash, PartialEq)]
77enum DatabaseOpenKey {
78 IntendedPath(PathBuf),
79 FileIdentity(crate::durable_file::DurableFileIdentity),
80}
81
82#[derive(Debug)]
83enum ProcessOpenState {
84 Opening { lease_id: OpenLeaseId },
85 Open { lease_id: OpenLeaseId },
86 Closing { lease_id: OpenLeaseId },
87}
88
89impl ProcessOpenState {
90 fn lease_id(&self) -> OpenLeaseId {
91 match self {
92 Self::Opening { lease_id } | Self::Open { lease_id } | Self::Closing { lease_id } => {
93 *lease_id
94 }
95 }
96 }
97}
98
99fn embedding_error(error: crate::embedding::EmbeddingError) -> MongrelError {
100 match error {
101 crate::embedding::EmbeddingError::LimitExceeded {
102 resource,
103 requested,
104 limit,
105 } => MongrelError::ResourceLimitExceeded {
106 resource,
107 requested,
108 limit,
109 },
110 crate::embedding::EmbeddingError::Execution(message) => {
111 if message.contains("deadline") {
112 MongrelError::DeadlineExceeded
113 } else {
114 MongrelError::Cancelled
115 }
116 }
117 error => MongrelError::Other(error.to_string()),
118 }
119}
120
121fn render_embedding_input(
122 schema: &Schema,
123 spec: &crate::embedding::GeneratedEmbeddingSpec,
124 cells: &[(u16, crate::memtable::Value)],
125) -> Result<String> {
126 let mut values = Vec::with_capacity(spec.source_columns.len());
127 for source_id in &spec.source_columns {
128 let column = schema
129 .columns
130 .iter()
131 .find(|column| column.id == *source_id)
132 .ok_or_else(|| MongrelError::Schema(format!("unknown embedding source {source_id}")))?;
133 let value = cells
134 .iter()
135 .find(|(id, _)| id == source_id)
136 .map(|(_, value)| embedding_input_value(value))
137 .transpose()?
138 .unwrap_or_default();
139 values.push((column.name.as_str(), value));
140 }
141 if spec.input_template.is_empty() {
142 return Ok(values
143 .into_iter()
144 .map(|(_, value)| value)
145 .collect::<Vec<_>>()
146 .join("\n"));
147 }
148 let mut rendered = spec.input_template.clone();
149 for (name, value) in values {
150 rendered = rendered.replace(&format!("{{{name}}}"), &value);
151 }
152 if rendered.contains('{') || rendered.contains('}') {
153 return Err(MongrelError::Schema(
154 "embedding input template contains an unknown placeholder".into(),
155 ));
156 }
157 Ok(rendered)
158}
159
160fn embedding_input_value(value: &crate::memtable::Value) -> Result<String> {
161 use crate::memtable::Value;
162 match value {
163 Value::Null => Ok(String::new()),
164 Value::Bool(value) => Ok(value.to_string()),
165 Value::Int64(value) => Ok(value.to_string()),
166 Value::Float64(value) => Ok(value.to_string()),
167 Value::Bytes(value) | Value::Json(value) => String::from_utf8(value.clone())
168 .map_err(|_| MongrelError::InvalidArgument("embedding source is not UTF-8".into())),
169 Value::Decimal(value) => Ok(value.to_string()),
170 Value::Interval {
171 months,
172 days,
173 nanos,
174 } => Ok(format!("{months}:{days}:{nanos}")),
175 Value::Uuid(value) => Ok(value.iter().map(|byte| format!("{byte:02x}")).collect()),
176 Value::Embedding(_) | Value::GeneratedEmbedding(_) => Err(MongrelError::InvalidArgument(
177 "embedding columns cannot be embedding text sources".into(),
178 )),
179 }
180}
181
182#[derive(Default)]
183struct ProcessOpenRegistry {
184 next_lease_id: OpenLeaseId,
185 entries: HashMap<DatabaseOpenKey, ProcessOpenState>,
186}
187
188fn process_open_registry() -> &'static Mutex<ProcessOpenRegistry> {
189 static REGISTRY: std::sync::OnceLock<Mutex<ProcessOpenRegistry>> = std::sync::OnceLock::new();
190 REGISTRY.get_or_init(|| Mutex::new(ProcessOpenRegistry::default()))
191}
192
193fn same_process_locked(path: &Path) -> MongrelError {
194 MongrelError::DatabaseLocked {
195 path: path.to_path_buf(),
196 message: "database is already open in this process; reuse the existing Arc<Database>"
197 .into(),
198 }
199}
200
201struct OpenReservation {
202 lease_id: OpenLeaseId,
203 keys: Vec<DatabaseOpenKey>,
204 committed: bool,
205}
206
207impl OpenReservation {
208 fn acquire(key: DatabaseOpenKey, display_path: &Path) -> Result<Self> {
209 let mut registry = process_open_registry().lock();
210 if registry.entries.contains_key(&key) {
211 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
212 return Err(same_process_locked(display_path));
213 }
214 registry.next_lease_id = registry.next_lease_id.checked_add(1).ok_or_else(|| {
215 MongrelError::Full("process database-open lease namespace exhausted".into())
216 })?;
217 let lease_id = registry.next_lease_id;
218 registry
219 .entries
220 .insert(key.clone(), ProcessOpenState::Opening { lease_id });
221 Ok(Self {
222 lease_id,
223 keys: vec![key],
224 committed: false,
225 })
226 }
227
228 fn into_lease(
229 mut self,
230 bootstrap_file: std::fs::File,
231 canonical_path: PathBuf,
232 ) -> ExclusiveDatabaseLease {
233 self.committed = true;
234 ExclusiveDatabaseLease {
235 lease_id: self.lease_id,
236 keys: std::mem::take(&mut self.keys),
237 bootstrap_file,
238 legacy_file: None,
239 canonical_path,
240 durable_root: None,
241 owner_pid: std::process::id(),
242 opened: false,
243 }
244 }
245}
246
247impl Drop for OpenReservation {
248 fn drop(&mut self) {
249 if self.committed {
250 return;
251 }
252 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
253 let mut registry = process_open_registry().lock();
254 for key in &self.keys {
255 if registry
256 .entries
257 .get(key)
258 .is_some_and(|state| state.lease_id() == self.lease_id)
259 {
260 registry.entries.remove(key);
261 }
262 }
263 }
264}
265
266struct ExclusiveDatabaseLease {
267 lease_id: OpenLeaseId,
268 keys: Vec<DatabaseOpenKey>,
269 bootstrap_file: std::fs::File,
270 legacy_file: Option<std::fs::File>,
271 canonical_path: PathBuf,
272 durable_root: Option<Arc<crate::durable_file::DurableRoot>>,
273 owner_pid: u32,
274 opened: bool,
275}
276
277impl ExclusiveDatabaseLease {
278 fn claim_root_identity(&mut self, root: &crate::durable_file::DurableRoot) -> Result<()> {
279 let key = DatabaseOpenKey::FileIdentity(root.file_identity()?);
280 if self.keys.contains(&key) {
281 return Ok(());
282 }
283 let mut registry = process_open_registry().lock();
284 if registry.entries.contains_key(&key) {
285 return Err(same_process_locked(&self.canonical_path));
286 }
287 registry.entries.insert(
288 key.clone(),
289 ProcessOpenState::Opening {
290 lease_id: self.lease_id,
291 },
292 );
293 self.keys.push(key);
294 Ok(())
295 }
296
297 fn mark_open(&mut self) -> Result<()> {
298 let mut registry = process_open_registry().lock();
299 if self.keys.iter().any(|key| {
300 registry
301 .entries
302 .get(key)
303 .is_none_or(|state| state.lease_id() != self.lease_id)
304 }) {
305 return Err(MongrelError::Conflict(
306 "database-open reservation changed during initialization".into(),
307 ));
308 }
309 for key in &self.keys {
310 registry.entries.insert(
311 key.clone(),
312 ProcessOpenState::Open {
313 lease_id: self.lease_id,
314 },
315 );
316 }
317 self.opened = true;
318 Ok(())
319 }
320}
321
322impl Drop for ExclusiveDatabaseLease {
323 fn drop(&mut self) {
324 if std::process::id() != self.owner_pid {
325 return;
326 }
327 if !self.opened {
328 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
329 }
330 {
331 let mut registry = process_open_registry().lock();
332 for key in &self.keys {
333 if registry
334 .entries
335 .get(key)
336 .is_some_and(|state| state.lease_id() == self.lease_id)
337 {
338 registry.entries.insert(
339 key.clone(),
340 ProcessOpenState::Closing {
341 lease_id: self.lease_id,
342 },
343 );
344 }
345 }
346 }
347 if let Some(file) = &self.legacy_file {
348 let _ = fs2::FileExt::unlock(file);
349 }
350 let _ = fs2::FileExt::unlock(&self.bootstrap_file);
351 let mut registry = process_open_registry().lock();
352 for key in &self.keys {
353 if registry
354 .entries
355 .get(key)
356 .is_some_and(|state| state.lease_id() == self.lease_id)
357 {
358 registry.entries.remove(key);
359 }
360 }
361 }
362}
363
364fn commit_prepare_checkpoint(
365 control: Option<&crate::ExecutionControl>,
366 index: usize,
367) -> Result<()> {
368 if index.is_multiple_of(256) {
369 if let Some(control) = control {
370 control.checkpoint()?;
371 }
372 }
373 Ok(())
374}
375
376fn finish_controlled_commit_attempt(
377 result: Result<Epoch>,
378 after_commit: &mut Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
379) -> Result<Epoch> {
380 let Some(after_commit) = after_commit.as_mut() else {
381 return result;
382 };
383 match result {
384 Ok(epoch) => match (**after_commit)(Some(epoch)) {
385 Ok(()) => Ok(epoch),
386 Err(error) => Err(MongrelError::DurableCommit {
387 epoch: epoch.0,
388 message: error.to_string(),
389 }),
390 },
391 Err(MongrelError::DurableCommit { epoch, message }) => {
392 let callback_error = (**after_commit)(Some(Epoch(epoch))).err();
393 Err(MongrelError::DurableCommit {
394 epoch,
395 message: callback_error
396 .map(|error| format!("{message}; commit callback: {error}"))
397 .unwrap_or(message),
398 })
399 }
400 Err(error) => match (**after_commit)(None) {
401 Ok(()) => Err(error),
402 Err(callback_error) => Err(MongrelError::Other(format!(
403 "{error}; commit callback: {callback_error}"
404 ))),
405 },
406 }
407}
408
409fn current_unix_nanos() -> u64 {
410 std::time::SystemTime::now()
411 .duration_since(std::time::UNIX_EPOCH)
412 .unwrap_or_default()
413 .as_nanos() as u64
414}
415
416fn read_encryption_salt(
417 root: &crate::durable_file::DurableRoot,
418) -> Result<[u8; crate::encryption::SALT_LEN]> {
419 let mut file = root
420 .open_regular(Path::new(META_DIR).join(KEYS_FILENAME))
421 .map_err(|error| MongrelError::NotFound(format!("encryption salt file: {error}")))?;
422 let length = file.metadata()?.len();
423 if length != crate::encryption::SALT_LEN as u64 {
424 return Err(MongrelError::Encryption(format!(
425 "invalid encryption salt length: got {length}, expected {}",
426 crate::encryption::SALT_LEN
427 )));
428 }
429 let mut salt = [0_u8; crate::encryption::SALT_LEN];
430 file.read_exact(&mut salt)?;
431 Ok(salt)
432}
433
434fn read_kms_key_envelope(
435 root: &crate::durable_file::DurableRoot,
436) -> Result<crate::security_hardening::KmsDatabaseKeyEnvelope> {
437 let file = root
438 .open_regular(Path::new(META_DIR).join(KMS_KEY_FILENAME))
439 .map_err(|error| MongrelError::NotFound(format!("KMS key envelope: {error}")))?;
440 let mut bytes = Vec::new();
441 file.take((MAX_KMS_KEY_ENVELOPE_BYTES + 1) as u64)
442 .read_to_end(&mut bytes)?;
443 if bytes.len() > MAX_KMS_KEY_ENVELOPE_BYTES {
444 return Err(MongrelError::Encryption(
445 "KMS key envelope exceeds 1 MiB".into(),
446 ));
447 }
448 serde_json::from_slice(&bytes)
449 .map_err(|error| MongrelError::Encryption(format!("invalid KMS key envelope: {error}")))
450}
451
452fn write_kms_key_envelope(
453 root: &Path,
454 envelope: &crate::security_hardening::KmsDatabaseKeyEnvelope,
455) -> Result<()> {
456 let bytes = serde_json::to_vec(envelope)
457 .map_err(|error| MongrelError::Encryption(format!("encode KMS key envelope: {error}")))?;
458 if bytes.len() > MAX_KMS_KEY_ENVELOPE_BYTES {
459 return Err(MongrelError::Encryption(
460 "KMS key envelope exceeds 1 MiB".into(),
461 ));
462 }
463 Ok(crate::durable_file::write_atomic(
464 &root.join(META_DIR).join(KMS_KEY_FILENAME),
465 &bytes,
466 )?)
467}
468
469fn unwrap_kms_database_key(
470 provider: &dyn crate::security_hardening::KeyManagementProvider,
471 envelope: &crate::security_hardening::KmsDatabaseKeyEnvelope,
472) -> Result<zeroize::Zeroizing<Vec<u8>>> {
473 if provider.provider_id() != envelope.provider_id {
474 return Err(MongrelError::Encryption(format!(
475 "KMS provider mismatch: database requires {:?}, got {:?}",
476 envelope.provider_id,
477 provider.provider_id()
478 )));
479 }
480 let key = provider
481 .unwrap_key(&envelope.wrapped_key)
482 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
483 if key.len() != crate::encryption::DEK_LEN {
484 return Err(MongrelError::Encryption(format!(
485 "KMS returned {} key bytes, expected {}",
486 key.len(),
487 crate::encryption::DEK_LEN
488 )));
489 }
490 Ok(key)
491}
492
493fn create_kms_kek(
494 root: &Path,
495 provider: &dyn crate::security_hardening::KeyManagementProvider,
496 kms_key_id: &str,
497) -> Result<Arc<crate::encryption::Kek>> {
498 if kms_key_id.is_empty() {
499 return Err(MongrelError::InvalidArgument(
500 "KMS key id must not be empty".into(),
501 ));
502 }
503 let salt = crate::encryption::random_salt()?;
504 let mut raw_key = zeroize::Zeroizing::new([0_u8; crate::encryption::DEK_LEN]);
505 crate::encryption::fill_random(raw_key.as_mut())?;
506 let wrapped_key = provider
507 .wrap_key(kms_key_id, raw_key.as_ref())
508 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
509 let envelope = crate::security_hardening::KmsDatabaseKeyEnvelope {
510 provider_id: provider.provider_id().to_owned(),
511 wrapped_key,
512 };
513 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
514 write_kms_key_envelope(root, &envelope)?;
515 Ok(Arc::new(crate::encryption::Kek::from_raw_key(
516 raw_key.as_ref(),
517 &salt,
518 )?))
519}
520
521fn open_kms_kek(
522 root: &crate::durable_file::DurableRoot,
523 provider: &dyn crate::security_hardening::KeyManagementProvider,
524) -> Result<Arc<crate::encryption::Kek>> {
525 let salt = read_encryption_salt(root)?;
526 let envelope = read_kms_key_envelope(root)?;
527 let raw_key = unwrap_kms_database_key(provider, &envelope)?;
528 Ok(Arc::new(crate::encryption::Kek::from_raw_key(
529 raw_key.as_ref(),
530 &salt,
531 )?))
532}
533
534fn persist_key_rotation(
535 journal: &crate::security_hardening::KeyRotationJournal,
536 record: &crate::security_hardening::KeyRotationRecord,
537) -> Result<()> {
538 journal
539 .persist(record)
540 .map_err(|error| MongrelError::Encryption(error.to_string()))
541}
542
543fn fail_key_rotation<T>(
544 journal: &crate::security_hardening::KeyRotationJournal,
545 record: &mut crate::security_hardening::KeyRotationRecord,
546 error: impl ToString,
547) -> Result<T> {
548 let message = error.to_string();
549 record.fail(&message);
550 persist_key_rotation(journal, record)?;
551 Err(MongrelError::Encryption(message))
552}
553
554fn advance_key_rotation(
555 journal: &crate::security_hardening::KeyRotationJournal,
556 record: &mut crate::security_hardening::KeyRotationRecord,
557) -> Result<()> {
558 let now = std::time::SystemTime::now()
559 .duration_since(std::time::UNIX_EPOCH)
560 .unwrap_or_default()
561 .as_micros() as u64;
562 record
563 .advance(now)
564 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
565 persist_key_rotation(journal, record)?;
566 let hook = match record.phase {
567 crate::security_hardening::KeyRotationPhase::WrappingNewKey => "kms.rotation.phase.1",
568 crate::security_hardening::KeyRotationPhase::DualRead => "kms.rotation.phase.2",
569 crate::security_hardening::KeyRotationPhase::Reencrypting => "kms.rotation.phase.3",
570 crate::security_hardening::KeyRotationPhase::Validating => "kms.rotation.phase.4",
571 crate::security_hardening::KeyRotationPhase::Published => "kms.rotation.phase.5",
572 crate::security_hardening::KeyRotationPhase::RetiringOldKey => "kms.rotation.phase.6",
573 crate::security_hardening::KeyRotationPhase::Succeeded => "kms.rotation.phase.7",
574 crate::security_hardening::KeyRotationPhase::Pending
575 | crate::security_hardening::KeyRotationPhase::Failed => return Ok(()),
576 };
577 mongreldb_fault::inject(hook).map_err(|error| MongrelError::Encryption(error.to_string()))
578}
579
580fn incremental_aggregate_cache_key(
581 table: &str,
582 conditions: &[crate::query::Condition],
583 column: Option<u16>,
584 agg: crate::engine::NativeAgg,
585 principal: Option<&crate::auth::Principal>,
586 security_version: u64,
587) -> u64 {
588 use std::hash::{Hash, Hasher};
589 let projection = column.as_ref().map(std::slice::from_ref);
590 let query_key = crate::query::canonical_query_key(conditions, projection, security_version);
591 let mut hasher = std::collections::hash_map::DefaultHasher::new();
592 table.hash(&mut hasher);
593 query_key.hash(&mut hasher);
594 match agg {
595 crate::engine::NativeAgg::Count => 0u8,
596 crate::engine::NativeAgg::Sum => 1,
597 crate::engine::NativeAgg::Min => 2,
598 crate::engine::NativeAgg::Max => 3,
599 crate::engine::NativeAgg::Avg => 4,
600 }
601 .hash(&mut hasher);
602 if let Some(principal) = principal {
603 principal.user_id.hash(&mut hasher);
604 principal.created_epoch.hash(&mut hasher);
605 principal.username.hash(&mut hasher);
606 principal.is_admin.hash(&mut hasher);
607 let mut roles = principal.roles.clone();
608 roles.sort_unstable();
609 roles.hash(&mut hasher);
610 }
611 hasher.finish()
612}
613
614fn read_history_retention(
615 root: &crate::durable_file::DurableRoot,
616 current_epoch: Epoch,
617) -> Result<(u64, Epoch)> {
618 const MAX_HISTORY_RETENTION_BYTES: u64 = 128;
619 let file = match root.open_regular(Path::new(META_DIR).join(HISTORY_RETENTION_FILENAME)) {
620 Ok(file) => file,
621 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
622 return Ok((0, current_epoch));
623 }
624 Err(error) => return Err(error.into()),
625 };
626 let length = file.metadata()?.len();
627 if length > MAX_HISTORY_RETENTION_BYTES {
628 return Err(MongrelError::ResourceLimitExceeded {
629 resource: "history retention bytes",
630 requested: usize::try_from(length).unwrap_or(usize::MAX),
631 limit: MAX_HISTORY_RETENTION_BYTES as usize,
632 });
633 }
634 let mut bytes = Vec::with_capacity(length as usize);
635 file.take(MAX_HISTORY_RETENTION_BYTES + 1)
636 .read_to_end(&mut bytes)?;
637 if bytes.len() as u64 != length {
638 return Err(MongrelError::Other(
639 "history retention length changed while reading".into(),
640 ));
641 }
642 let text = std::str::from_utf8(&bytes)
643 .map_err(|error| MongrelError::Other(format!("history retention encoding: {error}")))?;
644 let mut fields = text.split_whitespace();
645 let epochs = fields
646 .next()
647 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
648 .parse::<u64>()
649 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
650 let start = fields
651 .next()
652 .ok_or_else(|| MongrelError::Other("history retention start is missing".into()))?
653 .parse::<u64>()
654 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
655 if fields.next().is_some() || start > current_epoch.0 {
656 return Err(MongrelError::Other(
657 "history retention file has trailing fields or a future start epoch".into(),
658 ));
659 }
660 Ok((epochs, Epoch(start)))
661}
662
663fn write_history_retention<F>(
664 root: &Path,
665 epochs: u64,
666 start: Epoch,
667 after_publish: F,
668) -> Result<()>
669where
670 F: FnOnce(),
671{
672 let meta = root.join(META_DIR);
673 let path = meta.join(HISTORY_RETENTION_FILENAME);
674 let bytes = format!("{epochs} {}\n", start.0);
675 crate::durable_file::write_atomic_with_after(&path, bytes.as_bytes(), after_publish)?;
676 Ok(())
677}
678
679struct PreparedBackupDestination {
680 parent: crate::durable_file::DurableRoot,
681 destination_name: std::ffi::OsString,
682 destination_path: PathBuf,
683 stage_name: std::ffi::OsString,
684 stage: Option<Box<crate::durable_file::DurableRoot>>,
685}
686
687fn prepare_backup_destination(
688 source: &Path,
689 destination: &Path,
690) -> Result<PreparedBackupDestination> {
691 let destination_name = destination
692 .file_name()
693 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?
694 .to_os_string();
695 let requested_parent = destination
696 .parent()
697 .filter(|path| !path.as_os_str().is_empty())
698 .unwrap_or_else(|| Path::new("."));
699 crate::durable_file::create_directory_all(requested_parent)?;
700 let parent = crate::durable_file::DurableRoot::open(requested_parent)?;
701 prepare_backup_destination_in(source, &parent, &destination_name)
702}
703
704fn prepare_backup_destination_in(
705 source: &Path,
706 parent: &crate::durable_file::DurableRoot,
707 destination_name: &std::ffi::OsStr,
708) -> Result<PreparedBackupDestination> {
709 let source = source.canonicalize()?;
710 if parent.canonical_path().starts_with(&source) {
711 return Err(MongrelError::InvalidArgument(
712 "backup destination must not be inside the source database".into(),
713 ));
714 }
715 if parent.entry_exists(Path::new(&destination_name))? {
716 return Err(MongrelError::Conflict(format!(
717 "backup destination already exists: {}",
718 parent.canonical_path().join(destination_name).display()
719 )));
720 }
721 let mut stage_name = None;
722 for _ in 0..128 {
723 let mut nonce = [0_u8; 8];
724 crate::encryption::fill_random(&mut nonce)?;
725 let suffix = nonce
726 .iter()
727 .map(|byte| format!("{byte:02x}"))
728 .collect::<String>();
729 let name = std::ffi::OsString::from(format!(
730 ".{}.backup-stage-{}-{suffix}",
731 destination_name.to_string_lossy(),
732 std::process::id()
733 ));
734 match parent.create_directory_new(Path::new(&name)) {
735 Ok(()) => {
736 stage_name = Some(name);
737 break;
738 }
739 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
740 Err(error) => return Err(error.into()),
741 }
742 }
743 let stage_name = stage_name
744 .ok_or_else(|| MongrelError::Conflict("could not allocate backup staging path".into()))?;
745 let stage = parent.open_directory(Path::new(&stage_name))?;
746 Ok(PreparedBackupDestination {
747 destination_path: parent.canonical_path().join(destination_name),
748 destination_name: destination_name.to_os_string(),
749 stage_name,
750 stage: Some(Box::new(stage)),
751 parent: parent.try_clone()?,
752 })
753}
754
755fn copy_backup_boundary(
756 source_root: &Path,
757 destination_root: &crate::durable_file::DurableRoot,
758 deferred_runs: &HashSet<PathBuf>,
759 copied: &mut Vec<PathBuf>,
760 control: Option<&crate::ExecutionControl>,
761) -> Result<()> {
762 let mut visited = 0;
763 crate::durable_file::walk_regular_files_nofollow(
764 source_root,
765 |relative, is_directory| {
766 if visited % 256 == 0 {
767 if let Some(control) = control {
768 control.checkpoint()?;
769 }
770 }
771 visited += 1;
772 if backup_path_excluded(relative) {
773 return Ok(false);
774 }
775 if is_directory {
776 return Ok(true);
777 }
778 if deferred_runs.contains(relative) {
779 return Ok(false);
780 }
781 Ok(!(relative
782 .parent()
783 .and_then(Path::file_name)
784 .is_some_and(|parent| parent == "_runs")
785 && relative
786 .extension()
787 .is_some_and(|extension| extension == "sr")))
788 },
789 |relative| {
790 destination_root.create_directory_all(relative)?;
791 Ok(())
792 },
793 |relative, source| {
794 destination_root.copy_new_from(relative, source)?;
795 copied.push(relative.to_path_buf());
796 Ok(())
797 },
798 )
799}
800
801fn backup_path_excluded(relative: &Path) -> bool {
802 relative == Path::new("_meta/.lock")
803 || relative == Path::new("_meta/replica")
804 || relative == Path::new("_meta/repl_epoch")
805 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
806 || relative.components().any(|component| {
807 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
808 })
809}
810
811#[derive(Debug, Clone)]
812pub enum ExternalTriggerWrite {
813 Insert {
814 table: String,
815 cells: Vec<(u16, Value)>,
816 },
817 UpdateByPk {
818 table: String,
819 pk: Value,
820 cells: Vec<(u16, Value)>,
821 },
822 DeleteByPk {
823 table: String,
824 pk: Value,
825 },
826}
827
828impl ExternalTriggerWrite {
829 fn table(&self) -> &str {
830 match self {
831 Self::Insert { table, .. }
832 | Self::UpdateByPk { table, .. }
833 | Self::DeleteByPk { table, .. } => table,
834 }
835 }
836}
837
838#[derive(Debug, Clone, PartialEq)]
839pub enum ExternalTriggerBaseWrite {
840 Put {
841 table: String,
842 cells: Vec<(u16, Value)>,
843 },
844 Delete {
845 table: String,
846 row_id: RowId,
847 },
848}
849
850#[derive(Debug, Clone, PartialEq)]
851pub struct ExternalTriggerWriteResult {
852 pub state: Vec<u8>,
853 pub base_writes: Vec<ExternalTriggerBaseWrite>,
854}
855
856impl ExternalTriggerWriteResult {
857 pub fn new(state: Vec<u8>) -> Self {
858 Self {
859 state,
860 base_writes: Vec::new(),
861 }
862 }
863}
864
865pub trait ExternalTriggerBridge: Send + Sync {
866 fn apply_trigger_external_write(
867 &self,
868 entry: &ExternalTableEntry,
869 base_state: Vec<u8>,
870 op: ExternalTriggerWrite,
871 ) -> Result<ExternalTriggerWriteResult>;
872}
873
874struct SpilledRun {
876 table_id: u64,
877 run_id: u128,
878 pending_path: PathBuf,
879 final_path: PathBuf,
880 rows: Vec<crate::memtable::Row>,
881 row_count: u64,
882 min_rid: u64,
883 max_rid: u64,
884 content_hash: [u8; 32],
885}
886
887const SPILLED_WAL_PAYLOAD_MAX_BYTES: usize = 24 * 1024 * 1024;
888const SPILLED_WAL_TOTAL_MAX_BYTES: usize = 256 * 1024 * 1024;
889
890fn encode_spilled_row_chunks(
891 rows: &[crate::memtable::Row],
892 total_bytes: &mut usize,
893 total_limit: usize,
894 control: Option<&crate::ExecutionControl>,
895) -> Result<Vec<Vec<u8>>> {
896 let mut output = Vec::new();
897 let mut start = 0;
898 while start < rows.len() {
899 let mut estimated_bytes = std::mem::size_of::<u64>();
903 let mut end = start;
904 while end < rows.len() {
905 if end % 256 == 0 {
906 if let Some(control) = control {
907 control.checkpoint()?;
908 }
909 }
910 let row_bytes =
911 usize::try_from(bincode::serialized_size(&rows[end])?).map_err(|_| {
912 MongrelError::ResourceLimitExceeded {
913 resource: "spilled WAL row bytes",
914 requested: usize::MAX,
915 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
916 }
917 })?;
918 let next_bytes = estimated_bytes.checked_add(row_bytes).ok_or(
919 MongrelError::ResourceLimitExceeded {
920 resource: "spilled WAL row bytes",
921 requested: usize::MAX,
922 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
923 },
924 )?;
925 if next_bytes > SPILLED_WAL_PAYLOAD_MAX_BYTES {
926 break;
927 }
928 estimated_bytes = next_bytes;
929 end += 1;
930 }
931 if end == start {
932 return Err(MongrelError::ResourceLimitExceeded {
933 resource: "spilled WAL row bytes",
934 requested: estimated_bytes.saturating_add(1),
935 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
936 });
937 }
938 let payload = bincode::serialize(&rows[start..end])?;
939 if payload.len() > SPILLED_WAL_PAYLOAD_MAX_BYTES {
940 return Err(MongrelError::ResourceLimitExceeded {
941 resource: "spilled WAL row bytes",
942 requested: payload.len(),
943 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
944 });
945 }
946 let requested = total_bytes.checked_add(payload.len()).unwrap_or(usize::MAX);
947 if requested > total_limit {
948 return Err(MongrelError::ResourceLimitExceeded {
949 resource: "spilled WAL transaction bytes",
950 requested,
951 limit: total_limit,
952 });
953 }
954 *total_bytes = requested;
955 output.push(payload);
956 start = end;
957 }
958 Ok(output)
959}
960
961#[cfg(test)]
962mod spilled_wal_encoding_tests {
963 use super::*;
964
965 #[test]
966 fn logical_spill_payload_has_a_total_bound() {
967 let rows = (0..4)
968 .map(|row_id| crate::memtable::Row {
969 row_id: crate::rowid::RowId(row_id),
970 committed_epoch: Epoch::ZERO,
971 columns: [(1, Value::Bytes(vec![0; 64]))].into_iter().collect(),
972 deleted: false,
973 })
974 .collect::<Vec<_>>();
975 let mut total = 0;
976 let error = encode_spilled_row_chunks(&rows, &mut total, 32, None).unwrap_err();
977 assert!(matches!(
978 error,
979 MongrelError::ResourceLimitExceeded {
980 resource: "spilled WAL transaction bytes",
981 ..
982 }
983 ));
984 }
985}
986
987struct PreparedRunLinks {
992 links: Vec<(PathBuf, PathBuf)>,
993 armed: bool,
994}
995
996impl PreparedRunLinks {
997 fn prepare(spilled: &[SpilledRun]) -> Result<Self> {
998 let mut guard = Self {
999 links: Vec::with_capacity(spilled.len()),
1000 armed: true,
1001 };
1002 for run in spilled {
1003 crate::durable_file::rename(&run.pending_path, &run.final_path)?;
1004 guard
1005 .links
1006 .push((run.pending_path.clone(), run.final_path.clone()));
1007 }
1008 Ok(guard)
1009 }
1010
1011 fn disarm(&mut self) {
1012 self.armed = false;
1013 for (pending, _) in &self.links {
1014 if let Some(parent) = pending.parent() {
1015 let _ = std::fs::remove_dir_all(parent);
1016 }
1017 }
1018 }
1019}
1020
1021impl Drop for PreparedRunLinks {
1022 fn drop(&mut self) {
1023 if !self.armed {
1024 return;
1025 }
1026 for (pending, final_path) in self.links.iter().rev() {
1027 let _ = std::fs::rename(final_path, pending);
1028 }
1029 }
1030}
1031
1032struct TableApplyBatch {
1033 table_id: u64,
1034 handle: TableHandle,
1035 ops: Vec<crate::txn::StagedOp>,
1036}
1037
1038#[derive(Debug, Clone)]
1039struct TriggerRowImage {
1040 columns: HashMap<u16, Value>,
1041}
1042
1043impl TriggerRowImage {
1044 fn from_row(row: crate::memtable::Row) -> Self {
1045 Self {
1046 columns: row.columns,
1047 }
1048 }
1049
1050 fn from_cells(cells: &[(u16, Value)]) -> Self {
1051 Self {
1052 columns: cells.iter().cloned().collect(),
1053 }
1054 }
1055}
1056
1057#[derive(Debug, Clone)]
1058struct WriteEvent {
1059 table: String,
1060 kind: TriggerEvent,
1061 old: Option<TriggerRowImage>,
1062 new: Option<TriggerRowImage>,
1063 changed_columns: Vec<u16>,
1064 op_indices: Vec<usize>,
1065 put_idx: Option<usize>,
1066 trigger_stack: Vec<String>,
1067}
1068
1069#[derive(Default)]
1070struct TriggerExpansion {
1071 before: Vec<(u64, crate::txn::Staged)>,
1072 before_stacks: Vec<Vec<String>>,
1073 before_external: Vec<ExternalTriggerWrite>,
1074 after: Vec<(u64, crate::txn::Staged)>,
1075 after_stacks: Vec<Vec<String>>,
1076 after_external: Vec<ExternalTriggerWrite>,
1077 ignored_indices: std::collections::BTreeSet<usize>,
1078}
1079
1080#[derive(Clone, PartialEq)]
1081struct TriggerCatalogBinding {
1082 triggers: Vec<TriggerEntry>,
1083 tables: Vec<(String, u64, u64)>,
1084 external_tables: Vec<(String, u64, u64)>,
1085}
1086
1087fn trigger_catalog_binding(catalog: &Catalog) -> Option<TriggerCatalogBinding> {
1088 let mut triggers = catalog
1089 .triggers
1090 .iter()
1091 .filter(|entry| entry.trigger.enabled)
1092 .cloned()
1093 .collect::<Vec<_>>();
1094 if triggers.is_empty() {
1095 return None;
1096 }
1097 triggers.sort_by(|left, right| left.trigger.name.cmp(&right.trigger.name));
1098 let mut tables = catalog
1099 .tables
1100 .iter()
1101 .filter(|entry| matches!(entry.state, TableState::Live))
1102 .map(|entry| (entry.name.clone(), entry.table_id, entry.schema.schema_id))
1103 .collect::<Vec<_>>();
1104 tables.sort_unstable();
1105 let mut external_tables = catalog
1106 .external_tables
1107 .iter()
1108 .map(|entry| {
1109 (
1110 entry.name.clone(),
1111 entry.created_epoch,
1112 entry.declared_schema.schema_id,
1113 )
1114 })
1115 .collect::<Vec<_>>();
1116 external_tables.sort_unstable();
1117 Some(TriggerCatalogBinding {
1118 triggers,
1119 tables,
1120 external_tables,
1121 })
1122}
1123
1124struct TriggerProgramOutput<'a> {
1125 added: &'a mut Vec<(u64, crate::txn::Staged)>,
1126 added_stacks: &'a mut Vec<Vec<String>>,
1127 added_external: &'a mut Vec<ExternalTriggerWrite>,
1128 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
1129}
1130
1131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1132enum TriggerProgramOutcome {
1133 Continue,
1134 Ignore,
1135}
1136
1137#[derive(Debug, Clone)]
1139pub struct CheckIssue {
1140 pub table_id: u64,
1141 pub table_name: String,
1142 pub severity: String,
1143 pub description: String,
1144}
1145
1146#[derive(Debug, Clone)]
1148pub struct AuthorizedReadSnapshot {
1149 pub table: String,
1150 pub table_snapshot: Snapshot,
1151 pub data_generation: u64,
1152 pub security_version: u64,
1153 pub allowed_row_ids: Option<HashSet<RowId>>,
1154}
1155
1156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1158pub struct AuthorizedReadStamp {
1159 pub table_id: u64,
1160 pub schema_id: u64,
1161 pub data_generation: u64,
1162 pub security_version: u64,
1163 pub snapshot: Snapshot,
1164}
1165
1166type RlsCacheKey = (String, u64, u64, String);
1167
1168#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1170pub struct RlsCacheStats {
1171 pub entries: usize,
1172 pub bytes: usize,
1173 pub hits: u64,
1174 pub misses: u64,
1175 pub evictions: u64,
1176 pub build_nanos: u64,
1177 pub rows_evaluated: u64,
1178}
1179
1180const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
1181const CDC_MAX_WAL_RECORDS: usize = 1_000_000;
1182const CDC_MAX_WAL_REPLAY_BYTES: usize = 256 * 1024 * 1024;
1183const CDC_MAX_EVENTS: usize = 100_000;
1184const CDC_MAX_ROWS: usize = 1_000_000;
1185const CDC_MAX_INLINE_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
1186const CDC_MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
1187
1188fn charge_cdc_bytes(total: &mut usize, amount: usize, resource: &'static str) -> Result<()> {
1189 let requested = total.saturating_add(amount);
1190 if requested > CDC_MAX_RETAINED_BYTES {
1191 return Err(MongrelError::ResourceLimitExceeded {
1192 resource,
1193 requested,
1194 limit: CDC_MAX_RETAINED_BYTES,
1195 });
1196 }
1197 *total = requested;
1198 Ok(())
1199}
1200
1201fn cdc_row_storage_bytes(row: &crate::memtable::Row) -> usize {
1202 usize::try_from(row.estimated_bytes())
1203 .unwrap_or(usize::MAX)
1204 .saturating_add(std::mem::size_of::<crate::memtable::Row>())
1205}
1206
1207fn cdc_row_json_bytes(row: &crate::memtable::Row) -> usize {
1208 let value_slot = std::mem::size_of::<serde_json::Value>();
1209 row.columns.values().fold(512_usize, |bytes, value| {
1210 let values = match value {
1211 Value::Bytes(values) => values.len(),
1212 Value::Json(values) => values.len(),
1213 Value::Embedding(values) => values.len(),
1214 Value::GeneratedEmbedding(value) => value.vector.len(),
1215 _ => 1,
1216 };
1217 bytes.saturating_add(values.saturating_mul(value_slot))
1218 })
1219}
1220
1221fn cdc_rows_json_bytes(rows: &[crate::memtable::Row]) -> usize {
1222 rows.iter().fold(0_usize, |bytes, row| {
1223 bytes.saturating_add(cdc_row_json_bytes(row))
1224 })
1225}
1226
1227#[derive(Default)]
1228struct RlsCache {
1229 entries: HashMap<RlsCacheKey, (Arc<HashSet<RowId>>, usize)>,
1230 lru: VecDeque<RlsCacheKey>,
1231 bytes: usize,
1232 hits: u64,
1233 misses: u64,
1234 evictions: u64,
1235 build_nanos: u64,
1236 rows_evaluated: u64,
1237}
1238
1239impl RlsCache {
1240 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
1241 let value = self.entries.get(key).map(|(value, _)| Arc::clone(value));
1242 if value.is_some() {
1243 self.hits = self.hits.saturating_add(1);
1244 self.touch(key);
1245 } else {
1246 self.misses = self.misses.saturating_add(1);
1247 }
1248 value
1249 }
1250
1251 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
1252 let bytes = key
1253 .0
1254 .len()
1255 .saturating_add(key.3.len())
1256 .saturating_add(
1257 value
1258 .capacity()
1259 .saturating_mul(std::mem::size_of::<RowId>() * 3),
1260 )
1261 .saturating_add(std::mem::size_of::<RlsCacheKey>());
1262 if bytes > RLS_CACHE_MAX_BYTES {
1263 return;
1264 }
1265 if let Some((_, old_bytes)) = self.entries.remove(&key) {
1266 self.bytes = self.bytes.saturating_sub(old_bytes);
1267 }
1268 self.lru.retain(|candidate| candidate != &key);
1269 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
1270 let Some(oldest) = self.lru.pop_front() else {
1271 break;
1272 };
1273 if let Some((_, old_bytes)) = self.entries.remove(&oldest) {
1274 self.bytes = self.bytes.saturating_sub(old_bytes);
1275 self.evictions = self.evictions.saturating_add(1);
1276 }
1277 }
1278 self.bytes = self.bytes.saturating_add(bytes);
1279 self.lru.push_back(key.clone());
1280 self.entries.insert(key, (value, bytes));
1281 }
1282
1283 fn touch(&mut self, key: &RlsCacheKey) {
1284 self.lru.retain(|candidate| candidate != key);
1285 self.lru.push_back(key.clone());
1286 }
1287
1288 fn stats(&self) -> RlsCacheStats {
1289 RlsCacheStats {
1290 entries: self.entries.len(),
1291 bytes: self.bytes,
1292 hits: self.hits,
1293 misses: self.misses,
1294 evictions: self.evictions,
1295 build_nanos: self.build_nanos,
1296 rows_evaluated: self.rows_evaluated,
1297 }
1298 }
1299}
1300
1301#[derive(Clone)]
1303pub struct TableHandle {
1304 inner: TableHandleInner,
1305 generation_metrics: Arc<TableGenerationMetrics>,
1306}
1307
1308#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1309pub struct TableGenerationStats {
1310 pub active_read_generations: usize,
1311 pub max_live_read_generations: usize,
1312 pub cow_clone_count: u64,
1313 pub cow_clone_nanos: u64,
1314 pub estimated_cow_clone_bytes: u64,
1315 pub writer_wait_nanos: u64,
1316}
1317
1318#[derive(Default)]
1319#[doc(hidden)]
1320pub struct TableGenerationMetrics {
1321 active_read_generations: AtomicUsize,
1322 max_live_read_generations: AtomicUsize,
1323 cow_clone_count: AtomicU64,
1324 cow_clone_nanos: AtomicU64,
1325 estimated_cow_clone_bytes: AtomicU64,
1326 writer_wait_nanos: AtomicU64,
1327}
1328
1329impl TableGenerationMetrics {
1330 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
1331 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
1332 self.max_live_read_generations
1333 .fetch_max(active, Ordering::Relaxed);
1334 Arc::new(TableReadGeneration {
1335 table,
1336 metrics: Arc::clone(self),
1337 })
1338 }
1339
1340 fn stats(&self) -> TableGenerationStats {
1341 TableGenerationStats {
1342 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
1343 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
1344 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
1345 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
1346 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
1347 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
1348 }
1349 }
1350}
1351
1352pub struct TableReadGeneration {
1354 table: Table,
1355 metrics: Arc<TableGenerationMetrics>,
1356}
1357
1358impl std::ops::Deref for TableReadGeneration {
1359 type Target = Table;
1360
1361 fn deref(&self) -> &Self::Target {
1362 &self.table
1363 }
1364}
1365
1366impl Drop for TableReadGeneration {
1367 fn drop(&mut self) {
1368 self.metrics
1369 .active_read_generations
1370 .fetch_sub(1, Ordering::Relaxed);
1371 }
1372}
1373
1374#[derive(Clone)]
1375enum TableHandleInner {
1376 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
1377 Direct(Arc<Mutex<Table>>),
1378}
1379
1380pub enum TableGuard<'a> {
1381 CopyOnWrite {
1382 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
1383 metrics: Arc<TableGenerationMetrics>,
1384 },
1385 Direct {
1386 table: parking_lot::MutexGuard<'a, Table>,
1387 },
1388}
1389
1390impl TableHandle {
1391 fn new(table: Table) -> Self {
1392 Self {
1393 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
1394 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1395 }
1396 }
1397
1398 pub fn from_table(table: Table) -> Self {
1399 Self::new(table)
1400 }
1401
1402 pub fn lock(&self) -> TableGuard<'_> {
1403 let started = std::time::Instant::now();
1404 let guard = match &self.inner {
1405 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
1406 table: table.write(),
1407 metrics: Arc::clone(&self.generation_metrics),
1408 },
1409 TableHandleInner::Direct(table) => TableGuard::Direct {
1410 table: table.lock(),
1411 },
1412 };
1413 self.generation_metrics.writer_wait_nanos.fetch_add(
1414 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1415 Ordering::Relaxed,
1416 );
1417 guard
1418 }
1419
1420 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
1421 let started = std::time::Instant::now();
1422 let guard = match &self.inner {
1423 TableHandleInner::CopyOnWrite(table) => {
1424 table
1425 .try_write_for(timeout)
1426 .map(|table| TableGuard::CopyOnWrite {
1427 table,
1428 metrics: Arc::clone(&self.generation_metrics),
1429 })
1430 }
1431 TableHandleInner::Direct(table) => table
1432 .try_lock_for(timeout)
1433 .map(|table| TableGuard::Direct { table }),
1434 };
1435 self.generation_metrics.writer_wait_nanos.fetch_add(
1436 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1437 Ordering::Relaxed,
1438 );
1439 guard
1440 }
1441
1442 pub fn ptr_eq(&self, other: &Self) -> bool {
1443 match (&self.inner, &other.inner) {
1444 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1445 Arc::ptr_eq(left, right)
1446 }
1447 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1448 Arc::ptr_eq(left, right)
1449 }
1450 _ => false,
1451 }
1452 }
1453
1454 pub fn read_generation_with_context(
1455 &self,
1456 context: Option<&crate::query::AiExecutionContext>,
1457 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1458 let mut table = if let Some(context) = context {
1459 loop {
1460 context.checkpoint()?;
1461 let wait = context
1462 .remaining_duration()
1463 .unwrap_or(std::time::Duration::from_millis(5))
1464 .min(std::time::Duration::from_millis(5));
1465 if let Some(table) = self.try_lock_for(wait) {
1466 break table;
1467 }
1468 }
1469 } else {
1470 self.lock()
1471 };
1472 let snapshot = table.snapshot();
1473 let generation = table.clone_read_generation()?;
1474 Ok((self.generation_metrics.activate(generation), snapshot))
1475 }
1476
1477 pub fn generation_stats(&self) -> TableGenerationStats {
1478 self.generation_metrics.stats()
1479 }
1480}
1481
1482impl From<Arc<Mutex<Table>>> for TableHandle {
1483 fn from(table: Arc<Mutex<Table>>) -> Self {
1484 Self {
1485 inner: TableHandleInner::Direct(table),
1486 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1487 }
1488 }
1489}
1490
1491impl std::ops::Deref for TableGuard<'_> {
1492 type Target = Table;
1493
1494 fn deref(&self) -> &Self::Target {
1495 match self {
1496 Self::CopyOnWrite { table, .. } => table.as_ref(),
1497 Self::Direct { table } => table,
1498 }
1499 }
1500}
1501
1502impl std::ops::DerefMut for TableGuard<'_> {
1503 fn deref_mut(&mut self) -> &mut Self::Target {
1504 match self {
1505 Self::CopyOnWrite { table, metrics } => {
1506 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1507 let estimated_bytes = table.estimated_clone_bytes();
1508 let started = std::time::Instant::now();
1509 let table = Arc::make_mut(table);
1510 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1511 metrics.cow_clone_nanos.fetch_add(
1512 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1513 Ordering::Relaxed,
1514 );
1515 metrics
1516 .estimated_cow_clone_bytes
1517 .fetch_add(estimated_bytes, Ordering::Relaxed);
1518 table
1519 } else {
1520 Arc::make_mut(table)
1521 }
1522 }
1523 Self::Direct { table } => table,
1524 }
1525 }
1526}
1527
1528#[derive(Clone, Debug)]
1529pub struct ReadAuthorization {
1530 pub operation: crate::auth::ColumnOperation,
1531 pub columns: Vec<u16>,
1532 pub permissions: Vec<crate::auth::Permission>,
1533}
1534
1535#[derive(Default, Debug)]
1536struct TableWritePermissionNeeds {
1537 insert: bool,
1538 insert_columns: Vec<u16>,
1539 update: bool,
1540 update_columns: Vec<u16>,
1541 delete: bool,
1542 truncate: bool,
1543}
1544
1545#[cfg(test)]
1546thread_local! {
1547 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1548 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1549 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1550 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1551 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1552 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1553}
1554
1555fn summarize_write_permissions(
1556 staging: &[(u64, crate::txn::Staged)],
1557) -> HashMap<u64, TableWritePermissionNeeds> {
1558 use crate::txn::Staged;
1559
1560 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1561 for (table_id, operation) in staging {
1562 let table = needs.entry(*table_id).or_default();
1563 match operation {
1564 Staged::Put(cells) => {
1565 table.insert = true;
1566 table
1567 .insert_columns
1568 .extend(cells.iter().map(|(column, _)| *column));
1569 }
1570 Staged::Update {
1571 changed_columns, ..
1572 } => {
1573 table.update = true;
1574 table.update_columns.extend(changed_columns);
1575 }
1576 Staged::Delete(_) => table.delete = true,
1577 Staged::Truncate => table.truncate = true,
1578 }
1579 }
1580 for table in needs.values_mut() {
1581 table.insert_columns.sort_unstable();
1582 table.insert_columns.dedup();
1583 table.update_columns.sort_unstable();
1584 table.update_columns.dedup();
1585 }
1586 needs
1587}
1588
1589struct SecurityCoordinator {
1590 gate: RwLock<()>,
1592 version: AtomicU64,
1593}
1594
1595fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1596 static COORDINATORS: std::sync::OnceLock<
1597 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1598 > = std::sync::OnceLock::new();
1599
1600 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1601 let mut coordinators = COORDINATORS
1602 .get_or_init(|| Mutex::new(HashMap::new()))
1603 .lock();
1604 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1605 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1606 return coordinator;
1607 }
1608 let coordinator = Arc::new(SecurityCoordinator {
1609 gate: RwLock::new(()),
1610 version: AtomicU64::new(version),
1611 });
1612 coordinators.insert(root, Arc::downgrade(&coordinator));
1613 coordinator
1614}
1615
1616pub fn lock_table_with_context<'a>(
1617 handle: &'a TableHandle,
1618 context: Option<&crate::query::AiExecutionContext>,
1619) -> Result<TableGuard<'a>> {
1620 let Some(context) = context else {
1621 return Ok(handle.lock());
1622 };
1623 loop {
1624 context.checkpoint()?;
1625 let wait = context
1626 .remaining_duration()
1627 .unwrap_or(std::time::Duration::from_millis(5))
1628 .min(std::time::Duration::from_millis(5));
1629 if let Some(guard) = handle.try_lock_for(wait) {
1630 return Ok(guard);
1631 }
1632 }
1633}
1634
1635#[derive(Clone, Debug, Default)]
1641pub struct OpenOptions {
1642 pub lock_timeout_ms: u32,
1658 pub memory_budget_bytes: Option<u64>,
1663 pub temp_disk_budget_bytes: Option<u64>,
1668 pub offline_validation: bool,
1675}
1676
1677impl OpenOptions {
1678 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1681 self.lock_timeout_ms = ms;
1682 self
1683 }
1684
1685 pub fn with_memory_budget_bytes(mut self, bytes: u64) -> Self {
1687 self.memory_budget_bytes = Some(bytes);
1688 self
1689 }
1690
1691 pub fn with_temp_disk_budget_bytes(mut self, bytes: u64) -> Self {
1693 self.temp_disk_budget_bytes = Some(bytes);
1694 self
1695 }
1696
1697 pub fn with_offline_validation(mut self, offline_validation: bool) -> Self {
1700 self.offline_validation = offline_validation;
1701 self
1702 }
1703}
1704
1705#[derive(Debug, Clone)]
1709pub(crate) enum OpenModeGate {
1710 Normal,
1713 SharedCore,
1716 OfflineValidation,
1718 ClusterRuntime {
1722 cluster_id: mongreldb_types::ids::ClusterId,
1724 node_id: mongreldb_types::ids::NodeId,
1726 database_id: mongreldb_types::ids::DatabaseId,
1728 },
1729 Create(crate::storage_mode::StorageMode),
1731}
1732
1733pub const DEFAULT_MEMORY_BUDGET_BYTES: u64 = 1024 * 1024 * 1024;
1737
1738pub const DEFAULT_TEMP_DISK_BUDGET_BYTES: u64 = 4 * 1024 * 1024 * 1024;
1741
1742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1746pub(crate) struct CoreResourceConfig {
1747 pub memory_budget_bytes: u64,
1748 pub temp_disk_budget_bytes: u64,
1749}
1750
1751impl Default for CoreResourceConfig {
1752 fn default() -> Self {
1753 Self {
1754 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
1755 temp_disk_budget_bytes: DEFAULT_TEMP_DISK_BUDGET_BYTES,
1756 }
1757 }
1758}
1759
1760impl CoreResourceConfig {
1761 fn from_options(options: &OpenOptions) -> Result<Self> {
1762 let config = Self {
1763 memory_budget_bytes: options
1764 .memory_budget_bytes
1765 .unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES),
1766 temp_disk_budget_bytes: options
1767 .temp_disk_budget_bytes
1768 .unwrap_or(DEFAULT_TEMP_DISK_BUDGET_BYTES),
1769 };
1770 if config.memory_budget_bytes == 0 {
1771 return Err(MongrelError::InvalidArgument(
1772 "memory_budget_bytes must be nonzero".into(),
1773 ));
1774 }
1775 if config.temp_disk_budget_bytes == 0 {
1776 return Err(MongrelError::InvalidArgument(
1777 "temp_disk_budget_bytes must be nonzero".into(),
1778 ));
1779 }
1780 Ok(config)
1781 }
1782}
1783
1784#[derive(Debug, Clone)]
1787pub struct TablePinsReport {
1788 pub table_id: u64,
1790 pub table: String,
1792 pub pins: crate::retention::PinsReport,
1794}
1795
1796pub struct DatabaseCore {
1809 root: PathBuf,
1810 durable_root: Arc<crate::durable_file::DurableRoot>,
1811 lifecycle: Arc<crate::core::LifecycleController>,
1814 registry: std::sync::OnceLock<crate::manager::CoreRegistration>,
1818 read_only: bool,
1820 catalog: RwLock<Catalog>,
1821 security_coordinator: Arc<SecurityCoordinator>,
1822 security_catalog_disk_reads: AtomicU64,
1823 rls_cache: Mutex<RlsCache>,
1824 epoch: Arc<EpochAuthority>,
1825 snapshots: Arc<SnapshotRegistry>,
1826 memory_governor: crate::memory::MemoryGovernor,
1831 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1832 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1833 spill_manager: crate::spill::SpillManager,
1837 resource_groups: crate::resource::ResourceGroupRegistry,
1841 embedding_providers: crate::embedding::EmbeddingProviderRegistry,
1845 job_registry: Arc<crate::jobs::JobRegistry>,
1848 commit_lock: Arc<Mutex<()>>,
1849 kms_rotation_lock: Mutex<()>,
1850 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1855 next_txn_id: Arc<Mutex<u64>>,
1859 tables: RwLock<HashMap<u64, TableHandle>>,
1860 kek: Option<Arc<crate::encryption::Kek>>,
1861 ddl_lock: Mutex<()>,
1864 meta_dek: Option<[u8; META_DEK_LEN]>,
1865 spill_threshold: std::sync::atomic::AtomicU64,
1868 conflicts: crate::txn::ConflictIndex,
1871 active_txns: crate::txn::ActiveTxns,
1874 lock_manager: Arc<crate::locks::LockManager>,
1880 poisoned: Arc<std::sync::atomic::AtomicBool>,
1883 group: Arc<crate::txn::GroupCommit>,
1887 commit_log: RwLock<Arc<dyn mongreldb_log::CommitLog>>,
1891 standalone_commit_log: Arc<crate::commit_log::StandaloneCommitLog>,
1895 hlc: Arc<mongreldb_types::hlc::HlcClock>,
1901 idempotency: crate::txn::IdempotencyLedger,
1904 commit_ts_ledger: Mutex<std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp>>,
1912 active_spills: Arc<crate::retention::ActiveSpills>,
1915 replication_barrier: parking_lot::RwLock<()>,
1918 replication_wal_retention_segments: AtomicUsize,
1920 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1923 #[doc(hidden)]
1927 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1928 #[doc(hidden)]
1930 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1931 #[doc(hidden)]
1934 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1935 #[doc(hidden)]
1938 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1939 #[doc(hidden)]
1945 fk_lock_hook: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
1946 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1947 trigger_recursive: AtomicBool,
1948 trigger_max_depth: AtomicU32,
1949 trigger_max_loop_iterations: AtomicU32,
1950 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
1953 change_wake: tokio::sync::broadcast::Sender<()>,
1956 _lock: Mutex<Option<ExclusiveDatabaseLease>>,
1960}
1961
1962pub struct Database {
1972 core: Arc<DatabaseCore>,
1973 principal: RwLock<Option<crate::auth::Principal>>,
1983 auth_state: crate::auth_state::AuthState,
1989 shared: bool,
1996}
1997
1998impl std::ops::Deref for Database {
1999 type Target = DatabaseCore;
2000
2001 fn deref(&self) -> &DatabaseCore {
2002 &self.core
2003 }
2004}
2005
2006struct RunPins {
2007 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
2008 runs: Vec<(u64, u128)>,
2009}
2010
2011struct TxnLockGuard<'a> {
2016 locks: &'a crate::locks::LockManager,
2017 txn_id: u64,
2018}
2019
2020impl Drop for TxnLockGuard<'_> {
2021 fn drop(&mut self) {
2022 self.locks.release_all(self.txn_id);
2023 }
2024}
2025
2026struct BackupFilePins {
2027 root: PathBuf,
2028}
2029
2030struct PendingTableDir {
2031 path: PathBuf,
2032 armed: bool,
2033}
2034
2035impl PendingTableDir {
2036 fn new(path: PathBuf) -> Self {
2037 Self { path, armed: true }
2038 }
2039
2040 fn disarm(&mut self) {
2041 self.armed = false;
2042 }
2043}
2044
2045impl Drop for PendingTableDir {
2046 fn drop(&mut self) {
2047 if self.armed {
2048 let _ = std::fs::remove_dir_all(&self.path);
2049 }
2050 }
2051}
2052
2053impl Drop for BackupFilePins {
2054 fn drop(&mut self) {
2055 let _ = std::fs::remove_dir_all(&self.root);
2056 }
2057}
2058
2059impl Drop for RunPins {
2060 fn drop(&mut self) {
2061 let mut pins = self.pins.lock();
2062 for run in &self.runs {
2063 if let Some(count) = pins.get_mut(run) {
2064 *count -= 1;
2065 if *count == 0 {
2066 pins.remove(run);
2067 }
2068 }
2069 }
2070 }
2071}
2072
2073#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2076pub struct ChangeEvent {
2077 pub id: Option<String>,
2078 pub channel: String,
2079 pub table_id: Option<u64>,
2080 pub table: String,
2081 pub op: String,
2082 pub epoch: u64,
2083 pub txn_id: Option<u64>,
2084 pub message: Option<String>,
2085 pub data: Option<serde_json::Value>,
2086}
2087
2088#[derive(Debug, Clone)]
2089pub struct CdcBatch {
2090 pub events: Vec<ChangeEvent>,
2091 pub current_epoch: u64,
2092 pub earliest_epoch: Option<u64>,
2093 pub gap: bool,
2094}
2095
2096impl std::fmt::Debug for Database {
2103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2104 let cat = self.catalog.read();
2105 let principal_guard = self.principal.read();
2106 let principal: &str = principal_guard
2107 .as_ref()
2108 .map(|p| p.username.as_str())
2109 .unwrap_or("<none>");
2110 f.debug_struct("Database")
2111 .field("root", &self.root)
2112 .field("db_epoch", &cat.db_epoch)
2113 .field("open_generation", &"sidecar")
2114 .field("tables", &cat.tables.len())
2115 .field("visible_epoch", &self.epoch.visible().0)
2116 .field("encrypted", &self.kek.is_some())
2117 .field("require_auth", &cat.require_auth)
2118 .field("principal", &principal)
2119 .finish()
2120 }
2121}
2122
2123impl std::fmt::Debug for DatabaseCore {
2126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2127 let cat = self.catalog.read();
2128 f.debug_struct("DatabaseCore")
2129 .field("root", &self.root)
2130 .field("lifecycle", &self.lifecycle.state())
2131 .field("db_epoch", &cat.db_epoch)
2132 .field("tables", &cat.tables.len())
2133 .field("visible_epoch", &self.epoch.visible().0)
2134 .field("encrypted", &self.kek.is_some())
2135 .field("require_auth", &cat.require_auth)
2136 .finish()
2137 }
2138}
2139
2140impl DatabaseCore {
2141 fn ensure_owner_process(&self) -> Result<()> {
2142 let current_pid = std::process::id();
2143 let owner_pid = self
2144 ._lock
2145 .lock()
2146 .as_ref()
2147 .map(|lease| lease.owner_pid)
2148 .unwrap_or(current_pid);
2149 if current_pid == owner_pid {
2150 Ok(())
2151 } else {
2152 Err(MongrelError::ForkedProcess {
2153 owner_pid,
2154 current_pid,
2155 })
2156 }
2157 }
2158
2159 pub fn root(&self) -> &Path {
2161 self.durable_root.canonical_path()
2162 }
2163
2164 pub fn file_identity(&self) -> Result<crate::core::DatabaseFileIdentity> {
2166 crate::core::DatabaseFileIdentity::from_durable_root(&self.durable_root)
2167 }
2168
2169 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2171 self.lifecycle.state()
2172 }
2173
2174 pub fn is_open(&self) -> bool {
2176 self.lifecycle.is_open()
2177 }
2178
2179 pub fn operation_guard(self: &Arc<Self>) -> Result<crate::core::OperationGuard> {
2183 self.lifecycle.begin_operation()
2184 }
2185
2186 pub(crate) fn set_registry(
2190 &self,
2191 registration: crate::manager::CoreRegistration,
2192 ) -> Result<()> {
2193 self.registry
2194 .set(registration)
2195 .map_err(|_| MongrelError::Conflict("database core is already registry-bound".into()))
2196 }
2197
2198 pub fn shutdown(self: &Arc<Self>, drain_deadline: std::time::Duration) -> Result<()> {
2218 self.ensure_owner_process()?;
2219 let initiated = self.lifecycle.begin_shutdown();
2220 if !initiated {
2221 match self.lifecycle.state() {
2222 crate::core::LifecycleState::Closed => return Ok(()),
2223 crate::core::LifecycleState::Draining => {}
2226 state => {
2227 return Err(MongrelError::Conflict(format!(
2228 "database core cannot shut down from lifecycle state {state}"
2229 )))
2230 }
2231 }
2232 }
2233 if let Some(registration) = self.registry.get() {
2234 registration
2235 .manager
2236 .mark_closing(®istration.identity, self);
2237 }
2238 self.lifecycle.wait_drained(drain_deadline)?;
2239 let sync = self.shared_wal.lock().group_sync().map(|_| ());
2240 self.lifecycle.mark_closing();
2241 let lease = self._lock.lock().take();
2242 drop(lease);
2243 if let Some(registration) = self.registry.get() {
2244 registration
2245 .manager
2246 .entry_closed(®istration.identity, self);
2247 }
2248 self.lifecycle.mark_closed();
2249 sync
2250 }
2251}
2252
2253impl Database {
2254 pub fn open_metrics() -> DatabaseOpenMetrics {
2255 DatabaseOpenMetrics {
2256 lock_waits: DATABASE_OPEN_WAIT_COUNT.load(Ordering::Relaxed),
2257 failures: DATABASE_OPEN_FAILURE_COUNT.load(Ordering::Relaxed),
2258 }
2259 }
2260
2261 pub fn core(&self) -> Arc<DatabaseCore> {
2263 Arc::clone(&self.core)
2264 }
2265
2266 pub fn identity(&self) -> crate::handle::HandleIdentity {
2269 match self.principal.read().as_ref() {
2270 Some(principal) => crate::handle::HandleIdentity::CatalogUser {
2271 username: principal.username.clone(),
2272 user_id: principal.user_id,
2273 created_version: principal.created_epoch,
2274 },
2275 None => crate::handle::HandleIdentity::Credentialless,
2276 }
2277 }
2278
2279 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2281 self.core.lifecycle_state()
2282 }
2283
2284 pub fn operation_guard(&self) -> Result<crate::core::OperationGuard> {
2288 self.core.operation_guard()
2289 }
2290
2291 pub(crate) fn from_core(
2295 core: Arc<DatabaseCore>,
2296 principal: Option<crate::auth::Principal>,
2297 shared: bool,
2298 ) -> Self {
2299 let require_auth = core.catalog.read().require_auth;
2300 Self {
2301 core,
2302 principal: RwLock::new(principal.clone()),
2303 auth_state: crate::auth_state::AuthState::new(require_auth, principal),
2304 shared,
2305 }
2306 }
2307
2308 pub(crate) fn into_core(self) -> Arc<DatabaseCore> {
2310 self.core
2311 }
2312
2313 pub(crate) fn open_for_shared_core(root: impl AsRef<Path>) -> Result<Self> {
2318 Self::open_inner_with_lock_timeout(
2319 root,
2320 None,
2321 None,
2322 0,
2323 CoreResourceConfig::default(),
2324 OpenModeGate::SharedCore,
2325 )
2326 }
2327
2328 pub fn shutdown(self: Arc<Self>) -> Result<()> {
2334 if self.shared {
2335 return Err(MongrelError::Conflict(
2336 "shared-core facades do not own storage; use DatabaseHandle::shutdown".into(),
2337 ));
2338 }
2339 match Arc::try_unwrap(self) {
2340 Ok(database) => {
2341 database.ensure_owner_process()?;
2342 database.core.shutdown(std::time::Duration::from_secs(30))?;
2346 drop(database);
2347 Ok(())
2348 }
2349 Err(database) => Err(MongrelError::DatabaseBusy {
2350 strong_handles: Arc::strong_count(&database),
2351 }),
2352 }
2353 }
2354
2355 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
2356 if let Ok(canonical) = root.canonicalize() {
2357 let lock_dir = canonical.parent().ok_or_else(|| {
2358 std::io::Error::new(
2359 std::io::ErrorKind::InvalidInput,
2360 "database root must have a parent directory",
2361 )
2362 })?;
2363 return Ok((canonical.clone(), lock_dir.to_path_buf()));
2364 }
2365
2366 let absolute = if root.is_absolute() {
2367 root.to_path_buf()
2368 } else {
2369 std::env::current_dir()?.join(root)
2370 };
2371 let mut cursor = absolute.as_path();
2372 let mut suffix = Vec::new();
2373 while !cursor.exists() {
2374 let name = cursor.file_name().ok_or_else(|| {
2375 std::io::Error::new(
2376 std::io::ErrorKind::NotFound,
2377 format!("no existing ancestor for database root {}", root.display()),
2378 )
2379 })?;
2380 suffix.push(name.to_os_string());
2381 cursor = cursor.parent().ok_or_else(|| {
2382 std::io::Error::new(
2383 std::io::ErrorKind::NotFound,
2384 format!("no existing ancestor for database root {}", root.display()),
2385 )
2386 })?;
2387 }
2388 let lock_dir = cursor.canonicalize()?;
2389 let mut canonical = lock_dir.clone();
2390 for component in suffix.iter().rev() {
2391 canonical.push(component);
2392 }
2393 Ok((canonical, lock_dir))
2394 }
2395
2396 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<ExclusiveDatabaseLease> {
2397 use std::hash::{Hash, Hasher};
2398
2399 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
2400 let reservation =
2401 OpenReservation::acquire(DatabaseOpenKey::IntendedPath(canonical_path.clone()), root)?;
2402
2403 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2404 canonical_path.hash(&mut hasher);
2405 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
2406 let file = std::fs::OpenOptions::new()
2407 .create(true)
2408 .truncate(false)
2409 .write(true)
2410 .open(lock_path)?;
2411 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2412 return Err(MongrelError::DatabaseLocked {
2413 path: root.to_path_buf(),
2414 message: error.to_string(),
2415 });
2416 }
2417 Ok(reservation.into_lease(file, canonical_path))
2418 }
2419
2420 fn acquire_legacy_database_lock(
2421 lock: &mut ExclusiveDatabaseLease,
2422 root: &Path,
2423 timeout_ms: u32,
2424 ) -> Result<()> {
2425 let durable_root = lock
2426 .durable_root
2427 .as_ref()
2428 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
2429 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
2430 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2431 return Err(MongrelError::DatabaseLocked {
2432 path: root.to_path_buf(),
2433 message: error.to_string(),
2434 });
2435 }
2436 lock.legacy_file = Some(file);
2437 Ok(())
2438 }
2439
2440 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
2441 if path.exists() {
2442 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
2443 }
2444 let mut ancestor = path;
2445 while !ancestor.exists() {
2446 ancestor = ancestor.parent().ok_or_else(|| {
2447 MongrelError::NotFound(format!(
2448 "no existing ancestor for database root {}",
2449 path.display()
2450 ))
2451 })?;
2452 }
2453 let relative = path.strip_prefix(ancestor).map_err(|error| {
2454 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
2455 })?;
2456 crate::durable_file::DurableRoot::open(ancestor)?
2457 .create_directory_all_pinned(relative)
2458 .map_err(Into::into)
2459 }
2460
2461 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2462 let requested_root = root.as_ref();
2463 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
2464 let root = lock.canonical_path.clone();
2465 Self::reject_existing_database(&root)?;
2466 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
2467 if durable_root.canonical_path() != lock.canonical_path {
2468 return Err(MongrelError::Conflict(
2469 "database root changed while it was being created".into(),
2470 ));
2471 }
2472 lock.claim_root_identity(&durable_root)?;
2473 durable_root.create_directory_all(META_DIR)?;
2474 lock.durable_root = Some(durable_root);
2475 let io_root = lock
2476 .durable_root
2477 .as_ref()
2478 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2479 .io_path()?;
2480 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
2481 Self::reject_existing_database(&io_root)?;
2482 Ok((io_root, lock))
2483 }
2484
2485 fn begin_open(
2486 root: impl AsRef<Path>,
2487 lock_timeout_ms: u32,
2488 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2489 let root = root.as_ref();
2490 let canonical_root = root.canonicalize().map_err(|error| {
2491 if error.kind() == std::io::ErrorKind::NotFound {
2492 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
2493 } else {
2494 error.into()
2495 }
2496 })?;
2497 let durable_root = crate::durable_file::DurableRoot::open(&canonical_root)?;
2498 Self::begin_open_durable(durable_root, lock_timeout_ms)
2499 }
2500
2501 fn begin_open_durable(
2502 durable_root: crate::durable_file::DurableRoot,
2503 lock_timeout_ms: u32,
2504 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2505 let io_root = durable_root.io_path()?;
2506 let current_root = io_root.canonicalize()?;
2507 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
2508 lock.claim_root_identity(&durable_root)?;
2509 lock.durable_root = Some(Arc::new(durable_root));
2510 let io_root = lock
2511 .durable_root
2512 .as_ref()
2513 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2514 .io_path()?;
2515 if lock
2516 .durable_root
2517 .as_ref()
2518 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2519 .open_directory(META_DIR)
2520 .is_err()
2521 {
2522 return Err(MongrelError::NotFound(format!(
2523 "no database metadata found at {:?}",
2524 current_root
2525 )));
2526 }
2527 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
2528 Ok((io_root, lock))
2529 }
2530
2531 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
2533 let (root, lock) = Self::begin_create(root)?;
2534 Self::create_inner(
2535 root,
2536 None,
2537 lock,
2538 crate::storage_mode::StorageMode::Standalone,
2539 )
2540 }
2541
2542 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2545 let (root, lock) = Self::begin_create(root)?;
2546 let salt = crate::encryption::random_salt()?;
2547 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2548 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2549 Self::create_inner(
2550 root,
2551 Some(kek),
2552 lock,
2553 crate::storage_mode::StorageMode::Standalone,
2554 )
2555 }
2556
2557 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2560 let (root, lock) = Self::begin_create(root)?;
2561 let salt = crate::encryption::random_salt()?;
2562 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2563 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2564 Self::create_inner(
2565 root,
2566 Some(kek),
2567 lock,
2568 crate::storage_mode::StorageMode::Standalone,
2569 )
2570 }
2571
2572 pub fn create_with_kms(
2575 root: impl AsRef<Path>,
2576 provider: &dyn crate::security_hardening::KeyManagementProvider,
2577 kms_key_id: &str,
2578 ) -> Result<Self> {
2579 let (root, lock) = Self::begin_create(root)?;
2580 let kek = create_kms_kek(&root, provider, kms_key_id)?;
2581 Self::create_inner(
2582 root,
2583 Some(kek),
2584 lock,
2585 crate::storage_mode::StorageMode::Standalone,
2586 )
2587 }
2588
2589 pub fn create_cluster_replica(
2597 root: impl AsRef<Path>,
2598 cluster_id: mongreldb_types::ids::ClusterId,
2599 node_id: mongreldb_types::ids::NodeId,
2600 database_id: mongreldb_types::ids::DatabaseId,
2601 ) -> Result<Self> {
2602 let (root, lock) = Self::begin_create(root)?;
2603 Self::create_inner(
2604 root,
2605 None,
2606 lock,
2607 crate::storage_mode::StorageMode::ClusterReplica {
2608 cluster_id,
2609 node_id,
2610 database_id,
2611 },
2612 )
2613 }
2614
2615 pub fn open_cluster_replica(
2622 root: impl AsRef<Path>,
2623 expected: &crate::storage_mode::StorageMode,
2624 ) -> Result<Self> {
2625 let Some((cluster_id, node_id, database_id)) = expected.cluster_identity() else {
2626 return Err(MongrelError::InvalidArgument(format!(
2627 "open_cluster_replica requires a ClusterReplica identity, got {expected:?}"
2628 )));
2629 };
2630 let (root, lock) = Self::begin_open(root, 0)?;
2631 Self::open_inner_locked(
2632 root,
2633 None,
2634 lock,
2635 CoreResourceConfig::default(),
2636 OpenModeGate::ClusterRuntime {
2637 cluster_id,
2638 node_id,
2639 database_id,
2640 },
2641 )
2642 }
2643
2644 fn create_inner(
2645 root: PathBuf,
2646 kek: Option<Arc<crate::encryption::Kek>>,
2647 lock: ExclusiveDatabaseLease,
2648 storage_mode: crate::storage_mode::StorageMode,
2649 ) -> Result<Self> {
2650 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2651 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2652 let cat = Catalog::empty();
2653 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2654 Self::finish_open(
2655 root,
2656 cat,
2657 kek,
2658 meta_dek,
2659 false,
2660 None,
2661 None,
2662 None,
2663 lock,
2664 CoreResourceConfig::default(),
2665 OpenModeGate::Create(storage_mode),
2666 )
2667 }
2668
2669 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
2671 Self::open_inner(root, None, None)
2672 }
2673
2674 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2676 let (root, lock) = Self::begin_open(root, 0)?;
2677 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2678 MongrelError::Other("database root descriptor was not pinned".into())
2679 })?)?;
2680 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2681 Self::open_inner_locked(
2682 root,
2683 Some(kek),
2684 lock,
2685 CoreResourceConfig::default(),
2686 OpenModeGate::Normal,
2687 )
2688 }
2689
2690 pub fn open_encrypted_with_options(
2693 root: impl AsRef<Path>,
2694 passphrase: &str,
2695 options: OpenOptions,
2696 ) -> Result<Self> {
2697 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2698 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2699 MongrelError::Other("database root descriptor was not pinned".into())
2700 })?)?;
2701 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2702 let gate = if options.offline_validation {
2703 OpenModeGate::OfflineValidation
2704 } else {
2705 OpenModeGate::Normal
2706 };
2707 Self::open_inner_locked(
2708 root,
2709 Some(kek),
2710 lock,
2711 CoreResourceConfig::from_options(&options)?,
2712 gate,
2713 )
2714 }
2715
2716 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2718 let (root, lock) = Self::begin_open(root, 0)?;
2719 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2720 MongrelError::Other("database root descriptor was not pinned".into())
2721 })?)?;
2722 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2723 Self::open_inner_locked(
2724 root,
2725 Some(kek),
2726 lock,
2727 CoreResourceConfig::default(),
2728 OpenModeGate::Normal,
2729 )
2730 }
2731
2732 pub fn open_with_kms(
2735 root: impl AsRef<Path>,
2736 provider: &dyn crate::security_hardening::KeyManagementProvider,
2737 ) -> Result<Self> {
2738 Self::open_with_kms_and_options(root, provider, OpenOptions::default())
2739 }
2740
2741 pub fn open_with_kms_and_options(
2743 root: impl AsRef<Path>,
2744 provider: &dyn crate::security_hardening::KeyManagementProvider,
2745 options: OpenOptions,
2746 ) -> Result<Self> {
2747 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2748 let kek = open_kms_kek(
2749 lock.durable_root.as_deref().ok_or_else(|| {
2750 MongrelError::Other("database root descriptor was not pinned".into())
2751 })?,
2752 provider,
2753 )?;
2754 let gate = if options.offline_validation {
2755 OpenModeGate::OfflineValidation
2756 } else {
2757 OpenModeGate::Normal
2758 };
2759 Self::open_inner_locked(
2760 root,
2761 Some(kek),
2762 lock,
2763 CoreResourceConfig::from_options(&options)?,
2764 gate,
2765 )
2766 }
2767
2768 pub fn rotate_kms_key(
2772 &self,
2773 provider: &dyn crate::security_hardening::KeyManagementProvider,
2774 new_kms_key_id: &str,
2775 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2776 let _rotation_guard = self.core.kms_rotation_lock.lock();
2777 if new_kms_key_id.is_empty() {
2778 return Err(MongrelError::InvalidArgument(
2779 "KMS key id must not be empty".into(),
2780 ));
2781 }
2782 let durable_root = Arc::clone(&self.core.durable_root);
2783 let root = durable_root.io_path()?;
2784 let envelope = read_kms_key_envelope(&durable_root)?;
2785 if envelope.provider_id != provider.provider_id() {
2786 return Err(MongrelError::Encryption(format!(
2787 "KMS provider mismatch: database requires {:?}, got {:?}",
2788 envelope.provider_id,
2789 provider.provider_id()
2790 )));
2791 }
2792 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2793 if let Some(record) = journal
2794 .load()
2795 .map_err(|error| MongrelError::Encryption(error.to_string()))?
2796 .filter(|record| record.phase != crate::security_hardening::KeyRotationPhase::Succeeded)
2797 {
2798 if record.to_key_id != new_kms_key_id {
2799 return Err(MongrelError::Conflict(format!(
2800 "KMS rotation to {:?} is already in progress",
2801 record.to_key_id
2802 )));
2803 }
2804 return self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record);
2805 }
2806 let now_micros = std::time::SystemTime::now()
2807 .duration_since(std::time::UNIX_EPOCH)
2808 .unwrap_or_default()
2809 .as_micros() as u64;
2810 let mut record = crate::security_hardening::KeyRotationRecord::new(
2811 envelope.wrapped_key.key_version.clone(),
2812 String::new(),
2813 now_micros,
2814 );
2815 record.from_key_id = envelope.wrapped_key.kms_key_id.clone();
2816 record.to_key_id = new_kms_key_id.to_owned();
2817 persist_key_rotation(&journal, &record)?;
2818 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
2819 }
2820
2821 pub fn retry_kms_key_rotation(
2823 &self,
2824 provider: &dyn crate::security_hardening::KeyManagementProvider,
2825 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2826 let _rotation_guard = self.core.kms_rotation_lock.lock();
2827 let durable_root = Arc::clone(&self.core.durable_root);
2828 let root = durable_root.io_path()?;
2829 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2830 let mut record = journal
2831 .load()
2832 .map_err(|error| MongrelError::Encryption(error.to_string()))?
2833 .ok_or_else(|| MongrelError::NotFound("KMS rotation journal".into()))?;
2834 if record.phase != crate::security_hardening::KeyRotationPhase::Failed {
2835 return Err(MongrelError::Conflict(
2836 "KMS rotation is not in Failed state".into(),
2837 ));
2838 }
2839 record.retry();
2840 persist_key_rotation(&journal, &record)?;
2841 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
2842 }
2843
2844 fn run_kms_key_rotation(
2845 &self,
2846 provider: &dyn crate::security_hardening::KeyManagementProvider,
2847 durable_root: &crate::durable_file::DurableRoot,
2848 root: &Path,
2849 journal: &crate::security_hardening::KeyRotationJournal,
2850 mut record: crate::security_hardening::KeyRotationRecord,
2851 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2852 use crate::security_hardening::{KeyRotationPhase, KmsDatabaseKeyEnvelope};
2853 loop {
2854 match record.phase {
2855 KeyRotationPhase::Pending => advance_key_rotation(journal, &mut record)?,
2856 KeyRotationPhase::WrappingNewKey => {
2857 if record.staged_wrapped_key.is_none() {
2858 let current = read_kms_key_envelope(durable_root)?;
2859 let wrapped =
2860 match provider.rewrap_key(¤t.wrapped_key, &record.to_key_id) {
2861 Ok(wrapped) => wrapped,
2862 Err(error) => {
2863 return fail_key_rotation(journal, &mut record, error);
2864 }
2865 };
2866 record.to_version = wrapped.key_version.clone();
2867 record.staged_wrapped_key = Some(wrapped);
2868 persist_key_rotation(journal, &record)?;
2869 }
2870 advance_key_rotation(journal, &mut record)?;
2871 }
2872 KeyRotationPhase::DualRead => {
2873 let old_envelope = read_kms_key_envelope(durable_root)?;
2874 let new_envelope = KmsDatabaseKeyEnvelope {
2875 provider_id: provider.provider_id().to_owned(),
2876 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
2877 MongrelError::Encryption(
2878 "KMS rotation has no staged wrapped key".into(),
2879 )
2880 })?,
2881 };
2882 let old_raw = match unwrap_kms_database_key(provider, &old_envelope) {
2883 Ok(key) => key,
2884 Err(error) => {
2885 return fail_key_rotation(journal, &mut record, error);
2886 }
2887 };
2888 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
2889 Ok(key) => key,
2890 Err(error) => {
2891 return fail_key_rotation(journal, &mut record, error);
2892 }
2893 };
2894 if !bool::from(old_raw.as_slice().ct_eq(new_raw.as_slice())) {
2895 return fail_key_rotation(
2896 journal,
2897 &mut record,
2898 "KMS rewrap changed the database root key",
2899 );
2900 }
2901 advance_key_rotation(journal, &mut record)?;
2902 }
2903 KeyRotationPhase::Reencrypting => {
2904 advance_key_rotation(journal, &mut record)?;
2907 }
2908 KeyRotationPhase::Validating => {
2909 let new_envelope = KmsDatabaseKeyEnvelope {
2910 provider_id: provider.provider_id().to_owned(),
2911 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
2912 MongrelError::Encryption(
2913 "KMS rotation has no staged wrapped key".into(),
2914 )
2915 })?,
2916 };
2917 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
2918 Ok(key) => key,
2919 Err(error) => {
2920 return fail_key_rotation(journal, &mut record, error);
2921 }
2922 };
2923 let salt = read_encryption_salt(durable_root)?;
2924 let candidate = crate::encryption::Kek::from_raw_key(new_raw.as_ref(), &salt)?;
2925 let candidate_meta = candidate.derive_meta_key();
2926 let Some(active_meta) = self.core.meta_dek.as_ref() else {
2927 return fail_key_rotation(
2928 journal,
2929 &mut record,
2930 "KMS metadata exists on a plaintext database",
2931 );
2932 };
2933 if !bool::from(candidate_meta.as_ref().ct_eq(active_meta.as_slice())) {
2934 return fail_key_rotation(
2935 journal,
2936 &mut record,
2937 "KMS rewrap validation failed",
2938 );
2939 }
2940 advance_key_rotation(journal, &mut record)?;
2941 }
2942 KeyRotationPhase::Published => {
2943 let envelope = KmsDatabaseKeyEnvelope {
2944 provider_id: provider.provider_id().to_owned(),
2945 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
2946 MongrelError::Encryption(
2947 "KMS rotation has no staged wrapped key".into(),
2948 )
2949 })?,
2950 };
2951 write_kms_key_envelope(root, &envelope)?;
2952 advance_key_rotation(journal, &mut record)?;
2953 }
2954 KeyRotationPhase::RetiringOldKey => {
2955 advance_key_rotation(journal, &mut record)?;
2956 }
2957 KeyRotationPhase::Succeeded => return Ok(record),
2958 KeyRotationPhase::Failed => {
2959 return Err(MongrelError::Encryption(
2960 "failed KMS rotation must be explicitly retried".into(),
2961 ));
2962 }
2963 }
2964 }
2965 }
2966
2967 pub fn open_with_credentials(
2979 root: impl AsRef<Path>,
2980 username: &str,
2981 password: &str,
2982 ) -> Result<Self> {
2983 Self::open_inner_with_credentials(root, None, username, password)
2984 }
2985
2986 pub fn open_with_credentials_and_options(
2990 root: impl AsRef<Path>,
2991 username: &str,
2992 password: &str,
2993 options: OpenOptions,
2994 ) -> Result<Self> {
2995 let gate = if options.offline_validation {
2996 OpenModeGate::OfflineValidation
2997 } else {
2998 OpenModeGate::Normal
2999 };
3000 Self::open_inner_with_credentials_and_lock_timeout(
3001 root,
3002 None,
3003 username,
3004 password,
3005 options.lock_timeout_ms,
3006 CoreResourceConfig::from_options(&options)?,
3007 gate,
3008 )
3009 }
3010
3011 pub fn open_encrypted_with_credentials(
3014 root: impl AsRef<Path>,
3015 passphrase: &str,
3016 username: &str,
3017 password: &str,
3018 ) -> Result<Self> {
3019 let (root, lock) = Self::begin_open(root, 0)?;
3020 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3021 MongrelError::Other("database root descriptor was not pinned".into())
3022 })?)?;
3023 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3024 Self::open_inner_with_credentials_locked(
3025 root,
3026 Some(kek),
3027 username,
3028 password,
3029 lock,
3030 CoreResourceConfig::default(),
3031 OpenModeGate::Normal,
3032 )
3033 }
3034
3035 pub fn open_encrypted_with_credentials_and_options(
3039 root: impl AsRef<Path>,
3040 passphrase: &str,
3041 username: &str,
3042 password: &str,
3043 options: OpenOptions,
3044 ) -> Result<Self> {
3045 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
3046 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3047 MongrelError::Other("database root descriptor was not pinned".into())
3048 })?)?;
3049 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3050 let gate = if options.offline_validation {
3051 OpenModeGate::OfflineValidation
3052 } else {
3053 OpenModeGate::Normal
3054 };
3055 Self::open_inner_with_credentials_locked(
3056 root,
3057 Some(kek),
3058 username,
3059 password,
3060 lock,
3061 CoreResourceConfig::from_options(&options)?,
3062 gate,
3063 )
3064 }
3065
3066 pub fn open_with_kms_and_credentials(
3068 root: impl AsRef<Path>,
3069 provider: &dyn crate::security_hardening::KeyManagementProvider,
3070 username: &str,
3071 password: &str,
3072 ) -> Result<Self> {
3073 let (root, lock) = Self::begin_open(root, 0)?;
3074 let kek = open_kms_kek(
3075 lock.durable_root.as_deref().ok_or_else(|| {
3076 MongrelError::Other("database root descriptor was not pinned".into())
3077 })?,
3078 provider,
3079 )?;
3080 Self::open_inner_with_credentials_locked(
3081 root,
3082 Some(kek),
3083 username,
3084 password,
3085 lock,
3086 CoreResourceConfig::default(),
3087 OpenModeGate::Normal,
3088 )
3089 }
3090
3091 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
3098 let gate = if options.offline_validation {
3101 OpenModeGate::OfflineValidation
3102 } else {
3103 OpenModeGate::Normal
3104 };
3105 Self::open_inner_with_lock_timeout(
3106 root,
3107 None,
3108 None,
3109 options.lock_timeout_ms,
3110 CoreResourceConfig::from_options(&options)?,
3111 gate,
3112 )
3113 }
3114
3115 fn open_inner_with_lock_timeout(
3116 root: impl AsRef<Path>,
3117 kek: Option<Arc<crate::encryption::Kek>>,
3118 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3119 lock_timeout_ms: u32,
3120 resources: CoreResourceConfig,
3121 mode_gate: OpenModeGate,
3122 ) -> Result<Self> {
3123 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3124 Self::open_inner_locked(root, kek, lock, resources, mode_gate)
3125 }
3126
3127 fn open_inner_locked(
3128 root: PathBuf,
3129 kek: Option<Arc<crate::encryption::Kek>>,
3130 lock: ExclusiveDatabaseLease,
3131 resources: CoreResourceConfig,
3132 mode_gate: OpenModeGate,
3133 ) -> Result<Self> {
3134 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3135 let mut cat = catalog::read_durable(
3136 lock.durable_root.as_deref().ok_or_else(|| {
3137 MongrelError::Other("database root descriptor was not pinned".into())
3138 })?,
3139 meta_dek.as_ref(),
3140 )?
3141 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3142 let recovery_checkpoint = cat.clone();
3143
3144 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3147 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3148 lock.durable_root.as_deref().ok_or_else(|| {
3149 MongrelError::Other("database root descriptor was not pinned".into())
3150 })?,
3151 wal_dek.as_ref(),
3152 )?;
3153 recover_ddl_from_records(
3154 &root,
3155 Some(lock.durable_root.as_deref().ok_or_else(|| {
3156 MongrelError::Other("database root descriptor was not pinned".into())
3157 })?),
3158 &mut cat,
3159 meta_dek.as_ref(),
3160 false,
3161 None,
3162 &recovery_records,
3163 )?;
3164 Self::finish_open(
3165 root,
3166 cat,
3167 kek,
3168 meta_dek,
3169 true,
3170 Some(recovery_checkpoint),
3171 Some(recovery_records),
3172 None,
3173 lock,
3174 resources,
3175 mode_gate,
3176 )
3177 }
3178
3179 fn open_inner_with_credentials(
3186 root: impl AsRef<Path>,
3187 kek: Option<Arc<crate::encryption::Kek>>,
3188 username: &str,
3189 password: &str,
3190 ) -> Result<Self> {
3191 Self::open_inner_with_credentials_and_lock_timeout(
3192 root,
3193 kek,
3194 username,
3195 password,
3196 0,
3197 CoreResourceConfig::default(),
3198 OpenModeGate::Normal,
3199 )
3200 }
3201
3202 #[allow(clippy::too_many_arguments)]
3206 fn open_inner_with_credentials_and_lock_timeout(
3207 root: impl AsRef<Path>,
3208 kek: Option<Arc<crate::encryption::Kek>>,
3209 username: &str,
3210 password: &str,
3211 lock_timeout_ms: u32,
3212 resources: CoreResourceConfig,
3213 mode_gate: OpenModeGate,
3214 ) -> Result<Self> {
3215 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3216 Self::open_inner_with_credentials_locked(
3217 root, kek, username, password, lock, resources, mode_gate,
3218 )
3219 }
3220
3221 #[allow(clippy::too_many_arguments)]
3222 fn open_inner_with_credentials_locked(
3223 root: PathBuf,
3224 kek: Option<Arc<crate::encryption::Kek>>,
3225 username: &str,
3226 password: &str,
3227 lock: ExclusiveDatabaseLease,
3228 resources: CoreResourceConfig,
3229 mode_gate: OpenModeGate,
3230 ) -> Result<Self> {
3231 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3232 let mut cat = catalog::read_durable(
3233 lock.durable_root.as_deref().ok_or_else(|| {
3234 MongrelError::Other("database root descriptor was not pinned".into())
3235 })?,
3236 meta_dek.as_ref(),
3237 )?
3238 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3239 let recovery_checkpoint = cat.clone();
3240
3241 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3244 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3245 lock.durable_root.as_deref().ok_or_else(|| {
3246 MongrelError::Other("database root descriptor was not pinned".into())
3247 })?,
3248 wal_dek.as_ref(),
3249 )?;
3250 recover_ddl_from_records(
3251 &root,
3252 Some(lock.durable_root.as_deref().ok_or_else(|| {
3253 MongrelError::Other("database root descriptor was not pinned".into())
3254 })?),
3255 &mut cat,
3256 meta_dek.as_ref(),
3257 false,
3258 None,
3259 &recovery_records,
3260 )?;
3261
3262 if !cat.require_auth {
3265 return Err(MongrelError::AuthNotRequired);
3266 }
3267
3268 let user = cat
3273 .users
3274 .iter()
3275 .find(|u| u.username == username)
3276 .filter(|u| !u.password_hash.is_empty())
3277 .ok_or_else(|| MongrelError::InvalidCredentials {
3278 username: username.to_string(),
3279 })?;
3280 let password_ok = crate::auth::verify_password(password, &user.password_hash)
3281 .map_err(MongrelError::Other)?;
3282 if !password_ok {
3283 return Err(MongrelError::InvalidCredentials {
3284 username: username.to_string(),
3285 });
3286 }
3287
3288 let principal =
3290 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
3291 MongrelError::InvalidCredentials {
3292 username: username.to_string(),
3293 }
3294 })?;
3295
3296 Self::finish_open(
3297 root,
3298 cat,
3299 kek,
3300 meta_dek,
3301 true,
3302 Some(recovery_checkpoint),
3303 Some(recovery_records),
3304 Some(principal),
3305 lock,
3306 resources,
3307 mode_gate,
3308 )
3309 }
3310
3311 pub fn create_with_credentials(
3321 root: impl AsRef<Path>,
3322 admin_username: &str,
3323 admin_password: &str,
3324 ) -> Result<Self> {
3325 let (root, lock) = Self::begin_create(root)?;
3326 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
3327 }
3328
3329 pub fn create_encrypted_with_credentials(
3333 root: impl AsRef<Path>,
3334 passphrase: &str,
3335 admin_username: &str,
3336 admin_password: &str,
3337 ) -> Result<Self> {
3338 let (root, lock) = Self::begin_create(root)?;
3339 let salt = crate::encryption::random_salt()?;
3340 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
3341 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3342 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3343 }
3344
3345 pub fn create_with_kms_and_credentials(
3347 root: impl AsRef<Path>,
3348 provider: &dyn crate::security_hardening::KeyManagementProvider,
3349 kms_key_id: &str,
3350 admin_username: &str,
3351 admin_password: &str,
3352 ) -> Result<Self> {
3353 let (root, lock) = Self::begin_create(root)?;
3354 let kek = create_kms_kek(&root, provider, kms_key_id)?;
3355 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3356 }
3357
3358 fn create_inner_with_credentials(
3359 root: PathBuf,
3360 kek: Option<Arc<crate::encryption::Kek>>,
3361 admin_username: &str,
3362 admin_password: &str,
3363 lock: ExclusiveDatabaseLease,
3364 ) -> Result<Self> {
3365 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
3366 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3367
3368 let password_hash =
3370 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
3371 let scram_sha_256 =
3372 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
3373 let mysql_caching_sha2 =
3374 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
3375 let mut cat = Catalog::empty();
3376 cat.require_auth = true;
3377 cat.next_user_id = 2;
3378 cat.users.push(crate::auth::UserEntry {
3379 id: 1,
3380 username: admin_username.to_string(),
3381 password_hash,
3382 scram_sha_256: Some(scram_sha_256),
3383 mysql_caching_sha2: Some(mysql_caching_sha2),
3384 roles: Vec::new(),
3385 is_admin: true,
3386 created_epoch: 0,
3387 });
3388 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3389
3390 let admin_principal = crate::auth::Principal {
3393 user_id: 1,
3394 created_epoch: 0,
3395 username: admin_username.to_string(),
3396 is_admin: true,
3397 roles: Vec::new(),
3398 permissions: Vec::new(),
3399 };
3400 Self::finish_open(
3401 root,
3402 cat,
3403 kek,
3404 meta_dek,
3405 false,
3406 None,
3407 None,
3408 Some(admin_principal),
3409 lock,
3410 CoreResourceConfig::default(),
3411 OpenModeGate::Create(crate::storage_mode::StorageMode::Standalone),
3412 )
3413 }
3414
3415 fn reject_existing_database(root: &Path) -> Result<()> {
3416 if root.join(catalog::CATALOG_FILENAME).exists() {
3419 return Err(MongrelError::InvalidArgument(format!(
3420 "database already exists at {}; use Database::open() to open it, \
3421 or remove the directory first",
3422 root.display()
3423 )));
3424 }
3425 Ok(())
3426 }
3427
3428 fn open_inner(
3429 root: impl AsRef<Path>,
3430 kek: Option<Arc<crate::encryption::Kek>>,
3431 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3432 ) -> Result<Self> {
3433 Self::open_inner_with_lock_timeout(
3434 root,
3435 kek,
3436 None,
3437 0,
3438 CoreResourceConfig::default(),
3439 OpenModeGate::Normal,
3440 )
3441 }
3442
3443 pub(crate) fn open_offline_validation(root: impl AsRef<Path>) -> Result<Self> {
3446 Self::open_inner_with_lock_timeout(
3447 root,
3448 None,
3449 None,
3450 0,
3451 CoreResourceConfig::default(),
3452 OpenModeGate::OfflineValidation,
3453 )
3454 }
3455
3456 pub(crate) fn open_replica_recovery_durable(
3460 root: &crate::durable_file::DurableRoot,
3461 ) -> Result<Self> {
3462 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3463 Self::open_replica_recovery_inner(root, None, lock)
3464 }
3465
3466 pub(crate) fn open_encrypted_replica_recovery_durable(
3467 root: &crate::durable_file::DurableRoot,
3468 passphrase: &str,
3469 ) -> Result<Self> {
3470 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3471 let salt = read_encryption_salt(root)?;
3472 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3473 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
3474 }
3475
3476 fn open_replica_recovery_inner(
3477 root: PathBuf,
3478 kek: Option<Arc<crate::encryption::Kek>>,
3479 lock: ExclusiveDatabaseLease,
3480 ) -> Result<Self> {
3481 if !root.join(META_DIR).join("replica").is_file() {
3482 return Err(MongrelError::InvalidArgument(
3483 "recovery auth bypass requires a marked replica staging directory".into(),
3484 ));
3485 }
3486 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3487 let mut cat = catalog::read_durable(
3488 lock.durable_root.as_deref().ok_or_else(|| {
3489 MongrelError::Other("database root descriptor was not pinned".into())
3490 })?,
3491 meta_dek.as_ref(),
3492 )?
3493 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3494 let recovery_checkpoint = cat.clone();
3495 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3496 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3497 lock.durable_root.as_deref().ok_or_else(|| {
3498 MongrelError::Other("database root descriptor was not pinned".into())
3499 })?,
3500 wal_dek.as_ref(),
3501 )?;
3502 recover_ddl_from_records(
3503 &root,
3504 Some(lock.durable_root.as_deref().ok_or_else(|| {
3505 MongrelError::Other("database root descriptor was not pinned".into())
3506 })?),
3507 &mut cat,
3508 meta_dek.as_ref(),
3509 false,
3510 None,
3511 &recovery_records,
3512 )?;
3513 let principal = if cat.require_auth {
3514 cat.users
3515 .iter()
3516 .find(|user| user.is_admin)
3517 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
3518 .ok_or_else(|| {
3519 MongrelError::Schema(
3520 "authenticated replica catalog has no recoverable admin".into(),
3521 )
3522 })?
3523 .into()
3524 } else {
3525 None
3526 };
3527 Self::finish_open(
3528 root,
3529 cat,
3530 kek,
3531 meta_dek,
3532 true,
3533 Some(recovery_checkpoint),
3534 Some(recovery_records),
3535 principal,
3536 lock,
3537 CoreResourceConfig::default(),
3538 OpenModeGate::Normal,
3539 )
3540 }
3541
3542 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
3554 use fs2::FileExt;
3555 if timeout_ms == 0 {
3556 return f.try_lock_exclusive();
3557 }
3558 let deadline =
3560 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
3561 let mut next_sleep = std::time::Duration::from_millis(1);
3562 let mut recorded_wait = false;
3563 loop {
3564 match f.try_lock_exclusive() {
3565 Ok(()) => return Ok(()),
3566 Err(e) if Self::is_file_lock_contended(&e) => {
3567 if !recorded_wait {
3568 DATABASE_OPEN_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
3569 recorded_wait = true;
3570 }
3571 let now = std::time::Instant::now();
3572 if now >= deadline {
3573 return Err(std::io::Error::new(
3574 std::io::ErrorKind::WouldBlock,
3575 format!("could not acquire database lock within {timeout_ms}ms"),
3576 ));
3577 }
3578 let remaining = deadline - now;
3579 let sleep = next_sleep.min(remaining);
3580 std::thread::sleep(sleep);
3581 next_sleep = next_sleep
3584 .saturating_mul(10)
3585 .min(std::time::Duration::from_millis(50));
3586 }
3587 Err(e) => return Err(e),
3588 }
3589 }
3590 }
3591
3592 fn is_file_lock_contended(error: &std::io::Error) -> bool {
3593 if error.kind() == std::io::ErrorKind::WouldBlock {
3594 return true;
3595 }
3596 #[cfg(windows)]
3597 {
3598 return error.raw_os_error() == Some(33);
3601 }
3602 #[cfg(not(windows))]
3603 false
3604 }
3605
3606 #[allow(clippy::too_many_arguments)]
3607 fn finish_open(
3608 root: PathBuf,
3609 cat: Catalog,
3610 kek: Option<Arc<crate::encryption::Kek>>,
3611 meta_dek: Option<[u8; META_DEK_LEN]>,
3612 existing: bool,
3613 recovery_checkpoint: Option<Catalog>,
3614 recovery_records: Option<Vec<crate::wal::Record>>,
3615 principal: Option<crate::auth::Principal>,
3616 lock: ExclusiveDatabaseLease,
3617 resources: CoreResourceConfig,
3618 mode_gate: OpenModeGate,
3619 ) -> Result<Self> {
3620 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
3621 MongrelError::Other("database root descriptor was not pinned".into())
3622 })?);
3623 let storage_mode = if existing {
3628 crate::storage_mode::read(&durable_root)?
3629 } else {
3630 None
3631 };
3632 match (&mode_gate, &storage_mode) {
3633 (OpenModeGate::Create(_), _) => {}
3634 (OpenModeGate::Normal | OpenModeGate::SharedCore, mode) => {
3635 crate::storage_mode::check_open(mode.as_ref(), false)?
3636 }
3637 (OpenModeGate::OfflineValidation, mode) => {
3638 crate::storage_mode::check_open(mode.as_ref(), true)?
3639 }
3640 (
3641 OpenModeGate::ClusterRuntime {
3642 cluster_id,
3643 node_id,
3644 database_id,
3645 },
3646 Some(actual),
3647 ) => {
3648 let expected = crate::storage_mode::StorageMode::ClusterReplica {
3649 cluster_id: *cluster_id,
3650 node_id: *node_id,
3651 database_id: *database_id,
3652 };
3653 if *actual != expected {
3654 return Err(
3655 crate::storage_mode::StorageModeError::IdentityMismatch(format!(
3656 "expected {expected:?}, marker holds {actual:?}"
3657 ))
3658 .into(),
3659 );
3660 }
3661 }
3662 (OpenModeGate::ClusterRuntime { .. }, None) => {
3663 return Err(crate::storage_mode::StorageModeError::IdentityMismatch(
3664 "expected a ClusterReplica marker; directory has none".into(),
3665 )
3666 .into());
3667 }
3668 }
3669 let read_only = match &mode_gate {
3670 OpenModeGate::OfflineValidation | OpenModeGate::ClusterRuntime { .. } => true,
3671 OpenModeGate::Create(mode) => mode.cluster_identity().is_some(),
3674 OpenModeGate::Normal | OpenModeGate::SharedCore => false,
3675 } || if existing {
3676 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
3677 Ok(_) => true,
3678 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
3679 Err(error) => return Err(error.into()),
3680 }
3681 } else {
3682 false
3683 };
3684 let recovered_catalog = cat;
3685 let mut cat = recovered_catalog.clone();
3686 let abandoned = if existing && !read_only {
3687 let abandoned = cat
3688 .tables
3689 .iter()
3690 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
3691 .map(|entry| entry.table_id)
3692 .collect::<Vec<_>>();
3693 for entry in &mut cat.tables {
3694 if abandoned.contains(&entry.table_id) {
3695 entry.state = TableState::Dropped {
3696 at_epoch: cat.db_epoch,
3697 };
3698 }
3699 }
3700 abandoned
3701 } else {
3702 Vec::new()
3703 };
3704 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3705 let recovery_records = match (existing, recovery_records) {
3706 (true, Some(records)) => records,
3707 (true, None) => {
3708 return Err(MongrelError::Other(
3709 "existing open has no validated WAL recovery plan".into(),
3710 ))
3711 }
3712 (false, _) => Vec::new(),
3713 };
3714 let (history_epochs, history_start) =
3715 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
3716 let open_generation = if existing {
3717 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
3718 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3719 })?;
3720 let recovered_table_ids = cat
3721 .tables
3722 .iter()
3723 .filter(|entry| {
3724 checkpoint
3725 .tables
3726 .iter()
3727 .all(|checkpoint| checkpoint.table_id != entry.table_id)
3728 })
3729 .map(|entry| entry.table_id)
3730 .collect::<HashSet<_>>();
3731 let reconciled_table_ids = cat
3732 .tables
3733 .iter()
3734 .filter(|entry| {
3735 checkpoint
3736 .tables
3737 .iter()
3738 .find(|checkpoint| checkpoint.table_id == entry.table_id)
3739 .is_some_and(|checkpoint| {
3740 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
3741 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
3742 })
3743 })
3744 .map(|entry| entry.table_id)
3745 .collect::<HashSet<_>>();
3746 validate_shared_wal_recovery_plan(
3747 &durable_root,
3748 &cat,
3749 &recovered_table_ids,
3750 &reconciled_table_ids,
3751 meta_dek.as_ref(),
3752 kek.clone(),
3753 &recovery_records,
3754 )?;
3755 let retained_generation = recovery_records
3756 .iter()
3757 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
3758 .map(|record| record.txn_id >> 32)
3759 .max()
3760 .unwrap_or(0);
3761 let head_generation =
3762 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
3763 let durable_floor = match head_generation {
3764 Some(head) if retained_generation > head => {
3765 return Err(MongrelError::CorruptWal {
3766 offset: retained_generation,
3767 reason: format!(
3768 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
3769 ),
3770 })
3771 }
3772 Some(head) => head,
3773 None => retained_generation,
3774 };
3775 let stored = catalog::read_generation(&durable_root)?;
3776 if stored.is_some_and(|generation| generation < durable_floor) {
3777 return Err(MongrelError::Other(format!(
3778 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
3779 )));
3780 }
3781 let bumped = stored
3782 .unwrap_or(durable_floor)
3783 .max(durable_floor)
3784 .checked_add(1)
3785 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
3786 if bumped > u32::MAX as u64 {
3787 return Err(MongrelError::Full(
3788 "open-generation namespace exhausted".into(),
3789 ));
3790 }
3791 bumped
3792 } else {
3793 0
3794 };
3795 let principal = if cat.require_auth && !matches!(&mode_gate, OpenModeGate::SharedCore) {
3796 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3797 Some(
3798 Self::resolve_bound_principal_from_catalog(&cat, supplied)
3799 .ok_or(MongrelError::AuthRequired)?,
3800 )
3801 } else {
3802 principal
3803 };
3804 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
3805 if existing {
3806 for entry in &cat.tables {
3807 if !matches!(entry.state, TableState::Live) {
3808 continue;
3809 }
3810 match durable_root
3811 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
3812 {
3813 Ok(root) => {
3814 table_roots.insert(entry.table_id, Arc::new(root));
3815 }
3816 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3817 Err(error) => return Err(error.into()),
3818 }
3819 }
3820 }
3821
3822 if existing {
3826 let mut applied = recovery_checkpoint.ok_or_else(|| {
3827 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3828 })?;
3829 recover_ddl_from_records(
3830 &root,
3831 Some(&durable_root),
3832 &mut applied,
3833 meta_dek.as_ref(),
3834 true,
3835 Some(&table_roots),
3836 &recovery_records,
3837 )?;
3838 let catalog_value = |catalog: &Catalog| {
3839 serde_json::to_value(catalog)
3840 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
3841 };
3842 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
3843 return Err(MongrelError::CorruptWal {
3844 offset: 0,
3845 reason: "validated and applied DDL recovery plans differ".into(),
3846 });
3847 }
3848 if catalog_value(&cat)? != catalog_value(&applied)? {
3849 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3850 }
3851 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
3852 if !read_only {
3853 sweep_unreferenced_table_dirs(&root, &cat)?;
3854 }
3855 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
3856 Ok(()) => {}
3857 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3858 Err(error) => return Err(error.into()),
3859 }
3860 }
3861
3862 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
3863 let snapshots = Arc::new(SnapshotRegistry::new());
3864 snapshots.configure_history(history_epochs, history_start);
3865 let memory_governor = crate::memory::MemoryGovernor::new(
3870 crate::memory::GovernorConfig::new(resources.memory_budget_bytes),
3871 )
3872 .map_err(|error| MongrelError::InvalidArgument(format!("memory governor: {error}")))?;
3873 let spill_manager = crate::spill::SpillManager::open(
3877 &durable_root,
3878 crate::spill::SpillConfig::new(resources.temp_disk_budget_bytes),
3879 meta_dek,
3880 )?;
3881 let job_registry = Arc::new(crate::jobs::JobRegistry::open(&root, meta_dek.as_ref())?);
3885 let page_cache = Arc::new(crate::cache::Sharded::new(
3886 crate::cache::CACHE_SHARDS,
3887 || {
3888 crate::cache::PageCache::new(
3889 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3890 )
3891 .with_governor(
3892 memory_governor.clone(),
3893 crate::memory::MemoryClass::PageCache,
3894 )
3895 },
3896 ));
3897 memory_governor.register_reclaimable(&page_cache);
3898 let decoded_cache = Arc::new(crate::cache::Sharded::new(
3899 crate::cache::CACHE_SHARDS,
3900 || {
3901 crate::cache::DecodedPageCache::new(
3902 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3903 )
3904 .with_governor(
3905 memory_governor.clone(),
3906 crate::memory::MemoryClass::DecodedCache,
3907 )
3908 },
3909 ));
3910 memory_governor.register_reclaimable(&decoded_cache);
3911 let commit_lock = Arc::new(Mutex::new(()));
3912 let shared_wal = Arc::new(Mutex::new(if existing {
3913 crate::wal::SharedWal::open_durable_root_validated(
3914 Arc::clone(&durable_root),
3915 Epoch(cat.db_epoch),
3916 wal_dek.clone(),
3917 Some(&recovery_records),
3918 )?
3919 } else {
3920 crate::wal::SharedWal::create_with_durable_root(
3921 Arc::clone(&durable_root),
3922 Epoch(cat.db_epoch),
3923 wal_dek.clone(),
3924 )?
3925 }));
3926 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
3930 let lifecycle = Arc::new(crate::core::LifecycleController::new());
3935 let group = Arc::new(
3936 crate::txn::GroupCommit::new(shared_wal.lock().durable_seq())
3937 .with_lifecycle(Arc::clone(&lifecycle)),
3938 );
3939 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
3940 let txn_ids = Arc::new(Mutex::new(1u64));
3944 let _ = abandoned;
3945 let hlc = Arc::new(mongreldb_types::hlc::HlcClock::new(
3952 0,
3953 std::time::Duration::from_millis(500),
3954 ));
3955 let idempotency = crate::txn::IdempotencyLedger::open(Arc::clone(&durable_root), meta_dek)?;
3958 let standalone_commit_log = Arc::new(crate::commit_log::StandaloneCommitLog::new(
3959 Arc::clone(&shared_wal),
3960 Arc::clone(&group),
3961 Arc::clone(&epoch),
3962 Arc::clone(&commit_lock),
3963 Arc::clone(&txn_ids),
3964 root.clone(),
3965 wal_dek.clone(),
3966 Arc::clone(&hlc),
3967 ));
3968 let commit_log: Arc<dyn mongreldb_log::CommitLog> = standalone_commit_log.clone();
3969
3970 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
3975 let security_coordinator = security_coordinator(&root, cat.security_version);
3976 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> =
3977 if matches!(&mode_gate, OpenModeGate::SharedCore) {
3978 None
3982 } else {
3983 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
3984 auth_state.clone(),
3985 )))
3986 };
3987
3988 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
3994 for entry in &cat.tables {
3995 if !matches!(entry.state, TableState::Live) {
3996 continue;
3997 }
3998 let table_root = match table_roots.remove(&entry.table_id) {
3999 Some(root) => root,
4000 None => Arc::new(
4001 durable_root
4002 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
4003 ),
4004 };
4005 let tdir = table_root.io_path()?;
4006 let ctx = SharedCtx {
4007 root_guard: Some(table_root),
4008 epoch: Arc::clone(&epoch),
4009 page_cache: Arc::clone(&page_cache),
4010 decoded_cache: Arc::clone(&decoded_cache),
4011 snapshots: Arc::clone(&snapshots),
4012 kek: kek.clone(),
4013 commit_lock: Arc::clone(&commit_lock),
4014 shared: Some(crate::engine::SharedWalCtx {
4015 wal: Arc::clone(&shared_wal),
4016 group: Arc::clone(&group),
4017 poisoned: Arc::clone(&poisoned),
4018 txn_ids: Arc::clone(&txn_ids),
4019 change_wake: change_wake.clone(),
4020 lifecycle: Arc::clone(&lifecycle),
4021 }),
4022 table_name: Some(entry.name.clone()),
4023 auth: auth_checker.clone(),
4024 read_only,
4025 };
4026 let t = Table::open_in(&tdir, ctx)?;
4027 tables.insert(entry.table_id, TableHandle::new(t));
4028 }
4029
4030 if existing {
4036 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
4037 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
4038 if read_only {
4039 crate::replication::reconcile_replica_epoch_durable(
4040 &durable_root,
4041 epoch.visible().0,
4042 )?;
4043 }
4044 sweep_pending_txn_dirs(&root, &cat);
4047 }
4048
4049 catalog::write_generation(&durable_root, open_generation)?;
4051 match &mode_gate {
4056 OpenModeGate::Create(mode) => crate::storage_mode::write(&durable_root, mode)?,
4057 _ if existing && storage_mode.is_none() => {
4058 crate::storage_mode::write(
4059 &durable_root,
4060 &crate::storage_mode::StorageMode::Standalone,
4061 )?;
4062 }
4063 _ => {}
4064 }
4065 shared_wal.lock().seal_open_generation(open_generation)?;
4066 crate::replication::replication_identity_durable(&durable_root)?;
4067 let next_txn_id = (open_generation << 32) | 1;
4068 *txn_ids.lock() = next_txn_id;
4070 let mut lock = lock;
4071 lock.mark_open()?;
4072
4073 lifecycle.mark_open();
4077 let core = DatabaseCore {
4078 root,
4079 durable_root,
4080 lifecycle,
4081 registry: std::sync::OnceLock::new(),
4082 read_only,
4083 catalog: RwLock::new(cat),
4084 security_coordinator,
4085 security_catalog_disk_reads: AtomicU64::new(0),
4086 rls_cache: Mutex::new(RlsCache::default()),
4087 epoch,
4088 snapshots,
4089 memory_governor,
4090 page_cache,
4091 decoded_cache,
4092 spill_manager,
4093 resource_groups: crate::resource::ResourceGroupRegistry::with_defaults(),
4094 embedding_providers: crate::embedding::EmbeddingProviderRegistry::new(),
4095 job_registry,
4096 commit_lock,
4097 kms_rotation_lock: Mutex::new(()),
4098 shared_wal,
4099 next_txn_id: txn_ids,
4100 tables: RwLock::new(tables),
4101 kek,
4102 ddl_lock: Mutex::new(()),
4103 meta_dek,
4104 conflicts: crate::txn::ConflictIndex::new(),
4105 active_txns: crate::txn::ActiveTxns::new(),
4106 lock_manager: Arc::new(crate::locks::LockManager::new()),
4107 poisoned,
4108 group,
4109 commit_log: RwLock::new(commit_log),
4110 standalone_commit_log,
4111 hlc,
4112 idempotency,
4113 commit_ts_ledger: Mutex::new(commit_ts_ledger_from_recovery(&recovery_records)),
4114 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
4115 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
4116 replication_barrier: parking_lot::RwLock::new(()),
4117 replication_wal_retention_segments: AtomicUsize::new(0),
4118 backup_pins: Arc::new(Mutex::new(HashMap::new())),
4119 spill_hook: Mutex::new(None),
4120 security_commit_hook: Mutex::new(None),
4121 catalog_commit_hook: Mutex::new(None),
4122 backup_hook: Mutex::new(None),
4123 fk_lock_hook: Mutex::new(None),
4124 replication_hook: Mutex::new(None),
4125 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
4126 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
4127 trigger_max_loop_iterations: AtomicU32::new(
4128 TriggerConfig::default().max_loop_iterations,
4129 ),
4130 notify: {
4131 let (tx, _rx) = tokio::sync::broadcast::channel(256);
4132 tx
4133 },
4134 change_wake,
4135 _lock: Mutex::new(Some(lock)),
4136 };
4137 Ok(Self {
4138 core: Arc::new(core),
4139 principal: RwLock::new(principal),
4140 auth_state,
4141 shared: false,
4142 })
4143 }
4144
4145 pub fn visible_epoch(&self) -> Epoch {
4147 self.epoch.visible()
4148 }
4149
4150 pub fn catalog_version(&self) -> u64 {
4152 self.catalog.read().catalog_version
4153 }
4154
4155 pub fn storage_mode(&self) -> Result<Option<crate::storage_mode::StorageMode>> {
4159 Ok(crate::storage_mode::read(&self.durable_root)?)
4160 }
4161
4162 pub fn memory_governor(&self) -> &crate::memory::MemoryGovernor {
4165 &self.memory_governor
4166 }
4167
4168 pub fn resource_groups(&self) -> &crate::resource::ResourceGroupRegistry {
4171 &self.resource_groups
4172 }
4173
4174 pub fn embedding_providers(&self) -> &crate::embedding::EmbeddingProviderRegistry {
4178 &self.embedding_providers
4179 }
4180
4181 pub fn unregister_embedding_provider(&self, provider_id: &str) -> Result<bool> {
4183 let references = self
4184 .catalog
4185 .read()
4186 .tables
4187 .iter()
4188 .flat_map(|table| &table.schema.columns)
4189 .filter(|column| {
4190 column
4191 .embedding_source
4192 .as_ref()
4193 .and_then(crate::embedding::EmbeddingSource::provider_id)
4194 == Some(provider_id)
4195 })
4196 .count();
4197 self.embedding_providers
4198 .unregister_unreferenced(provider_id, references)
4199 .map_err(embedding_error)
4200 }
4201
4202 fn materialize_generated_embeddings(
4203 &self,
4204 staging: &mut [(u64, crate::txn::Staged)],
4205 control: Option<&crate::ExecutionControl>,
4206 ) -> Result<()> {
4207 let schemas = {
4208 let catalog = self.catalog.read();
4209 catalog
4210 .tables
4211 .iter()
4212 .map(|table| (table.table_id, table.schema.clone()))
4213 .collect::<HashMap<_, _>>()
4214 };
4215 let fallback_control = crate::ExecutionControl::new(None);
4216 let control = control.unwrap_or(&fallback_control);
4217 for (table_id, staged) in staging {
4218 let (cells, mut update_changed_columns) = match staged {
4219 crate::txn::Staged::Put(cells) => (cells, None),
4220 crate::txn::Staged::Update {
4221 new_row,
4222 changed_columns,
4223 ..
4224 } => (new_row, Some(changed_columns)),
4225 crate::txn::Staged::Delete(_) | crate::txn::Staged::Truncate => continue,
4226 };
4227 let schema = schemas.get(table_id).ok_or_else(|| {
4228 MongrelError::Schema(format!("table id {table_id} disappeared during embedding"))
4229 })?;
4230 for target in &schema.columns {
4231 let Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) =
4232 target.embedding_source.as_ref()
4233 else {
4234 continue;
4235 };
4236 let text = render_embedding_input(schema, spec, cells)?;
4237 let reservation_bytes = text
4238 .len()
4239 .saturating_add((spec.dimension as usize).saturating_mul(4));
4240 let _reservation = self
4241 .memory_governor
4242 .try_reserve(
4243 reservation_bytes as u64,
4244 crate::memory::MemoryClass::AiCandidates,
4245 )
4246 .map_err(|error| MongrelError::ResourceLimitExceeded {
4247 resource: "embedding memory",
4248 requested: reservation_bytes,
4249 limit: match error {
4250 crate::memory::MemoryError::Exhausted { available, .. } => {
4251 available as usize
4252 }
4253 _ => 0,
4254 },
4255 })?;
4256 let source =
4257 crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec: spec.clone() };
4258 let provider = self
4259 .embedding_providers
4260 .resolve(&source)
4261 .map_err(embedding_error)?;
4262 let vectors = self
4263 .embedding_providers
4264 .embed_controlled(
4265 &source,
4266 &[text.as_str()],
4267 spec.dimension,
4268 control,
4269 "generated-column-write",
4270 crate::embedding::EmbeddingLimits::default(),
4271 )
4272 .map_err(embedding_error)?;
4273 cells.retain(|(column, _)| *column != target.id);
4274 cells.push((
4275 target.id,
4276 crate::memtable::Value::GeneratedEmbedding(Box::new(
4277 crate::embedding::GeneratedEmbeddingValue {
4278 vector: vectors.into_iter().next().ok_or_else(|| {
4279 MongrelError::Other("embedding provider returned no vector".into())
4280 })?,
4281 metadata: crate::embedding::GeneratedEmbeddingMetadata {
4282 provider_id: provider.provider_id().to_owned(),
4283 model_id: provider.model_id().to_owned(),
4284 model_version: provider.model_version().to_owned(),
4285 preprocessing_version: provider.preprocessing_version().to_owned(),
4286 source_fingerprint: Sha256::digest(text.as_bytes()).into(),
4287 status: crate::embedding::EmbeddingGenerationStatus::Ready,
4288 last_error_category: None,
4289 attempt_count: 1,
4290 },
4291 },
4292 )),
4293 ));
4294 if let Some(changed_columns) = update_changed_columns.as_deref_mut() {
4295 changed_columns.push(target.id);
4296 }
4297 }
4298 if let Some(changed_columns) = update_changed_columns {
4299 changed_columns.sort_unstable();
4300 changed_columns.dedup();
4301 }
4302 }
4303 Ok(())
4304 }
4305
4306 pub fn lock_rows_for_update(
4310 &self,
4311 txn_id: u64,
4312 table_id: u64,
4313 row_ids: &[crate::rowid::RowId],
4314 control: Option<&crate::ExecutionControl>,
4315 ) -> Result<()> {
4316 for &row_id in row_ids {
4317 self.acquire_txn_lock(
4318 txn_id,
4319 crate::locks::LockKey::row(table_id, row_id),
4320 crate::locks::LockMode::Exclusive,
4321 control,
4322 )?;
4323 }
4324 Ok(())
4325 }
4326
4327 pub fn spill_manager(&self) -> &crate::spill::SpillManager {
4331 &self.spill_manager
4332 }
4333
4334 pub fn job_registry(&self) -> &Arc<crate::jobs::JobRegistry> {
4336 &self.job_registry
4337 }
4338
4339 pub fn version_pins_report(&self) -> Vec<TablePinsReport> {
4344 let names: HashMap<u64, String> = self
4345 .catalog
4346 .read()
4347 .tables
4348 .iter()
4349 .map(|entry| (entry.table_id, entry.name.clone()))
4350 .collect();
4351 let handles: Vec<_> = self
4352 .tables
4353 .read()
4354 .iter()
4355 .map(|(table_id, handle)| (*table_id, handle.clone()))
4356 .collect();
4357 handles
4358 .into_iter()
4359 .map(|(table_id, handle)| {
4360 let pins = handle.lock().version_pins_report();
4361 TablePinsReport {
4362 table_id,
4363 table: names.get(&table_id).cloned().unwrap_or_default(),
4364 pins,
4365 }
4366 })
4367 .collect()
4368 }
4369
4370 pub fn lock_manager(&self) -> &Arc<crate::locks::LockManager> {
4372 &self.lock_manager
4373 }
4374
4375 pub fn release_txn_locks(&self, txn_id: u64) {
4380 self.lock_manager.release_all(txn_id);
4381 }
4382
4383 fn txn_lock_request(
4386 txn_id: u64,
4387 mode: crate::locks::LockMode,
4388 control: Option<&crate::ExecutionControl>,
4389 ) -> crate::locks::LockRequest {
4390 let control = control
4391 .cloned()
4392 .unwrap_or_else(|| crate::ExecutionControl::new(None));
4393 crate::locks::LockRequest::new(txn_id, mode, control)
4394 }
4395
4396 pub(crate) fn acquire_txn_lock(
4400 &self,
4401 txn_id: u64,
4402 key: crate::locks::LockKey,
4403 mode: crate::locks::LockMode,
4404 control: Option<&crate::ExecutionControl>,
4405 ) -> Result<()> {
4406 self.lock_manager
4407 .acquire(key, Self::txn_lock_request(txn_id, mode, control))
4408 .map_err(MongrelError::from)
4409 }
4410
4411 fn acquire_schema_barrier_exclusive(&self) -> Result<TxnLockGuard<'_>> {
4416 let txn_id = self.alloc_txn_id()?;
4417 self.acquire_txn_lock(
4418 txn_id,
4419 crate::locks::LockKey::schema_barrier(),
4420 crate::locks::LockMode::Exclusive,
4421 None,
4422 )?;
4423 Ok(TxnLockGuard {
4424 locks: &self.lock_manager,
4425 txn_id,
4426 })
4427 }
4428
4429 pub fn commit_log(&self) -> Arc<dyn mongreldb_log::CommitLog> {
4434 Arc::clone(&self.commit_log.read())
4435 }
4436
4437 pub fn bind_cluster_commit_log(
4444 &self,
4445 commit_log: Arc<dyn mongreldb_log::CommitLog>,
4446 ) -> Result<()> {
4447 match self.storage_mode()? {
4448 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4449 *self.commit_log.write() = commit_log;
4450 Ok(())
4451 }
4452 mode => Err(MongrelError::InvalidArgument(format!(
4453 "commit-log substitution requires ClusterReplica storage, got {mode:?}"
4454 ))),
4455 }
4456 }
4457
4458 pub fn detach_cluster_commit_log(&self) -> Result<()> {
4464 match self.storage_mode()? {
4465 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4466 let standalone: Arc<dyn mongreldb_log::CommitLog> =
4467 self.standalone_commit_log.clone();
4468 *self.commit_log.write() = standalone;
4469 Ok(())
4470 }
4471 mode => Err(MongrelError::InvalidArgument(format!(
4472 "commit-log detachment requires ClusterReplica storage, got {mode:?}"
4473 ))),
4474 }
4475 }
4476
4477 pub(crate) fn hlc_clock(&self) -> &mongreldb_types::hlc::HlcClock {
4482 &self.hlc
4483 }
4484
4485 pub fn catalog_snapshot(&self) -> Catalog {
4487 self.catalog.read().clone()
4488 }
4489
4490 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
4492 let catalog = self.catalog.read();
4493 match key {
4494 "user_version" => Ok(catalog.user_version),
4495 "application_id" => Ok(catalog.application_id),
4496 _ => Err(MongrelError::InvalidArgument(format!(
4497 "unsupported persistent SQL pragma {key:?}"
4498 ))),
4499 }
4500 }
4501
4502 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
4505 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
4506 }
4507
4508 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
4509 &self,
4510 key: &str,
4511 value: i64,
4512 mut before_commit: F,
4513 ) -> Result<Option<Epoch>>
4514 where
4515 F: FnMut() -> Result<()>,
4516 {
4517 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
4518 }
4519
4520 fn set_sql_pragma_i64_with_epoch_inner(
4521 &self,
4522 key: &str,
4523 value: i64,
4524 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
4525 ) -> Result<Option<Epoch>> {
4526 use crate::wal::DdlOp;
4527
4528 self.require(&crate::auth::Permission::Ddl)?;
4529 if self.read_only {
4530 return Err(MongrelError::ReadOnlyReplica);
4531 }
4532 if self.poisoned.load(Ordering::Relaxed) {
4533 return Err(MongrelError::Other(
4534 "database poisoned by fsync error".into(),
4535 ));
4536 }
4537 let _ddl = self.ddl_lock.lock();
4538 let _security_write = self.security_write()?;
4539 self.require(&crate::auth::Permission::Ddl)?;
4540 let mut next_catalog = self.catalog.read().clone();
4541 let target = match key {
4542 "user_version" => &mut next_catalog.user_version,
4543 "application_id" => &mut next_catalog.application_id,
4544 _ => {
4545 return Err(MongrelError::InvalidArgument(format!(
4546 "unsupported persistent SQL pragma {key:?}"
4547 )))
4548 }
4549 };
4550 if *target == Some(value) {
4551 return Ok(None);
4552 }
4553 *target = Some(value);
4554
4555 let _commit = self.commit_lock.lock();
4556 let epoch = self.epoch.bump_assigned();
4557 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4558 let txn_id = self.alloc_txn_id()?;
4559 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4560 let commit_seq = {
4561 let mut wal = self.shared_wal.lock();
4562 if let Some(before_commit) = before_commit {
4563 before_commit()?;
4564 }
4565 let append: Result<u64> = (|| {
4566 wal.append(
4567 txn_id,
4568 WAL_TABLE_ID,
4569 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
4570 key: key.to_string(),
4571 value,
4572 }),
4573 )?;
4574 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4575 wal.append_commit(txn_id, epoch, &[])
4576 })();
4577 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4578 };
4579 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4580 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4581 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
4582 Ok(Some(epoch))
4583 }
4584
4585 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
4586 self.catalog
4587 .read()
4588 .materialized_views
4589 .iter()
4590 .find(|definition| definition.name == name)
4591 .cloned()
4592 }
4593
4594 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
4595 self.catalog.read().materialized_views.clone()
4596 }
4597
4598 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
4599 self.catalog.read().security.clone()
4600 }
4601
4602 pub fn security_active_for(&self, table: &str) -> bool {
4603 self.catalog.read().security.table_has_security(table)
4604 }
4605
4606 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
4607 if self.catalog.read().security_version == expected_version {
4608 return Ok(());
4609 }
4610 self.security_catalog_disk_reads
4611 .fetch_add(1, Ordering::Relaxed);
4612 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
4613 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
4614 let principal = self.principal.read().clone();
4615 let principal = if fresh.require_auth
4616 && principal
4617 .as_ref()
4618 .is_some_and(|principal| principal.user_id != 0)
4619 {
4620 principal
4621 .as_ref()
4622 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
4623 } else {
4624 principal
4625 };
4626 self.auth_state.set_require_auth(fresh.require_auth);
4627 *self.catalog.write() = fresh;
4628 *self.principal.write() = principal.clone();
4629 self.auth_state.set_principal(principal);
4630 Ok(())
4631 }
4632
4633 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
4634 let guard = self.security_coordinator.gate.write();
4635 let version = self.security_coordinator.version.load(Ordering::Acquire);
4636 self.refresh_security_catalog_if_stale(version)?;
4637 Ok(guard)
4638 }
4639
4640 pub(crate) fn admit_operation(&self) -> Result<crate::core::OperationGuard> {
4667 self.core.operation_guard()
4668 }
4669
4670 fn apply_catalog_command_to(
4676 &self,
4677 next_catalog: &mut Catalog,
4678 command: crate::catalog_cmds::CatalogCommand,
4679 ) -> Result<crate::catalog_cmds::CatalogDelta> {
4680 let record = crate::catalog_cmds::CatalogCommandRecord::next(next_catalog, command);
4681 next_catalog.apply_command(&record)
4682 }
4683
4684 pub fn apply_replicated_records(&self, records: &[crate::wal::Record]) -> Result<bool> {
4702 use crate::wal::Op;
4703 use std::sync::atomic::Ordering;
4704
4705 let _operation = self.admit_operation()?;
4706 if self.poisoned.load(Ordering::Relaxed) {
4707 return Err(MongrelError::Other(
4708 "database poisoned by fsync error".into(),
4709 ));
4710 }
4711 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
4714 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
4715 })?;
4716 if records.iter().any(|record| record.txn_id != txn_id) {
4717 return Err(MongrelError::InvalidArgument(
4718 "replicated transaction payload mixes transaction ids".into(),
4719 ));
4720 }
4721 let commits = records
4722 .iter()
4723 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
4724 .count();
4725 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
4726 return Err(MongrelError::InvalidArgument(
4727 "replicated transaction payload must end in exactly one commit marker".into(),
4728 ));
4729 }
4730 let commit_epoch = match records.last().map(|r| &r.op) {
4731 Some(Op::TxnCommit { epoch, .. }) => *epoch,
4732 _ => unreachable!("validated above"),
4733 };
4734 if let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) {
4743 if !added_runs.is_empty() {
4744 return Err(MongrelError::InvalidArgument(
4745 "replicated spilled-run commits are not appliable: the leader must translate \
4746 the commit through translate_records_for_replication before proposal"
4747 .into(),
4748 ));
4749 }
4750 }
4751 if commit_epoch <= self.epoch.visible().0 {
4755 return Ok(false);
4756 }
4757 {
4762 let mut wal = self.shared_wal.lock();
4763 for record in records {
4764 wal.append(record.txn_id, 0, record.op.clone())?;
4765 }
4766 if let Err(error) = wal.group_sync() {
4767 self.poisoned.store(true, Ordering::Relaxed);
4768 return Err(error);
4769 }
4770 }
4771 let replication_pins: Vec<crate::retention::PinGuard> = {
4774 let tables = self.tables.read();
4775 let floor = Epoch(self.epoch.visible().0);
4776 tables
4777 .values()
4778 .map(|handle| {
4779 let t = handle.lock();
4780 Arc::clone(t.pin_registry())
4781 .pin(crate::retention::PinSource::Replication, floor)
4782 })
4783 .collect()
4784 };
4785 let tables = self.tables.read().clone();
4786 let catalog = self.catalog.read().clone();
4787 recover_shared_wal(&self.durable_root, &tables, &catalog, &self.epoch, records)?;
4788 if let Some(Op::TxnCommit { epoch, .. }) = records.last().map(|r| &r.op) {
4793 let commit_ts = records
4794 .iter()
4795 .rev()
4796 .find_map(|record| match &record.op {
4797 Op::CommitTimestamp { unix_nanos } => {
4798 Some(mongreldb_types::hlc::HlcTimestamp {
4799 physical_micros: unix_nanos / 1_000,
4800 logical: 0,
4801 node_tiebreaker: 0,
4802 })
4803 }
4804 _ => None,
4805 })
4806 .unwrap_or_else(|| {
4807 self.hlc
4808 .now()
4809 .unwrap_or(mongreldb_types::hlc::HlcTimestamp {
4810 physical_micros: 0,
4811 logical: 0,
4812 node_tiebreaker: 0,
4813 })
4814 });
4815 self.record_commit_ts(Epoch(*epoch), commit_ts);
4816 }
4817 drop(replication_pins);
4818 Ok(true)
4819 }
4820
4821 pub fn validate_staged_txn_writes(&self, staged: &[Vec<u8>]) -> Result<()> {
4831 let tables = self.tables.read();
4832 for payload in staged {
4833 match StagedTxnWrite::decode(payload)? {
4834 StagedTxnWrite::Put { table_id, rows } => {
4835 let handle = tables.get(&table_id).ok_or_else(|| {
4836 MongrelError::InvalidArgument(format!(
4837 "staged write targets unmounted table {table_id}"
4838 ))
4839 })?;
4840 let rows: Vec<crate::memtable::Row> =
4841 bincode::deserialize(&rows).map_err(|error| {
4842 MongrelError::InvalidArgument(format!(
4843 "staged put payload for table {table_id} cannot decode: {error}"
4844 ))
4845 })?;
4846 let schema = handle.lock().schema().clone();
4847 for row in &rows {
4848 validate_recovered_row(&schema, row).map_err(|error| {
4849 MongrelError::InvalidArgument(format!(
4850 "staged row for table {table_id} is not appliable: {error}"
4851 ))
4852 })?;
4853 }
4854 }
4855 StagedTxnWrite::Delete { table_id, row_ids } => {
4856 if !tables.contains_key(&table_id) {
4857 return Err(MongrelError::InvalidArgument(format!(
4858 "staged delete targets unmounted table {table_id}"
4859 )));
4860 }
4861 if row_ids.contains(&u64::MAX) {
4862 return Err(MongrelError::InvalidArgument(format!(
4863 "staged delete for table {table_id} names an exhausted row id"
4864 )));
4865 }
4866 }
4867 }
4868 }
4869 Ok(())
4870 }
4871
4872 pub fn apply_staged_txn_writes(
4891 &self,
4892 txn_tag: u64,
4893 staged: &[Vec<u8>],
4894 commit_ts: mongreldb_types::hlc::HlcTimestamp,
4895 ) -> Result<bool> {
4896 use crate::wal::Op;
4897
4898 let mut writes = Vec::with_capacity(staged.len());
4900 for payload in staged {
4901 writes.push(StagedTxnWrite::decode(payload)?);
4902 }
4903 let generation = *self.next_txn_id.lock() >> 32;
4909 let txn_id = (generation << 32) | (txn_tag & 0x7FFF_FFFF) | 0x8000_0000;
4910 let mut records = Vec::with_capacity(writes.len() + 2);
4911 for write in writes {
4912 let op = match write {
4913 StagedTxnWrite::Put { table_id, rows } => Op::Put { table_id, rows },
4914 StagedTxnWrite::Delete { table_id, row_ids } => Op::Delete {
4915 table_id,
4916 row_ids: row_ids.into_iter().map(crate::RowId).collect(),
4917 },
4918 };
4919 records.push(crate::wal::Record::new(Epoch(0), txn_id, op));
4920 }
4921 let epoch = self.epoch.visible().0 + 1;
4922 let unix_nanos = commit_ts.physical_micros.saturating_mul(1_000);
4926 records.push(crate::wal::Record::new(
4927 Epoch(0),
4928 txn_id,
4929 Op::CommitTimestamp { unix_nanos },
4930 ));
4931 records.push(crate::wal::Record::new(
4932 Epoch(0),
4933 txn_id,
4934 Op::TxnCommit {
4935 epoch,
4936 added_runs: Vec::new(),
4937 },
4938 ));
4939 let applied = self.apply_replicated_records(&records)?;
4940 if applied {
4941 self.record_commit_ts(Epoch(epoch), commit_ts);
4942 }
4943 Ok(applied)
4944 }
4945
4946 pub fn apply_replicated_catalog_command(
4955 &self,
4956 record: &crate::catalog_cmds::CatalogCommandRecord,
4957 ) -> Result<crate::catalog_cmds::CatalogDelta> {
4958 let _operation = self.admit_operation()?;
4959 let _g = self.ddl_lock.lock();
4960 let mut next_catalog = self.catalog.read().clone();
4961 let delta = next_catalog.apply_command(record)?;
4962 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
4963 return Ok(delta);
4964 }
4965 let referenced_epoch = match &delta {
4971 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => Some(entry.created_epoch),
4972 crate::catalog_cmds::CatalogDelta::TableDropped { at_epoch, .. }
4973 | crate::catalog_cmds::CatalogDelta::TableRenamed { at_epoch, .. } => Some(*at_epoch),
4974 _ => None,
4975 };
4976 if let Some(referenced) = referenced_epoch {
4977 self.epoch.advance_recovered(Epoch(referenced));
4978 next_catalog.db_epoch = next_catalog.db_epoch.max(referenced);
4979 }
4980 match &delta {
4981 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => {
4982 if !self.tables.read().contains_key(&entry.table_id) {
4987 self.mount_catalog_entry(entry)?;
4988 }
4989 }
4990 crate::catalog_cmds::CatalogDelta::TableDropped { table_id, .. } => {
4991 self.tables.write().remove(table_id);
4992 }
4993 _ => {}
4994 }
4995 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref())?;
4999 *self.catalog.write() = next_catalog;
5000 Ok(delta)
5001 }
5002
5003 fn mount_catalog_entry(&self, entry: &crate::catalog::CatalogEntry) -> Result<()> {
5008 let table_relative = Path::new(TABLES_DIR).join(entry.table_id.to_string());
5009 let table_root = Arc::new(
5010 self.durable_root
5011 .create_directory_all_pinned(&table_relative)?,
5012 );
5013 let tdir = table_root.io_path()?;
5014 let ctx = SharedCtx {
5015 root_guard: Some(table_root),
5016 epoch: Arc::clone(&self.epoch),
5017 page_cache: Arc::clone(&self.page_cache),
5018 decoded_cache: Arc::clone(&self.decoded_cache),
5019 snapshots: Arc::clone(&self.snapshots),
5020 kek: self.kek.clone(),
5021 commit_lock: Arc::clone(&self.commit_lock),
5022 shared: Some(crate::engine::SharedWalCtx {
5023 wal: Arc::clone(&self.shared_wal),
5024 group: Arc::clone(&self.group),
5025 poisoned: Arc::clone(&self.poisoned),
5026 txn_ids: Arc::clone(&self.next_txn_id),
5027 change_wake: self.change_wake.clone(),
5028 lifecycle: Arc::clone(&self.lifecycle),
5029 }),
5030 table_name: Some(entry.name.clone()),
5031 auth: self.table_auth_checker(),
5032 read_only: self.read_only,
5033 };
5034 let table = Table::create_in(&tdir, entry.schema.clone(), entry.table_id, ctx)?;
5035 self.tables
5036 .write()
5037 .insert(entry.table_id, TableHandle::new(table));
5038 Ok(())
5039 }
5040
5041 fn publish_catalog_candidate(
5042 &self,
5043 catalog: Catalog,
5044 epoch: Epoch,
5045 epoch_guard: &mut EpochGuard<'_>,
5046 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5047 ) -> Result<()> {
5048 self.publish_catalog_candidate_with_prelude(
5049 catalog,
5050 epoch,
5051 epoch_guard,
5052 before_publish,
5053 Vec::new(),
5054 )
5055 }
5056
5057 fn publish_catalog_candidate_with_prelude(
5058 &self,
5059 catalog: Catalog,
5060 epoch: Epoch,
5061 epoch_guard: &mut EpochGuard<'_>,
5062 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5063 prelude: Vec<(u64, crate::wal::Op)>,
5064 ) -> Result<()> {
5065 use crate::wal::DdlOp;
5066
5067 if self.read_only {
5068 return Err(MongrelError::ReadOnlyReplica);
5069 }
5070 if self.poisoned.load(Ordering::Relaxed) {
5071 return Err(MongrelError::Other(
5072 "database poisoned by fsync error".into(),
5073 ));
5074 }
5075 let _operation = self.admit_operation()?;
5077 if let Some(before_publish) = before_publish.as_mut() {
5078 (**before_publish)()?;
5079 }
5080 if catalog.db_epoch != epoch.0 {
5081 return Err(MongrelError::InvalidArgument(format!(
5082 "catalog epoch {} does not match commit epoch {}",
5083 catalog.db_epoch, epoch.0
5084 )));
5085 }
5086 {
5087 let current = self.catalog.read();
5088 validate_catalog_transition(¤t, &catalog)?;
5089 }
5090 validate_recovered_catalog(&catalog)?;
5091 let catalog_json = DdlOp::encode_catalog(&catalog)?;
5092 let txn_id = self.alloc_txn_id()?;
5093 let commit_seq = {
5094 let mut wal = self.shared_wal.lock();
5095 let append: Result<u64> = (|| {
5096 for (table_id, op) in prelude {
5097 wal.append(txn_id, table_id, op)?;
5098 }
5099 wal.append(
5100 txn_id,
5101 WAL_TABLE_ID,
5102 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
5103 )?;
5104 wal.append_commit(txn_id, epoch, &[])
5105 })();
5106 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5107 };
5108 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5109 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
5110 self.finish_durable_publish(epoch, epoch_guard, &receipt, checkpoint)
5111 }
5112
5113 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
5117 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
5118 let version = catalog.security_version;
5119 let principal = self.principal.read().clone();
5120 let principal = if catalog.require_auth
5121 && principal
5122 .as_ref()
5123 .is_some_and(|principal| principal.user_id != 0)
5124 {
5125 principal.as_ref().and_then(|principal| {
5126 Self::resolve_bound_principal_from_catalog(&catalog, principal)
5127 })
5128 } else {
5129 principal
5130 };
5131 *self.catalog.write() = catalog;
5132 self.security_coordinator
5133 .version
5134 .store(version, Ordering::Release);
5135 self.auth_state
5136 .set_require_auth(self.catalog.read().require_auth);
5137 *self.principal.write() = principal.clone();
5138 self.auth_state.set_principal(principal);
5139 checkpoint
5140 }
5141
5142 fn finish_durable_publish(
5143 &self,
5144 epoch: Epoch,
5145 epoch_guard: &mut EpochGuard<'_>,
5146 receipt: &mongreldb_log::CommitReceipt,
5147 post_step: Result<()>,
5148 ) -> Result<()> {
5149 if let Err(error) = self.publish_committed(receipt, epoch) {
5150 self.poisoned.store(true, Ordering::Relaxed);
5155 self.lifecycle.poison();
5156 return Err(MongrelError::DurableCommit {
5157 epoch: epoch.0,
5158 message: error.to_string(),
5159 });
5160 }
5161 epoch_guard.disarm();
5162 match post_step {
5163 Ok(()) => Ok(()),
5164 Err(error) => {
5165 self.poisoned.store(true, Ordering::Relaxed);
5166 self.lifecycle.poison();
5167 Err(MongrelError::DurableCommit {
5168 epoch: epoch.0,
5169 message: error.to_string(),
5170 })
5171 }
5172 }
5173 }
5174
5175 fn publish_committed(
5181 &self,
5182 receipt: &mongreldb_log::CommitReceipt,
5183 epoch: Epoch,
5184 ) -> Result<()> {
5185 debug_assert_eq!(
5186 receipt.log_position.index, epoch.0,
5187 "commit receipt position must match the published epoch"
5188 );
5189 mongreldb_fault::inject("commit.publish.before").map_err(crate::commit_log::fault_as_io)?;
5190 self.epoch.publish_in_order(epoch);
5191 mongreldb_fault::inject("commit.publish.after").map_err(crate::commit_log::fault_as_io)?;
5192 Ok(())
5193 }
5194
5195 fn await_durable_commit(
5204 &self,
5205 txn_id: u64,
5206 commit_seq: u64,
5207 epoch: Epoch,
5208 ) -> Result<mongreldb_log::CommitReceipt> {
5209 match self
5210 .standalone_commit_log
5211 .seal_transaction(txn_id, epoch, commit_seq, None)
5212 {
5213 Ok(receipt) => {
5214 self.record_commit_ts(epoch, receipt.commit_ts);
5215 Ok(receipt)
5216 }
5217 Err(error) => {
5218 self.poisoned.store(true, Ordering::Relaxed);
5219 self.lifecycle.poison();
5220 Err(MongrelError::CommitOutcomeUnknown {
5221 epoch: epoch.0,
5222 message: error.to_string(),
5223 })
5224 }
5225 }
5226 }
5227
5228 fn await_durable_commit_with_ts(
5233 &self,
5234 txn_id: u64,
5235 commit_seq: u64,
5236 epoch: Epoch,
5237 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5238 ) -> Result<mongreldb_log::CommitReceipt> {
5239 match self.standalone_commit_log.seal_transaction(
5240 txn_id,
5241 epoch,
5242 commit_seq,
5243 Some(commit_ts),
5244 ) {
5245 Ok(receipt) => {
5246 self.record_commit_ts(epoch, receipt.commit_ts);
5247 Ok(receipt)
5248 }
5249 Err(error) => {
5250 self.poisoned.store(true, Ordering::Relaxed);
5251 self.lifecycle.poison();
5252 Err(MongrelError::CommitOutcomeUnknown {
5253 epoch: epoch.0,
5254 message: error.to_string(),
5255 })
5256 }
5257 }
5258 }
5259
5260 fn record_commit_ts(&self, epoch: Epoch, commit_ts: mongreldb_types::hlc::HlcTimestamp) {
5265 let mut ledger = self.commit_ts_ledger.lock();
5266 ledger.insert(epoch.0, commit_ts);
5267 while ledger.len() > COMMIT_TS_LEDGER_CAP {
5268 ledger.pop_first();
5269 }
5270 }
5271
5272 pub fn commit_ts_for_epoch(&self, epoch: Epoch) -> Option<mongreldb_types::hlc::HlcTimestamp> {
5286 self.commit_ts_ledger.lock().get(&epoch.0).copied()
5287 }
5288
5289 pub fn epoch_at_or_before_commit_ts(
5295 &self,
5296 timestamp: mongreldb_types::hlc::HlcTimestamp,
5297 ) -> Option<Epoch> {
5298 self.commit_ts_ledger
5299 .lock()
5300 .iter()
5301 .rev()
5302 .find_map(|(epoch, commit_ts)| (*commit_ts <= timestamp).then_some(Epoch(*epoch)))
5303 }
5304
5305 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
5306 self.poisoned.store(true, Ordering::Relaxed);
5307 self.lifecycle.poison();
5308 MongrelError::CommitOutcomeUnknown {
5309 epoch: epoch.0,
5310 message: error.to_string(),
5311 }
5312 }
5313
5314 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
5316 self.set_security_catalog_as_with_epoch(security, None)
5317 .map(|_| ())
5318 }
5319
5320 pub fn set_security_catalog_as(
5322 &self,
5323 security: crate::security::SecurityCatalog,
5324 principal: Option<&crate::auth::Principal>,
5325 ) -> Result<()> {
5326 self.set_security_catalog_as_with_epoch(security, principal)
5327 .map(|_| ())
5328 }
5329
5330 pub fn set_security_catalog_as_with_epoch(
5332 &self,
5333 security: crate::security::SecurityCatalog,
5334 principal: Option<&crate::auth::Principal>,
5335 ) -> Result<Epoch> {
5336 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
5337 }
5338
5339 pub fn set_security_catalog_as_with_epoch_controlled<F>(
5342 &self,
5343 security: crate::security::SecurityCatalog,
5344 principal: Option<&crate::auth::Principal>,
5345 mut before_commit: F,
5346 ) -> Result<Epoch>
5347 where
5348 F: FnMut() -> Result<()>,
5349 {
5350 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
5351 }
5352
5353 fn set_security_catalog_as_with_epoch_inner(
5354 &self,
5355 security: crate::security::SecurityCatalog,
5356 principal: Option<&crate::auth::Principal>,
5357 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
5358 ) -> Result<Epoch> {
5359 use crate::wal::DdlOp;
5360 use std::sync::atomic::Ordering;
5361
5362 let command = crate::catalog_cmds::CatalogCommand::SetSecurityCatalog {
5366 security: security.clone(),
5367 };
5368 self.require_for(
5369 principal,
5370 &crate::catalog_cmds::required_permission(&command),
5371 )?;
5372 if self.poisoned.load(Ordering::Relaxed) {
5373 return Err(MongrelError::Other(
5374 "database poisoned by fsync error".into(),
5375 ));
5376 }
5377 let _operation = self.admit_operation()?;
5380 let _ddl = self.ddl_lock.lock();
5381 let _security_write = self.security_write()?;
5384 self.require_for(
5385 principal,
5386 &crate::catalog_cmds::required_permission(&command),
5387 )?;
5388 let mut next_catalog = self.catalog.read().clone();
5389 validate_security_catalog(&next_catalog, &security)?;
5390 let payload = DdlOp::encode_security(&security)?;
5391 let _commit = self.commit_lock.lock();
5392 let epoch = self.epoch.bump_assigned();
5393 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5394 let txn_id = self.alloc_txn_id()?;
5395 self.apply_catalog_command_to(&mut next_catalog, command)?;
5396 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
5397 let commit_seq = {
5398 let mut wal = self.shared_wal.lock();
5399 if let Some(before_commit) = before_commit {
5400 before_commit()?;
5401 }
5402 let append: Result<u64> = (|| {
5403 wal.append(
5404 txn_id,
5405 WAL_TABLE_ID,
5406 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
5407 security_json: payload,
5408 }),
5409 )?;
5410 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
5411 wal.append_commit(txn_id, epoch, &[])
5412 })();
5413 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5414 };
5415 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5416 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
5417 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
5418 Ok(epoch)
5419 }
5420
5421 pub fn require_for(
5422 &self,
5423 principal: Option<&crate::auth::Principal>,
5424 permission: &crate::auth::Permission,
5425 ) -> Result<()> {
5426 let Some(principal) = principal else {
5427 return self.require(permission);
5428 };
5429 let resolved;
5430 let principal = if principal.user_id != 0 {
5431 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5432 .ok_or(MongrelError::AuthRequired)?;
5433 &resolved
5434 } else {
5435 principal
5436 };
5437 #[cfg(test)]
5438 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5439 if principal.has_permission(permission) {
5440 Ok(())
5441 } else {
5442 Err(MongrelError::PermissionDenied {
5443 required: permission.clone(),
5444 principal: principal.username.clone(),
5445 })
5446 }
5447 }
5448
5449 fn require_exact_principal_current(
5453 &self,
5454 principal: Option<&crate::auth::Principal>,
5455 permission: &crate::auth::Permission,
5456 ) -> Result<()> {
5457 let catalog = self.catalog.read();
5458 if !catalog.require_auth {
5459 return Ok(());
5460 }
5461 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
5462 let current = if supplied.user_id == 0 {
5463 supplied.clone()
5464 } else {
5465 Self::resolve_bound_principal_from_catalog(&catalog, supplied)
5466 .ok_or(MongrelError::AuthRequired)?
5467 };
5468 if current.has_permission(permission) {
5469 Ok(())
5470 } else {
5471 Err(MongrelError::PermissionDenied {
5472 required: permission.clone(),
5473 principal: current.username,
5474 })
5475 }
5476 }
5477
5478 pub(crate) fn with_exact_principal_current<T, F>(
5479 &self,
5480 principal: Option<&crate::auth::Principal>,
5481 permission: &crate::auth::Permission,
5482 operation: F,
5483 ) -> Result<T>
5484 where
5485 F: FnOnce() -> Result<T>,
5486 {
5487 let _security = self.security_coordinator.gate.read();
5488 self.require_exact_principal_current(principal, permission)?;
5489 operation()
5490 }
5491
5492 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
5493 self.principal.read().clone()
5494 }
5495
5496 #[cfg(test)]
5497 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
5498 *self.principal.write() = principal.clone();
5499 self.auth_state.set_principal(principal);
5500 }
5501
5502 pub fn require_columns_for(
5503 &self,
5504 table: &str,
5505 operation: crate::auth::ColumnOperation,
5506 column_ids: &[u16],
5507 principal: Option<&crate::auth::Principal>,
5508 ) -> Result<()> {
5509 if principal.is_none() && !self.auth_state.require_auth() {
5510 return Ok(());
5511 }
5512 let cached = self.principal.read().clone();
5513 let principal = principal.or(cached.as_ref());
5514 let Some(principal) = principal else {
5515 let permission = match operation {
5516 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5517 table: table.to_string(),
5518 },
5519 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5520 table: table.to_string(),
5521 },
5522 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5523 table: table.to_string(),
5524 },
5525 };
5526 return self.require(&permission);
5527 };
5528 let catalog = self.catalog.read();
5529 let resolved;
5530 let principal = if principal.user_id != 0 {
5531 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
5532 .ok_or(MongrelError::AuthRequired)?;
5533 &resolved
5534 } else {
5535 principal
5536 };
5537 let schema = &catalog
5538 .live(table)
5539 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5540 .schema;
5541 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
5542 }
5543
5544 fn require_columns_for_principal(
5545 table: &str,
5546 schema: &Schema,
5547 operation: crate::auth::ColumnOperation,
5548 column_ids: &[u16],
5549 principal: &crate::auth::Principal,
5550 ) -> Result<()> {
5551 #[cfg(test)]
5552 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5553 match principal.column_access(table, operation) {
5554 crate::auth::ColumnAccess::All => Ok(()),
5555 crate::auth::ColumnAccess::Columns(allowed) => {
5556 let denied = column_ids.iter().find_map(|column_id| {
5557 schema
5558 .columns
5559 .iter()
5560 .find(|column| column.id == *column_id)
5561 .filter(|column| !allowed.contains(&column.name))
5562 });
5563 if denied.is_none() {
5564 Ok(())
5565 } else {
5566 Err(MongrelError::PermissionDenied {
5567 required: match operation {
5568 crate::auth::ColumnOperation::Select => {
5569 crate::auth::Permission::SelectColumns {
5570 table: table.to_string(),
5571 columns: denied
5572 .into_iter()
5573 .map(|column| column.name.clone())
5574 .collect(),
5575 }
5576 }
5577 crate::auth::ColumnOperation::Insert => {
5578 crate::auth::Permission::InsertColumns {
5579 table: table.to_string(),
5580 columns: denied
5581 .into_iter()
5582 .map(|column| column.name.clone())
5583 .collect(),
5584 }
5585 }
5586 crate::auth::ColumnOperation::Update => {
5587 crate::auth::Permission::UpdateColumns {
5588 table: table.to_string(),
5589 columns: denied
5590 .into_iter()
5591 .map(|column| column.name.clone())
5592 .collect(),
5593 }
5594 }
5595 },
5596 principal: principal.username.clone(),
5597 })
5598 }
5599 }
5600 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5601 required: match operation {
5602 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5603 table: table.to_string(),
5604 },
5605 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5606 table: table.to_string(),
5607 },
5608 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5609 table: table.to_string(),
5610 },
5611 },
5612 principal: principal.username.clone(),
5613 }),
5614 }
5615 }
5616
5617 pub fn select_column_ids_for(
5618 &self,
5619 table: &str,
5620 principal: Option<&crate::auth::Principal>,
5621 ) -> Result<Vec<u16>> {
5622 let catalog = self.catalog.read();
5623 let columns = catalog
5624 .live(table)
5625 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5626 .schema
5627 .columns
5628 .iter()
5629 .map(|column| (column.id, column.name.clone()))
5630 .collect::<Vec<_>>();
5631 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
5632 drop(catalog);
5633 let Some(principal) = principal.as_ref() else {
5634 self.require(&crate::auth::Permission::Select {
5635 table: table.to_string(),
5636 })?;
5637 return Ok(columns.iter().map(|(id, _)| *id).collect());
5638 };
5639 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
5640 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
5641 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
5642 .iter()
5643 .filter(|(_, name)| allowed.contains(name))
5644 .map(|(id, _)| *id)
5645 .collect()),
5646 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5647 required: crate::auth::Permission::Select {
5648 table: table.to_string(),
5649 },
5650 principal: principal.username.clone(),
5651 }),
5652 }
5653 }
5654
5655 pub fn secure_rows_for(
5656 &self,
5657 table: &str,
5658 rows: Vec<crate::memtable::Row>,
5659 principal: Option<&crate::auth::Principal>,
5660 ) -> Result<Vec<crate::memtable::Row>> {
5661 self.secure_rows_for_with_context(table, rows, principal, None)
5662 }
5663
5664 pub fn secure_rows_for_with_context(
5665 &self,
5666 table: &str,
5667 rows: Vec<crate::memtable::Row>,
5668 principal: Option<&crate::auth::Principal>,
5669 context: Option<&crate::query::AiExecutionContext>,
5670 ) -> Result<Vec<crate::memtable::Row>> {
5671 let (security, principal) = {
5672 let catalog = self.catalog.read();
5673 (
5674 catalog.security.clone(),
5675 self.principal_for_authorized_read(&catalog, principal, false)?,
5676 )
5677 };
5678 if !security.table_has_security(table) {
5679 return Ok(rows);
5680 }
5681 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5682 let mut output = Vec::new();
5683 for mut row in rows {
5684 if let Some(context) = context {
5685 context.consume(1)?;
5686 }
5687 if security.row_allowed(
5688 table,
5689 crate::security::PolicyCommand::Select,
5690 &row,
5691 principal,
5692 false,
5693 ) {
5694 security.apply_masks(table, &mut row, principal);
5695 output.push(row);
5696 }
5697 }
5698 Ok(output)
5699 }
5700
5701 pub fn mask_search_hits_for(
5704 &self,
5705 table: &str,
5706 hits: &mut [crate::query::SearchHit],
5707 principal: Option<&crate::auth::Principal>,
5708 ) -> Result<()> {
5709 let (security, principal) = {
5710 let catalog = self.catalog.read();
5711 (
5712 catalog.security.clone(),
5713 self.principal_for_authorized_read(&catalog, principal, false)?,
5714 )
5715 };
5716 if !security.table_has_security(table) {
5717 return Ok(());
5718 }
5719 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5720 for hit in hits {
5721 security.apply_masks_to_cells(table, &mut hit.cells, principal);
5722 }
5723 Ok(())
5724 }
5725
5726 pub fn mask_rows_for(
5728 &self,
5729 table: &str,
5730 rows: &mut [crate::memtable::Row],
5731 principal: Option<&crate::auth::Principal>,
5732 ) -> Result<()> {
5733 let (security, principal) = {
5734 let catalog = self.catalog.read();
5735 (
5736 catalog.security.clone(),
5737 self.principal_for_authorized_read(&catalog, principal, false)?,
5738 )
5739 };
5740 if !security.table_has_security(table) {
5741 return Ok(());
5742 }
5743 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5744 for row in rows {
5745 security.apply_masks(table, row, principal);
5746 }
5747 Ok(())
5748 }
5749
5750 pub fn authorized_candidate_ids_for(
5752 &self,
5753 table: &str,
5754 principal: Option<&crate::auth::Principal>,
5755 ) -> Result<Option<std::collections::HashSet<RowId>>> {
5756 Ok(self
5757 .authorized_read_snapshot(table, principal)?
5758 .allowed_row_ids)
5759 }
5760
5761 fn allowed_row_ids_locked(
5762 &self,
5763 table_name: &str,
5764 table: &Table,
5765 table_snapshot: Snapshot,
5766 security_state: (&crate::security::SecurityCatalog, u64),
5767 principal: Option<&crate::auth::Principal>,
5768 context: Option<&crate::query::AiExecutionContext>,
5769 ) -> Result<Option<Arc<HashSet<RowId>>>> {
5770 let (security, security_version) = security_state;
5771 if !security.rls_enabled(table_name) {
5772 return Ok(None);
5773 }
5774 let authorization_started = std::time::Instant::now();
5775 let principal = principal.ok_or(MongrelError::AuthRequired)?;
5776 let mut roles = principal.roles.clone();
5777 roles.sort_unstable();
5778 let principal_key = format!(
5779 "{}:{}:{}:{}:{roles:?}",
5780 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
5781 );
5782 let cache_key = (
5783 table_name.to_string(),
5784 table.data_generation(),
5785 security_version,
5786 principal_key,
5787 );
5788 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
5789 crate::trace::QueryTrace::record(|trace| {
5790 trace.rls_cache_hit = true;
5791 trace.authorization_nanos = trace
5792 .authorization_nanos
5793 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5794 });
5795 return Ok(Some(allowed));
5796 }
5797 if let Some(context) = context {
5798 context.checkpoint()?;
5799 }
5800 let started = std::time::Instant::now();
5802 let rows = table.visible_rows(table_snapshot)?;
5803 let rows_evaluated = rows.len() as u64;
5804 let mut allowed = HashSet::new();
5805 for chunk in rows.chunks(256) {
5806 if let Some(context) = context {
5807 context.consume(chunk.len())?;
5808 }
5809 allowed.extend(chunk.iter().filter_map(|row| {
5810 security
5811 .row_allowed(
5812 table_name,
5813 crate::security::PolicyCommand::Select,
5814 row,
5815 principal,
5816 false,
5817 )
5818 .then_some(row.row_id)
5819 }));
5820 }
5821 let allowed = Arc::new(allowed);
5822 let mut cache = self.rls_cache.lock();
5823 cache.build_nanos = cache
5824 .build_nanos
5825 .saturating_add(started.elapsed().as_nanos() as u64);
5826 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
5827 cache.insert(cache_key, Arc::clone(&allowed));
5828 crate::trace::QueryTrace::record(|trace| {
5829 trace.rls_rows_evaluated = trace
5830 .rls_rows_evaluated
5831 .saturating_add(rows_evaluated as usize);
5832 trace.authorization_nanos = trace
5833 .authorization_nanos
5834 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5835 });
5836 Ok(Some(allowed))
5837 }
5838
5839 fn principal_for_authorized_read(
5840 &self,
5841 catalog: &Catalog,
5842 principal: Option<&crate::auth::Principal>,
5843 catalog_bound: bool,
5844 ) -> Result<Option<crate::auth::Principal>> {
5845 let principal = principal.cloned().or_else(|| self.principal.read().clone());
5846 let Some(principal) = principal else {
5847 return Ok(None);
5848 };
5849 if catalog_bound || principal.user_id != 0 {
5850 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
5851 .map(Some)
5852 .ok_or(MongrelError::AuthRequired);
5853 }
5854 Ok(Some(principal))
5855 }
5856
5857 pub fn with_authorized_read<T, F>(
5861 &self,
5862 table_name: &str,
5863 principal: Option<&crate::auth::Principal>,
5864 catalog_bound: bool,
5865 read: F,
5866 ) -> Result<T>
5867 where
5868 F: FnMut(
5869 &mut Table,
5870 Snapshot,
5871 Option<&HashSet<RowId>>,
5872 Option<&crate::auth::Principal>,
5873 ) -> Result<T>,
5874 {
5875 self.with_authorized_read_context(
5876 table_name,
5877 principal,
5878 catalog_bound,
5879 None,
5880 None,
5881 None,
5882 read,
5883 )
5884 }
5885
5886 #[allow(clippy::too_many_arguments)]
5887 pub fn with_authorized_read_context<T, F>(
5888 &self,
5889 table_name: &str,
5890 principal: Option<&crate::auth::Principal>,
5891 catalog_bound: bool,
5892 authorization: Option<&ReadAuthorization>,
5893 context: Option<&crate::query::AiExecutionContext>,
5894 snapshot_override: Option<Snapshot>,
5895 read: F,
5896 ) -> Result<T>
5897 where
5898 F: FnMut(
5899 &mut Table,
5900 Snapshot,
5901 Option<&HashSet<RowId>>,
5902 Option<&crate::auth::Principal>,
5903 ) -> Result<T>,
5904 {
5905 self.with_authorized_read_context_stamped(
5906 table_name,
5907 principal,
5908 catalog_bound,
5909 authorization,
5910 context,
5911 snapshot_override,
5912 read,
5913 )
5914 .map(|(result, _)| result)
5915 }
5916
5917 #[allow(clippy::too_many_arguments)]
5918 pub fn with_authorized_read_context_stamped<T, F>(
5919 &self,
5920 table_name: &str,
5921 principal: Option<&crate::auth::Principal>,
5922 catalog_bound: bool,
5923 authorization: Option<&ReadAuthorization>,
5924 context: Option<&crate::query::AiExecutionContext>,
5925 snapshot_override: Option<Snapshot>,
5926 mut read: F,
5927 ) -> Result<(T, AuthorizedReadStamp)>
5928 where
5929 F: FnMut(
5930 &mut Table,
5931 Snapshot,
5932 Option<&HashSet<RowId>>,
5933 Option<&crate::auth::Principal>,
5934 ) -> Result<T>,
5935 {
5936 if principal.is_none() && self.principal.read().is_some() {
5937 self.refresh_principal()?;
5938 }
5939 const RETRIES: usize = 3;
5940 let handle = self.table(table_name)?;
5941 for attempt in 0..RETRIES {
5942 crate::trace::QueryTrace::record(|trace| {
5943 trace.authorization_retries = attempt;
5944 });
5945 let (security, security_version, effective_principal) = {
5946 let catalog = self.catalog.read();
5947 (
5948 catalog.security.clone(),
5949 catalog.security_version,
5950 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
5951 )
5952 };
5953 if let Some(authorization) = authorization {
5954 for permission in &authorization.permissions {
5955 self.require_for(effective_principal.as_ref(), permission)?;
5956 }
5957 self.require_columns_for(
5958 table_name,
5959 authorization.operation,
5960 &authorization.columns,
5961 effective_principal.as_ref(),
5962 )?;
5963 }
5964 let result = {
5965 let mut table = lock_table_with_context(&handle, context)?;
5966 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
5967 let allowed = self.allowed_row_ids_locked(
5968 table_name,
5969 &table,
5970 snapshot,
5971 (&security, security_version),
5972 effective_principal.as_ref(),
5973 context,
5974 )?;
5975 let stamp = AuthorizedReadStamp {
5976 table_id: table.table_id(),
5977 schema_id: table.schema().schema_id,
5978 data_generation: table.data_generation(),
5979 security_version,
5980 snapshot,
5981 };
5982 let result = read(
5983 &mut table,
5984 snapshot,
5985 allowed.as_deref(),
5986 effective_principal.as_ref(),
5987 )?;
5988 (result, stamp)
5989 };
5990 if let Some(context) = context {
5991 context.checkpoint()?;
5992 }
5993 if self.catalog.read().security_version == security_version {
5994 return Ok(result);
5995 }
5996 if attempt + 1 == RETRIES {
5997 return Err(MongrelError::Conflict(
5998 "security policy changed during scored read".into(),
5999 ));
6000 }
6001 }
6002 Err(MongrelError::Conflict(
6003 "authorization retry loop exhausted".into(),
6004 ))
6005 }
6006
6007 fn with_authorized_aggregate_table<T, F>(
6008 &self,
6009 table_name: &str,
6010 columns: &[u16],
6011 principal: Option<&crate::auth::Principal>,
6012 catalog_bound: bool,
6013 allow_table_security: bool,
6014 mut aggregate: F,
6015 ) -> Result<T>
6016 where
6017 F: FnMut(
6018 &mut Table,
6019 Option<&crate::security::CandidateAuthorization<'_>>,
6020 Option<&crate::auth::Principal>,
6021 u64,
6022 ) -> Result<T>,
6023 {
6024 if principal.is_none() && self.principal.read().is_some() {
6025 self.refresh_principal()?;
6026 }
6027 const RETRIES: usize = 3;
6028 let handle = self.table(table_name)?;
6029 for attempt in 0..RETRIES {
6030 let (security, security_version, effective_principal) = {
6031 let catalog = self.catalog.read();
6032 (
6033 catalog.security.clone(),
6034 catalog.security_version,
6035 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6036 )
6037 };
6038 self.require_columns_for(
6039 table_name,
6040 crate::auth::ColumnOperation::Select,
6041 columns,
6042 effective_principal.as_ref(),
6043 )?;
6044 if !allow_table_security && security.table_has_security(table_name) {
6045 return Err(MongrelError::InvalidArgument(
6046 "incremental aggregate is unsupported while RLS or column masks are active"
6047 .into(),
6048 ));
6049 }
6050 let result = {
6051 let mut table = handle.lock();
6052 let authorization = if security.rls_enabled(table_name) {
6053 Some(crate::security::CandidateAuthorization {
6054 table: table_name,
6055 security: &security,
6056 principal: effective_principal
6057 .as_ref()
6058 .ok_or(MongrelError::AuthRequired)?,
6059 })
6060 } else {
6061 None
6062 };
6063 aggregate(
6064 &mut table,
6065 authorization.as_ref(),
6066 effective_principal.as_ref(),
6067 security_version,
6068 )?
6069 };
6070 if self.catalog.read().security_version == security_version {
6071 return Ok(result);
6072 }
6073 if attempt + 1 == RETRIES {
6074 return Err(MongrelError::Conflict(
6075 "security policy changed during aggregate read".into(),
6076 ));
6077 }
6078 }
6079 Err(MongrelError::Conflict(
6080 "aggregate authorization retry loop exhausted".into(),
6081 ))
6082 }
6083
6084 pub fn with_authorized_scored_read_context<T, F>(
6088 &self,
6089 table_name: &str,
6090 principal: Option<&crate::auth::Principal>,
6091 catalog_bound: bool,
6092 authorization: Option<&ReadAuthorization>,
6093 context: Option<&crate::query::AiExecutionContext>,
6094 mut read: F,
6095 ) -> Result<T>
6096 where
6097 F: FnMut(
6098 &mut Table,
6099 Snapshot,
6100 Option<&crate::security::CandidateAuthorization<'_>>,
6101 Option<&crate::auth::Principal>,
6102 ) -> Result<T>,
6103 {
6104 self.with_authorized_scored_read_context_at(
6105 table_name,
6106 principal,
6107 catalog_bound,
6108 authorization,
6109 context,
6110 None,
6111 |table, snapshot, authorization, principal| {
6112 let mut table = table.clone();
6113 read(&mut table, snapshot, authorization, principal)
6114 },
6115 )
6116 }
6117
6118 #[allow(clippy::too_many_arguments)]
6119 pub fn with_authorized_scored_read_context_at<T, F>(
6120 &self,
6121 table_name: &str,
6122 principal: Option<&crate::auth::Principal>,
6123 catalog_bound: bool,
6124 authorization: Option<&ReadAuthorization>,
6125 context: Option<&crate::query::AiExecutionContext>,
6126 snapshot_override: Option<Snapshot>,
6127 read: F,
6128 ) -> Result<T>
6129 where
6130 F: FnMut(
6131 &Table,
6132 Snapshot,
6133 Option<&crate::security::CandidateAuthorization<'_>>,
6134 Option<&crate::auth::Principal>,
6135 ) -> Result<T>,
6136 {
6137 self.with_authorized_scored_read_context_at_stamped(
6138 table_name,
6139 principal,
6140 catalog_bound,
6141 authorization,
6142 context,
6143 snapshot_override,
6144 read,
6145 )
6146 .map(|(result, _)| result)
6147 }
6148
6149 #[allow(clippy::too_many_arguments)]
6150 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
6151 &self,
6152 table_name: &str,
6153 principal: Option<&crate::auth::Principal>,
6154 catalog_bound: bool,
6155 authorization: Option<&ReadAuthorization>,
6156 context: Option<&crate::query::AiExecutionContext>,
6157 snapshot_override: Option<Snapshot>,
6158 mut read: F,
6159 ) -> Result<(T, AuthorizedReadStamp)>
6160 where
6161 F: FnMut(
6162 &Table,
6163 Snapshot,
6164 Option<&crate::security::CandidateAuthorization<'_>>,
6165 Option<&crate::auth::Principal>,
6166 ) -> Result<T>,
6167 {
6168 if principal.is_none() && self.principal.read().is_some() {
6169 self.refresh_principal()?;
6170 }
6171 const RETRIES: usize = 3;
6172 let handle = self.table(table_name)?;
6173 for attempt in 0..RETRIES {
6174 if let Some(context) = context {
6175 context.checkpoint()?;
6176 }
6177 crate::trace::QueryTrace::record(|trace| {
6178 trace.authorization_retries = attempt;
6179 });
6180 let (security, security_version, effective_principal) = {
6181 let catalog = self.catalog.read();
6182 (
6183 catalog.security.clone(),
6184 catalog.security_version,
6185 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6186 )
6187 };
6188 if let Some(authorization) = authorization {
6189 for permission in &authorization.permissions {
6190 self.require_for(effective_principal.as_ref(), permission)?;
6191 }
6192 self.require_columns_for(
6193 table_name,
6194 authorization.operation,
6195 &authorization.columns,
6196 effective_principal.as_ref(),
6197 )?;
6198 }
6199 let result = {
6200 let (table, snapshot, _snapshot_guard, _run_pins) =
6201 self.scored_read_generation(&handle, context, snapshot_override)?;
6202 let candidate_authorization = if security.rls_enabled(table_name) {
6203 Some(crate::security::CandidateAuthorization {
6204 table: table_name,
6205 security: &security,
6206 principal: effective_principal
6207 .as_ref()
6208 .ok_or(MongrelError::AuthRequired)?,
6209 })
6210 } else {
6211 None
6212 };
6213 let stamp = AuthorizedReadStamp {
6214 table_id: table.table_id(),
6215 schema_id: table.schema().schema_id,
6216 data_generation: table.data_generation(),
6217 security_version,
6218 snapshot,
6219 };
6220 let result = read(
6221 table.as_ref(),
6222 snapshot,
6223 candidate_authorization.as_ref(),
6224 effective_principal.as_ref(),
6225 )?;
6226 (result, stamp)
6227 };
6228 if let Some(context) = context {
6229 context.checkpoint()?;
6230 }
6231 if self.catalog.read().security_version == security_version {
6232 return Ok(result);
6233 }
6234 if attempt + 1 == RETRIES {
6235 return Err(MongrelError::Conflict(
6236 "security policy changed during scored read".into(),
6237 ));
6238 }
6239 }
6240 Err(MongrelError::Conflict(
6241 "scored-read authorization retry loop exhausted".into(),
6242 ))
6243 }
6244
6245 fn scored_read_generation(
6246 &self,
6247 handle: &TableHandle,
6248 context: Option<&crate::query::AiExecutionContext>,
6249 snapshot_override: Option<Snapshot>,
6250 ) -> Result<(
6251 Arc<TableReadGeneration>,
6252 Snapshot,
6253 crate::retention::OwnedSnapshotGuard,
6254 RunPins,
6255 )> {
6256 let mut table = if let Some(context) = context {
6257 loop {
6258 context.checkpoint()?;
6259 let wait = context
6260 .remaining_duration()
6261 .unwrap_or(std::time::Duration::from_millis(5))
6262 .min(std::time::Duration::from_millis(5));
6263 if let Some(table) = handle.try_lock_for(wait) {
6264 break table;
6265 }
6266 }
6267 } else {
6268 handle.lock()
6269 };
6270 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
6271 self.snapshot_at_owned(snapshot.epoch)?
6272 } else {
6273 let snapshot = table.snapshot();
6274 let guard = self.snapshots.register_owned(snapshot.epoch);
6275 (snapshot, guard)
6276 };
6277 let table_id = table.table_id();
6278 let run_keys: Vec<_> = table
6279 .active_run_ids()
6280 .map(|run_id| (table_id, run_id))
6281 .collect();
6282 let generation = handle
6283 .generation_metrics
6284 .activate(table.clone_read_generation()?);
6285 let run_pins = self.pin_runs(&run_keys);
6286 Ok((generation, snapshot, snapshot_guard, run_pins))
6287 }
6288
6289 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
6290 let mut pins = self.backup_pins.lock();
6291 for run in runs {
6292 *pins.entry(*run).or_insert(0) += 1;
6293 }
6294 drop(pins);
6295 RunPins {
6296 pins: Arc::clone(&self.backup_pins),
6297 runs: runs.to_vec(),
6298 }
6299 }
6300
6301 pub fn query_for_current_principal(
6305 &self,
6306 table_name: &str,
6307 query: &crate::query::Query,
6308 projection: Option<&[u16]>,
6309 ) -> Result<Vec<crate::memtable::Row>> {
6310 let condition_columns = crate::query::condition_columns(&query.conditions);
6311 let catalog_bound = self
6312 .principal
6313 .read()
6314 .as_ref()
6315 .is_some_and(|principal| principal.user_id != 0);
6316 self.with_authorized_read(
6317 table_name,
6318 None,
6319 catalog_bound,
6320 |table, snapshot, allowed, principal| {
6321 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6322 self.require_columns_for(
6323 table_name,
6324 crate::auth::ColumnOperation::Select,
6325 &condition_columns,
6326 principal,
6327 )?;
6328 if let Some(projection) = projection {
6329 self.require_columns_for(
6330 table_name,
6331 crate::auth::ColumnOperation::Select,
6332 projection,
6333 principal,
6334 )?;
6335 }
6336 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
6337 let projection =
6338 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6339 for row in &mut rows {
6340 row.columns.retain(|column, _| {
6341 allowed_columns.contains(column)
6342 && projection
6343 .as_ref()
6344 .is_none_or(|projection| projection.contains(column))
6345 });
6346 }
6347 self.secure_rows_for(table_name, rows, principal)
6348 },
6349 )
6350 }
6351
6352 pub fn query_for_current_principal_controlled(
6356 &self,
6357 table_name: &str,
6358 query: &crate::query::Query,
6359 projection: Option<&[u16]>,
6360 control: &crate::ExecutionControl,
6361 ) -> Result<Vec<crate::memtable::Row>> {
6362 let catalog_bound = self
6363 .principal
6364 .read()
6365 .as_ref()
6366 .is_some_and(|principal| principal.user_id != 0);
6367 self.query_for_principal_controlled(
6368 table_name,
6369 query,
6370 projection,
6371 None,
6372 catalog_bound,
6373 control,
6374 )
6375 }
6376
6377 pub fn query_as_principal_controlled(
6381 &self,
6382 table_name: &str,
6383 query: &crate::query::Query,
6384 projection: Option<&[u16]>,
6385 principal: Option<&crate::auth::Principal>,
6386 control: &crate::ExecutionControl,
6387 ) -> Result<Vec<crate::memtable::Row>> {
6388 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6389 self.query_for_principal_controlled(
6390 table_name,
6391 query,
6392 projection,
6393 principal,
6394 catalog_bound,
6395 control,
6396 )
6397 }
6398
6399 fn query_for_principal_controlled(
6400 &self,
6401 table_name: &str,
6402 query: &crate::query::Query,
6403 projection: Option<&[u16]>,
6404 principal: Option<&crate::auth::Principal>,
6405 catalog_bound: bool,
6406 control: &crate::ExecutionControl,
6407 ) -> Result<Vec<crate::memtable::Row>> {
6408 control.checkpoint()?;
6409 let context = crate::query::AiExecutionContext::with_control(
6410 control.clone(),
6411 usize::MAX,
6412 crate::query::MAX_FUSED_CANDIDATES,
6413 );
6414 let condition_columns = crate::query::condition_columns(&query.conditions);
6415 self.with_authorized_read_context(
6416 table_name,
6417 principal,
6418 catalog_bound,
6419 None,
6420 Some(&context),
6421 None,
6422 |table, snapshot, allowed, principal| {
6423 control.checkpoint()?;
6424 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6425 self.require_columns_for(
6426 table_name,
6427 crate::auth::ColumnOperation::Select,
6428 &condition_columns,
6429 principal,
6430 )?;
6431 if let Some(projection) = projection {
6432 self.require_columns_for(
6433 table_name,
6434 crate::auth::ColumnOperation::Select,
6435 projection,
6436 principal,
6437 )?;
6438 }
6439 let rows =
6440 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
6441 let projection =
6442 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6443 let mut projected = Vec::with_capacity(rows.len());
6444 for (index, mut row) in rows.into_iter().enumerate() {
6445 if index & 255 == 0 {
6446 control.checkpoint()?;
6447 }
6448 row.columns.retain(|column, _| {
6449 allowed_columns.contains(column)
6450 && projection
6451 .as_ref()
6452 .is_none_or(|projection| projection.contains(column))
6453 });
6454 projected.push(row);
6455 }
6456 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
6457 },
6458 )
6459 }
6460
6461 pub fn approx_aggregate_for_current_principal(
6464 &self,
6465 table_name: &str,
6466 conditions: &[crate::query::Condition],
6467 column: Option<u16>,
6468 agg: crate::engine::ApproxAgg,
6469 z: f64,
6470 ) -> Result<Option<crate::engine::ApproxResult>> {
6471 if !z.is_finite() || z <= 0.0 {
6472 return Err(MongrelError::InvalidArgument(
6473 "z must be finite and > 0".into(),
6474 ));
6475 }
6476 let mut columns = crate::query::condition_columns(conditions);
6477 columns.extend(column);
6478 columns.sort_unstable();
6479 columns.dedup();
6480 self.with_authorized_aggregate_table(
6481 table_name,
6482 &columns,
6483 None,
6484 true,
6485 true,
6486 |table, authorization, _, _| {
6487 table.approx_aggregate_with_candidate_authorization(
6488 conditions,
6489 column,
6490 agg,
6491 z,
6492 authorization,
6493 )
6494 },
6495 )
6496 }
6497
6498 pub fn incremental_aggregate_for_current_principal(
6502 &self,
6503 table_name: &str,
6504 conditions: &[crate::query::Condition],
6505 column: Option<u16>,
6506 agg: crate::engine::NativeAgg,
6507 ) -> Result<crate::engine::IncrementalAggResult> {
6508 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
6509 }
6510
6511 pub fn incremental_aggregate_for_principal(
6515 &self,
6516 table_name: &str,
6517 conditions: &[crate::query::Condition],
6518 column: Option<u16>,
6519 agg: crate::engine::NativeAgg,
6520 principal: Option<&crate::auth::Principal>,
6521 catalog_bound: bool,
6522 ) -> Result<crate::engine::IncrementalAggResult> {
6523 let mut columns = crate::query::condition_columns(conditions);
6524 columns.extend(column);
6525 columns.sort_unstable();
6526 columns.dedup();
6527 self.with_authorized_aggregate_table(
6528 table_name,
6529 &columns,
6530 principal,
6531 catalog_bound,
6532 false,
6533 |table, _, principal, security_version| {
6534 let cache_key = incremental_aggregate_cache_key(
6535 table_name,
6536 conditions,
6537 column,
6538 agg,
6539 principal,
6540 security_version,
6541 );
6542 table.aggregate_incremental(cache_key, conditions, column, agg)
6543 },
6544 )
6545 }
6546
6547 pub fn get_for_current_principal(
6550 &self,
6551 table_name: &str,
6552 row_id: RowId,
6553 ) -> Result<Option<crate::memtable::Row>> {
6554 self.with_authorized_read(
6555 table_name,
6556 None,
6557 true,
6558 |table, snapshot, allowed, principal| {
6559 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6560 let Some(row) = table.get(row_id, snapshot) else {
6561 return Ok(None);
6562 };
6563 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
6564 return Ok(None);
6565 }
6566 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6567 if let Some(row) = rows.first_mut() {
6568 row.columns
6569 .retain(|column, _| allowed_columns.contains(column));
6570 }
6571 Ok(rows.pop())
6572 },
6573 )
6574 }
6575
6576 pub fn ann_rerank_for_current_principal(
6579 &self,
6580 table_name: &str,
6581 request: &crate::query::AnnRerankRequest,
6582 ) -> Result<Vec<crate::query::AnnRerankHit>> {
6583 self.with_authorized_scored_read_context_at(
6584 table_name,
6585 None,
6586 true,
6587 Some(&ReadAuthorization {
6588 operation: crate::auth::ColumnOperation::Select,
6589 columns: vec![request.column_id],
6590 permissions: Vec::new(),
6591 }),
6592 None,
6593 None,
6594 |table, snapshot, authorization, principal| {
6595 self.require_columns_for(
6596 table_name,
6597 crate::auth::ColumnOperation::Select,
6598 &[request.column_id],
6599 principal,
6600 )?;
6601 table.ann_rerank_at_with_candidate_authorization_on_generation(
6602 request,
6603 snapshot,
6604 authorization,
6605 None,
6606 )
6607 },
6608 )
6609 }
6610
6611 pub fn search_for_current_principal(
6615 &self,
6616 table_name: &str,
6617 request: &crate::query::SearchRequest,
6618 ) -> Result<Vec<crate::query::SearchHit>> {
6619 let principal = self.principal_snapshot();
6620 self.search_for_principal_with_context(table_name, request, principal.as_ref(), None)
6621 }
6622
6623 pub fn search_for_principal_with_context(
6629 &self,
6630 table_name: &str,
6631 request: &crate::query::SearchRequest,
6632 principal: Option<&crate::auth::Principal>,
6633 context: Option<&crate::query::AiExecutionContext>,
6634 ) -> Result<Vec<crate::query::SearchHit>> {
6635 let mut columns = crate::query::condition_columns(&request.must);
6636 for named in &request.retrievers {
6637 columns.push(named.retriever.column_id());
6638 }
6639 if let Some(crate::query::Rerank::ExactVector {
6640 embedding_column, ..
6641 }) = &request.rerank
6642 {
6643 columns.push(*embedding_column);
6644 }
6645 if let Some(proj) = &request.projection {
6646 columns.extend(proj);
6647 }
6648 columns.sort_unstable();
6649 columns.dedup();
6650 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6651 self.with_authorized_scored_read_context_at(
6652 table_name,
6653 principal,
6654 catalog_bound,
6655 Some(&ReadAuthorization {
6656 operation: crate::auth::ColumnOperation::Select,
6657 columns: columns.clone(),
6658 permissions: Vec::new(),
6659 }),
6660 context,
6661 None,
6662 |table, snapshot, authorization, principal| {
6663 self.require_columns_for(
6664 table_name,
6665 crate::auth::ColumnOperation::Select,
6666 &columns,
6667 principal,
6668 )?;
6669 let hits = table.search_at_with_candidate_authorization_on_generation(
6670 request,
6671 snapshot,
6672 authorization,
6673 context,
6674 )?;
6675 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6676 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
6677 for mut hit in hits {
6678 let row = crate::memtable::Row {
6679 row_id: hit.row_id,
6680 committed_epoch: crate::Epoch(0),
6681 columns: hit
6682 .cells
6683 .iter()
6684 .cloned()
6685 .collect::<std::collections::HashMap<u16, crate::Value>>(),
6686 deleted: false,
6687 };
6688 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6689 if let Some(mut row) = rows.pop() {
6690 row.columns
6691 .retain(|column, _| allowed_columns.contains(column));
6692 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
6693 hit.cells.sort_by_key(|(column, _)| *column);
6694 secured.push(hit);
6695 }
6696 }
6697 Ok(secured)
6698 },
6699 )
6700 }
6701
6702 pub fn authorized_read_snapshot(
6705 &self,
6706 table: &str,
6707 principal: Option<&crate::auth::Principal>,
6708 ) -> Result<AuthorizedReadSnapshot> {
6709 let (security, security_version, effective_principal) = {
6710 let catalog = self.catalog.read();
6711 (
6712 catalog.security.clone(),
6713 catalog.security_version,
6714 self.principal_for_authorized_read(&catalog, principal, false)?,
6715 )
6716 };
6717 let handle = self.table(table)?;
6718 let (table_snapshot, data_generation, allowed_row_ids) = {
6719 let table_handle = handle.lock();
6720 let table_snapshot = table_handle.snapshot();
6721 let data_generation = table_handle.data_generation();
6722 let allowed = self.allowed_row_ids_locked(
6723 table,
6724 &table_handle,
6725 table_snapshot,
6726 (&security, security_version),
6727 effective_principal.as_ref(),
6728 None,
6729 )?;
6730 (
6731 table_snapshot,
6732 data_generation,
6733 allowed.map(|allowed| (*allowed).clone()),
6734 )
6735 };
6736 Ok(AuthorizedReadSnapshot {
6737 table: table.to_string(),
6738 table_snapshot,
6739 data_generation,
6740 security_version,
6741 allowed_row_ids,
6742 })
6743 }
6744
6745 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
6746 if self.catalog.read().security_version != snapshot.security_version {
6747 return false;
6748 }
6749 self.table(&snapshot.table)
6750 .ok()
6751 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
6752 }
6753
6754 pub fn rls_cache_stats(&self) -> RlsCacheStats {
6755 self.rls_cache.lock().stats()
6756 }
6757
6758 pub fn rows_for(
6760 &self,
6761 table: &str,
6762 principal: Option<&crate::auth::Principal>,
6763 ) -> Result<Vec<crate::memtable::Row>> {
6764 if principal.is_none() && self.principal.read().is_some() {
6765 self.refresh_principal()?;
6766 }
6767 let allowed = self.select_column_ids_for(table, principal)?;
6768 let handle = self.table(table)?;
6769 let rows = {
6770 let table = handle.lock();
6771 table.visible_rows(table.snapshot())?
6772 };
6773 let mut rows = self.secure_rows_for(table, rows, principal)?;
6774 for row in &mut rows {
6775 row.columns.retain(|column, _| allowed.contains(column));
6776 }
6777 Ok(rows)
6778 }
6779
6780 pub fn rows_at_epoch_for_current_principal(
6783 &self,
6784 table_name: &str,
6785 snapshot: Snapshot,
6786 ) -> Result<Vec<crate::memtable::Row>> {
6787 self.with_authorized_read_context(
6788 table_name,
6789 None,
6790 true,
6791 Some(&ReadAuthorization {
6792 operation: crate::auth::ColumnOperation::Select,
6793 columns: Vec::new(),
6794 permissions: Vec::new(),
6795 }),
6796 None,
6797 Some(snapshot),
6798 |table, snapshot, allowed, principal| {
6799 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6800 let mut rows = table.visible_rows(snapshot)?;
6801 if let Some(allowed) = allowed {
6802 rows.retain(|row| allowed.contains(&row.row_id));
6803 }
6804 rows = self.secure_rows_for(table_name, rows, principal)?;
6805 for row in &mut rows {
6806 row.columns
6807 .retain(|column, _| allowed_columns.contains(column));
6808 }
6809 Ok(rows)
6810 },
6811 )
6812 }
6813
6814 pub fn count_for(
6816 &self,
6817 table: &str,
6818 principal: Option<&crate::auth::Principal>,
6819 ) -> Result<u64> {
6820 if principal.is_none() && self.principal.read().is_some() {
6821 self.refresh_principal()?;
6822 }
6823 self.select_column_ids_for(table, principal)?;
6824 if self.security_active_for(table) {
6825 Ok(self.rows_for(table, principal)?.len() as u64)
6826 } else {
6827 Ok(self.table(table)?.lock().count())
6828 }
6829 }
6830
6831 pub fn put_for(
6833 &self,
6834 table: &str,
6835 mut cells: Vec<(u16, crate::memtable::Value)>,
6836 principal: Option<&crate::auth::Principal>,
6837 ) -> Result<RowId> {
6838 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
6839 self.require_columns_for(
6840 table,
6841 crate::auth::ColumnOperation::Insert,
6842 &columns,
6843 principal,
6844 )?;
6845 let handle = self.table(table)?;
6846 let mut table_handle = handle.lock();
6847 table_handle.fill_auto_inc(&mut cells)?;
6848 table_handle.apply_defaults(&mut cells)?;
6849 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
6850 row.columns.extend(cells.iter().cloned());
6851 self.check_row_policy_for(
6852 table,
6853 crate::security::PolicyCommand::Insert,
6854 &row,
6855 true,
6856 principal,
6857 )?;
6858 table_handle.put(cells)
6859 }
6860
6861 pub fn check_row_policy_for(
6862 &self,
6863 table: &str,
6864 command: crate::security::PolicyCommand,
6865 row: &crate::memtable::Row,
6866 check_new: bool,
6867 principal: Option<&crate::auth::Principal>,
6868 ) -> Result<()> {
6869 let security = self.catalog.read().security.clone();
6870 if !security.rls_enabled(table) {
6871 return Ok(());
6872 }
6873 let cached = self.principal.read().clone();
6874 let principal = principal
6875 .or(cached.as_ref())
6876 .ok_or(MongrelError::AuthRequired)?;
6877 if security.row_allowed(table, command, row, principal, check_new) {
6878 return Ok(());
6879 }
6880 let required = match command {
6881 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
6882 table: table.to_string(),
6883 },
6884 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
6885 table: table.to_string(),
6886 },
6887 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
6888 table: table.to_string(),
6889 },
6890 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
6891 crate::auth::Permission::Delete {
6892 table: table.to_string(),
6893 }
6894 }
6895 };
6896 Err(MongrelError::PermissionDenied {
6897 required,
6898 principal: principal.username.clone(),
6899 })
6900 }
6901
6902 pub fn set_materialized_view(
6905 &self,
6906 definition: crate::catalog::MaterializedViewEntry,
6907 ) -> Result<()> {
6908 self.set_materialized_view_with_epoch(definition)
6909 .map(|_| ())
6910 }
6911
6912 pub fn set_materialized_view_with_epoch(
6914 &self,
6915 definition: crate::catalog::MaterializedViewEntry,
6916 ) -> Result<Epoch> {
6917 use crate::wal::DdlOp;
6918 use std::sync::atomic::Ordering;
6919
6920 let command = crate::catalog_cmds::CatalogCommand::CreateMaterializedView {
6921 definition: definition.clone(),
6922 };
6923 self.require(&crate::catalog_cmds::required_permission(&command))?;
6924 if self.poisoned.load(Ordering::Relaxed) {
6925 return Err(MongrelError::Other(
6926 "database poisoned by fsync error".into(),
6927 ));
6928 }
6929 if definition.name.is_empty() || definition.query.trim().is_empty() {
6930 return Err(MongrelError::InvalidArgument(
6931 "materialized view name and query must not be empty".into(),
6932 ));
6933 }
6934 let _operation = self.admit_operation()?;
6937 let _ddl = self.ddl_lock.lock();
6938 let _security_write = self.security_write()?;
6939 self.require(&crate::catalog_cmds::required_permission(&command))?;
6940 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
6944 let table_id = self
6945 .catalog
6946 .read()
6947 .live(&definition.name)
6948 .ok_or_else(|| {
6949 MongrelError::NotFound(format!(
6950 "materialized view table {:?} not found",
6951 definition.name
6952 ))
6953 })?
6954 .table_id;
6955 let definition_json = DdlOp::encode_materialized_view(&definition)?;
6956 let _commit = self.commit_lock.lock();
6957 let epoch = self.epoch.bump_assigned();
6958 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6959 let txn_id = self.alloc_txn_id()?;
6960 let mut next_catalog = self.catalog.read().clone();
6961 self.apply_catalog_command_to(&mut next_catalog, command)?;
6962 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
6963 let commit_seq = {
6964 let mut wal = self.shared_wal.lock();
6965 let append: Result<u64> = (|| {
6966 wal.append(
6967 txn_id,
6968 table_id,
6969 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
6970 name: definition.name.clone(),
6971 definition_json,
6972 }),
6973 )?;
6974 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
6975 wal.append_commit(txn_id, epoch, &[])
6976 })();
6977 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
6978 };
6979 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
6980
6981 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
6982 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
6983 Ok(epoch)
6984 }
6985
6986 pub fn root(&self) -> &Path {
6988 self.durable_root.canonical_path()
6989 }
6990
6991 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
6994 Arc::clone(&self.durable_root)
6995 }
6996
6997 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
7001 self.kek
7002 .as_deref()
7003 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
7004 }
7005
7006 pub fn is_read_only_replica(&self) -> bool {
7007 self.read_only
7008 }
7009
7010 pub fn ensure_consistent_read(&self) -> Result<()> {
7014 self.ensure_owner_process()?;
7015 if self.poisoned.load(Ordering::Relaxed) {
7016 return Err(MongrelError::Other(
7017 "database poisoned by post-commit failure; reopen required".into(),
7018 ));
7019 }
7020 Ok(())
7021 }
7022
7023 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
7024 self.replication_wal_retention_segments
7025 .store(segments, std::sync::atomic::Ordering::Relaxed);
7026 }
7027
7028 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
7033 let admin = crate::auth::Permission::Admin;
7034 self.require(&admin)?;
7035 let _operation = self.admit_operation()?;
7037 let operation_principal = self.principal_snapshot();
7038 let _barrier = self.replication_barrier.write();
7039 let _ddl = self.ddl_lock.lock();
7040 let _security = self.security_coordinator.gate.read();
7041 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7042 let mut handles: Vec<_> = self
7043 .tables
7044 .read()
7045 .iter()
7046 .map(|(id, handle)| (*id, handle.clone()))
7047 .collect();
7048 handles.sort_by_key(|(id, _)| *id);
7049 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7050 let _commit = self.commit_lock.lock();
7051 let mut wal = self.shared_wal.lock();
7052 wal.group_sync()?;
7053 let io_root = self.durable_root.io_path()?;
7054 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7055 let records = crate::wal::SharedWal::replay_with_dek(&io_root, wal_dek.as_ref())?;
7056 let epoch = records
7057 .iter()
7058 .filter_map(|record| match &record.op {
7059 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
7060 _ => None,
7061 })
7062 .max()
7063 .unwrap_or(0)
7064 .max(self.epoch.committed().0);
7065 let files = crate::replication::capture_files(&io_root)?;
7066 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7067 drop(wal);
7068 Ok(crate::replication::ReplicationSnapshot::new(
7069 source_id, epoch, files,
7070 ))
7071 }
7072
7073 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
7081 let control = crate::ExecutionControl::new(None);
7082 self.hot_backup_controlled(destination, &control, || true)
7083 }
7084
7085 pub(crate) fn hot_backup_to_durable_child(
7086 &self,
7087 parent: &crate::durable_file::DurableRoot,
7088 child: &Path,
7089 control: &crate::ExecutionControl,
7090 ) -> Result<crate::backup::BackupReport> {
7091 let mut components = child.components();
7092 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
7093 || components.next().is_some()
7094 {
7095 return Err(MongrelError::InvalidArgument(
7096 "durable backup child must be one relative path component".into(),
7097 ));
7098 }
7099 let destination_name = child.file_name().ok_or_else(|| {
7100 MongrelError::InvalidArgument("durable backup child has no filename".into())
7101 })?;
7102 let source_root = self.durable_root.io_path()?;
7103 let prepared = prepare_backup_destination_in(&source_root, parent, destination_name)?;
7104 self.hot_backup_prepared(prepared, control, || true)
7105 }
7106
7107 #[doc(hidden)]
7110 pub fn hot_backup_controlled<F>(
7111 &self,
7112 destination: impl AsRef<Path>,
7113 control: &crate::ExecutionControl,
7114 before_publish: F,
7115 ) -> Result<crate::backup::BackupReport>
7116 where
7117 F: FnOnce() -> bool,
7118 {
7119 let source_root = self.durable_root.io_path()?;
7120 let prepared = prepare_backup_destination(&source_root, destination.as_ref())?;
7121 self.hot_backup_prepared(prepared, control, before_publish)
7122 }
7123
7124 fn hot_backup_prepared<F>(
7125 &self,
7126 mut prepared: PreparedBackupDestination,
7127 control: &crate::ExecutionControl,
7128 before_publish: F,
7129 ) -> Result<crate::backup::BackupReport>
7130 where
7131 F: FnOnce() -> bool,
7132 {
7133 let admin = crate::auth::Permission::Admin;
7134 self.require(&admin)?;
7135 let _operation = self.admit_operation()?;
7138 let operation_principal = self.principal_snapshot();
7139 control.checkpoint()?;
7140 let destination = prepared.destination_path.clone();
7141 let mut before_publish = Some(before_publish);
7142
7143 let outcome = (|| {
7144 control.checkpoint()?;
7145 let barrier = self.replication_barrier.write();
7146 let ddl = self.ddl_lock.lock();
7147 let security = self.security_coordinator.gate.read();
7148 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7149 let mut handles: Vec<_> = self
7150 .tables
7151 .read()
7152 .iter()
7153 .map(|(id, handle)| (*id, handle.clone()))
7154 .collect();
7155 handles.sort_by_key(|(id, _)| *id);
7156 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7157 let commit = self.commit_lock.lock();
7158 let mut wal = self.shared_wal.lock();
7159 wal.group_sync()?;
7160 let epoch = self.epoch.committed().0;
7161 let boundary_unix_nanos = current_unix_nanos();
7162 let source_root = self.durable_root.io_path()?;
7163
7164 let pin_nonce = std::time::SystemTime::now()
7165 .duration_since(std::time::UNIX_EPOCH)
7166 .unwrap_or_default()
7167 .as_nanos();
7168 let file_pin_root = source_root
7169 .join(META_DIR)
7170 .join("backup-pins")
7171 .join(format!("{}-{pin_nonce}", std::process::id()));
7172 std::fs::create_dir_all(&file_pin_root)?;
7173 let _file_pins = BackupFilePins {
7174 root: file_pin_root.clone(),
7175 };
7176 let mut run_files = Vec::new();
7177 for (index, (table_id, _)) in handles.iter().enumerate() {
7178 if index % 256 == 0 {
7179 control.checkpoint()?;
7180 }
7181 let table = &table_guards[index];
7182 for (run_index, run) in table.run_refs().iter().enumerate() {
7183 if run_index % 256 == 0 {
7184 control.checkpoint()?;
7185 }
7186 let source = table.run_path(run.run_id as u64);
7187 let relative = Path::new(TABLES_DIR)
7188 .join(table_id.to_string())
7189 .join(crate::engine::RUNS_DIR)
7190 .join(format!("r-{}.sr", run.run_id));
7191 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
7192 if std::fs::hard_link(&source, &pinned).is_err() {
7193 crate::backup::copy_file_synced(&source, &pinned)?;
7194 }
7195 run_files.push(((*table_id, run.run_id), pinned, relative));
7196 }
7197 }
7198 crate::durable_file::sync_directory(&file_pin_root)?;
7199 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
7200 {
7201 let mut pins = self.backup_pins.lock();
7202 for key in &run_keys {
7203 *pins.entry(*key).or_insert(0) += 1;
7204 }
7205 }
7206 let _run_pins = RunPins {
7207 pins: Arc::clone(&self.backup_pins),
7208 runs: run_keys,
7209 };
7210 let _registry_pins: Vec<crate::retention::PinGuard> = table_guards
7217 .iter()
7218 .map(|table| {
7219 table
7220 .pin_registry()
7221 .pin(crate::retention::PinSource::BackupPitr, Epoch(epoch))
7222 })
7223 .collect();
7224 let deferred: HashSet<_> = run_files
7225 .iter()
7226 .map(|(_, _, relative)| relative.clone())
7227 .collect();
7228 let mut copied = Vec::new();
7229 copy_backup_boundary(
7230 &source_root,
7231 prepared.stage.as_deref().ok_or_else(|| {
7232 MongrelError::Other("backup staging root was already released".into())
7233 })?,
7234 &deferred,
7235 &mut copied,
7236 Some(control),
7237 )?;
7238
7239 drop(wal);
7240 drop(commit);
7241 drop(table_guards);
7242 drop(security);
7243 drop(ddl);
7244 drop(barrier);
7245
7246 if let Some(hook) = self.backup_hook.lock().as_ref() {
7247 hook();
7248 }
7249 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
7250 if index % 256 == 0 {
7251 control.checkpoint()?;
7252 }
7253 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
7254 prepared
7255 .stage
7256 .as_deref()
7257 .ok_or_else(|| {
7258 MongrelError::Other("backup staging root was already released".into())
7259 })?
7260 .copy_new_from(&relative, &mut source)?;
7261 copied.push(relative);
7262 }
7263
7264 let manifest = crate::backup::BackupManifest::create_controlled_durable(
7265 prepared.stage.as_deref().ok_or_else(|| {
7266 MongrelError::Other("backup staging root was already released".into())
7267 })?,
7268 epoch,
7269 &copied,
7270 control,
7271 )?;
7272 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
7273 MongrelError::Other("backup staging root was already released".into())
7274 })?)?;
7275 control.checkpoint()?;
7276 let publish = before_publish.take().ok_or_else(|| {
7277 MongrelError::Other("backup publication callback already consumed".into())
7278 })?;
7279 if !publish() {
7280 return Err(MongrelError::Cancelled);
7281 }
7282 let final_security = self.security_coordinator.gate.read();
7283 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7284 drop(prepared.stage.take().ok_or_else(|| {
7288 MongrelError::Other("backup staging root was already released".into())
7289 })?);
7290 let published = std::cell::Cell::new(false);
7291 if let Err(error) = prepared.parent.rename_directory_new_with_after(
7292 Path::new(&prepared.stage_name),
7293 &prepared.parent,
7294 Path::new(&prepared.destination_name),
7295 || published.set(true),
7296 ) {
7297 if published.get() {
7298 return Err(MongrelError::CommitOutcomeUnknown {
7299 epoch,
7300 message: format!("backup publication was not durable: {error}"),
7301 });
7302 }
7303 return Err(error.into());
7304 }
7305 drop(final_security);
7306 Ok(crate::backup::BackupReport {
7307 destination,
7308 epoch,
7309 boundary_unix_nanos,
7310 files: manifest.files.len(),
7311 bytes: manifest.total_bytes(),
7312 })
7313 })();
7314
7315 if outcome.is_err() {
7316 drop(prepared.stage.take());
7317 let _ = prepared
7318 .parent
7319 .remove_directory_all(Path::new(&prepared.stage_name));
7320 }
7321 outcome
7322 }
7323
7324 pub fn replication_batch_since(
7327 &self,
7328 since_epoch: u64,
7329 ) -> Result<crate::replication::ReplicationBatch> {
7330 use crate::wal::Op;
7331
7332 let admin = crate::auth::Permission::Admin;
7333 self.require(&admin)?;
7334 let _operation = self.admit_operation()?;
7336 let operation_principal = self.principal_snapshot();
7337
7338 let mut wal = self.shared_wal.lock();
7339 wal.group_sync()?;
7340 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7341 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
7342 drop(wal);
7343
7344 let commits: HashMap<u64, u64> = records
7345 .iter()
7346 .filter_map(|record| match &record.op {
7347 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
7348 _ => None,
7349 })
7350 .collect();
7351 let earliest_epoch = commits.values().copied().min();
7352 let current_epoch = commits
7353 .values()
7354 .copied()
7355 .max()
7356 .unwrap_or(0)
7357 .max(self.epoch.committed().0);
7358 let selected: HashSet<u64> = commits
7359 .iter()
7360 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
7361 .collect();
7362 let retention_gap = since_epoch < current_epoch
7363 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
7364 let spilled = records.iter().any(|record| {
7365 selected.contains(&record.txn_id)
7366 && matches!(
7367 &record.op,
7368 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
7369 )
7370 });
7371 let records = records
7372 .into_iter()
7373 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
7374 .filter(|record| selected.contains(&record.txn_id))
7375 .collect();
7376 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7377 let batch = crate::replication::ReplicationBatch::complete_for_source(
7378 source_id,
7379 since_epoch,
7380 current_epoch,
7381 earliest_epoch,
7382 retention_gap,
7383 spilled,
7384 records,
7385 )?;
7386 if let Some(hook) = self.replication_hook.lock().as_ref() {
7387 hook();
7388 }
7389 let _security = self.security_coordinator.gate.read();
7390 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7391 Ok(batch)
7392 }
7393
7394 pub fn append_replication_batch(
7398 &self,
7399 batch: &crate::replication::ReplicationBatch,
7400 ) -> Result<u64> {
7401 use crate::wal::Op;
7402
7403 if !self.read_only {
7404 return Err(MongrelError::InvalidArgument(
7405 "replication batches may only target a marked replica".into(),
7406 ));
7407 }
7408 let _operation = self.admit_operation()?;
7410 let current = crate::replication::replica_epoch(&self.root)?;
7411 if batch.is_source_bound() {
7412 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
7413 if batch.source_id != source_id {
7414 return Err(MongrelError::Conflict(
7415 "replication batch source does not match follower binding".into(),
7416 ));
7417 }
7418 }
7419 if batch.requires_snapshot {
7420 return Err(MongrelError::Conflict(
7421 "replication snapshot required for this batch".into(),
7422 ));
7423 }
7424 batch.validate_proof()?;
7425 if batch.from_epoch != current {
7426 if batch.from_epoch < current && batch.current_epoch == current {
7427 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7428 let _wal = self.shared_wal.lock();
7429 let existing: HashSet<(u64, u64)> =
7430 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7431 .into_iter()
7432 .filter_map(|record| match record.op {
7433 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7434 _ => None,
7435 })
7436 .collect();
7437 let already_applied = batch.records.iter().all(|record| match &record.op {
7438 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
7439 _ => true,
7440 });
7441 if already_applied {
7442 return Ok(current);
7443 }
7444 }
7445 return Err(MongrelError::Conflict(format!(
7446 "replication batch starts at epoch {}, follower is at epoch {current}",
7447 batch.from_epoch
7448 )));
7449 }
7450 if batch.current_epoch < current {
7451 return Err(MongrelError::InvalidArgument(format!(
7452 "replication batch current epoch {} precedes follower epoch {current}",
7453 batch.current_epoch
7454 )));
7455 }
7456 let records = &batch.records;
7457 let mut commits = HashMap::new();
7458 let mut commit_epochs = HashSet::new();
7459 let mut commit_timestamps = HashMap::new();
7460 for record in records {
7461 match &record.op {
7462 Op::TxnCommit { epoch, added_runs } => {
7463 if !added_runs.is_empty() {
7464 return Err(MongrelError::Conflict(
7465 "replication snapshot required for spilled-run transaction".into(),
7466 ));
7467 }
7468 if commits.insert(record.txn_id, *epoch).is_some() {
7469 return Err(MongrelError::InvalidArgument(format!(
7470 "duplicate commit for replication transaction {}",
7471 record.txn_id
7472 )));
7473 }
7474 if *epoch <= current || *epoch > batch.current_epoch {
7475 return Err(MongrelError::InvalidArgument(format!(
7476 "replication commit epoch {epoch} is outside ({current}, {}]",
7477 batch.current_epoch
7478 )));
7479 }
7480 if !commit_epochs.insert(*epoch) {
7481 return Err(MongrelError::InvalidArgument(format!(
7482 "duplicate replication commit epoch {epoch}"
7483 )));
7484 }
7485 }
7486 Op::CommitTimestamp { unix_nanos } => {
7487 commit_timestamps.insert(record.txn_id, *unix_nanos);
7488 }
7489 _ => {}
7490 }
7491 }
7492 for record in records {
7493 if record.txn_id == crate::wal::SYSTEM_TXN_ID
7494 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
7495 {
7496 return Err(MongrelError::InvalidArgument(
7497 "replication batch contains a non-committed record".into(),
7498 ));
7499 }
7500 if !commits.contains_key(&record.txn_id) {
7501 return Err(MongrelError::InvalidArgument(format!(
7502 "incomplete replication transaction {}",
7503 record.txn_id
7504 )));
7505 }
7506 }
7507 let target_epoch = commits
7508 .values()
7509 .copied()
7510 .filter(|epoch| *epoch > current)
7511 .max()
7512 .unwrap_or(current);
7513 if target_epoch != batch.current_epoch {
7514 return Err(MongrelError::InvalidArgument(format!(
7515 "replication batch ends at epoch {target_epoch}, expected {}",
7516 batch.current_epoch
7517 )));
7518 }
7519 let mut selected: HashSet<u64> = commits
7520 .iter()
7521 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
7522 .collect();
7523 if selected.is_empty() {
7524 return Ok(current);
7525 }
7526 let mut wal = self.shared_wal.lock();
7527 wal.group_sync()?;
7528 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7529 let existing: HashSet<(u64, u64)> =
7530 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7531 .into_iter()
7532 .filter_map(|record| match record.op {
7533 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7534 _ => None,
7535 })
7536 .collect();
7537 selected.retain(|txn_id| {
7538 commits
7539 .get(txn_id)
7540 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
7541 });
7542 for record in records {
7543 if !selected.contains(&record.txn_id) {
7544 continue;
7545 }
7546 match &record.op {
7547 Op::TxnCommit { epoch, added_runs } => {
7548 let timestamp = commit_timestamps
7549 .get(&record.txn_id)
7550 .copied()
7551 .unwrap_or_else(current_unix_nanos);
7552 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
7553 }
7554 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
7555 op => {
7556 wal.append(record.txn_id, 0, op.clone())?;
7557 }
7558 }
7559 }
7560 if !selected.is_empty() {
7561 wal.group_sync()?;
7562 }
7563 drop(wal);
7564
7565 let mut recovered_catalog = self.catalog.read().clone();
7569 if let Err(error) = recover_ddl_from_wal(
7570 &self.root,
7571 Some(&self.durable_root),
7572 &mut recovered_catalog,
7573 self.meta_dek.as_ref(),
7574 wal_dek.as_ref(),
7575 true,
7576 None,
7577 ) {
7578 return Err(MongrelError::DurableCommit {
7579 epoch: target_epoch,
7580 message: format!(
7581 "replication WAL is durable but catalog checkpoint failed: {error}"
7582 ),
7583 });
7584 }
7585 let _security = self.security_coordinator.gate.write();
7586 let old_security_version = self.catalog.read().security_version;
7587 let security_changed = old_security_version != recovered_catalog.security_version
7588 || self.catalog.read().require_auth != recovered_catalog.require_auth;
7589 let require_auth = recovered_catalog.require_auth;
7590 let principal = if security_changed {
7591 None
7592 } else {
7593 self.principal.read().as_ref().and_then(|principal| {
7594 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
7595 })
7596 };
7597 if require_auth {
7598 self.auth_state.set_require_auth(true);
7599 }
7600 self.auth_state.set_principal(principal.clone());
7601 *self.principal.write() = principal;
7602 let security_version = recovered_catalog.security_version;
7603 *self.catalog.write() = recovered_catalog;
7604 self.security_coordinator
7605 .version
7606 .store(security_version, Ordering::Release);
7607 if !require_auth {
7608 self.auth_state.set_require_auth(false);
7609 }
7610 if let Err(error) =
7611 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
7612 {
7613 return Err(MongrelError::DurableCommit {
7614 epoch: target_epoch,
7615 message: format!(
7616 "replication WAL and catalog are durable but follower watermark failed: {error}"
7617 ),
7618 });
7619 }
7620 Ok(target_epoch)
7621 }
7622
7623 pub fn table_id(&self, name: &str) -> Result<u64> {
7626 let cat = self.catalog.read();
7627 cat.live(name)
7628 .map(|e| e.table_id)
7629 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
7630 }
7631
7632 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
7637 let catalog = self.catalog.read();
7638 catalog
7639 .live(name)
7640 .map(|entry| (entry.table_id, entry.schema.schema_id))
7641 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
7642 }
7643
7644 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
7645 self.catalog
7646 .read()
7647 .building(name)
7648 .map(|entry| entry.table_id)
7649 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
7650 }
7651
7652 pub fn procedures(&self) -> Vec<StoredProcedure> {
7653 self.catalog
7654 .read()
7655 .procedures
7656 .iter()
7657 .map(|p| p.procedure.clone())
7658 .collect()
7659 }
7660
7661 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
7662 self.catalog
7663 .read()
7664 .procedures
7665 .iter()
7666 .find(|p| p.procedure.name == name)
7667 .map(|p| p.procedure.clone())
7668 }
7669
7670 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
7671 self.create_procedure_inner(procedure, None)
7672 }
7673
7674 pub fn create_procedure_controlled<F>(
7675 &self,
7676 procedure: StoredProcedure,
7677 mut before_publish: F,
7678 ) -> Result<StoredProcedure>
7679 where
7680 F: FnMut() -> Result<()>,
7681 {
7682 self.create_procedure_inner(procedure, Some(&mut before_publish))
7683 }
7684
7685 fn create_procedure_inner(
7686 &self,
7687 mut procedure: StoredProcedure,
7688 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7689 ) -> Result<StoredProcedure> {
7690 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
7691 procedure: procedure.clone(),
7692 };
7693 self.require(&crate::catalog_cmds::required_permission(&command))?;
7694 let _g = self.ddl_lock.lock();
7695 let _security_write = self.security_write()?;
7696 self.require(&crate::catalog_cmds::required_permission(&command))?;
7697 procedure.validate()?;
7698 self.validate_procedure_references(&procedure)?;
7699 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7703 let commit_lock = Arc::clone(&self.commit_lock);
7704 let _c = commit_lock.lock();
7705 let epoch = self.epoch.bump_assigned();
7706 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7707 procedure.created_epoch = epoch.0;
7708 procedure.updated_epoch = epoch.0;
7709 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
7710 procedure: procedure.clone(),
7711 };
7712 let mut next_catalog = self.catalog.read().clone();
7713 self.apply_catalog_command_to(&mut next_catalog, command)?;
7714 next_catalog.db_epoch = epoch.0;
7715 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7716 Ok(procedure)
7717 }
7718
7719 pub fn create_or_replace_procedure(
7720 &self,
7721 procedure: StoredProcedure,
7722 ) -> Result<StoredProcedure> {
7723 self.create_or_replace_procedure_inner(procedure, None)
7724 }
7725
7726 pub fn create_or_replace_procedure_controlled<F>(
7727 &self,
7728 procedure: StoredProcedure,
7729 mut before_publish: F,
7730 ) -> Result<StoredProcedure>
7731 where
7732 F: FnMut() -> Result<()>,
7733 {
7734 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
7735 }
7736
7737 fn create_or_replace_procedure_inner(
7738 &self,
7739 procedure: StoredProcedure,
7740 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7741 ) -> Result<StoredProcedure> {
7742 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
7743 procedure: procedure.clone(),
7744 };
7745 self.require(&crate::catalog_cmds::required_permission(&command))?;
7746 let _g = self.ddl_lock.lock();
7747 let _security_write = self.security_write()?;
7748 self.require(&crate::catalog_cmds::required_permission(&command))?;
7749 procedure.validate()?;
7750 self.validate_procedure_references(&procedure)?;
7751 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7755 let commit_lock = Arc::clone(&self.commit_lock);
7756 let _c = commit_lock.lock();
7757 let epoch = self.epoch.bump_assigned();
7758 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7759 let mut next_catalog = self.catalog.read().clone();
7760 let replaced = match next_catalog
7765 .procedures
7766 .iter()
7767 .position(|p| p.procedure.name == procedure.name)
7768 {
7769 Some(idx) => next_catalog.procedures[idx]
7770 .procedure
7771 .replaced(procedure.clone(), epoch.0)?,
7772 None => {
7773 let mut next = procedure;
7774 next.created_epoch = epoch.0;
7775 next.updated_epoch = epoch.0;
7776 next
7777 }
7778 };
7779 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
7780 procedure: replaced.clone(),
7781 };
7782 self.apply_catalog_command_to(&mut next_catalog, command)?;
7783 next_catalog.db_epoch = epoch.0;
7784 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7785 Ok(replaced)
7786 }
7787
7788 pub fn drop_procedure(&self, name: &str) -> Result<()> {
7789 self.drop_procedure_with_epoch(name).map(|_| ())
7790 }
7791
7792 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
7793 self.drop_procedure_with_epoch_inner(name, None)
7794 }
7795
7796 pub fn drop_procedure_with_epoch_controlled<F>(
7797 &self,
7798 name: &str,
7799 mut before_publish: F,
7800 ) -> Result<Epoch>
7801 where
7802 F: FnMut() -> Result<()>,
7803 {
7804 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
7805 }
7806
7807 fn drop_procedure_with_epoch_inner(
7808 &self,
7809 name: &str,
7810 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7811 ) -> Result<Epoch> {
7812 let command = crate::catalog_cmds::CatalogCommand::DropProcedure {
7813 name: name.to_string(),
7814 };
7815 self.require(&crate::catalog_cmds::required_permission(&command))?;
7816 let _g = self.ddl_lock.lock();
7817 let _security_write = self.security_write()?;
7818 self.require(&crate::catalog_cmds::required_permission(&command))?;
7819 let commit_lock = Arc::clone(&self.commit_lock);
7820 let _c = commit_lock.lock();
7821 let epoch = self.epoch.bump_assigned();
7822 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7823 let mut next_catalog = self.catalog.read().clone();
7824 self.apply_catalog_command_to(&mut next_catalog, command)?;
7825 next_catalog.db_epoch = epoch.0;
7826 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7827 Ok(epoch)
7828 }
7829
7830 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
7835 self.catalog.read().users.clone()
7836 }
7837
7838 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
7841 self.catalog
7842 .read()
7843 .users
7844 .iter()
7845 .find(|user| user.username == username)
7846 .map(|user| (user.id, user.created_epoch))
7847 }
7848
7849 pub fn user_scram_verifier(
7851 &self,
7852 username: &str,
7853 ) -> Option<crate::security_hardening::ScramVerifier> {
7854 self.catalog
7855 .read()
7856 .users
7857 .iter()
7858 .find(|user| user.username == username)
7859 .and_then(|user| user.scram_sha_256.clone())
7860 }
7861
7862 pub fn security_version(&self) -> u64 {
7865 self.catalog.read().security_version
7866 }
7867
7868 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
7870 self.catalog.read().roles.clone()
7871 }
7872
7873 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
7875 self.require(&crate::auth::Permission::Admin)?;
7876 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
7877 let scram = crate::auth::scram_verifier(password).map_err(MongrelError::Other)?;
7878 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(password);
7879 self.create_user_with_password_hash_inner(username, hash, Some((scram, mysql)), None)
7880 }
7881
7882 pub fn create_user_with_password_hash(
7884 &self,
7885 username: &str,
7886 hash: String,
7887 ) -> Result<crate::auth::UserEntry> {
7888 self.create_user_with_password_hash_inner(username, hash, None, None)
7889 }
7890
7891 pub fn create_user_with_password_hash_controlled<F>(
7892 &self,
7893 username: &str,
7894 hash: String,
7895 mut before_publish: F,
7896 ) -> Result<crate::auth::UserEntry>
7897 where
7898 F: FnMut() -> Result<()>,
7899 {
7900 self.create_user_with_password_hash_inner(username, hash, None, Some(&mut before_publish))
7901 }
7902
7903 fn create_user_with_password_hash_inner(
7904 &self,
7905 username: &str,
7906 hash: String,
7907 verifiers: Option<(
7908 crate::security_hardening::ScramVerifier,
7909 crate::auth::MysqlCachingSha2Verifier,
7910 )>,
7911 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7912 ) -> Result<crate::auth::UserEntry> {
7913 let command = match &verifiers {
7917 Some((scram_sha_256, mysql_caching_sha2)) => {
7918 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
7919 username: username.to_string(),
7920 password_hash: hash.clone(),
7921 scram_sha_256: scram_sha_256.clone(),
7922 mysql_caching_sha2: mysql_caching_sha2.clone(),
7923 is_admin: false,
7924 created_epoch: 0,
7925 }
7926 }
7927 None => crate::catalog_cmds::CatalogCommand::CreateUser {
7928 username: username.to_string(),
7929 password_hash: hash.clone(),
7930 is_admin: false,
7931 created_epoch: 0,
7932 },
7933 };
7934 self.require(&crate::catalog_cmds::required_permission(&command))?;
7935 let _ddl = self.ddl_lock.lock();
7936 let _security_write = self.security_write()?;
7937 self.require(&crate::catalog_cmds::required_permission(&command))?;
7938 let _commit = self.commit_lock.lock();
7939 let epoch = self.epoch.bump_assigned();
7940 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7941 let command = match verifiers {
7942 Some((scram_sha_256, mysql_caching_sha2)) => {
7943 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
7944 username: username.to_string(),
7945 password_hash: hash,
7946 scram_sha_256,
7947 mysql_caching_sha2,
7948 is_admin: false,
7949 created_epoch: epoch.0,
7950 }
7951 }
7952 None => crate::catalog_cmds::CatalogCommand::CreateUser {
7953 username: username.to_string(),
7954 password_hash: hash,
7955 is_admin: false,
7956 created_epoch: epoch.0,
7957 },
7958 };
7959 let mut next_catalog = self.catalog.read().clone();
7960 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
7961 let crate::catalog_cmds::CatalogDelta::UserUpserted(entry) = delta else {
7962 return Err(MongrelError::Other(
7963 "CreateUser resolved to an unexpected catalog delta".into(),
7964 ));
7965 };
7966 next_catalog.db_epoch = epoch.0;
7967 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
7968 Ok(entry)
7969 }
7970
7971 pub fn drop_user(&self, username: &str) -> Result<()> {
7973 self.drop_user_with_epoch(username).map(|_| ())
7974 }
7975
7976 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
7977 self.drop_user_with_epoch_inner(username, None)
7978 }
7979
7980 pub fn drop_user_with_epoch_controlled<F>(
7981 &self,
7982 username: &str,
7983 mut before_publish: F,
7984 ) -> Result<Epoch>
7985 where
7986 F: FnMut() -> Result<()>,
7987 {
7988 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
7989 }
7990
7991 fn drop_user_with_epoch_inner(
7992 &self,
7993 username: &str,
7994 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
7995 ) -> Result<Epoch> {
7996 let command = crate::catalog_cmds::CatalogCommand::DropUser {
7997 username: username.to_string(),
7998 };
7999 self.require(&crate::catalog_cmds::required_permission(&command))?;
8000 let _ddl = self.ddl_lock.lock();
8001 let _security_write = self.security_write()?;
8002 self.require(&crate::catalog_cmds::required_permission(&command))?;
8003 let _commit = self.commit_lock.lock();
8004 let epoch = self.epoch.bump_assigned();
8005 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8006 let mut next_catalog = self.catalog.read().clone();
8007 self.apply_catalog_command_to(&mut next_catalog, command)?;
8008 next_catalog.db_epoch = epoch.0;
8009 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8010 Ok(epoch)
8011 }
8012
8013 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
8015 self.alter_user_password_with_epoch(username, new_password)
8016 .map(|_| ())
8017 }
8018
8019 pub fn alter_user_password_with_epoch(
8020 &self,
8021 username: &str,
8022 new_password: &str,
8023 ) -> Result<Epoch> {
8024 self.require(&crate::auth::Permission::Admin)?;
8025 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
8026 let scram = crate::auth::scram_verifier(new_password).map_err(MongrelError::Other)?;
8027 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(new_password);
8028 self.alter_user_password_hash_with_epoch_inner(username, hash, Some((scram, mysql)), None)
8029 }
8030
8031 pub fn alter_user_password_hash_with_epoch(
8032 &self,
8033 username: &str,
8034 hash: String,
8035 ) -> Result<Epoch> {
8036 self.alter_user_password_hash_with_epoch_inner(username, hash, None, None)
8037 }
8038
8039 pub fn alter_user_password_hash_with_epoch_controlled<F>(
8040 &self,
8041 username: &str,
8042 hash: String,
8043 mut before_publish: F,
8044 ) -> Result<Epoch>
8045 where
8046 F: FnMut() -> Result<()>,
8047 {
8048 self.alter_user_password_hash_with_epoch_inner(
8049 username,
8050 hash,
8051 None,
8052 Some(&mut before_publish),
8053 )
8054 }
8055
8056 fn alter_user_password_hash_with_epoch_inner(
8057 &self,
8058 username: &str,
8059 hash: String,
8060 verifiers: Option<(
8061 crate::security_hardening::ScramVerifier,
8062 crate::auth::MysqlCachingSha2Verifier,
8063 )>,
8064 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8065 ) -> Result<Epoch> {
8066 let command = match verifiers {
8067 Some((scram_sha_256, mysql_caching_sha2)) => {
8068 crate::catalog_cmds::CatalogCommand::AlterUserPasswordWithAuthVerifiers {
8069 username: username.to_string(),
8070 password_hash: hash,
8071 scram_sha_256,
8072 mysql_caching_sha2,
8073 }
8074 }
8075 None => crate::catalog_cmds::CatalogCommand::AlterUserPassword {
8076 username: username.to_string(),
8077 password_hash: hash,
8078 },
8079 };
8080 self.require(&crate::catalog_cmds::required_permission(&command))?;
8081 let _ddl = self.ddl_lock.lock();
8082 let _security_write = self.security_write()?;
8083 self.require(&crate::catalog_cmds::required_permission(&command))?;
8084 let _commit = self.commit_lock.lock();
8085 let epoch = self.epoch.bump_assigned();
8086 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8087 let mut next_catalog = self.catalog.read().clone();
8088 self.apply_catalog_command_to(&mut next_catalog, command)?;
8089 next_catalog.db_epoch = epoch.0;
8090 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8091 Ok(epoch)
8092 }
8093
8094 pub fn verify_user(
8097 &self,
8098 username: &str,
8099 password: &str,
8100 ) -> Result<Option<crate::auth::UserEntry>> {
8101 let cat = self.catalog.read();
8102 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
8103 return Ok(None);
8104 };
8105 if user.password_hash.is_empty() {
8106 return Ok(None);
8107 }
8108 let ok = crate::auth::verify_password(password, &user.password_hash)
8109 .map_err(MongrelError::Other)?;
8110 if ok {
8111 Ok(Some(user.clone()))
8112 } else {
8113 Ok(None)
8114 }
8115 }
8116
8117 pub fn authenticate_principal(
8121 &self,
8122 username: &str,
8123 password: &str,
8124 ) -> Result<Option<crate::auth::Principal>> {
8125 self.authenticate_principal_inner(username, password, || {})
8126 }
8127
8128 fn authenticate_principal_inner<F>(
8129 &self,
8130 username: &str,
8131 password: &str,
8132 after_verify: F,
8133 ) -> Result<Option<crate::auth::Principal>>
8134 where
8135 F: FnOnce(),
8136 {
8137 let catalog = self.catalog.read();
8138 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
8139 return Ok(None);
8140 };
8141 if user.password_hash.is_empty()
8142 || !crate::auth::verify_password(password, &user.password_hash)
8143 .map_err(MongrelError::Other)?
8144 {
8145 return Ok(None);
8146 }
8147 after_verify();
8148 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
8149 }
8150
8151 pub fn authenticate_mysql_caching_sha2_principal(
8154 &self,
8155 username: &str,
8156 nonce: &[u8],
8157 proof: &[u8],
8158 ) -> Option<crate::auth::Principal> {
8159 let catalog = self.catalog.read();
8160 let user = catalog
8161 .users
8162 .iter()
8163 .find(|user| user.username == username)?;
8164 if !user.mysql_caching_sha2.as_ref()?.verify(nonce, proof) {
8165 return None;
8166 }
8167 Self::resolve_user_principal_from_catalog(&catalog, user)
8168 }
8169
8170 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
8172 self.set_user_admin_with_epoch(username, is_admin)
8173 .map(|_| ())
8174 }
8175
8176 pub fn set_user_admin_with_epoch(
8177 &self,
8178 username: &str,
8179 is_admin: bool,
8180 ) -> Result<Option<Epoch>> {
8181 self.set_user_admin_with_epoch_inner(username, is_admin, None)
8182 }
8183
8184 pub fn set_user_admin_with_epoch_controlled<F>(
8185 &self,
8186 username: &str,
8187 is_admin: bool,
8188 mut before_publish: F,
8189 ) -> Result<Option<Epoch>>
8190 where
8191 F: FnMut() -> Result<()>,
8192 {
8193 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
8194 }
8195
8196 fn set_user_admin_with_epoch_inner(
8197 &self,
8198 username: &str,
8199 is_admin: bool,
8200 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8201 ) -> Result<Option<Epoch>> {
8202 let command = crate::catalog_cmds::CatalogCommand::SetUserAdmin {
8203 username: username.to_string(),
8204 is_admin,
8205 };
8206 self.require(&crate::catalog_cmds::required_permission(&command))?;
8207 let _ddl = self.ddl_lock.lock();
8208 let _security_write = self.security_write()?;
8209 self.require(&crate::catalog_cmds::required_permission(&command))?;
8210 let _commit = self.commit_lock.lock();
8211 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8215 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8216 return Ok(None);
8217 }
8218 let epoch = self.epoch.bump_assigned();
8219 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8220 let mut next_catalog = self.catalog.read().clone();
8221 self.apply_catalog_command_to(&mut next_catalog, command)?;
8222 next_catalog.db_epoch = epoch.0;
8223 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8224 Ok(Some(epoch))
8225 }
8226
8227 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
8229 self.create_role_inner(name, None)
8230 }
8231
8232 pub fn create_role_controlled<F>(
8233 &self,
8234 name: &str,
8235 mut before_publish: F,
8236 ) -> Result<crate::auth::RoleEntry>
8237 where
8238 F: FnMut() -> Result<()>,
8239 {
8240 self.create_role_inner(name, Some(&mut before_publish))
8241 }
8242
8243 fn create_role_inner(
8244 &self,
8245 name: &str,
8246 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8247 ) -> Result<crate::auth::RoleEntry> {
8248 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8249 name: name.to_string(),
8250 created_epoch: 0, };
8252 self.require(&crate::catalog_cmds::required_permission(&command))?;
8253 let _ddl = self.ddl_lock.lock();
8254 let _security_write = self.security_write()?;
8255 self.require(&crate::catalog_cmds::required_permission(&command))?;
8256 let _commit = self.commit_lock.lock();
8257 let epoch = self.epoch.bump_assigned();
8258 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8259 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8260 name: name.to_string(),
8261 created_epoch: epoch.0,
8262 };
8263 let mut next_catalog = self.catalog.read().clone();
8264 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8265 let crate::catalog_cmds::CatalogDelta::RoleUpserted(entry) = delta else {
8266 return Err(MongrelError::Other(
8267 "CreateRole resolved to an unexpected catalog delta".into(),
8268 ));
8269 };
8270 next_catalog.db_epoch = epoch.0;
8271 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8272 Ok(entry)
8273 }
8274
8275 pub fn drop_role(&self, name: &str) -> Result<()> {
8277 self.drop_role_with_epoch(name).map(|_| ())
8278 }
8279
8280 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
8281 self.drop_role_with_epoch_inner(name, None)
8282 }
8283
8284 pub fn drop_role_with_epoch_controlled<F>(
8285 &self,
8286 name: &str,
8287 mut before_publish: F,
8288 ) -> Result<Epoch>
8289 where
8290 F: FnMut() -> Result<()>,
8291 {
8292 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
8293 }
8294
8295 fn drop_role_with_epoch_inner(
8296 &self,
8297 name: &str,
8298 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8299 ) -> Result<Epoch> {
8300 let command = crate::catalog_cmds::CatalogCommand::DropRole {
8301 name: name.to_string(),
8302 };
8303 self.require(&crate::catalog_cmds::required_permission(&command))?;
8304 let _ddl = self.ddl_lock.lock();
8305 let _security_write = self.security_write()?;
8306 self.require(&crate::catalog_cmds::required_permission(&command))?;
8307 let _commit = self.commit_lock.lock();
8308 let epoch = self.epoch.bump_assigned();
8309 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8310 let mut next_catalog = self.catalog.read().clone();
8311 self.apply_catalog_command_to(&mut next_catalog, command)?;
8312 next_catalog.db_epoch = epoch.0;
8313 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8314 Ok(epoch)
8315 }
8316
8317 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
8319 self.grant_role_with_epoch(username, role_name).map(|_| ())
8320 }
8321
8322 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8323 self.grant_role_with_epoch_inner(username, role_name, None)
8324 }
8325
8326 pub fn grant_role_with_epoch_controlled<F>(
8327 &self,
8328 username: &str,
8329 role_name: &str,
8330 mut before_publish: F,
8331 ) -> Result<Option<Epoch>>
8332 where
8333 F: FnMut() -> Result<()>,
8334 {
8335 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8336 }
8337
8338 fn grant_role_with_epoch_inner(
8339 &self,
8340 username: &str,
8341 role_name: &str,
8342 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8343 ) -> Result<Option<Epoch>> {
8344 let command = crate::catalog_cmds::CatalogCommand::GrantRole {
8345 username: username.to_string(),
8346 role: role_name.to_string(),
8347 };
8348 self.require(&crate::catalog_cmds::required_permission(&command))?;
8349 let _ddl = self.ddl_lock.lock();
8350 let _security_write = self.security_write()?;
8351 self.require(&crate::catalog_cmds::required_permission(&command))?;
8352 let _commit = self.commit_lock.lock();
8353 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8357 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8358 return Ok(None);
8359 }
8360 let epoch = self.epoch.bump_assigned();
8361 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8362 let mut next_catalog = self.catalog.read().clone();
8363 self.apply_catalog_command_to(&mut next_catalog, command)?;
8364 next_catalog.db_epoch = epoch.0;
8365 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8366 Ok(Some(epoch))
8367 }
8368
8369 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
8371 self.revoke_role_with_epoch(username, role_name).map(|_| ())
8372 }
8373
8374 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8375 self.revoke_role_with_epoch_inner(username, role_name, None)
8376 }
8377
8378 pub fn revoke_role_with_epoch_controlled<F>(
8379 &self,
8380 username: &str,
8381 role_name: &str,
8382 mut before_publish: F,
8383 ) -> Result<Option<Epoch>>
8384 where
8385 F: FnMut() -> Result<()>,
8386 {
8387 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8388 }
8389
8390 fn revoke_role_with_epoch_inner(
8391 &self,
8392 username: &str,
8393 role_name: &str,
8394 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8395 ) -> Result<Option<Epoch>> {
8396 let command = crate::catalog_cmds::CatalogCommand::RevokeRole {
8397 username: username.to_string(),
8398 role: role_name.to_string(),
8399 };
8400 self.require(&crate::catalog_cmds::required_permission(&command))?;
8401 let _ddl = self.ddl_lock.lock();
8402 let _security_write = self.security_write()?;
8403 self.require(&crate::catalog_cmds::required_permission(&command))?;
8404 let _commit = self.commit_lock.lock();
8405 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8409 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8410 return Ok(None);
8411 }
8412 let epoch = self.epoch.bump_assigned();
8413 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8414 let mut next_catalog = self.catalog.read().clone();
8415 self.apply_catalog_command_to(&mut next_catalog, command)?;
8416 next_catalog.db_epoch = epoch.0;
8417 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8418 Ok(Some(epoch))
8419 }
8420
8421 pub fn grant_permission(
8423 &self,
8424 role_name: &str,
8425 permission: crate::auth::Permission,
8426 ) -> Result<()> {
8427 self.grant_permission_with_epoch(role_name, permission)
8428 .map(|_| ())
8429 }
8430
8431 pub fn grant_permission_with_epoch(
8432 &self,
8433 role_name: &str,
8434 permission: crate::auth::Permission,
8435 ) -> Result<Option<Epoch>> {
8436 self.grant_permission_with_epoch_inner(role_name, permission, None)
8437 }
8438
8439 pub fn grant_permission_with_epoch_controlled<F>(
8440 &self,
8441 role_name: &str,
8442 permission: crate::auth::Permission,
8443 mut before_publish: F,
8444 ) -> Result<Option<Epoch>>
8445 where
8446 F: FnMut() -> Result<()>,
8447 {
8448 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8449 }
8450
8451 fn grant_permission_with_epoch_inner(
8452 &self,
8453 role_name: &str,
8454 permission: crate::auth::Permission,
8455 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8456 ) -> Result<Option<Epoch>> {
8457 let command = crate::catalog_cmds::CatalogCommand::GrantPermission {
8458 role: role_name.to_string(),
8459 permission,
8460 };
8461 self.require(&crate::catalog_cmds::required_permission(&command))?;
8462 let _ddl = self.ddl_lock.lock();
8463 let _security_write = self.security_write()?;
8464 self.require(&crate::catalog_cmds::required_permission(&command))?;
8465 let _commit = self.commit_lock.lock();
8466 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8470 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8471 return Ok(None);
8472 }
8473 let epoch = self.epoch.bump_assigned();
8474 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8475 let mut next_catalog = self.catalog.read().clone();
8476 self.apply_catalog_command_to(&mut next_catalog, command)?;
8477 next_catalog.db_epoch = epoch.0;
8478 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8479 Ok(Some(epoch))
8480 }
8481
8482 pub fn revoke_permission(
8484 &self,
8485 role_name: &str,
8486 permission: crate::auth::Permission,
8487 ) -> Result<()> {
8488 self.revoke_permission_with_epoch(role_name, permission)
8489 .map(|_| ())
8490 }
8491
8492 pub fn revoke_permission_with_epoch(
8493 &self,
8494 role_name: &str,
8495 permission: crate::auth::Permission,
8496 ) -> Result<Option<Epoch>> {
8497 self.revoke_permission_with_epoch_inner(role_name, permission, None)
8498 }
8499
8500 pub fn revoke_permission_with_epoch_controlled<F>(
8501 &self,
8502 role_name: &str,
8503 permission: crate::auth::Permission,
8504 mut before_publish: F,
8505 ) -> Result<Option<Epoch>>
8506 where
8507 F: FnMut() -> Result<()>,
8508 {
8509 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8510 }
8511
8512 fn revoke_permission_with_epoch_inner(
8513 &self,
8514 role_name: &str,
8515 permission: crate::auth::Permission,
8516 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8517 ) -> Result<Option<Epoch>> {
8518 let command = crate::catalog_cmds::CatalogCommand::RevokePermission {
8519 role: role_name.to_string(),
8520 permission,
8521 };
8522 self.require(&crate::catalog_cmds::required_permission(&command))?;
8523 let _ddl = self.ddl_lock.lock();
8524 let _security_write = self.security_write()?;
8525 self.require(&crate::catalog_cmds::required_permission(&command))?;
8526 let _commit = self.commit_lock.lock();
8527 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8531 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8532 return Ok(None);
8533 }
8534 let epoch = self.epoch.bump_assigned();
8535 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8536 let mut next_catalog = self.catalog.read().clone();
8537 self.apply_catalog_command_to(&mut next_catalog, command)?;
8538 next_catalog.db_epoch = epoch.0;
8539 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8540 Ok(Some(epoch))
8541 }
8542
8543 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
8546 let cat = self.catalog.read();
8547 Self::resolve_principal_from_catalog(&cat, username)
8548 }
8549
8550 pub fn resolve_current_principal(
8553 &self,
8554 principal: &crate::auth::Principal,
8555 ) -> Option<crate::auth::Principal> {
8556 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
8557 }
8558
8559 fn resolve_principal_from_catalog(
8564 cat: &Catalog,
8565 username: &str,
8566 ) -> Option<crate::auth::Principal> {
8567 let user = cat.users.iter().find(|u| u.username == username)?;
8568 Self::resolve_user_principal_from_catalog(cat, user)
8569 }
8570
8571 fn resolve_bound_principal_from_catalog(
8572 cat: &Catalog,
8573 principal: &crate::auth::Principal,
8574 ) -> Option<crate::auth::Principal> {
8575 let user = cat.users.iter().find(|user| {
8576 user.id == principal.user_id
8577 && user.created_epoch == principal.created_epoch
8578 && user.username == principal.username
8579 })?;
8580 Self::resolve_user_principal_from_catalog(cat, user)
8581 }
8582
8583 fn resolve_user_principal_from_catalog(
8584 cat: &Catalog,
8585 user: &crate::auth::UserEntry,
8586 ) -> Option<crate::auth::Principal> {
8587 let mut permissions = Vec::new();
8588 for role_name in &user.roles {
8589 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
8590 permissions.extend(role.permissions.iter().cloned());
8591 }
8592 }
8593 Some(crate::auth::Principal {
8594 user_id: user.id,
8595 created_epoch: user.created_epoch,
8596 username: user.username.clone(),
8597 is_admin: user.is_admin,
8598 roles: user.roles.clone(),
8599 permissions,
8600 })
8601 }
8602
8603 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
8605 match self.resolve_principal(username) {
8606 Some(p) => p.has_permission(permission),
8607 None => false,
8608 }
8609 }
8610
8611 pub fn require_auth_enabled(&self) -> bool {
8615 self.catalog.read().require_auth
8616 }
8617
8618 pub fn principal(&self) -> Option<crate::auth::Principal> {
8622 self.principal.read().clone()
8623 }
8624
8625 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
8631 if self.shared {
8632 return None;
8633 }
8634 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
8635 self.auth_state.clone(),
8636 )))
8637 }
8638
8639 pub fn refresh_principal(&self) -> Result<()> {
8653 let previous = match self.principal.read().clone() {
8654 Some(principal) => principal,
8655 None => return Ok(()),
8656 };
8657 if previous.user_id == 0 {
8661 return Ok(());
8662 }
8663 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
8664 self.refresh_security_catalog_if_stale(observed_version)?;
8665 let cat = self.catalog.read();
8666 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
8667 Some(p) => {
8668 *self.principal.write() = Some(p.clone());
8669 self.auth_state.set_principal(Some(p));
8673 Ok(())
8674 }
8675 None => Err(MongrelError::InvalidCredentials {
8676 username: previous.username,
8677 }),
8678 }
8679 }
8680
8681 pub fn security_catalog_disk_read_count(&self) -> u64 {
8684 self.security_catalog_disk_reads.load(Ordering::Relaxed)
8685 }
8686
8687 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
8699 if self.shared {
8700 return Err(MongrelError::Conflict(
8706 "enable_auth requires an exclusive Database owner; shared cores reject \
8707 auth-mode transitions"
8708 .into(),
8709 ));
8710 }
8711 let password_hash =
8712 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
8713 let scram_sha_256 =
8714 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
8715 let mysql_caching_sha2 =
8716 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
8717 let _ddl = self.ddl_lock.lock();
8718 let _security_write = self.security_write()?;
8719 let _commit = self.commit_lock.lock();
8720 let epoch = self.epoch.bump_assigned();
8721 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8722 let mut next_catalog = self.catalog.read().clone();
8723 if next_catalog.require_auth {
8724 return Err(MongrelError::InvalidArgument(
8725 "database already has require_auth enabled".into(),
8726 ));
8727 }
8728 if next_catalog
8729 .users
8730 .iter()
8731 .any(|u| u.username == admin_username)
8732 {
8733 return Err(MongrelError::InvalidArgument(format!(
8734 "user {admin_username:?} already exists"
8735 )));
8736 }
8737 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
8738 let id = next_catalog.next_user_id;
8739 next_catalog.next_user_id = id
8740 .checked_add(1)
8741 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
8742 next_catalog.users.push(crate::auth::UserEntry {
8743 id,
8744 username: admin_username.to_string(),
8745 password_hash,
8746 scram_sha_256: Some(scram_sha_256),
8747 mysql_caching_sha2: Some(mysql_caching_sha2),
8748 roles: Vec::new(),
8749 is_admin: true,
8750 created_epoch: epoch.0,
8751 });
8752 next_catalog.require_auth = true;
8753 advance_security_version(&mut next_catalog)?;
8754 next_catalog.db_epoch = epoch.0;
8755 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
8756 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
8760 let principal = crate::auth::Principal {
8761 user_id: id,
8762 created_epoch: epoch.0,
8763 username: admin_username.to_string(),
8764 is_admin: true,
8765 roles: Vec::new(),
8766 permissions: Vec::new(),
8767 };
8768 *self.principal.write() = Some(principal.clone());
8769 self.auth_state.set_principal(Some(principal));
8770 }
8771 publish
8772 }
8773
8774 pub fn disable_auth(&self) -> Result<()> {
8791 let _ddl = self.ddl_lock.lock();
8792 let _security_write = self.security_write()?;
8793 let _commit = self.commit_lock.lock();
8794 let epoch = self.epoch.bump_assigned();
8795 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8796 let mut next_catalog = self.catalog.read().clone();
8797 if !next_catalog.require_auth {
8798 return Err(MongrelError::InvalidArgument(
8799 "database does not have require_auth enabled".into(),
8800 ));
8801 }
8802 next_catalog.require_auth = false;
8803 advance_security_version(&mut next_catalog)?;
8804 next_catalog.db_epoch = epoch.0;
8805 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
8806 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
8808 *self.principal.write() = None;
8809 }
8810 publish
8811 }
8812
8813 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
8820 self.ensure_owner_process()?;
8821 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
8822 return Err(MongrelError::ReadOnlyReplica);
8823 }
8824 if self.principal.read().is_some() {
8825 self.refresh_principal().map_err(|error| match error {
8826 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
8827 error => error,
8828 })?;
8829 }
8830 if !self.catalog.read().require_auth {
8831 return Ok(());
8832 }
8833 let guard = self.principal.read();
8834 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
8835 if p.has_permission(perm) {
8836 Ok(())
8837 } else {
8838 Err(MongrelError::PermissionDenied {
8839 required: perm.clone(),
8840 principal: p.username.clone(),
8841 })
8842 }
8843 }
8844
8845 pub fn require_table(
8850 &self,
8851 table: &str,
8852 perm: crate::auth_state::RequiredPermission,
8853 ) -> Result<()> {
8854 self.require(&perm.into_permission(table))
8855 }
8856
8857 pub fn triggers(&self) -> Vec<StoredTrigger> {
8858 self.catalog
8859 .read()
8860 .triggers
8861 .iter()
8862 .map(|t| t.trigger.clone())
8863 .collect()
8864 }
8865
8866 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
8867 self.catalog
8868 .read()
8869 .triggers
8870 .iter()
8871 .find(|t| t.trigger.name == name)
8872 .map(|t| t.trigger.clone())
8873 }
8874
8875 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
8876 self.create_trigger_inner(trigger, None, None)
8877 }
8878
8879 pub fn create_trigger_controlled<F>(
8880 &self,
8881 trigger: StoredTrigger,
8882 mut before_publish: F,
8883 ) -> Result<StoredTrigger>
8884 where
8885 F: FnMut() -> Result<()>,
8886 {
8887 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
8888 }
8889
8890 pub fn create_trigger_as_controlled<F>(
8891 &self,
8892 trigger: StoredTrigger,
8893 principal: Option<&crate::auth::Principal>,
8894 mut before_publish: F,
8895 ) -> Result<StoredTrigger>
8896 where
8897 F: FnMut() -> Result<()>,
8898 {
8899 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
8900 }
8901
8902 fn create_trigger_inner(
8903 &self,
8904 mut trigger: StoredTrigger,
8905 principal: Option<&crate::auth::Principal>,
8906 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8907 ) -> Result<StoredTrigger> {
8908 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
8909 trigger: trigger.clone(),
8910 };
8911 self.require_for(
8912 principal,
8913 &crate::catalog_cmds::required_permission(&command),
8914 )?;
8915 let _g = self.ddl_lock.lock();
8916 let _security_write = self.security_write()?;
8917 self.require_for(
8918 principal,
8919 &crate::catalog_cmds::required_permission(&command),
8920 )?;
8921 trigger.validate()?;
8922 self.validate_trigger_references(&trigger)
8923 .map_err(trigger_validation_error)?;
8924 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8928 let commit_lock = Arc::clone(&self.commit_lock);
8929 let _c = commit_lock.lock();
8930 let epoch = self.epoch.bump_assigned();
8931 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8932 trigger.created_epoch = epoch.0;
8933 trigger.updated_epoch = epoch.0;
8934 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
8935 trigger: trigger.clone(),
8936 };
8937 let mut next_catalog = self.catalog.read().clone();
8938 self.apply_catalog_command_to(&mut next_catalog, command)?;
8939 next_catalog.db_epoch = epoch.0;
8940 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8941 Ok(trigger)
8942 }
8943
8944 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
8945 self.create_or_replace_trigger_inner(trigger, None, None)
8946 }
8947
8948 pub fn create_or_replace_trigger_controlled<F>(
8949 &self,
8950 trigger: StoredTrigger,
8951 mut before_publish: F,
8952 ) -> Result<StoredTrigger>
8953 where
8954 F: FnMut() -> Result<()>,
8955 {
8956 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
8957 }
8958
8959 pub fn create_or_replace_trigger_as_controlled<F>(
8960 &self,
8961 trigger: StoredTrigger,
8962 principal: Option<&crate::auth::Principal>,
8963 mut before_publish: F,
8964 ) -> Result<StoredTrigger>
8965 where
8966 F: FnMut() -> Result<()>,
8967 {
8968 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
8969 }
8970
8971 fn create_or_replace_trigger_inner(
8972 &self,
8973 trigger: StoredTrigger,
8974 principal: Option<&crate::auth::Principal>,
8975 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8976 ) -> Result<StoredTrigger> {
8977 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
8978 trigger: trigger.clone(),
8979 };
8980 self.require_for(
8981 principal,
8982 &crate::catalog_cmds::required_permission(&command),
8983 )?;
8984 let _g = self.ddl_lock.lock();
8985 let _security_write = self.security_write()?;
8986 self.require_for(
8987 principal,
8988 &crate::catalog_cmds::required_permission(&command),
8989 )?;
8990 trigger.validate()?;
8991 self.validate_trigger_references(&trigger)
8992 .map_err(trigger_validation_error)?;
8993 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8997 let commit_lock = Arc::clone(&self.commit_lock);
8998 let _c = commit_lock.lock();
8999 let epoch = self.epoch.bump_assigned();
9000 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9001 let mut next_catalog = self.catalog.read().clone();
9002 let replaced = match next_catalog
9007 .triggers
9008 .iter()
9009 .position(|t| t.trigger.name == trigger.name)
9010 {
9011 Some(idx) => next_catalog.triggers[idx]
9012 .trigger
9013 .replaced(trigger.clone(), epoch.0)?,
9014 None => {
9015 let mut next = trigger;
9016 next.created_epoch = epoch.0;
9017 next.updated_epoch = epoch.0;
9018 next
9019 }
9020 };
9021 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9022 trigger: replaced.clone(),
9023 };
9024 self.apply_catalog_command_to(&mut next_catalog, command)?;
9025 next_catalog.db_epoch = epoch.0;
9026 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9027 Ok(replaced)
9028 }
9029
9030 pub fn drop_trigger(&self, name: &str) -> Result<()> {
9031 self.drop_trigger_with_epoch(name).map(|_| ())
9032 }
9033
9034 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
9036 self.drop_triggers_with_epoch(&[name.to_string()])
9037 }
9038
9039 pub fn drop_trigger_with_epoch_controlled<F>(
9040 &self,
9041 name: &str,
9042 before_publish: F,
9043 ) -> Result<Epoch>
9044 where
9045 F: FnMut() -> Result<()>,
9046 {
9047 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
9048 }
9049
9050 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
9052 self.drop_triggers_with_epoch_inner(names, None, None)
9053 }
9054
9055 pub fn drop_triggers_with_epoch_controlled<F>(
9056 &self,
9057 names: &[String],
9058 mut before_publish: F,
9059 ) -> Result<Epoch>
9060 where
9061 F: FnMut() -> Result<()>,
9062 {
9063 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
9064 }
9065
9066 pub fn drop_triggers_with_epoch_as_controlled<F>(
9067 &self,
9068 names: &[String],
9069 principal: Option<&crate::auth::Principal>,
9070 mut before_publish: F,
9071 ) -> Result<Epoch>
9072 where
9073 F: FnMut() -> Result<()>,
9074 {
9075 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
9076 }
9077
9078 fn drop_triggers_with_epoch_inner(
9079 &self,
9080 names: &[String],
9081 principal: Option<&crate::auth::Principal>,
9082 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9083 ) -> Result<Epoch> {
9084 if names.is_empty() {
9085 return Err(MongrelError::InvalidArgument(
9086 "at least one trigger name is required".into(),
9087 ));
9088 }
9089 let commands: Vec<crate::catalog_cmds::CatalogCommand> = names
9090 .iter()
9091 .map(|name| crate::catalog_cmds::CatalogCommand::DropTrigger { name: name.clone() })
9092 .collect();
9093 for command in &commands {
9094 self.require_for(
9095 principal,
9096 &crate::catalog_cmds::required_permission(command),
9097 )?;
9098 }
9099 let _g = self.ddl_lock.lock();
9100 let _security_write = self.security_write()?;
9101 for command in &commands {
9102 self.require_for(
9103 principal,
9104 &crate::catalog_cmds::required_permission(command),
9105 )?;
9106 }
9107 {
9112 let cat = self.catalog.read();
9113 for command in &commands {
9114 crate::catalog_cmds::apply(&cat, command)?;
9115 }
9116 }
9117 let commit_lock = Arc::clone(&self.commit_lock);
9118 let _c = commit_lock.lock();
9119 let epoch = self.epoch.bump_assigned();
9120 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9121 let mut next_catalog = self.catalog.read().clone();
9122 for command in commands {
9123 self.apply_catalog_command_to(&mut next_catalog, command)?;
9124 }
9125 next_catalog.db_epoch = epoch.0;
9126 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9127 Ok(epoch)
9128 }
9129
9130 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
9131 self.catalog.read().external_tables.clone()
9132 }
9133
9134 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
9135 self.catalog
9136 .read()
9137 .external_tables
9138 .iter()
9139 .find(|t| t.name == name)
9140 .cloned()
9141 }
9142
9143 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
9144 self.create_external_table_inner(entry, None)
9145 }
9146
9147 pub fn create_external_table_controlled<F>(
9148 &self,
9149 entry: ExternalTableEntry,
9150 mut before_publish: F,
9151 ) -> Result<ExternalTableEntry>
9152 where
9153 F: FnMut() -> Result<()>,
9154 {
9155 self.create_external_table_inner(entry, Some(&mut before_publish))
9156 }
9157
9158 fn create_external_table_inner(
9159 &self,
9160 mut entry: ExternalTableEntry,
9161 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9162 ) -> Result<ExternalTableEntry> {
9163 self.require(&crate::auth::Permission::Ddl)?;
9164 let _g = self.ddl_lock.lock();
9165 let _security_write = self.security_write()?;
9166 self.require(&crate::auth::Permission::Ddl)?;
9167 entry.validate()?;
9168 {
9169 let cat = self.catalog.read();
9170 if cat.live(&entry.name).is_some()
9171 || cat.external_tables.iter().any(|t| t.name == entry.name)
9172 {
9173 return Err(MongrelError::InvalidArgument(format!(
9174 "table {:?} already exists",
9175 entry.name
9176 )));
9177 }
9178 }
9179 let commit_lock = Arc::clone(&self.commit_lock);
9180 let _c = commit_lock.lock();
9181 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
9185 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
9186 let epoch = self.epoch.bump_assigned();
9187 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9188 entry.created_epoch = epoch.0;
9189 let mut next_catalog = self.catalog.read().clone();
9190 next_catalog.external_tables.push(entry.clone());
9191 next_catalog.db_epoch = epoch.0;
9192 self.publish_catalog_candidate_with_prelude(
9193 next_catalog,
9194 epoch,
9195 &mut _epoch_guard,
9196 before_publish,
9197 vec![(
9198 EXTERNAL_TABLE_ID,
9199 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9200 name: entry.name.clone(),
9201 generation_epoch: epoch.0,
9202 }),
9203 )],
9204 )?;
9205 Ok(entry)
9206 }
9207
9208 pub fn drop_external_table(&self, name: &str) -> Result<()> {
9209 self.drop_external_table_with_epoch(name).map(|_| ())
9210 }
9211
9212 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
9214 self.drop_external_table_with_epoch_inner(name, None)
9215 }
9216
9217 pub fn drop_external_table_with_epoch_controlled<F>(
9218 &self,
9219 name: &str,
9220 mut before_publish: F,
9221 ) -> Result<Epoch>
9222 where
9223 F: FnMut() -> Result<()>,
9224 {
9225 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
9226 }
9227
9228 fn drop_external_table_with_epoch_inner(
9229 &self,
9230 name: &str,
9231 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9232 ) -> Result<Epoch> {
9233 self.require(&crate::auth::Permission::Ddl)?;
9234 let _g = self.ddl_lock.lock();
9235 let _security_write = self.security_write()?;
9236 self.require(&crate::auth::Permission::Ddl)?;
9237 let commit_lock = Arc::clone(&self.commit_lock);
9238 let _c = commit_lock.lock();
9239 let epoch = self.epoch.bump_assigned();
9240 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9241 let mut next_catalog = self.catalog.read().clone();
9242 let before = next_catalog.external_tables.len();
9243 next_catalog.external_tables.retain(|t| t.name != name);
9244 if next_catalog.external_tables.len() == before {
9245 return Err(MongrelError::NotFound(format!(
9246 "external table {name:?} not found"
9247 )));
9248 }
9249 next_catalog.db_epoch = epoch.0;
9250 self.publish_catalog_candidate_with_prelude(
9251 next_catalog,
9252 epoch,
9253 &mut _epoch_guard,
9254 before_publish,
9255 vec![(
9256 EXTERNAL_TABLE_ID,
9257 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9258 name: name.to_string(),
9259 generation_epoch: epoch.0,
9260 }),
9261 )],
9262 )?;
9263 let state_dir = self.root.join(VTAB_DIR).join(name);
9264 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
9265 return Err(MongrelError::DurableCommit {
9266 epoch: epoch.0,
9267 message: format!(
9268 "external table was dropped but connector-state cleanup failed: {error}"
9269 ),
9270 });
9271 }
9272 Ok(epoch)
9273 }
9274
9275 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
9276 let txn_id = self.alloc_txn_id()?;
9277 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9278 self.commit_transaction_with_external_states(
9279 txn_id,
9280 self.epoch.visible(),
9281 Vec::new(),
9282 vec![(name.to_string(), state.to_vec())],
9283 Vec::new(),
9284 principal,
9285 catalog_bound,
9286 None,
9287 crate::txn::TxnCommitContext::internal(),
9288 )
9289 .map(|(epoch, _)| epoch)
9290 }
9291
9292 pub fn trigger_config(&self) -> TriggerConfig {
9293 use std::sync::atomic::Ordering;
9294 TriggerConfig {
9295 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
9296 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
9297 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
9298 }
9299 }
9300
9301 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
9302 use std::sync::atomic::Ordering;
9303 if config.max_depth == 0 {
9304 return Err(MongrelError::InvalidArgument(
9305 "trigger max_depth must be greater than 0".into(),
9306 ));
9307 }
9308 self.trigger_recursive
9309 .store(config.recursive_triggers, Ordering::Relaxed);
9310 self.trigger_max_depth
9311 .store(config.max_depth, Ordering::Relaxed);
9312 self.trigger_max_loop_iterations
9313 .store(config.max_loop_iterations, Ordering::Relaxed);
9314 Ok(())
9315 }
9316
9317 pub fn set_recursive_triggers(&self, recursive: bool) {
9318 use std::sync::atomic::Ordering;
9319 self.trigger_recursive.store(recursive, Ordering::Relaxed);
9320 }
9321
9322 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
9326 self.notify.subscribe()
9327 }
9328
9329 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
9330 self.change_wake.subscribe()
9331 }
9332
9333 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
9338 let control = crate::ExecutionControl::new(None);
9339 self.change_events_since_controlled(last_event_id, &control)
9340 }
9341
9342 pub fn change_events_since_controlled(
9344 &self,
9345 last_event_id: Option<&str>,
9346 control: &crate::ExecutionControl,
9347 ) -> Result<CdcBatch> {
9348 use crate::wal::Op;
9349
9350 control.checkpoint()?;
9351 let resume = match last_event_id {
9352 Some(id) => {
9353 let (epoch, index) = id.split_once(':').ok_or_else(|| {
9354 MongrelError::InvalidArgument(format!(
9355 "invalid CDC event id {id:?}; expected <epoch>:<index>"
9356 ))
9357 })?;
9358 Some((
9359 epoch.parse::<u64>().map_err(|error| {
9360 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
9361 })?,
9362 index.parse::<u32>().map_err(|error| {
9363 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
9364 })?,
9365 ))
9366 }
9367 None => None,
9368 };
9369
9370 let mut wal = self.shared_wal.lock();
9371 wal.group_sync()?;
9372 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
9373 let records = crate::wal::SharedWal::replay_with_dek_controlled(
9374 &self.root,
9375 wal_dek.as_ref(),
9376 control,
9377 CDC_MAX_WAL_RECORDS,
9378 CDC_MAX_WAL_REPLAY_BYTES,
9379 )?;
9380 drop(wal);
9381 control.checkpoint()?;
9382
9383 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
9384 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
9385 for (index, record) in records.iter().enumerate() {
9386 if index % 256 == 0 {
9387 control.checkpoint()?;
9388 }
9389 if let Op::TxnCommit { epoch, added_runs } = &record.op {
9390 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
9391 }
9392 if let Op::SpilledRows { table_id, rows } = &record.op {
9393 spilled_payloads
9394 .entry((record.txn_id, *table_id))
9395 .or_default()
9396 .push(rows);
9397 }
9398 }
9399 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
9400 let current_epoch = self.epoch.committed().0;
9401 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
9402 let gap = resume.is_some_and(|(epoch, _)| {
9403 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
9404 });
9405 if gap {
9406 return Ok(CdcBatch {
9407 events: Vec::new(),
9408 current_epoch,
9409 earliest_epoch,
9410 gap: true,
9411 });
9412 }
9413
9414 let table_names: HashMap<u64, String> = self
9415 .catalog
9416 .read()
9417 .tables
9418 .iter()
9419 .map(|entry| (entry.table_id, entry.name.clone()))
9420 .collect();
9421 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
9422 let mut retained_bytes = 0_usize;
9423 for (index, record) in records.iter().enumerate() {
9424 if index % 256 == 0 {
9425 control.checkpoint()?;
9426 }
9427 if !commits.contains_key(&record.txn_id) {
9428 continue;
9429 }
9430 let Op::BeforeImage {
9431 table_id,
9432 row_id,
9433 row,
9434 } = &record.op
9435 else {
9436 continue;
9437 };
9438 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9439 return Err(MongrelError::ResourceLimitExceeded {
9440 resource: "CDC before-image bytes",
9441 requested: row.len(),
9442 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9443 });
9444 }
9445 let before: crate::memtable::Row = bincode::deserialize(row)?;
9446 if before_images.len() >= CDC_MAX_ROWS {
9447 return Err(MongrelError::ResourceLimitExceeded {
9448 resource: "CDC before-image rows",
9449 requested: before_images.len().saturating_add(1),
9450 limit: CDC_MAX_ROWS,
9451 });
9452 }
9453 charge_cdc_bytes(
9454 &mut retained_bytes,
9455 cdc_row_storage_bytes(&before),
9456 "CDC retained bytes",
9457 )?;
9458 before_images.insert((record.txn_id, *table_id, row_id.0), before);
9459 }
9460 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
9461 let mut events = Vec::new();
9462 let mut decoded_rows = before_images.len();
9463 for (record_index, record) in records.iter().enumerate() {
9464 if record_index % 256 == 0 {
9465 control.checkpoint()?;
9466 }
9467 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
9468 continue;
9469 };
9470 let event = match &record.op {
9471 Op::Put { table_id, rows } => {
9472 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9473 return Err(MongrelError::ResourceLimitExceeded {
9474 resource: "CDC inline row bytes",
9475 requested: rows.len(),
9476 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9477 });
9478 }
9479 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
9480 decoded_rows = decoded_rows.saturating_add(rows.len());
9481 if decoded_rows > CDC_MAX_ROWS {
9482 return Err(MongrelError::ResourceLimitExceeded {
9483 resource: "CDC decoded rows",
9484 requested: decoded_rows,
9485 limit: CDC_MAX_ROWS,
9486 });
9487 }
9488 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
9489 let mut peak_bytes = retained_bytes;
9490 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9491 let data = serde_json::to_value(rows)
9492 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
9493 Some((*table_id, "put", data, event_bytes))
9494 }
9495 Op::Delete { table_id, row_ids } => {
9496 let before = row_ids
9497 .iter()
9498 .filter_map(|row_id| {
9499 before_images
9500 .get(&(record.txn_id, *table_id, row_id.0))
9501 .cloned()
9502 })
9503 .collect::<Vec<_>>();
9504 let event_bytes = cdc_rows_json_bytes(&before)
9505 .saturating_add(
9506 row_ids
9507 .len()
9508 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
9509 )
9510 .saturating_add(512);
9511 let mut peak_bytes = retained_bytes;
9512 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9513 Some((
9514 *table_id,
9515 "delete",
9516 serde_json::json!({
9517 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
9518 "before": before,
9519 }),
9520 event_bytes,
9521 ))
9522 }
9523 Op::TruncateTable { table_id } => {
9524 Some((*table_id, "truncate", serde_json::Value::Null, 512))
9525 }
9526 _ => None,
9527 };
9528 if let Some((table_id, op, data, event_bytes)) = event {
9529 let index = operation_indices.entry(record.txn_id).or_insert(0);
9530 let event_position = (*commit_epoch, *index);
9531 *index = index.saturating_add(1);
9532 if resume.is_some_and(|position| event_position <= position) {
9533 continue;
9534 }
9535 if events.len() >= CDC_MAX_EVENTS {
9536 return Err(MongrelError::ResourceLimitExceeded {
9537 resource: "CDC events",
9538 requested: events.len().saturating_add(1),
9539 limit: CDC_MAX_EVENTS,
9540 });
9541 }
9542 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
9543 events.push(ChangeEvent {
9544 id: Some(format!("{}:{}", event_position.0, event_position.1)),
9545 channel: "changes".into(),
9546 table_id: Some(table_id),
9547 table: table_names.get(&table_id).cloned().unwrap_or_default(),
9548 op: op.into(),
9549 epoch: *commit_epoch,
9550 txn_id: Some(record.txn_id),
9551 message: None,
9552 data: Some(data),
9553 });
9554 }
9555 if let Op::TxnCommit { added_runs, .. } = &record.op {
9556 for run in added_runs {
9557 control.checkpoint()?;
9558 let index = operation_indices.entry(record.txn_id).or_insert(0);
9559 let event_position = (*commit_epoch, *index);
9560 *index = index.saturating_add(1);
9561 if resume.is_some_and(|position| event_position <= position) {
9562 continue;
9563 }
9564 let mut rows = if let Some(payloads) =
9565 spilled_payloads.get(&(record.txn_id, run.table_id))
9566 {
9567 let mut rows = Vec::new();
9568 for payload in payloads {
9569 control.checkpoint()?;
9570 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9571 return Err(MongrelError::ResourceLimitExceeded {
9572 resource: "CDC spilled row bytes",
9573 requested: payload.len(),
9574 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9575 });
9576 }
9577 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
9578 if decoded_rows
9579 .saturating_add(rows.len())
9580 .saturating_add(chunk.len())
9581 > CDC_MAX_ROWS
9582 {
9583 return Err(MongrelError::ResourceLimitExceeded {
9584 resource: "CDC decoded rows",
9585 requested: decoded_rows
9586 .saturating_add(rows.len())
9587 .saturating_add(chunk.len()),
9588 limit: CDC_MAX_ROWS,
9589 });
9590 }
9591 rows.extend(chunk);
9592 }
9593 rows
9594 } else {
9595 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
9596 return Ok(CdcBatch {
9597 events: Vec::new(),
9598 current_epoch,
9599 earliest_epoch,
9600 gap: true,
9601 });
9602 };
9603 let table = handle.lock();
9604 let mut reader = match table.open_reader(run.run_id) {
9605 Ok(reader) => reader,
9606 Err(_) => {
9607 return Ok(CdcBatch {
9608 events: Vec::new(),
9609 current_epoch,
9610 earliest_epoch,
9611 gap: true,
9612 })
9613 }
9614 };
9615 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
9616 let rows = reader.all_rows_controlled(control, remaining)?;
9617 drop(reader);
9618 drop(table);
9619 rows
9620 };
9621 for row in &mut rows {
9622 row.committed_epoch = Epoch(*commit_epoch);
9623 }
9624 decoded_rows = decoded_rows.saturating_add(rows.len());
9625 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
9626 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
9627 if events.len() >= CDC_MAX_EVENTS {
9628 return Err(MongrelError::ResourceLimitExceeded {
9629 resource: "CDC events",
9630 requested: events.len().saturating_add(1),
9631 limit: CDC_MAX_EVENTS,
9632 });
9633 }
9634 events.push(ChangeEvent {
9635 id: Some(format!("{}:{}", event_position.0, event_position.1)),
9636 channel: "changes".into(),
9637 table_id: Some(run.table_id),
9638 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
9639 op: "put_run".into(),
9640 epoch: *commit_epoch,
9641 txn_id: Some(record.txn_id),
9642 message: None,
9643 data: Some(serde_json::json!({
9644 "run_id": run.run_id.to_string(),
9645 "row_count": run.row_count,
9646 "min_row_id": run.min_row_id,
9647 "max_row_id": run.max_row_id,
9648 "rows": rows,
9649 })),
9650 });
9651 }
9652 }
9653 }
9654 control.checkpoint()?;
9655 Ok(CdcBatch {
9656 events,
9657 current_epoch,
9658 earliest_epoch,
9659 gap: false,
9660 })
9661 }
9662
9663 pub fn notify(&self, channel: &str, message: Option<String>) {
9666 let _ = self.notify.send(ChangeEvent {
9667 id: None,
9668 channel: channel.to_string(),
9669 table_id: None,
9670 table: String::new(),
9671 op: "notify".into(),
9672 epoch: self.epoch.visible().0,
9673 txn_id: None,
9674 message,
9675 data: None,
9676 });
9677 }
9678
9679 pub fn call_procedure(
9680 &self,
9681 name: &str,
9682 args: HashMap<String, crate::Value>,
9683 ) -> Result<ProcedureCallResult> {
9684 self.call_procedure_as(name, args, None)
9685 }
9686
9687 pub fn call_procedure_as(
9688 &self,
9689 name: &str,
9690 args: HashMap<String, crate::Value>,
9691 principal: Option<&crate::auth::Principal>,
9692 ) -> Result<ProcedureCallResult> {
9693 let control = crate::ExecutionControl::new(None);
9694 self.call_procedure_as_controlled(name, args, principal, &control, || true)
9695 }
9696
9697 #[doc(hidden)]
9700 pub fn call_procedure_as_bound(
9701 &self,
9702 expected: &StoredProcedure,
9703 args: HashMap<String, crate::Value>,
9704 principal: Option<&crate::auth::Principal>,
9705 ) -> Result<ProcedureCallResult> {
9706 self.require_for(principal, &crate::auth::Permission::All)?;
9707 let procedure = self.procedure(&expected.name).ok_or_else(|| {
9708 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
9709 })?;
9710 if &procedure != expected {
9711 return Err(MongrelError::Conflict(format!(
9712 "procedure {:?} changed after request authorization",
9713 expected.name
9714 )));
9715 }
9716 let control = crate::ExecutionControl::new(None);
9717 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
9718 }
9719
9720 #[doc(hidden)]
9725 pub fn call_procedure_as_controlled<F>(
9726 &self,
9727 name: &str,
9728 args: HashMap<String, crate::Value>,
9729 principal: Option<&crate::auth::Principal>,
9730 control: &crate::ExecutionControl,
9731 before_commit: F,
9732 ) -> Result<ProcedureCallResult>
9733 where
9734 F: FnOnce() -> bool,
9735 {
9736 self.require_for(principal, &crate::auth::Permission::All)?;
9740 let procedure = self
9741 .procedure(name)
9742 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
9743 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
9744 }
9745
9746 fn execute_procedure_as_controlled<F>(
9747 &self,
9748 procedure: StoredProcedure,
9749 args: HashMap<String, crate::Value>,
9750 principal: Option<&crate::auth::Principal>,
9751 control: &crate::ExecutionControl,
9752 before_commit: F,
9753 ) -> Result<ProcedureCallResult>
9754 where
9755 F: FnOnce() -> bool,
9756 {
9757 let args = bind_procedure_args(&procedure, args)?;
9758 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
9759 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
9760 if has_writes {
9761 let mut tx = self.begin_as(principal.cloned());
9762 let run = (|| {
9763 for (step_index, step) in procedure.body.steps.iter().enumerate() {
9764 if step_index % 256 == 0 {
9765 control.checkpoint()?;
9766 }
9767 let output = self.execute_procedure_step(
9768 step,
9769 &args,
9770 &outputs,
9771 Some(&mut tx),
9772 principal,
9773 Some(control),
9774 )?;
9775 outputs.insert(step.id().to_string(), output);
9776 }
9777 control.checkpoint()?;
9778 eval_return_output(&procedure.body.return_value, &args, &outputs)
9779 })();
9780 match run {
9781 Ok(output) => {
9782 control.checkpoint()?;
9783 if !before_commit() {
9784 tx.rollback();
9785 return Err(MongrelError::Cancelled);
9786 }
9787 let epoch = tx.commit()?.0;
9788 Ok(ProcedureCallResult {
9789 epoch: Some(epoch),
9790 output,
9791 })
9792 }
9793 Err(e) => {
9794 tx.rollback();
9795 Err(e)
9796 }
9797 }
9798 } else {
9799 for (step_index, step) in procedure.body.steps.iter().enumerate() {
9800 if step_index % 256 == 0 {
9801 control.checkpoint()?;
9802 }
9803 let output = self.execute_procedure_step(
9804 step,
9805 &args,
9806 &outputs,
9807 None,
9808 principal,
9809 Some(control),
9810 )?;
9811 outputs.insert(step.id().to_string(), output);
9812 }
9813 control.checkpoint()?;
9814 Ok(ProcedureCallResult {
9815 epoch: None,
9816 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
9817 })
9818 }
9819 }
9820
9821 fn execute_procedure_step(
9822 &self,
9823 step: &ProcedureStep,
9824 args: &HashMap<String, crate::Value>,
9825 outputs: &HashMap<String, ProcedureCallOutput>,
9826 tx: Option<&mut crate::txn::Transaction<'_>>,
9827 principal: Option<&crate::auth::Principal>,
9828 control: Option<&crate::ExecutionControl>,
9829 ) -> Result<ProcedureCallOutput> {
9830 if let Some(control) = control {
9831 control.checkpoint()?;
9832 }
9833 match step {
9834 ProcedureStep::NativeQuery {
9835 table,
9836 conditions,
9837 projection,
9838 limit,
9839 ..
9840 } => {
9841 let mut q = crate::Query::new();
9842 for condition in conditions {
9843 q = q.and(eval_condition(condition, args, outputs)?);
9844 }
9845 let fallback_control = crate::ExecutionControl::new(None);
9846 let query_control = control.unwrap_or(&fallback_control);
9847 let mut rows = self.query_for_principal_controlled(
9848 table,
9849 &q,
9850 projection.as_deref(),
9851 principal,
9852 false,
9853 query_control,
9854 )?;
9855 if let Some(limit) = limit {
9856 rows.truncate(*limit);
9857 }
9858 let mut output = Vec::with_capacity(rows.len());
9859 for (row_index, row) in rows.into_iter().enumerate() {
9860 if row_index % 256 == 0 {
9861 if let Some(control) = control {
9862 control.checkpoint()?;
9863 }
9864 }
9865 output.push(ProcedureCallRow {
9866 row_id: Some(row.row_id),
9867 columns: row.columns,
9868 });
9869 }
9870 Ok(ProcedureCallOutput::Rows(output))
9871 }
9872 ProcedureStep::Put {
9873 table,
9874 cells,
9875 returning,
9876 ..
9877 } => {
9878 let tx = tx.ok_or_else(|| {
9879 MongrelError::InvalidArgument(
9880 "write procedure step requires a transaction".into(),
9881 )
9882 })?;
9883 let cells = eval_cells(cells, args, outputs)?;
9884 if *returning {
9885 let out = tx.put_returning(table, cells)?;
9886 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
9887 row_id: None,
9888 columns: out.row.columns.into_iter().collect(),
9889 }))
9890 } else {
9891 tx.put(table, cells)?;
9892 Ok(ProcedureCallOutput::Null)
9893 }
9894 }
9895 ProcedureStep::Upsert {
9896 table,
9897 cells,
9898 update_cells,
9899 returning,
9900 ..
9901 } => {
9902 let tx = tx.ok_or_else(|| {
9903 MongrelError::InvalidArgument(
9904 "write procedure step requires a transaction".into(),
9905 )
9906 })?;
9907 let cells = eval_cells(cells, args, outputs)?;
9908 let action = match update_cells {
9909 Some(update_cells) => {
9910 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
9911 }
9912 None => crate::UpsertAction::DoNothing,
9913 };
9914 let out = tx.upsert(table, cells, action)?;
9915 if *returning {
9916 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
9917 row_id: None,
9918 columns: out.row.columns.into_iter().collect(),
9919 }))
9920 } else {
9921 Ok(ProcedureCallOutput::Null)
9922 }
9923 }
9924 ProcedureStep::DeleteByPk { table, pk, .. } => {
9925 let tx = tx.ok_or_else(|| {
9926 MongrelError::InvalidArgument(
9927 "write procedure step requires a transaction".into(),
9928 )
9929 })?;
9930 let pk = eval_value(pk, args, outputs)?;
9931 let handle = self.table(table)?;
9932 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
9933 MongrelError::NotFound("procedure delete_by_pk target not found".into())
9934 })?;
9935 tx.delete(table, row_id)?;
9936 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
9937 }
9938 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
9939 "DeleteRows procedure step is not supported by the core executor yet".into(),
9940 )),
9941 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
9942 "SqlQuery procedure step must be executed by mongreldb-query".into(),
9943 )),
9944 }
9945 }
9946
9947 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
9948 let cat = self.catalog.read();
9949 for step in &procedure.body.steps {
9950 let Some(table_name) = step.table() else {
9951 continue;
9952 };
9953 let schema = &cat
9954 .live(table_name)
9955 .ok_or_else(|| {
9956 MongrelError::InvalidArgument(format!(
9957 "procedure {:?} references unknown table {table_name:?}",
9958 procedure.name
9959 ))
9960 })?
9961 .schema;
9962 match step {
9963 ProcedureStep::NativeQuery {
9964 conditions,
9965 projection,
9966 ..
9967 } => {
9968 for condition in conditions {
9969 validate_condition_columns(condition, schema)?;
9970 }
9971 if let Some(projection) = projection {
9972 for id in projection {
9973 validate_column_id(*id, schema)?;
9974 }
9975 }
9976 }
9977 ProcedureStep::Put { cells, .. } => {
9978 for cell in cells {
9979 validate_column_id(cell.column_id, schema)?;
9980 }
9981 }
9982 ProcedureStep::Upsert {
9983 cells,
9984 update_cells,
9985 ..
9986 } => {
9987 for cell in cells {
9988 validate_column_id(cell.column_id, schema)?;
9989 }
9990 if let Some(update_cells) = update_cells {
9991 for cell in update_cells {
9992 validate_column_id(cell.column_id, schema)?;
9993 }
9994 }
9995 }
9996 ProcedureStep::DeleteByPk { .. } => {
9997 if schema.primary_key().is_none() {
9998 return Err(MongrelError::InvalidArgument(format!(
9999 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
10000 procedure.name
10001 )));
10002 }
10003 }
10004 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
10005 }
10006 }
10007 Ok(())
10008 }
10009
10010 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
10011 let cat = self.catalog.read();
10012 let target_schema = match &trigger.target {
10013 TriggerTarget::Table(target_name) => cat
10014 .live(target_name)
10015 .ok_or_else(|| {
10016 MongrelError::InvalidArgument(format!(
10017 "trigger {:?} references unknown target table {target_name:?}",
10018 trigger.name
10019 ))
10020 })?
10021 .schema
10022 .clone(),
10023 TriggerTarget::View(_) => Schema {
10024 columns: trigger.target_columns.clone(),
10025 ..Schema::default()
10026 },
10027 };
10028 for col in &trigger.update_of {
10029 if target_schema.column(col).is_none() {
10030 return Err(MongrelError::InvalidArgument(format!(
10031 "trigger {:?} UPDATE OF references unknown column {col:?}",
10032 trigger.name
10033 )));
10034 }
10035 }
10036 if let Some(expr) = &trigger.when {
10037 validate_trigger_expr(expr, &target_schema, trigger.event)?;
10038 }
10039 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
10040 for step in &trigger.program.steps {
10041 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
10042 {
10043 return Err(MongrelError::InvalidArgument(
10044 "SetNew trigger steps are only valid in BEFORE triggers".into(),
10045 ));
10046 }
10047 validate_trigger_step(
10048 step,
10049 &cat,
10050 &target_schema,
10051 trigger.event,
10052 &mut select_schemas,
10053 )?;
10054 }
10055 Ok(())
10056 }
10057
10058 pub fn begin(&self) -> crate::txn::Transaction<'_> {
10060 self.begin_with_isolation(crate::txn::IsolationLevel::default())
10061 }
10062
10063 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
10064 let principal = self.principal.read().clone();
10065 let catalog_bound = principal
10066 .as_ref()
10067 .is_some_and(|principal| principal.user_id != 0);
10068 (principal, catalog_bound)
10069 }
10070
10071 pub fn begin_as(
10072 &self,
10073 principal: Option<crate::auth::Principal>,
10074 ) -> crate::txn::Transaction<'_> {
10075 let catalog_bound = principal
10076 .as_ref()
10077 .is_some_and(|principal| principal.user_id != 0);
10078 let txn_id = self.alloc_txn_id();
10079 let read = Snapshot::at(self.epoch.visible());
10080 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10081 .with_principal(principal, catalog_bound)
10082 }
10083
10084 pub fn begin_with_isolation(
10086 &self,
10087 level: crate::txn::IsolationLevel,
10088 ) -> crate::txn::Transaction<'_> {
10089 let txn_id = self.alloc_txn_id();
10090 let read = Snapshot::at(self.epoch.visible());
10093 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10094 crate::txn::Transaction::new(self, txn_id, read, level)
10095 .with_principal(principal, catalog_bound)
10096 }
10097
10098 pub fn begin_with_external_trigger_bridge<'a>(
10101 &'a self,
10102 bridge: &'a dyn ExternalTriggerBridge,
10103 ) -> crate::txn::Transaction<'a> {
10104 let txn_id = self.alloc_txn_id();
10105 let read = Snapshot::at(self.epoch.visible());
10106 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10107 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10108 .with_external_trigger_bridge(bridge)
10109 .with_principal(principal, catalog_bound)
10110 }
10111
10112 pub fn begin_with_external_trigger_bridge_as<'a>(
10113 &'a self,
10114 bridge: &'a dyn ExternalTriggerBridge,
10115 principal: Option<crate::auth::Principal>,
10116 ) -> crate::txn::Transaction<'a> {
10117 let catalog_bound = principal
10118 .as_ref()
10119 .is_some_and(|principal| principal.user_id != 0);
10120 let txn_id = self.alloc_txn_id();
10121 let read = Snapshot::at(self.epoch.visible());
10122 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10123 .with_external_trigger_bridge(bridge)
10124 .with_principal(principal, catalog_bound)
10125 }
10126
10127 pub fn transaction<T>(
10129 &self,
10130 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10131 ) -> Result<T> {
10132 let mut tx = self.begin();
10133 match f(&mut tx) {
10134 Ok(out) => {
10135 tx.commit()?;
10136 Ok(out)
10137 }
10138 Err(e) => {
10139 tx.rollback();
10140 Err(e)
10141 }
10142 }
10143 }
10144
10145 pub fn transaction_with_row_ids<T>(
10146 &self,
10147 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10148 ) -> Result<(T, Vec<RowId>)> {
10149 let mut tx = self.begin();
10150 match f(&mut tx) {
10151 Ok(output) => {
10152 let (_, row_ids) = tx.commit_with_row_ids()?;
10153 Ok((output, row_ids))
10154 }
10155 Err(error) => {
10156 tx.rollback();
10157 Err(error)
10158 }
10159 }
10160 }
10161
10162 pub fn transaction_for_current_principal<T>(
10163 &self,
10164 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10165 ) -> Result<T> {
10166 if self.principal.read().is_some() {
10167 self.refresh_principal()?;
10168 }
10169 let mut transaction = self.begin_as(self.principal.read().clone());
10170 match f(&mut transaction) {
10171 Ok(output) => {
10172 transaction.commit()?;
10173 Ok(output)
10174 }
10175 Err(error) => {
10176 transaction.rollback();
10177 Err(error)
10178 }
10179 }
10180 }
10181
10182 pub fn transaction_for_current_principal_with_epoch<T>(
10183 &self,
10184 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10185 ) -> Result<(Epoch, T)> {
10186 if self.principal.read().is_some() {
10187 self.refresh_principal()?;
10188 }
10189 let mut transaction = self.begin_as(self.principal.read().clone());
10190 match f(&mut transaction) {
10191 Ok(output) => {
10192 let epoch = transaction.commit()?;
10193 Ok((epoch, output))
10194 }
10195 Err(error) => {
10196 transaction.rollback();
10197 Err(error)
10198 }
10199 }
10200 }
10201
10202 pub fn transaction_with_row_ids_for_current_principal<T>(
10203 &self,
10204 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10205 ) -> Result<(T, Vec<RowId>)> {
10206 if self.principal.read().is_some() {
10207 self.refresh_principal()?;
10208 }
10209 let mut transaction = self.begin_as(self.principal.read().clone());
10210 match f(&mut transaction) {
10211 Ok(output) => {
10212 let (_, row_ids) = transaction.commit_with_row_ids()?;
10213 Ok((output, row_ids))
10214 }
10215 Err(error) => {
10216 transaction.rollback();
10217 Err(error)
10218 }
10219 }
10220 }
10221
10222 pub fn transaction_with_external_trigger_bridge<'a, T>(
10225 &'a self,
10226 bridge: &'a dyn ExternalTriggerBridge,
10227 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10228 ) -> Result<T> {
10229 let mut tx = self.begin_with_external_trigger_bridge(bridge);
10230 match f(&mut tx) {
10231 Ok(out) => {
10232 tx.commit()?;
10233 Ok(out)
10234 }
10235 Err(e) => {
10236 tx.rollback();
10237 Err(e)
10238 }
10239 }
10240 }
10241
10242 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
10243 &'a self,
10244 bridge: &'a dyn ExternalTriggerBridge,
10245 principal: Option<crate::auth::Principal>,
10246 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10247 ) -> Result<T> {
10248 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
10249 match f(&mut tx) {
10250 Ok(output) => {
10251 tx.commit()?;
10252 Ok(output)
10253 }
10254 Err(error) => {
10255 tx.rollback();
10256 Err(error)
10257 }
10258 }
10259 }
10260
10261 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
10264 self.active_txns.register(epoch)
10265 }
10266
10267 fn fill_auto_increment_for_staging(
10268 &self,
10269 txn_id: u64,
10270 staging: &mut [(u64, crate::txn::Staged)],
10271 control: Option<&crate::ExecutionControl>,
10272 ) -> Result<()> {
10273 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
10274 for (index, (table_id, staged)) in staging.iter().enumerate() {
10275 commit_prepare_checkpoint(control, index)?;
10276 if matches!(staged, crate::txn::Staged::Put(_)) {
10277 puts_by_table.entry(*table_id).or_default().push(index);
10278 }
10279 }
10280
10281 {
10288 let mut barrier_tables: Vec<u64> = puts_by_table
10289 .iter()
10290 .filter(|(table_id, indexes)| {
10291 indexes.iter().any(|index| {
10292 matches!(
10293 &staging[*index].1,
10294 crate::txn::Staged::Put(cells)
10295 if self.table_auto_inc_would_allocate(**table_id, cells)
10296 )
10297 })
10298 })
10299 .map(|(table_id, _)| *table_id)
10300 .collect();
10301 barrier_tables.sort_unstable();
10302 for table_id in barrier_tables {
10303 self.acquire_txn_lock(
10304 txn_id,
10305 crate::locks::LockKey::sequence_barrier(&format!("auto_inc:{table_id}")),
10306 crate::locks::LockMode::Exclusive,
10307 control,
10308 )?;
10309 }
10310 }
10311
10312 let tables = self.tables.read();
10313 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
10314 commit_prepare_checkpoint(control, table_index)?;
10315 if let Some(handle) = tables.get(&table_id) {
10316 #[cfg(test)]
10317 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10318 let mut t = handle.lock();
10319 for (fill_index, index) in indexes.into_iter().enumerate() {
10320 commit_prepare_checkpoint(control, fill_index)?;
10321 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
10322 t.fill_auto_inc(cells)?;
10323 }
10324 }
10325 }
10326 }
10327 Ok(())
10328 }
10329
10330 fn expand_table_triggers(
10331 &self,
10332 txn_id: u64,
10333 staging: &mut Vec<(u64, crate::txn::Staged)>,
10334 read_epoch: Epoch,
10335 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
10336 external_states: &mut Vec<(String, Vec<u8>)>,
10337 control: Option<&crate::ExecutionControl>,
10338 ) -> Result<()> {
10339 commit_prepare_checkpoint(control, 0)?;
10340 let mut external_writes = Vec::new();
10341 let config = self.trigger_config();
10342 if config.recursive_triggers {
10343 let chunk = std::mem::take(staging);
10344 let stacks = vec![Vec::new(); chunk.len()];
10345 *staging = self.expand_trigger_chunk(
10346 txn_id,
10347 chunk,
10348 stacks,
10349 read_epoch,
10350 0,
10351 config.max_depth,
10352 &mut external_writes,
10353 &config,
10354 control,
10355 )?;
10356 self.apply_external_trigger_writes(
10357 external_writes,
10358 external_trigger_bridge,
10359 external_states,
10360 staging,
10361 control,
10362 )?;
10363 return Ok(());
10364 }
10365
10366 let mut expansion =
10367 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
10368 if !expansion.before.is_empty() {
10369 let mut final_staging = expansion.before;
10370 final_staging.extend(filter_ignored_staging(
10371 std::mem::take(staging),
10372 &expansion.ignored_indices,
10373 ));
10374 *staging = final_staging;
10375 } else if !expansion.ignored_indices.is_empty() {
10376 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
10377 }
10378 staging.append(&mut expansion.after);
10379 external_writes.append(&mut expansion.before_external);
10380 external_writes.append(&mut expansion.after_external);
10381 self.apply_external_trigger_writes(
10382 external_writes,
10383 external_trigger_bridge,
10384 external_states,
10385 staging,
10386 control,
10387 )?;
10388 Ok(())
10389 }
10390
10391 #[allow(clippy::too_many_arguments)]
10392 fn expand_trigger_chunk(
10393 &self,
10394 txn_id: u64,
10395 mut chunk: Vec<(u64, crate::txn::Staged)>,
10396 stacks: Vec<Vec<String>>,
10397 read_epoch: Epoch,
10398 depth: u32,
10399 max_depth: u32,
10400 external_writes: &mut Vec<ExternalTriggerWrite>,
10401 config: &TriggerConfig,
10402 control: Option<&crate::ExecutionControl>,
10403 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
10404 if chunk.is_empty() {
10405 return Ok(Vec::new());
10406 }
10407 commit_prepare_checkpoint(control, 0)?;
10408 self.fill_auto_increment_for_staging(txn_id, &mut chunk, control)?;
10409 let expansion = self.expand_table_triggers_once(
10410 &mut chunk,
10411 read_epoch,
10412 Some(&stacks),
10413 config,
10414 control,
10415 )?;
10416 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
10417 let stack = expansion
10418 .before_stacks
10419 .first()
10420 .or_else(|| expansion.after_stacks.first())
10421 .cloned()
10422 .unwrap_or_default();
10423 return Err(MongrelError::TriggerValidation(format!(
10424 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
10425 Self::format_trigger_stack(&stack)
10426 )));
10427 }
10428
10429 let mut out = Vec::new();
10430 external_writes.extend(expansion.before_external);
10431 out.extend(self.expand_trigger_chunk(
10432 txn_id,
10433 expansion.before,
10434 expansion.before_stacks,
10435 read_epoch,
10436 depth + 1,
10437 max_depth,
10438 external_writes,
10439 config,
10440 control,
10441 )?);
10442 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
10443 external_writes.extend(expansion.after_external);
10444 out.extend(self.expand_trigger_chunk(
10445 txn_id,
10446 expansion.after,
10447 expansion.after_stacks,
10448 read_epoch,
10449 depth + 1,
10450 max_depth,
10451 external_writes,
10452 config,
10453 control,
10454 )?);
10455 Ok(out)
10456 }
10457
10458 fn apply_external_trigger_writes(
10459 &self,
10460 writes: Vec<ExternalTriggerWrite>,
10461 bridge: Option<&dyn ExternalTriggerBridge>,
10462 external_states: &mut Vec<(String, Vec<u8>)>,
10463 staging: &mut Vec<(u64, crate::txn::Staged)>,
10464 control: Option<&crate::ExecutionControl>,
10465 ) -> Result<()> {
10466 if writes.is_empty() {
10467 return Ok(());
10468 }
10469 let bridge = bridge.ok_or_else(|| {
10470 MongrelError::TriggerValidation(
10471 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
10472 )
10473 })?;
10474 for (write_index, write) in writes.into_iter().enumerate() {
10475 commit_prepare_checkpoint(control, write_index)?;
10476 let table = write.table().to_string();
10477 let entry = self.external_table(&table).ok_or_else(|| {
10478 MongrelError::NotFound(format!("external table {table:?} not found"))
10479 })?;
10480 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
10481 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
10482 external_states.push((table, result.state));
10483 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
10484 commit_prepare_checkpoint(control, base_index)?;
10485 match base_write {
10486 ExternalTriggerBaseWrite::Put { table, cells } => {
10487 let table_id = self.table_id(&table)?;
10488 staging.push((table_id, crate::txn::Staged::Put(cells)));
10489 }
10490 ExternalTriggerBaseWrite::Delete { table, row_id } => {
10491 let table_id = self.table_id(&table)?;
10492 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
10493 }
10494 }
10495 }
10496 }
10497 dedup_external_states_in_place(external_states);
10498 Ok(())
10499 }
10500
10501 fn expand_table_triggers_once(
10502 &self,
10503 staging: &mut Vec<(u64, crate::txn::Staged)>,
10504 read_epoch: Epoch,
10505 trigger_stacks: Option<&[Vec<String>]>,
10506 config: &TriggerConfig,
10507 control: Option<&crate::ExecutionControl>,
10508 ) -> Result<TriggerExpansion> {
10509 commit_prepare_checkpoint(control, 0)?;
10510 let triggers: Vec<StoredTrigger> = self
10511 .catalog
10512 .read()
10513 .triggers
10514 .iter()
10515 .filter(|entry| {
10516 entry.trigger.enabled
10517 && matches!(
10518 entry.trigger.timing,
10519 TriggerTiming::Before | TriggerTiming::After
10520 )
10521 && matches!(entry.trigger.target, TriggerTarget::Table(_))
10522 })
10523 .map(|entry| entry.trigger.clone())
10524 .collect();
10525 if triggers.is_empty() || staging.is_empty() {
10526 return Ok(TriggerExpansion::default());
10527 }
10528
10529 let before_triggers = triggers
10530 .iter()
10531 .filter(|trigger| trigger.timing == TriggerTiming::Before)
10532 .cloned()
10533 .collect::<Vec<_>>();
10534 let after_triggers = triggers
10535 .iter()
10536 .filter(|trigger| trigger.timing == TriggerTiming::After)
10537 .cloned()
10538 .collect::<Vec<_>>();
10539
10540 let mut before_added = Vec::new();
10541 let mut before_stacks = Vec::new();
10542 let mut before_external = Vec::new();
10543 let mut ignored_indices = std::collections::BTreeSet::new();
10544 if !before_triggers.is_empty() {
10545 let before_events =
10546 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
10547 let mut out = TriggerProgramOutput {
10548 added: &mut before_added,
10549 added_stacks: &mut before_stacks,
10550 added_external: &mut before_external,
10551 ignored_indices: &mut ignored_indices,
10552 };
10553 self.execute_triggers_for_events(
10554 &before_triggers,
10555 &before_events,
10556 Some(staging),
10557 &mut out,
10558 config,
10559 read_epoch,
10560 control,
10561 )?;
10562 }
10563
10564 let after_events = if after_triggers.is_empty() {
10565 Vec::new()
10566 } else {
10567 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
10568 .into_iter()
10569 .filter(|event| {
10570 !event
10571 .op_indices
10572 .iter()
10573 .any(|idx| ignored_indices.contains(idx))
10574 })
10575 .collect()
10576 };
10577
10578 let mut after_added = Vec::new();
10579 let mut after_stacks = Vec::new();
10580 let mut after_external = Vec::new();
10581 let mut out = TriggerProgramOutput {
10582 added: &mut after_added,
10583 added_stacks: &mut after_stacks,
10584 added_external: &mut after_external,
10585 ignored_indices: &mut ignored_indices,
10586 };
10587 self.execute_triggers_for_events(
10588 &after_triggers,
10589 &after_events,
10590 None,
10591 &mut out,
10592 config,
10593 read_epoch,
10594 control,
10595 )?;
10596 Ok(TriggerExpansion {
10597 before: before_added,
10598 before_stacks,
10599 before_external,
10600 after: after_added,
10601 after_stacks,
10602 after_external,
10603 ignored_indices,
10604 })
10605 }
10606
10607 #[allow(clippy::too_many_arguments)]
10608 fn execute_triggers_for_events(
10609 &self,
10610 triggers: &[StoredTrigger],
10611 events: &[WriteEvent],
10612 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
10613 out: &mut TriggerProgramOutput<'_>,
10614 config: &TriggerConfig,
10615 read_epoch: Epoch,
10616 control: Option<&crate::ExecutionControl>,
10617 ) -> Result<()> {
10618 let mut checkpoint_index = 0_usize;
10619 for event in events {
10620 for trigger in triggers {
10621 commit_prepare_checkpoint(control, checkpoint_index)?;
10622 checkpoint_index += 1;
10623 if event
10624 .op_indices
10625 .iter()
10626 .any(|idx| out.ignored_indices.contains(idx))
10627 {
10628 break;
10629 }
10630 let matches = {
10631 let cat = self.catalog.read();
10632 trigger_matches_event(trigger, event, &cat)?
10633 };
10634 if !matches {
10635 continue;
10636 }
10637 if let Some(when) = &trigger.when {
10638 if !eval_trigger_expr(when, event)? {
10639 continue;
10640 }
10641 }
10642 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
10643 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
10644 return Err(MongrelError::TriggerValidation(format!(
10645 "trigger recursion cycle detected; trigger stack: {}",
10646 Self::format_trigger_stack(&trigger_stack)
10647 )));
10648 }
10649 let outcome = match staging.as_mut() {
10650 Some(staging) => self.execute_trigger_program(
10651 trigger,
10652 event,
10653 Some(&mut **staging),
10654 out,
10655 &trigger_stack,
10656 config,
10657 read_epoch,
10658 control,
10659 )?,
10660 None => self.execute_trigger_program(
10661 trigger,
10662 event,
10663 None,
10664 out,
10665 &trigger_stack,
10666 config,
10667 read_epoch,
10668 control,
10669 )?,
10670 };
10671 if outcome == TriggerProgramOutcome::Ignore {
10672 out.ignored_indices.extend(event.op_indices.iter().copied());
10673 break;
10674 }
10675 }
10676 }
10677 Ok(())
10678 }
10679
10680 fn trigger_events_for_staging(
10681 &self,
10682 staging: &[(u64, crate::txn::Staged)],
10683 read_epoch: Epoch,
10684 trigger_stacks: Option<&[Vec<String>]>,
10685 control: Option<&crate::ExecutionControl>,
10686 ) -> Result<Vec<WriteEvent>> {
10687 use crate::txn::Staged;
10688 use std::collections::{HashMap, VecDeque};
10689
10690 let snapshot = Snapshot::at(read_epoch);
10691 let cat = self.catalog.read();
10692 let mut table_names = HashMap::new();
10693 let mut table_schemas = HashMap::new();
10694 for entry in cat
10695 .tables
10696 .iter()
10697 .filter(|entry| matches!(entry.state, TableState::Live))
10698 {
10699 table_names.insert(entry.table_id, entry.name.clone());
10700 table_schemas.insert(entry.table_id, entry.schema.clone());
10701 }
10702 drop(cat);
10703
10704 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
10705 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
10706 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
10707
10708 for (idx, (table_id, staged)) in staging.iter().enumerate() {
10709 commit_prepare_checkpoint(control, idx)?;
10710 let Some(schema) = table_schemas.get(table_id) else {
10711 continue;
10712 };
10713 let Some(pk) = schema.primary_key() else {
10714 continue;
10715 };
10716 match staged {
10717 Staged::Delete(row_id) => {
10718 let handle = self.table_by_id(*table_id)?;
10719 let Some(row) = handle.lock().get(*row_id, snapshot) else {
10720 continue;
10721 };
10722 let Some(pk_value) = row.columns.get(&pk.id) else {
10723 continue;
10724 };
10725 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
10726 delete_by_key
10727 .entry((*table_id, pk_value.encode_key()))
10728 .or_default()
10729 .push_back(idx);
10730 }
10731 Staged::Put(cells) => {
10732 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
10733 put_by_key
10734 .entry((*table_id, value.encode_key()))
10735 .or_default()
10736 .push_back(idx);
10737 }
10738 }
10739 Staged::Update { row_id, .. } => {
10740 let handle = self.table_by_id(*table_id)?;
10741 let row = handle.lock().get(*row_id, snapshot);
10742 if let Some(row) = row {
10743 old_rows.insert(idx, TriggerRowImage::from_row(row));
10744 }
10745 }
10746 Staged::Truncate => {}
10747 }
10748 }
10749
10750 let mut paired_delete = std::collections::HashSet::new();
10751 let mut paired_put = std::collections::HashSet::new();
10752 let mut events = Vec::new();
10753
10754 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
10755 commit_prepare_checkpoint(control, pair_index)?;
10756 let Some(puts) = put_by_key.get_mut(key) else {
10757 continue;
10758 };
10759 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
10760 paired_delete.insert(delete_idx);
10761 paired_put.insert(put_idx);
10762 let (table_id, _) = &staging[put_idx];
10763 let Some(table_name) = table_names.get(table_id).cloned() else {
10764 continue;
10765 };
10766 let old = old_rows.get(&delete_idx).cloned();
10767 let new = match &staging[put_idx].1 {
10768 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
10769 _ => None,
10770 };
10771 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
10772 events.push(WriteEvent {
10773 table: table_name,
10774 kind: TriggerEvent::Update,
10775 old,
10776 new,
10777 changed_columns,
10778 op_indices: vec![delete_idx, put_idx],
10779 put_idx: Some(put_idx),
10780 trigger_stack: Self::trigger_stack_for_indices(
10781 trigger_stacks,
10782 &[delete_idx, put_idx],
10783 ),
10784 });
10785 }
10786 }
10787
10788 for (idx, (table_id, staged)) in staging.iter().enumerate() {
10789 commit_prepare_checkpoint(control, idx)?;
10790 let Some(table_name) = table_names.get(table_id).cloned() else {
10791 continue;
10792 };
10793 match staged {
10794 Staged::Put(cells) if !paired_put.contains(&idx) => {
10795 let new = Some(TriggerRowImage::from_cells(cells));
10796 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
10797 events.push(WriteEvent {
10798 table: table_name,
10799 kind: TriggerEvent::Insert,
10800 old: None,
10801 new,
10802 changed_columns,
10803 op_indices: vec![idx],
10804 put_idx: Some(idx),
10805 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
10806 });
10807 }
10808 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
10809 let old = match old_rows.get(&idx).cloned() {
10810 Some(old) => Some(old),
10811 None => {
10812 let handle = self.table_by_id(*table_id)?;
10813 let row = handle.lock().get(*row_id, snapshot);
10814 row.map(TriggerRowImage::from_row)
10815 }
10816 };
10817 let Some(old) = old else {
10818 continue;
10819 };
10820 let changed_columns = old.columns.keys().copied().collect();
10821 events.push(WriteEvent {
10822 table: table_name,
10823 kind: TriggerEvent::Delete,
10824 old: Some(old),
10825 new: None,
10826 changed_columns,
10827 op_indices: vec![idx],
10828 put_idx: None,
10829 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
10830 });
10831 }
10832 Staged::Update { new_row: cells, .. } => {
10833 let old = old_rows.get(&idx).cloned();
10834 let new = Some(TriggerRowImage::from_cells(cells));
10835 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
10836 events.push(WriteEvent {
10837 table: table_name,
10838 kind: TriggerEvent::Update,
10839 old,
10840 new,
10841 changed_columns,
10842 op_indices: vec![idx],
10843 put_idx: Some(idx),
10844 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
10845 });
10846 }
10847 Staged::Truncate => {}
10848 _ => {}
10849 }
10850 }
10851
10852 Ok(events)
10853 }
10854
10855 #[allow(clippy::too_many_arguments)]
10856 fn execute_trigger_program(
10857 &self,
10858 trigger: &StoredTrigger,
10859 event: &WriteEvent,
10860 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
10861 out: &mut TriggerProgramOutput<'_>,
10862 trigger_stack: &[String],
10863 config: &TriggerConfig,
10864 read_epoch: Epoch,
10865 control: Option<&crate::ExecutionControl>,
10866 ) -> Result<TriggerProgramOutcome> {
10867 let mut event = event.clone();
10868 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
10869 self.execute_trigger_steps(
10870 trigger,
10871 &trigger.program.steps,
10872 &mut event,
10873 staging,
10874 out,
10875 trigger_stack,
10876 config,
10877 &mut select_results,
10878 0,
10879 None,
10880 read_epoch,
10881 control,
10882 )
10883 }
10884
10885 #[allow(clippy::too_many_arguments)]
10886 fn execute_trigger_steps(
10887 &self,
10888 trigger: &StoredTrigger,
10889 steps: &[TriggerStep],
10890 event: &mut WriteEvent,
10891 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
10892 out: &mut TriggerProgramOutput<'_>,
10893 trigger_stack: &[String],
10894 config: &TriggerConfig,
10895 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
10896 depth: u32,
10897 selected: Option<&TriggerRowImage>,
10898 read_epoch: Epoch,
10899 control: Option<&crate::ExecutionControl>,
10900 ) -> Result<TriggerProgramOutcome> {
10901 let _ = depth;
10902 for (step_index, step) in steps.iter().enumerate() {
10903 commit_prepare_checkpoint(control, step_index)?;
10904 match step {
10905 TriggerStep::SetNew { cells } => {
10906 if trigger.timing != TriggerTiming::Before {
10907 return Err(MongrelError::InvalidArgument(
10908 "SetNew trigger step is only valid in BEFORE triggers".into(),
10909 ));
10910 }
10911 let put_idx = event.put_idx.ok_or_else(|| {
10912 MongrelError::InvalidArgument(
10913 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
10914 )
10915 })?;
10916 let staging = staging.as_deref_mut().ok_or_else(|| {
10917 MongrelError::InvalidArgument(
10918 "SetNew trigger step requires mutable trigger staging".into(),
10919 )
10920 })?;
10921 let mut update_changed_columns = None;
10922 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
10923 Some(crate::txn::Staged::Put(cells)) => cells,
10924 Some(crate::txn::Staged::Update {
10925 new_row,
10926 changed_columns,
10927 ..
10928 }) => {
10929 update_changed_columns = Some(changed_columns);
10930 new_row
10931 }
10932 _ => {
10933 return Err(MongrelError::InvalidArgument(
10934 "SetNew trigger step target row is not mutable".into(),
10935 ))
10936 }
10937 };
10938 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
10939 row_cells.retain(|(id, _)| *id != column_id);
10940 row_cells.push((column_id, value.clone()));
10941 if let Some(changed_columns) = &mut update_changed_columns {
10942 changed_columns.push(column_id);
10943 }
10944 if let Some(new) = &mut event.new {
10945 new.columns.insert(column_id, value);
10946 }
10947 }
10948 row_cells.sort_by_key(|(id, _)| *id);
10949 if let Some(changed_columns) = update_changed_columns {
10950 changed_columns.sort_unstable();
10951 changed_columns.dedup();
10952 }
10953 }
10954 TriggerStep::Insert { table, cells } => {
10955 let cells = eval_trigger_cells(cells, event, selected)?;
10956 if let Ok(table_id) = self.table_id(table) {
10957 out.added.push((table_id, crate::txn::Staged::Put(cells)));
10958 out.added_stacks.push(trigger_stack.to_vec());
10959 } else if self.external_table(table).is_some() {
10960 out.added_external.push(ExternalTriggerWrite::Insert {
10961 table: table.clone(),
10962 cells,
10963 });
10964 } else {
10965 return Err(MongrelError::NotFound(format!(
10966 "trigger {:?} insert target {table:?} not found",
10967 trigger.name
10968 )));
10969 }
10970 }
10971 TriggerStep::UpdateByPk { table, pk, cells } => {
10972 let pk = eval_trigger_value(pk, event, selected)?;
10973 let cells = eval_trigger_cells(cells, event, selected)?;
10974 if self.external_table(table).is_some() {
10975 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
10976 table: table.clone(),
10977 pk,
10978 cells,
10979 });
10980 } else {
10981 let row_id = self
10982 .table(table)?
10983 .lock()
10984 .lookup_pk(&pk.encode_key())
10985 .ok_or_else(|| {
10986 MongrelError::NotFound(format!(
10987 "trigger {:?} update target not found",
10988 trigger.name
10989 ))
10990 })?;
10991 let handle = self.table(table)?;
10992 let snapshot = Snapshot::at(self.epoch.visible());
10993 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
10994 MongrelError::NotFound(format!(
10995 "trigger {:?} update target not visible",
10996 trigger.name
10997 ))
10998 })?;
10999 let mut changed_columns = cells
11000 .iter()
11001 .map(|(column_id, _)| *column_id)
11002 .collect::<Vec<_>>();
11003 changed_columns.sort_unstable();
11004 changed_columns.dedup();
11005 let mut merged = old.columns;
11006 for (column_id, value) in cells {
11007 merged.insert(column_id, value);
11008 }
11009 out.added.push((
11010 self.table_id(table)?,
11011 crate::txn::Staged::Update {
11012 row_id,
11013 new_row: merged.into_iter().collect(),
11014 changed_columns,
11015 },
11016 ));
11017 out.added_stacks.push(trigger_stack.to_vec());
11018 }
11019 }
11020 TriggerStep::DeleteByPk { table, pk } => {
11021 let pk = eval_trigger_value(pk, event, selected)?;
11022 if self.external_table(table).is_some() {
11023 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
11024 table: table.clone(),
11025 pk,
11026 });
11027 } else {
11028 let row_id = self
11029 .table(table)?
11030 .lock()
11031 .lookup_pk(&pk.encode_key())
11032 .ok_or_else(|| {
11033 MongrelError::NotFound(format!(
11034 "trigger {:?} delete target not found",
11035 trigger.name
11036 ))
11037 })?;
11038 out.added
11039 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
11040 out.added_stacks.push(trigger_stack.to_vec());
11041 }
11042 }
11043 TriggerStep::Select {
11044 id,
11045 table,
11046 conditions,
11047 } => {
11048 let schema = self.table(table)?.lock().schema().clone();
11049 let snapshot = Snapshot::at(read_epoch);
11050 let handle = self.table(table)?;
11051 let rows = match control {
11052 Some(control) => {
11053 handle.lock().visible_rows_controlled(snapshot, control)?
11054 }
11055 None => handle.lock().visible_rows(snapshot)?,
11056 };
11057 let mut matched = Vec::new();
11058 for (row_index, row) in rows.into_iter().enumerate() {
11059 commit_prepare_checkpoint(control, row_index)?;
11060 let image = TriggerRowImage::from_row(row);
11061 let passes = conditions
11062 .iter()
11063 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11064 .collect::<Result<Vec<_>>>()?
11065 .into_iter()
11066 .all(|b| b);
11067 if passes {
11068 matched.push(image);
11069 }
11070 }
11071 if let Some(pk) = schema.primary_key() {
11072 matched.sort_by(|a, b| {
11073 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
11074 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
11075 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
11076 });
11077 }
11078 select_results.insert(id.clone(), matched);
11079 }
11080 TriggerStep::Foreach { id, steps } => {
11081 let rows = select_results.get(id).ok_or_else(|| {
11082 MongrelError::InvalidArgument(format!(
11083 "trigger {:?} foreach references unknown select id {id:?}",
11084 trigger.name
11085 ))
11086 })?;
11087 if rows.len() > config.max_loop_iterations as usize {
11088 return Err(MongrelError::InvalidArgument(format!(
11089 "trigger {:?} foreach exceeded max_loop_iterations ({})",
11090 trigger.name, config.max_loop_iterations
11091 )));
11092 }
11093 for (row_index, row) in rows.clone().into_iter().enumerate() {
11094 commit_prepare_checkpoint(control, row_index)?;
11095 let result = self.execute_trigger_steps(
11096 trigger,
11097 steps,
11098 event,
11099 staging.as_deref_mut(),
11100 out,
11101 trigger_stack,
11102 config,
11103 select_results,
11104 depth + 1,
11105 Some(&row),
11106 read_epoch,
11107 control,
11108 )?;
11109 if result == TriggerProgramOutcome::Ignore {
11110 return Ok(TriggerProgramOutcome::Ignore);
11111 }
11112 }
11113 }
11114 TriggerStep::DeleteWhere { table, conditions } => {
11115 let schema = self.table(table)?.lock().schema().clone();
11116 let snapshot = Snapshot::at(read_epoch);
11117 let handle = self.table(table)?;
11118 let rows = match control {
11119 Some(control) => {
11120 handle.lock().visible_rows_controlled(snapshot, control)?
11121 }
11122 None => handle.lock().visible_rows(snapshot)?,
11123 };
11124 let table_id = self.table_id(table)?;
11125 let mut to_delete = Vec::new();
11126 for (row_index, row) in rows.into_iter().enumerate() {
11127 commit_prepare_checkpoint(control, row_index)?;
11128 let image = TriggerRowImage::from_row(row.clone());
11129 let passes = conditions
11130 .iter()
11131 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11132 .collect::<Result<Vec<_>>>()?
11133 .into_iter()
11134 .all(|b| b);
11135 if passes {
11136 to_delete.push((table_id, row.row_id));
11137 }
11138 }
11139 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
11140 commit_prepare_checkpoint(control, row_index)?;
11141 out.added
11142 .push((table_id, crate::txn::Staged::Delete(row_id)));
11143 out.added_stacks.push(trigger_stack.to_vec());
11144 }
11145 }
11146 TriggerStep::UpdateWhere {
11147 table,
11148 conditions,
11149 cells,
11150 } => {
11151 let schema = self.table(table)?.lock().schema().clone();
11152 let snapshot = Snapshot::at(read_epoch);
11153 let handle = self.table(table)?;
11154 let rows = match control {
11155 Some(control) => {
11156 handle.lock().visible_rows_controlled(snapshot, control)?
11157 }
11158 None => handle.lock().visible_rows(snapshot)?,
11159 };
11160 let table_id = self.table_id(table)?;
11161 let mut changed_columns =
11162 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
11163 changed_columns.sort_unstable();
11164 changed_columns.dedup();
11165 let mut to_update = Vec::new();
11166 for (row_index, row) in rows.into_iter().enumerate() {
11167 commit_prepare_checkpoint(control, row_index)?;
11168 let image = TriggerRowImage::from_row(row.clone());
11169 let passes = conditions
11170 .iter()
11171 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11172 .collect::<Result<Vec<_>>>()?
11173 .into_iter()
11174 .all(|b| b);
11175 if passes {
11176 let new_cells = cells
11177 .iter()
11178 .map(|cell| {
11179 Ok((
11180 cell.column_id,
11181 eval_trigger_value(&cell.value, event, Some(&image))?,
11182 ))
11183 })
11184 .collect::<Result<Vec<_>>>()?;
11185 let mut merged = row.columns.clone();
11186 for (column_id, value) in new_cells {
11187 merged.insert(column_id, value);
11188 }
11189 to_update.push((table_id, row.row_id, merged));
11190 }
11191 }
11192 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
11193 {
11194 commit_prepare_checkpoint(control, row_index)?;
11195 out.added.push((
11196 table_id,
11197 crate::txn::Staged::Update {
11198 row_id,
11199 new_row: merged.into_iter().collect(),
11200 changed_columns: changed_columns.clone(),
11201 },
11202 ));
11203 out.added_stacks.push(trigger_stack.to_vec());
11204 }
11205 }
11206 TriggerStep::Raise { action, message } => match action {
11207 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
11208 TriggerRaiseAction::Abort
11209 | TriggerRaiseAction::Fail
11210 | TriggerRaiseAction::Rollback => {
11211 let message = eval_trigger_value(message, event, selected)?;
11212 return Err(MongrelError::TriggerValidation(format!(
11213 "trigger {:?} raised: {}; trigger stack: {}",
11214 trigger.name,
11215 trigger_message(message),
11216 Self::format_trigger_stack(trigger_stack)
11217 )));
11218 }
11219 },
11220 }
11221 }
11222 Ok(TriggerProgramOutcome::Continue)
11223 }
11224
11225 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
11226 let Some(stacks) = stacks else {
11227 return Vec::new();
11228 };
11229 let mut out = Vec::new();
11230 for idx in indices {
11231 let Some(stack) = stacks.get(*idx) else {
11232 continue;
11233 };
11234 for name in stack {
11235 if !out.iter().any(|existing| existing == name) {
11236 out.push(name.clone());
11237 }
11238 }
11239 }
11240 out
11241 }
11242
11243 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
11244 let mut out = stack.to_vec();
11245 out.push(trigger_name.to_string());
11246 out
11247 }
11248
11249 fn format_trigger_stack(stack: &[String]) -> String {
11250 if stack.is_empty() {
11251 "<root>".into()
11252 } else {
11253 stack.join(" -> ")
11254 }
11255 }
11256
11257 fn acquire_unique_key_claims(
11278 &self,
11279 txn_id: u64,
11280 staging: &[(u64, crate::txn::Staged)],
11281 control: Option<&crate::ExecutionControl>,
11282 ) -> Result<()> {
11283 let catalog = self.catalog.read();
11284 let has_uniques = staging.iter().any(|(table_id, staged)| {
11285 matches!(
11286 staged,
11287 crate::txn::Staged::Put(_) | crate::txn::Staged::Update { .. }
11288 ) && catalog.tables.iter().any(|entry| {
11289 entry.table_id == *table_id
11290 && (entry.schema.primary_key().is_some()
11291 || !entry.schema.constraints.uniques.is_empty())
11292 })
11293 });
11294 if !has_uniques {
11295 return Ok(());
11296 }
11297 let mut claims: Vec<(u64, Vec<u8>)> = Vec::new();
11298 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11299 commit_prepare_checkpoint(control, staged_index)?;
11300 let cells = match staged {
11301 crate::txn::Staged::Put(cells) => cells,
11302 crate::txn::Staged::Update { new_row, .. } => new_row,
11303 _ => continue,
11304 };
11305 let Some(entry) = catalog
11306 .tables
11307 .iter()
11308 .find(|entry| entry.table_id == *table_id)
11309 else {
11310 continue;
11311 };
11312 for column in &entry.schema.columns {
11313 if !column
11314 .flags
11315 .contains(crate::schema::ColumnFlags::PRIMARY_KEY)
11316 {
11317 continue;
11318 }
11319 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == column.id) {
11320 let mut key = b"pk:".to_vec();
11321 key.extend_from_slice(&value.encode_key());
11322 claims.push((*table_id, key));
11323 }
11324 }
11325 let cells_map: HashMap<u16, Value> = cells.iter().cloned().collect();
11330 for uc in &entry.schema.constraints.uniques {
11331 if let Some(composite) =
11332 crate::constraint::encode_composite_key(&uc.columns, &cells_map)
11333 {
11334 let mut key = format!("uq{}:", uc.id).into_bytes();
11335 key.extend_from_slice(&composite);
11336 claims.push((*table_id, key));
11337 }
11338 }
11339 }
11340 claims.sort();
11341 claims.dedup();
11342 for (table_id, key) in claims {
11343 self.acquire_txn_lock(
11344 txn_id,
11345 crate::locks::LockKey::key(table_id, key),
11346 crate::locks::LockMode::Exclusive,
11347 control,
11348 )?;
11349 }
11350 Ok(())
11351 }
11352
11353 fn acquire_fk_lock(
11357 &self,
11358 txn_id: u64,
11359 table_id: u64,
11360 key: &[u8],
11361 mode: crate::locks::LockMode,
11362 control: Option<&crate::ExecutionControl>,
11363 ) -> Result<()> {
11364 let mut namespaced = b"fk:".to_vec();
11365 namespaced.extend_from_slice(key);
11366 self.acquire_txn_lock(
11367 txn_id,
11368 crate::locks::LockKey::key(table_id, namespaced),
11369 mode,
11370 control,
11371 )?;
11372 let hook = self.fk_lock_hook.lock().clone();
11375 if let Some(hook) = hook {
11376 hook();
11377 }
11378 Ok(())
11379 }
11380
11381 fn validate_constraints(
11382 &self,
11383 txn_id: u64,
11384 staging: &mut Vec<(u64, crate::txn::Staged)>,
11385 read_epoch: Epoch,
11386 principal: Option<&crate::auth::Principal>,
11387 control: Option<&crate::ExecutionControl>,
11388 ) -> Result<()> {
11389 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
11390 use crate::memtable::Row;
11391 use crate::txn::Staged;
11392 use std::collections::HashSet;
11393
11394 commit_prepare_checkpoint(control, 0)?;
11395 let snapshot = Snapshot::at(read_epoch);
11396 let cat = self.catalog.read().clone();
11397
11398 let live: Vec<(u64, String, crate::schema::Schema)> = cat
11400 .tables
11401 .iter()
11402 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
11403 .map(|e| (e.table_id, e.name.clone(), e.schema.clone()))
11404 .collect();
11405
11406 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
11408 if !any_constraints {
11409 self.materialize_generated_embeddings(staging, control)?;
11410 return Ok(());
11411 }
11412
11413 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
11415 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
11416 if let Some(r) = rows_cache.get(&table_id) {
11417 return Ok(r.clone());
11418 }
11419 let handle = self.table_by_id(table_id)?;
11420 let rows = match control {
11421 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
11422 None => handle.lock().visible_rows(snapshot)?,
11423 };
11424 rows_cache.insert(table_id, rows.clone());
11425 Ok(rows)
11426 };
11427
11428 let mut processed_updates = HashSet::new();
11433 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
11434 let mut update_pass = 0_usize;
11435 loop {
11436 commit_prepare_checkpoint(control, update_pass)?;
11437 update_pass += 1;
11438 let updates: Vec<PendingUpdate> = staging
11439 .iter()
11440 .enumerate()
11441 .filter_map(|(index, (table_id, op))| match op {
11442 Staged::Update {
11443 row_id,
11444 new_row: cells,
11445 ..
11446 } if !processed_updates.contains(&index) => {
11447 Some((index, *table_id, *row_id, cells.clone()))
11448 }
11449 _ => None,
11450 })
11451 .collect();
11452 if updates.is_empty() {
11453 break;
11454 }
11455 let mut new_ops = Vec::new();
11456 for (update_index, (index, table_id, row_id, new_cells)) in
11457 updates.into_iter().enumerate()
11458 {
11459 commit_prepare_checkpoint(control, update_index)?;
11460 processed_updates.insert(index);
11461 let Some(tname) = live
11462 .iter()
11463 .find(|(id, _, _)| *id == table_id)
11464 .map(|(_, name, _)| name.as_str())
11465 else {
11466 continue;
11467 };
11468 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
11469 continue;
11470 };
11471 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
11472 for (child_id, _child_name, child_schema) in &live {
11473 for fk in &child_schema.constraints.foreign_keys {
11474 if fk.ref_table != tname {
11475 continue;
11476 }
11477 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
11478 else {
11479 continue;
11480 };
11481 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
11482 == Some(old_key.as_slice())
11483 {
11484 continue;
11485 }
11486 if fk.on_update == FkAction::Restrict {
11487 continue;
11488 }
11489 self.acquire_fk_lock(
11494 txn_id,
11495 table_id,
11496 &old_key,
11497 crate::locks::LockMode::Exclusive,
11498 control,
11499 )?;
11500 let child_rows = load_rows(*child_id)?;
11501 for (child_index, child) in child_rows.into_iter().enumerate() {
11502 commit_prepare_checkpoint(control, child_index)?;
11503 if encode_composite_key(&fk.columns, &child.columns).as_deref()
11504 != Some(old_key.as_slice())
11505 {
11506 continue;
11507 }
11508 if staging.iter().any(|(id, op)| {
11509 *id == *child_id
11510 && matches!(op, Staged::Delete(id) if *id == child.row_id)
11511 }) {
11512 continue;
11513 }
11514 let mut cells: Vec<(u16, Value)> = child
11515 .columns
11516 .iter()
11517 .map(|(column_id, value)| (*column_id, value.clone()))
11518 .collect();
11519 for (child_column, parent_column) in
11520 fk.columns.iter().zip(&fk.ref_columns)
11521 {
11522 cells.retain(|(column_id, _)| column_id != child_column);
11523 let value = match fk.on_update {
11524 FkAction::Cascade => {
11525 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
11526 }
11527 FkAction::SetNull => Value::Null,
11528 FkAction::Restrict => {
11529 return Err(MongrelError::Other(
11530 "restricted foreign-key update reached cascade preparation"
11531 .into(),
11532 ));
11533 }
11534 };
11535 cells.push((*child_column, value));
11536 }
11537 cells.sort_by_key(|(column_id, _)| *column_id);
11538 if let Some(existing_index) = staging.iter().position(|(id, op)| {
11539 *id == *child_id
11540 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
11541 }) {
11542 if let Staged::Update {
11543 new_row: existing,
11544 changed_columns,
11545 ..
11546 } = &mut staging[existing_index].1 {
11547 changed_columns.extend(fk.columns.iter().copied());
11548 changed_columns.sort_unstable();
11549 changed_columns.dedup();
11550 if *existing != cells {
11551 *existing = cells;
11552 processed_updates.remove(&existing_index);
11553 }
11554 }
11555 } else {
11556 new_ops.push((
11557 *child_id,
11558 Staged::Update {
11559 row_id: child.row_id,
11560 new_row: cells,
11561 changed_columns: fk.columns.clone(),
11562 },
11563 ));
11564 }
11565 }
11566 }
11567 }
11568 }
11569 staging.extend(new_ops);
11570 }
11571
11572 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
11577 let mut cascade_pass = 0_usize;
11578 loop {
11579 commit_prepare_checkpoint(control, cascade_pass)?;
11580 cascade_pass += 1;
11581 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
11582 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
11583 .iter()
11584 .filter_map(|(t, op)| match op {
11585 Staged::Delete(rid) => Some((*t, *rid)),
11586 _ => None,
11587 })
11588 .collect();
11589 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
11590 commit_prepare_checkpoint(control, delete_index)?;
11591 if !cascaded.insert((table_id, rid.0)) {
11592 continue;
11593 }
11594 let Some(tname) = live
11595 .iter()
11596 .find(|(t, _, _)| *t == table_id)
11597 .map(|(_, n, _)| n.as_str())
11598 else {
11599 continue;
11600 };
11601 let parent_handle = self.table_by_id(table_id)?;
11602 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
11603 continue;
11604 };
11605 for (child_id, _child_name, child_schema) in &live {
11606 for fk in &child_schema.constraints.foreign_keys {
11607 if fk.ref_table != tname {
11608 continue;
11609 }
11610 let Some(parent_key) =
11611 encode_composite_key(&fk.ref_columns, &parent_row.columns)
11612 else {
11613 continue;
11614 };
11615 let key_preserved = staging.iter().any(|(t, op)| {
11624 if *t != table_id {
11625 return false;
11626 }
11627 let Staged::Put(cells) = op else {
11628 return false;
11629 };
11630 let map: HashMap<u16, crate::memtable::Value> =
11631 cells.iter().cloned().collect();
11632 encode_composite_key(&fk.ref_columns, &map).as_deref()
11633 == Some(parent_key.as_slice())
11634 });
11635 if key_preserved {
11636 continue;
11637 }
11638 self.acquire_fk_lock(
11645 txn_id,
11646 table_id,
11647 &parent_key,
11648 crate::locks::LockMode::Exclusive,
11649 control,
11650 )?;
11651 match fk.on_delete {
11652 FkAction::Restrict => continue,
11653 FkAction::Cascade => {
11654 let child_rows = load_rows(*child_id)?;
11655 for (child_index, cr) in child_rows.iter().enumerate() {
11656 commit_prepare_checkpoint(control, child_index)?;
11657 if !cascaded.contains(&(*child_id, cr.row_id.0))
11658 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
11659 == Some(parent_key.as_slice())
11660 {
11661 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
11662 }
11663 }
11664 }
11665 FkAction::SetNull => {
11666 let child_rows = load_rows(*child_id)?;
11667 for (child_index, cr) in child_rows.iter().enumerate() {
11668 commit_prepare_checkpoint(control, child_index)?;
11669 if !cascaded.contains(&(*child_id, cr.row_id.0))
11670 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
11671 == Some(parent_key.as_slice())
11672 {
11673 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
11676 .columns
11677 .iter()
11678 .map(|(k, v)| (*k, v.clone()))
11679 .collect();
11680 for cid in &fk.columns {
11681 cells.retain(|(k, _)| k != cid);
11682 cells.push((*cid, crate::memtable::Value::Null));
11683 }
11684 new_ops.push((
11685 *child_id,
11686 Staged::Update {
11687 row_id: cr.row_id,
11688 new_row: cells,
11689 changed_columns: fk.columns.clone(),
11690 },
11691 ));
11692 }
11693 }
11694 }
11695 }
11696 }
11697 }
11698 }
11699 if new_ops.is_empty() {
11700 break;
11701 }
11702 staging.extend(new_ops);
11703 }
11704
11705 self.validate_write_permissions(staging, principal, control)?;
11709 self.validate_security_writes(staging, read_epoch, principal, control)?;
11710 self.materialize_generated_embeddings(staging, control)?;
11711
11712 let staged_deletes: HashSet<(u64, u64)> = staging
11716 .iter()
11717 .filter_map(|(t, op)| match op {
11718 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
11719 _ => None,
11720 })
11721 .collect();
11722
11723 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
11725
11726 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
11728 commit_prepare_checkpoint(control, operation_index)?;
11729 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id) else {
11730 continue;
11731 };
11732 let cells_map: HashMap<u16, crate::memtable::Value>;
11733 match op {
11734 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
11735 cells_map = cells.iter().cloned().collect();
11736
11737 if !schema.constraints.checks.is_empty() {
11739 validate_checks(&schema.constraints.checks, &cells_map)?;
11740 }
11741
11742 for uc in &schema.constraints.uniques {
11744 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
11745 continue; };
11747 let marker = (*table_id, uc.id, key.clone());
11748 if !seen_unique.insert(marker) {
11749 return Err(MongrelError::Conflict(format!(
11750 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
11751 uc.name
11752 )));
11753 }
11754 let rows = load_rows(*table_id)?;
11755 for (row_index, r) in rows.iter().enumerate() {
11756 commit_prepare_checkpoint(control, row_index)?;
11757 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
11760 continue;
11761 }
11762 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
11763 if theirs == key {
11764 return Err(MongrelError::Conflict(format!(
11765 "UNIQUE constraint '{}' on table '{tname}' violated",
11766 uc.name
11767 )));
11768 }
11769 }
11770 }
11771 }
11772
11773 for fk in &schema.constraints.foreign_keys {
11775 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
11776 continue; };
11778 let Some(parent_id) = cat
11779 .tables
11780 .iter()
11781 .find(|t| t.name == fk.ref_table)
11782 .map(|t| t.table_id)
11783 else {
11784 return Err(MongrelError::InvalidArgument(format!(
11785 "FOREIGN KEY '{}' references unknown table '{}'",
11786 fk.name, fk.ref_table
11787 )));
11788 };
11789 self.acquire_fk_lock(
11794 txn_id,
11795 parent_id,
11796 &child_key,
11797 crate::locks::LockMode::Shared,
11798 control,
11799 )?;
11800 let parent_rows = load_rows(parent_id)?;
11801 let mut found = false;
11802 for (row_index, r) in parent_rows.iter().enumerate() {
11803 commit_prepare_checkpoint(control, row_index)?;
11804 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
11805 continue;
11806 }
11807 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
11808 if pkey == child_key {
11809 found = true;
11810 break;
11811 }
11812 }
11813 }
11814 if !found {
11821 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
11822 commit_prepare_checkpoint(control, staged_index)?;
11823 if *st_table != parent_id {
11824 continue;
11825 }
11826 if let Staged::Put(pcells)
11827 | Staged::Update {
11828 new_row: pcells, ..
11829 } = st_op
11830 {
11831 let pmap: HashMap<u16, crate::memtable::Value> =
11832 pcells.iter().cloned().collect();
11833 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
11834 {
11835 if pkey == child_key {
11836 found = true;
11837 break;
11838 }
11839 }
11840 }
11841 }
11842 }
11843 if !found {
11844 return Err(MongrelError::Conflict(format!(
11845 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
11846 fk.name, fk.ref_table
11847 )));
11848 }
11849 }
11850
11851 if let Staged::Update { row_id, .. } = op {
11856 let parent_handle = self.table_by_id(*table_id)?;
11857 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
11858 continue;
11859 };
11860 for (child_id, child_name, child_schema) in &live {
11861 for fk in &child_schema.constraints.foreign_keys {
11862 if fk.ref_table != *tname || fk.on_update != FkAction::Restrict {
11863 continue;
11864 }
11865 let Some(old_key) =
11866 encode_composite_key(&fk.ref_columns, &old_parent.columns)
11867 else {
11868 continue;
11869 };
11870 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
11871 == Some(old_key.as_slice())
11872 {
11873 continue;
11874 }
11875 self.acquire_fk_lock(
11880 txn_id,
11881 *table_id,
11882 &old_key,
11883 crate::locks::LockMode::Exclusive,
11884 control,
11885 )?;
11886 for (child_index, child) in
11887 load_rows(*child_id)?.into_iter().enumerate()
11888 {
11889 commit_prepare_checkpoint(control, child_index)?;
11890 if encode_composite_key(&fk.columns, &child.columns).as_deref()
11891 != Some(old_key.as_slice())
11892 {
11893 continue;
11894 }
11895 let replacement = staging.iter().find_map(|(id, op)| {
11896 if *id != *child_id {
11897 return None;
11898 }
11899 match op {
11900 Staged::Delete(id) if *id == child.row_id => Some(None),
11901 Staged::Update {
11902 row_id,
11903 new_row: cells,
11904 ..
11905 } if *row_id == child.row_id => {
11906 let map: HashMap<u16, Value> =
11907 cells.iter().cloned().collect();
11908 Some(encode_composite_key(&fk.columns, &map))
11909 }
11910 _ => None,
11911 }
11912 });
11913 if replacement.is_some_and(|key| {
11914 key.as_deref() != Some(old_key.as_slice())
11915 }) {
11916 continue;
11917 }
11918 return Err(MongrelError::Conflict(format!(
11919 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
11920 fk.name
11921 )));
11922 }
11923 }
11924 }
11925 }
11926 }
11927 Staged::Delete(rid) => {
11928 let parent_handle = self.table_by_id(*table_id)?;
11932 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
11933 continue;
11934 };
11935 for (child_id, child_name, child_schema) in &live {
11936 for fk in &child_schema.constraints.foreign_keys {
11937 if fk.ref_table != *tname || fk.on_delete != FkAction::Restrict {
11938 continue;
11939 }
11940 let Some(parent_key) =
11941 encode_composite_key(&fk.ref_columns, &parent_row.columns)
11942 else {
11943 continue;
11944 };
11945 let child_rows = load_rows(*child_id)?;
11946 for (row_index, r) in child_rows.iter().enumerate() {
11947 commit_prepare_checkpoint(control, row_index)?;
11948 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
11951 continue;
11952 }
11953 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
11954 if ck == parent_key {
11955 return Err(MongrelError::Conflict(format!(
11956 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
11957 fk.name
11958 )));
11959 }
11960 }
11961 }
11962 }
11963 }
11964 }
11965 Staged::Truncate => {
11966 for (child_id, child_name, child_schema) in &live {
11970 for fk in &child_schema.constraints.foreign_keys {
11971 if fk.ref_table != *tname {
11972 continue;
11973 }
11974 let child_rows = load_rows(*child_id)?;
11975 if child_rows
11976 .iter()
11977 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
11978 {
11979 return Err(MongrelError::Conflict(format!(
11980 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
11981 fk.name
11982 )));
11983 }
11984 }
11985 }
11986 }
11987 }
11988 }
11989 Ok(())
11990 }
11991
11992 fn validate_write_permissions(
11993 &self,
11994 staging: &[(u64, crate::txn::Staged)],
11995 principal: Option<&crate::auth::Principal>,
11996 control: Option<&crate::ExecutionControl>,
11997 ) -> Result<()> {
11998 commit_prepare_checkpoint(control, 0)?;
11999 if principal.is_none() && !self.auth_state.require_auth() {
12000 return Ok(());
12001 }
12002 let principal = principal.ok_or(MongrelError::AuthRequired)?;
12003 let needs = summarize_write_permissions(staging);
12004 let catalog = self.catalog.read();
12005
12006 if needs.values().any(|need| need.truncate) {
12007 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
12008 }
12009 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
12010 commit_prepare_checkpoint(control, need_index)?;
12011 let entry = catalog
12012 .tables
12013 .iter()
12014 .find(|entry| {
12015 entry.table_id == table_id
12016 && matches!(entry.state, TableState::Live | TableState::Building { .. })
12017 })
12018 .ok_or_else(|| {
12019 MongrelError::NotFound(format!(
12020 "live table {table_id} not found during write validation"
12021 ))
12022 })?;
12023 if matches!(entry.state, TableState::Building { .. }) {
12024 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
12025 continue;
12026 }
12027 if need.insert {
12028 Self::require_columns_for_principal(
12029 &entry.name,
12030 &entry.schema,
12031 crate::auth::ColumnOperation::Insert,
12032 &need.insert_columns,
12033 principal,
12034 )?;
12035 }
12036 if need.update {
12037 Self::require_columns_for_principal(
12038 &entry.name,
12039 &entry.schema,
12040 crate::auth::ColumnOperation::Update,
12041 &need.update_columns,
12042 principal,
12043 )?;
12044 }
12045 if need.delete {
12046 self.require_for(
12047 Some(principal),
12048 &crate::auth::Permission::Delete {
12049 table: entry.name.clone(),
12050 },
12051 )?;
12052 }
12053 }
12054 Ok(())
12055 }
12056
12057 fn validate_security_writes(
12058 &self,
12059 staging: &[(u64, crate::txn::Staged)],
12060 read_epoch: Epoch,
12061 explicit_principal: Option<&crate::auth::Principal>,
12062 control: Option<&crate::ExecutionControl>,
12063 ) -> Result<()> {
12064 commit_prepare_checkpoint(control, 0)?;
12065 use crate::security::PolicyCommand;
12066 use crate::txn::Staged;
12067
12068 let catalog = self.catalog.read();
12069 if catalog.security.rls_tables.is_empty() {
12070 return Ok(());
12071 }
12072 let security = catalog.security.clone();
12073 let table_names = catalog
12074 .tables
12075 .iter()
12076 .filter(|entry| matches!(entry.state, TableState::Live))
12077 .map(|entry| (entry.table_id, entry.name.clone()))
12078 .collect::<HashMap<_, _>>();
12079 drop(catalog);
12080 if !staging.iter().any(|(table_id, _)| {
12081 table_names
12082 .get(table_id)
12083 .is_some_and(|table| security.rls_enabled(table))
12084 }) {
12085 return Ok(());
12086 }
12087 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
12088
12089 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
12090 commit_prepare_checkpoint(control, operation_index)?;
12091 let Some(table) = table_names.get(table_id) else {
12092 continue;
12093 };
12094 if !security.rls_enabled(table) || principal.is_admin {
12095 continue;
12096 }
12097 let denied = |command| MongrelError::PermissionDenied {
12098 required: match command {
12099 PolicyCommand::Insert => crate::auth::Permission::Insert {
12100 table: table.clone(),
12101 },
12102 PolicyCommand::Update => crate::auth::Permission::Update {
12103 table: table.clone(),
12104 },
12105 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
12106 crate::auth::Permission::Delete {
12107 table: table.clone(),
12108 }
12109 }
12110 },
12111 principal: principal.username.clone(),
12112 };
12113 match operation {
12114 Staged::Put(cells) => {
12115 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
12116 row.columns.extend(cells.iter().cloned());
12117 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
12118 return Err(denied(PolicyCommand::Insert));
12119 }
12120 }
12121 Staged::Update {
12122 row_id,
12123 new_row: cells,
12124 ..
12125 } => {
12126 let old = self
12127 .table_by_id(*table_id)?
12128 .lock()
12129 .get(*row_id, Snapshot::at(read_epoch))
12130 .ok_or_else(|| {
12131 MongrelError::NotFound(format!("row {} not found", row_id.0))
12132 })?;
12133 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
12134 return Err(denied(PolicyCommand::Update));
12135 }
12136 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
12137 new.columns.extend(cells.iter().cloned());
12138 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
12139 return Err(denied(PolicyCommand::Update));
12140 }
12141 }
12142 Staged::Delete(row_id) => {
12143 let old = self
12144 .table_by_id(*table_id)?
12145 .lock()
12146 .get(*row_id, Snapshot::at(read_epoch))
12147 .ok_or_else(|| {
12148 MongrelError::NotFound(format!("row {} not found", row_id.0))
12149 })?;
12150 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
12151 return Err(denied(PolicyCommand::Delete));
12152 }
12153 }
12154 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
12155 }
12156 }
12157 Ok(())
12158 }
12159
12160 #[allow(clippy::too_many_arguments)]
12167 pub(crate) fn commit_transaction_with_external_states(
12168 &self,
12169 txn_id: u64,
12170 read_epoch: Epoch,
12171 staging: Vec<(u64, crate::txn::Staged)>,
12172 external_states: Vec<(String, Vec<u8>)>,
12173 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12174 security_principal: Option<crate::auth::Principal>,
12175 principal_catalog_bound: bool,
12176 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12177 context: crate::txn::TxnCommitContext,
12178 ) -> Result<(Epoch, Vec<RowId>)> {
12179 self.commit_transaction_with_external_states_inner(
12180 txn_id,
12181 read_epoch,
12182 staging,
12183 external_states,
12184 materialized_view_updates,
12185 security_principal,
12186 principal_catalog_bound,
12187 external_trigger_bridge,
12188 context,
12189 None,
12190 None,
12191 )
12192 }
12193
12194 #[allow(clippy::too_many_arguments)]
12195 pub(crate) fn commit_transaction_with_external_states_controlled(
12196 &self,
12197 txn_id: u64,
12198 read_epoch: Epoch,
12199 staging: Vec<(u64, crate::txn::Staged)>,
12200 external_states: Vec<(String, Vec<u8>)>,
12201 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12202 security_principal: Option<crate::auth::Principal>,
12203 principal_catalog_bound: bool,
12204 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12205 context: crate::txn::TxnCommitContext,
12206 control: &crate::ExecutionControl,
12207 before_commit: &mut dyn FnMut() -> Result<()>,
12208 ) -> Result<(Epoch, Vec<RowId>)> {
12209 self.commit_transaction_with_external_states_inner(
12210 txn_id,
12211 read_epoch,
12212 staging,
12213 external_states,
12214 materialized_view_updates,
12215 security_principal,
12216 principal_catalog_bound,
12217 external_trigger_bridge,
12218 context,
12219 Some(control),
12220 Some(before_commit),
12221 )
12222 }
12223
12224 #[allow(clippy::too_many_arguments)]
12225 fn commit_transaction_with_external_states_inner(
12226 &self,
12227 txn_id: u64,
12228 read_epoch: Epoch,
12229 mut staging: Vec<(u64, crate::txn::Staged)>,
12230 external_states: Vec<(String, Vec<u8>)>,
12231 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12232 mut security_principal: Option<crate::auth::Principal>,
12233 principal_catalog_bound: bool,
12234 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12235 context: crate::txn::TxnCommitContext,
12236 control: Option<&crate::ExecutionControl>,
12237 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
12238 ) -> Result<(Epoch, Vec<RowId>)> {
12239 use crate::memtable::Row;
12240 use crate::txn::{Staged, StagedOp, WriteKey};
12241 use crate::wal::Op;
12242 use std::collections::hash_map::DefaultHasher;
12243 use std::hash::{Hash, Hasher};
12244 use std::sync::atomic::Ordering;
12245
12246 if txn_id == crate::wal::SYSTEM_TXN_ID {
12247 return Err(MongrelError::Full(
12248 "per-open transaction id namespace exhausted; reopen the database".into(),
12249 ));
12250 }
12251 if self.read_only {
12252 return Err(MongrelError::ReadOnlyReplica);
12253 }
12254 commit_prepare_checkpoint(control, 0)?;
12255 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
12256 self.refresh_security_catalog_if_stale(observed_security_version)?;
12257 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
12258 if self.auth_state.require_auth() && security_principal.is_none() {
12259 return Err(MongrelError::AuthRequired);
12260 }
12261 {
12262 let catalog = self.catalog.read();
12263 if principal_catalog_bound
12264 || security_principal
12265 .as_ref()
12266 .is_some_and(|principal| principal.user_id != 0)
12267 {
12268 let principal = security_principal
12269 .as_ref()
12270 .ok_or(MongrelError::AuthRequired)?;
12271 security_principal =
12272 Self::resolve_bound_principal_from_catalog(&catalog, principal);
12273 if security_principal.is_none() {
12274 return Err(MongrelError::AuthRequired);
12275 }
12276 }
12277 }
12278 let _replication_guard = self.replication_barrier.read();
12279 if self.poisoned.load(Ordering::Relaxed) {
12280 return Err(MongrelError::Other(
12281 "database poisoned by fsync error".into(),
12282 ));
12283 }
12284 let _operation = self.admit_operation()?;
12288
12289 if context.state.is_some() {
12298 self.acquire_txn_lock(
12299 txn_id,
12300 crate::locks::LockKey::schema_barrier(),
12301 crate::locks::LockMode::Shared,
12302 control,
12303 )?;
12304 }
12305 let _txn_lock_guard = TxnLockGuard {
12306 locks: &self.lock_manager,
12307 txn_id,
12308 };
12309
12310 let idempotency_request = match &context.idempotency {
12316 Some(request) => match self.idempotency.check_and_reserve(request)? {
12317 crate::txn::IdempotencyCheck::Replay(receipt) => {
12318 let epoch = Epoch(receipt.log_position.index);
12319 if let Some(state) = &context.state {
12320 state.committed(receipt);
12321 }
12322 return Ok((epoch, Vec::new()));
12323 }
12324 crate::txn::IdempotencyCheck::Reserved => Some(request.clone()),
12325 },
12326 None => None,
12327 };
12328 let mut idempotency_guard = idempotency_request.as_ref().map(|request| {
12330 crate::txn::IdempotencyReservationGuard::new(&self.idempotency, request.clone())
12331 });
12332 let mut external_states = dedup_external_states(external_states);
12333 if !external_states.is_empty() {
12334 let cat = self.catalog.read();
12335 for (name, _) in &external_states {
12336 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
12337 return Err(MongrelError::NotFound(format!(
12338 "external table {name:?} not found"
12339 )));
12340 }
12341 }
12342 }
12343 let prepared_materialized_views = {
12344 let mut deduplicated = HashMap::new();
12345 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
12346 {
12347 commit_prepare_checkpoint(control, definition_index)?;
12348 if definition.name.is_empty() || definition.query.trim().is_empty() {
12349 return Err(MongrelError::InvalidArgument(
12350 "materialized view name and query must not be empty".into(),
12351 ));
12352 }
12353 deduplicated.insert(definition.name.clone(), definition);
12354 }
12355 let catalog = self.catalog.read();
12356 let mut prepared = Vec::with_capacity(deduplicated.len());
12357 for (definition_index, definition) in deduplicated.into_values().enumerate() {
12358 commit_prepare_checkpoint(control, definition_index)?;
12359 let table_id = catalog
12360 .live(&definition.name)
12361 .ok_or_else(|| {
12362 MongrelError::NotFound(format!(
12363 "materialized view table {:?} not found",
12364 definition.name
12365 ))
12366 })?
12367 .table_id;
12368 prepared.push((table_id, definition));
12369 }
12370 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
12371 prepared
12372 };
12373
12374 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12377 self.expand_table_triggers(
12378 txn_id,
12379 &mut staging,
12380 read_epoch,
12381 external_trigger_bridge,
12382 &mut external_states,
12383 control,
12384 )?;
12385 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12386 external_states = dedup_external_states(external_states);
12387 let expected_external_generations = {
12388 let catalog = self.catalog.read();
12389 let mut generations = HashMap::with_capacity(external_states.len());
12390 for (name, _) in &external_states {
12391 let entry = catalog
12392 .external_tables
12393 .iter()
12394 .find(|entry| entry.name == *name)
12395 .ok_or_else(|| {
12396 MongrelError::NotFound(format!("external table {name:?} not found"))
12397 })?;
12398 generations.insert(name.clone(), entry.created_epoch);
12399 }
12400 generations
12401 };
12402
12403 self.acquire_unique_key_claims(txn_id, &staging, control)?;
12409 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
12413 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
12414 self.validate_constraints(
12420 txn_id,
12421 &mut staging,
12422 read_epoch,
12423 security_principal.as_ref(),
12424 control,
12425 )?;
12426 let mut normalized = Vec::with_capacity(staging.len() * 2);
12427 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
12428 commit_prepare_checkpoint(control, staged_index)?;
12429 match op {
12430 crate::txn::Staged::Update {
12431 row_id,
12432 new_row: cells,
12433 ..
12434 } => {
12435 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
12436 normalized.push((table_id, crate::txn::Staged::Put(cells)));
12437 }
12438 op => normalized.push((table_id, op)),
12439 }
12440 }
12441 staging = normalized;
12442 let has_changes = !staging.is_empty()
12443 || !external_states.is_empty()
12444 || !prepared_materialized_views.is_empty();
12445 let truncated_tables: HashSet<u64> = staging
12446 .iter()
12447 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
12448 .collect();
12449
12450 let write_keys = {
12451 let cat = self.catalog.read();
12452 let mut keys: Vec<WriteKey> = Vec::new();
12453 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12454 commit_prepare_checkpoint(control, staged_index)?;
12455 match staged {
12456 Staged::Put(cells) => {
12457 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
12458 for col in &entry.schema.columns {
12459 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
12460 if let Some((_, val)) =
12461 cells.iter().find(|(id, _)| *id == col.id)
12462 {
12463 let mut h = DefaultHasher::new();
12464 val.encode_key().hash(&mut h);
12465 keys.push(WriteKey::Unique {
12466 table_id: *table_id,
12467 index_id: 0,
12468 key_hash: h.finish(),
12469 });
12470 }
12471 }
12472 }
12473 for uc in &entry.schema.constraints.uniques {
12480 if let Some(key_bytes) = crate::constraint::encode_composite_key(
12481 &uc.columns,
12482 &cells.iter().cloned().collect(),
12483 ) {
12484 let mut h = DefaultHasher::new();
12485 key_bytes.hash(&mut h);
12486 keys.push(WriteKey::Unique {
12487 table_id: *table_id,
12488 index_id: uc.id | 0x8000,
12489 key_hash: h.finish(),
12490 });
12491 }
12492 }
12493 }
12494 }
12495 Staged::Delete(rid) => keys.push(WriteKey::Row {
12496 table_id: *table_id,
12497 row_id: rid.0,
12498 }),
12499 Staged::Truncate => keys.push(WriteKey::Table {
12500 table_id: *table_id,
12501 }),
12502 Staged::Update { .. } => {
12503 return Err(MongrelError::Other(
12504 "transaction contains an unnormalized update during preparation".into(),
12505 ));
12506 }
12507 }
12508 }
12509 for (external_index, (name, _)) in external_states.iter().enumerate() {
12510 commit_prepare_checkpoint(control, external_index)?;
12511 let mut h = DefaultHasher::new();
12512 name.hash(&mut h);
12513 keys.push(WriteKey::Unique {
12514 table_id: EXTERNAL_TABLE_ID,
12515 index_id: 0,
12516 key_hash: h.finish(),
12517 });
12518 }
12519 keys
12520 };
12521
12522 let min_active = self.active_txns.min_read_epoch();
12524 if min_active < u64::MAX {
12525 self.conflicts.prune_below(Epoch(min_active));
12526 }
12527
12528 let ssi_keys = match context.isolation.canonical() {
12535 crate::txn::IsolationLevel::Serializable => {
12536 crate::txn::ssi_validation_keys(&context.read_set, &context.predicate_set)
12537 }
12538 _ => Vec::new(),
12539 };
12540
12541 if self.conflicts.conflicts(&write_keys, read_epoch) {
12545 return Err(MongrelError::Conflict(
12546 "write-write conflict (pre-validate, first-committer-wins)".into(),
12547 ));
12548 }
12549 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
12550 return Err(MongrelError::SerializationFailure {
12551 message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
12552 .into(),
12553 });
12554 }
12555 let pre_validate_version = self.conflicts.version();
12556
12557 let mut spilled: Vec<SpilledRun> = Vec::new();
12561 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
12562 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
12566 {
12567 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
12568 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
12569 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12570 commit_prepare_checkpoint(control, staged_index)?;
12571 if let Staged::Put(cells) = staged {
12572 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
12573 bytes.saturating_add(value.estimated_bytes())
12574 });
12575 let table_bytes = table_bytes.entry(*table_id).or_default();
12576 *table_bytes = table_bytes.saturating_add(bytes);
12577 put_indexes.entry(*table_id).or_default().push(staged_index);
12578 }
12579 }
12580 let tables = self.tables.read();
12581 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
12582 commit_prepare_checkpoint(control, table_index)?;
12583 if bytes
12584 <= self
12585 .spill_threshold
12586 .load(std::sync::atomic::Ordering::Relaxed)
12587 {
12588 continue;
12589 }
12590 let Some(handle) = tables.get(&table_id) else {
12591 continue;
12592 };
12593 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
12594 let mut t = handle.lock();
12595 let tdir = t.table_dir().to_path_buf();
12596 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
12597 std::fs::create_dir_all(&txn_dir)?;
12598 let run_id = t.alloc_run_id()? as u128;
12599 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
12600 let final_path = t.run_path(run_id as u64);
12601
12602 let mut rows: Vec<Row> = Vec::new();
12603 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
12604 commit_prepare_checkpoint(control, put_index)?;
12605 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
12606 return Err(MongrelError::Other(
12607 "transaction put index no longer references a put".into(),
12608 ));
12609 };
12610 t.validate_cells_not_null(cells)?;
12611 let row_id = t.alloc_row_id()?;
12612 let mut row = Row::new(row_id, Epoch(0));
12613 row.columns.extend(std::mem::take(cells));
12614 rows.push(row);
12615 }
12616 let schema = t.schema_ref().clone();
12617 let kek = t.kek_ref().cloned();
12618 let specs = t.indexable_column_specs();
12619 drop(t);
12620
12621 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
12622 .uniform_epoch(true);
12623 if let Some(ref kek) = kek {
12624 writer = writer.with_encryption(kek.as_ref(), specs);
12625 }
12626 commit_prepare_checkpoint(control, 0)?;
12627 let header = writer.write(&pending_path, &rows)?;
12628 commit_prepare_checkpoint(control, 0)?;
12629 let row_count = header.row_count;
12630 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
12631 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
12632
12633 spilled.push(SpilledRun {
12634 table_id,
12635 run_id,
12636 pending_path,
12637 final_path,
12638 rows,
12639 row_count,
12640 min_rid,
12641 max_rid,
12642 content_hash: header.content_hash,
12643 });
12644 spilled_tables.insert(table_id);
12645 }
12646 }
12647
12648 if spill_guard.is_some() {
12650 if let Some(hook) = self.spill_hook.lock().as_ref() {
12651 hook();
12652 }
12653 }
12654
12655 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
12666 .take(staging.len())
12667 .collect();
12668 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
12669 .take(staging.len())
12670 .collect();
12671 {
12672 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
12673 for (index, (table_id, staged)) in staging.iter().enumerate() {
12674 commit_prepare_checkpoint(control, index)?;
12675 if matches!(staged, Staged::Delete(_))
12676 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
12677 {
12678 indexes_by_table.entry(*table_id).or_default().push(index);
12679 }
12680 }
12681 let tables = self.tables.read();
12682 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
12683 commit_prepare_checkpoint(control, table_index)?;
12684 let handle = tables.get(&table_id).ok_or_else(|| {
12685 MongrelError::NotFound(format!("table {table_id} not mounted"))
12686 })?;
12687 #[cfg(test)]
12688 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
12689 let mut t = handle.lock();
12690 for (prepare_index, index) in indexes.into_iter().enumerate() {
12691 commit_prepare_checkpoint(control, prepare_index)?;
12692 match &staging[index].1 {
12693 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
12694 t.validate_cells_not_null(cells)?;
12695 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
12696 for (column, value) in cells {
12697 row.columns.insert(*column, value.clone());
12698 }
12699 prebuilt[index] = Some(row);
12700 }
12701 Staged::Delete(row_id) => {
12702 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
12703 }
12704 Staged::Put(_) | Staged::Truncate => {}
12705 Staged::Update { .. } => {
12706 return Err(MongrelError::Other(
12707 "transaction contains an unnormalized update during row preparation"
12708 .into(),
12709 ));
12710 }
12711 }
12712 }
12713 }
12714 }
12715
12716 let prepared_table_handles = {
12720 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
12721 let put_table_ids: HashSet<u64> = staging
12722 .iter()
12723 .filter_map(|(table_id, staged)| {
12724 matches!(staged, Staged::Put(_)).then_some(*table_id)
12725 })
12726 .collect();
12727 let tables = self.tables.read();
12728 let mut handles = HashMap::with_capacity(table_ids.len());
12729 for (table_index, table_id) in table_ids.into_iter().enumerate() {
12730 commit_prepare_checkpoint(control, table_index)?;
12731 let handle = tables.get(&table_id).ok_or_else(|| {
12732 MongrelError::NotFound(format!("table {table_id} not mounted"))
12733 })?;
12734 if put_table_ids.contains(&table_id) {
12735 match control {
12736 Some(control) => {
12737 handle.lock().prepare_durable_publish_controlled(control)?
12738 }
12739 None => handle.lock().prepare_durable_publish()?,
12740 }
12741 }
12742 handles.insert(table_id, handle.clone());
12743 }
12744 handles
12745 };
12746
12747 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
12751
12752 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
12753 .iter()
12754 .map(|run| {
12755 (
12756 run.table_id,
12757 run.rows.iter().map(|row| row.row_id).collect(),
12758 )
12759 })
12760 .collect();
12761 let committed_row_ids = staging
12762 .iter()
12763 .enumerate()
12764 .filter_map(|(index, (table_id, staged))| {
12765 if !matches!(staged, Staged::Put(_)) {
12766 return None;
12767 }
12768 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
12769 spilled_row_ids
12770 .get_mut(table_id)
12771 .and_then(VecDeque::pop_front)
12772 })
12773 })
12774 .collect();
12775
12776 let mut prepared_external = Vec::with_capacity(external_states.len());
12777 for (external_index, (name, state)) in external_states.iter().enumerate() {
12778 commit_prepare_checkpoint(control, external_index)?;
12779 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
12780 prepared_external.push((name.clone(), state.clone(), pending));
12781 }
12782
12783 if context.state.is_some() {
12793 self.lock_manager
12794 .release(txn_id, &crate::locks::LockKey::schema_barrier());
12795 }
12796 let added_runs: Vec<crate::wal::AddedRun> = spilled
12797 .iter()
12798 .map(|s| crate::wal::AddedRun {
12799 table_id: s.table_id,
12800 run_id: s.run_id,
12801 row_count: s.row_count,
12802 level: 0,
12803 min_row_id: s.min_rid,
12804 max_row_id: s.max_rid,
12805 content_hash: s.content_hash,
12806 })
12807 .collect();
12808 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
12809 hook();
12810 }
12811 let security_guard = self.security_coordinator.gate.read();
12815 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
12816 return Err(MongrelError::Conflict(
12817 "security policy changed during write".into(),
12818 ));
12819 }
12820 if spill_guard.is_some() {
12821 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
12822 hook();
12823 }
12824 }
12825 let commit_guard = self.commit_lock.lock();
12826 let catalog_generation_result = (|| {
12827 {
12828 let catalog = self.catalog.read();
12829 for table_id in prepared_table_handles.keys() {
12830 let is_current = catalog.tables.iter().any(|entry| {
12831 entry.table_id == *table_id
12832 && matches!(entry.state, TableState::Live | TableState::Building { .. })
12833 });
12834 if !is_current {
12835 return Err(MongrelError::Conflict(format!(
12836 "table {table_id} changed during transaction preparation"
12837 )));
12838 }
12839 }
12840 for (name, created_epoch) in &expected_external_generations {
12841 let current = catalog
12842 .external_tables
12843 .iter()
12844 .find(|entry| entry.name == *name)
12845 .map(|entry| entry.created_epoch);
12846 if current != Some(*created_epoch) {
12847 return Err(MongrelError::Conflict(format!(
12848 "external table {name:?} changed during transaction preparation"
12849 )));
12850 }
12851 }
12852 for (table_id, definition) in &prepared_materialized_views {
12853 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
12854 if current != Some(*table_id) {
12855 return Err(MongrelError::Conflict(format!(
12856 "materialized view {:?} changed during transaction preparation",
12857 definition.name
12858 )));
12859 }
12860 }
12861 if trigger_catalog_binding(&catalog) != trigger_binding {
12862 return Err(MongrelError::Conflict(
12863 "trigger or referenced table generation changed during transaction preparation"
12864 .into(),
12865 ));
12866 }
12867 }
12868 let tables = self.tables.read();
12869 for (table_id, prepared) in &prepared_table_handles {
12870 if !tables
12871 .get(table_id)
12872 .is_some_and(|current| current.ptr_eq(prepared))
12873 {
12874 return Err(MongrelError::Conflict(format!(
12875 "table {table_id} mount changed during transaction preparation"
12876 )));
12877 }
12878 }
12879 Ok(())
12880 })();
12881 if let Err(error) = catalog_generation_result {
12882 drop(commit_guard);
12883 for (_, _, pending) in &prepared_external {
12884 let _ = std::fs::remove_file(pending);
12885 }
12886 return Err(error);
12887 }
12888 let new_epoch = self.epoch.assigned().next();
12892 let mut spilled_wal_bytes = 0;
12893 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
12894 let spill_prepare = (|| {
12895 for run in &mut spilled {
12896 for row in &mut run.rows {
12897 row.committed_epoch = new_epoch;
12898 }
12899 for rows in encode_spilled_row_chunks(
12900 &run.rows,
12901 &mut spilled_wal_bytes,
12902 SPILLED_WAL_TOTAL_MAX_BYTES,
12903 control,
12904 )? {
12905 spilled_wal_records.push((
12906 run.table_id,
12907 Op::SpilledRows {
12908 table_id: run.table_id,
12909 rows,
12910 },
12911 ));
12912 }
12913 }
12914 Result::<()>::Ok(())
12915 })();
12916 if let Err(error) = spill_prepare {
12917 for (_, _, pending) in &prepared_external {
12918 let _ = std::fs::remove_file(pending);
12919 }
12920 return Err(error);
12921 }
12922 let (
12923 new_epoch,
12924 mut _epoch_guard,
12925 applies,
12926 committed_materialized_views,
12927 commit_seq,
12928 commit_ts,
12929 ) = {
12930 let mut wal = self.shared_wal.lock();
12931
12932 if self.conflicts.version() != pre_validate_version {
12937 if self.conflicts.conflicts(&write_keys, read_epoch) {
12938 drop(wal);
12941 for (_, _, pending) in &prepared_external {
12942 let _ = std::fs::remove_file(pending);
12943 }
12944 return Err(MongrelError::Conflict(
12945 "write-write conflict (sequencer delta re-check)".into(),
12946 ));
12947 }
12948 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
12949 drop(wal);
12953 for (_, _, pending) in &prepared_external {
12954 let _ = std::fs::remove_file(pending);
12955 }
12956 return Err(MongrelError::SerializationFailure {
12957 message:
12958 "a concurrent commit invalidated this transaction's reads (sequencer re-check)"
12959 .into(),
12960 });
12961 }
12962 }
12963
12964 if let Some(control) = control {
12965 if let Err(error) = control.checkpoint() {
12966 drop(wal);
12967 for (_, _, pending) in &prepared_external {
12968 let _ = std::fs::remove_file(pending);
12969 }
12970 return Err(error);
12971 }
12972 }
12973 let mut applies = Vec::<TableApplyBatch>::new();
12974 let mut apply_indexes = HashMap::<u64, usize>::new();
12975 let mut committed_materialized_views = Vec::new();
12976 let mut wal_records = spilled_wal_records;
12977
12978 let mut index = 0;
12979 while index < staging.len() {
12980 let table_id = staging[index].0;
12981 let handle = prepared_table_handles
12982 .get(&table_id)
12983 .cloned()
12984 .ok_or_else(|| {
12985 MongrelError::NotFound(format!("table {table_id} not prepared"))
12986 })?;
12987 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
12988 let index = applies.len();
12989 applies.push(TableApplyBatch {
12990 table_id,
12991 handle,
12992 ops: Vec::new(),
12993 });
12994 index
12995 });
12996
12997 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
13000 {
13001 index += 1;
13002 continue;
13003 }
13004
13005 match &staging[index].1 {
13006 Staged::Put(_) => {
13007 let mut rows = Vec::new();
13008 while index < staging.len()
13009 && staging[index].0 == table_id
13010 && matches!(&staging[index].1, Staged::Put(_))
13011 {
13012 let mut row = prebuilt[index].take().ok_or_else(|| {
13013 MongrelError::Other(
13014 "transaction prepare lost a prebuilt put row".into(),
13015 )
13016 })?;
13017 row.committed_epoch = new_epoch;
13018 rows.push(row);
13019 index += 1;
13020 }
13021 let payload = bincode::serialize(&rows)
13022 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
13023 wal_records.push((
13024 table_id,
13025 Op::Put {
13026 table_id,
13027 rows: payload,
13028 },
13029 ));
13030 applies[batch_index].ops.push(StagedOp::Put(rows));
13031 }
13032 Staged::Delete(_) => {
13033 let mut row_ids = Vec::new();
13034 while index < staging.len()
13035 && staging[index].0 == table_id
13036 && matches!(&staging[index].1, Staged::Delete(_))
13037 {
13038 let Staged::Delete(row_id) = &staging[index].1 else {
13039 return Err(MongrelError::Other(
13040 "transaction delete batch changed during WAL preparation"
13041 .into(),
13042 ));
13043 };
13044 if let Some(before) = &delete_images[index] {
13045 wal_records.push((
13046 table_id,
13047 Op::BeforeImage {
13048 table_id,
13049 row_id: *row_id,
13050 row: bincode::serialize(before).map_err(|error| {
13051 MongrelError::Other(format!(
13052 "before-image serialize: {error}"
13053 ))
13054 })?,
13055 },
13056 ));
13057 }
13058 row_ids.push(*row_id);
13059 index += 1;
13060 }
13061 wal_records.push((
13062 table_id,
13063 Op::Delete {
13064 table_id,
13065 row_ids: row_ids.clone(),
13066 },
13067 ));
13068 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
13069 }
13070 Staged::Truncate => {
13071 wal_records.push((table_id, Op::TruncateTable { table_id }));
13072 applies[batch_index].ops.push(StagedOp::Truncate);
13073 index += 1;
13074 }
13075 Staged::Update { .. } => {
13076 return Err(MongrelError::Other(
13077 "transaction contains an unnormalized update at the sequencer".into(),
13078 ));
13079 }
13080 }
13081 }
13082
13083 for (name, state, _) in &prepared_external {
13084 wal_records.push((
13085 EXTERNAL_TABLE_ID,
13086 Op::ExternalTableState {
13087 name: name.clone(),
13088 state: state.clone(),
13089 },
13090 ));
13091 }
13092
13093 for (table_id, definition) in &prepared_materialized_views {
13094 let mut definition = definition.clone();
13095 definition.last_refresh_epoch = new_epoch.0;
13096 wal_records.push((
13097 *table_id,
13098 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
13099 name: definition.name.clone(),
13100 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
13101 }),
13102 ));
13103 committed_materialized_views.push(definition);
13104 }
13105 if !committed_materialized_views.is_empty() {
13106 let mut next_catalog = self.catalog.read().clone();
13107 for definition in &committed_materialized_views {
13108 if let Some(existing) = next_catalog
13109 .materialized_views
13110 .iter_mut()
13111 .find(|existing| existing.name == definition.name)
13112 {
13113 *existing = definition.clone();
13114 } else {
13115 next_catalog.materialized_views.push(definition.clone());
13116 }
13117 }
13118 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13119 wal_records.push((
13120 WAL_TABLE_ID,
13121 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
13122 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
13123 }),
13124 ));
13125 }
13126
13127 if let Some(control) = control {
13128 if let Err(error) = control.checkpoint() {
13129 drop(wal);
13130 for (_, _, pending) in &prepared_external {
13131 let _ = std::fs::remove_file(pending);
13132 }
13133 return Err(error);
13134 }
13135 }
13136 if let Some(before_commit) = before_commit.as_mut() {
13137 if let Err(error) = before_commit() {
13138 drop(wal);
13139 for (_, _, pending) in &prepared_external {
13140 let _ = std::fs::remove_file(pending);
13141 }
13142 return Err(error);
13143 }
13144 }
13145
13146 let assigned_epoch = self.epoch.bump_assigned();
13147 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
13148 if assigned_epoch != new_epoch {
13149 for (_, _, pending) in &prepared_external {
13150 let _ = std::fs::remove_file(pending);
13151 }
13152 return Err(MongrelError::Conflict(
13153 "commit epoch changed while sequencer lock was held".into(),
13154 ));
13155 }
13156
13157 let commit_ts = match context.read_ts {
13165 Some(read_ts) => self.hlc.commit_timestamp([read_ts]),
13166 None => self.hlc.now().map_err(|skew| {
13167 MongrelError::Other(format!(
13168 "clock skew rejected commit timestamp allocation: {skew}"
13169 ))
13170 })?,
13171 };
13172 if let Err(fault) = mongreldb_fault::inject("txn.decision.before") {
13176 for (_, _, pending) in &prepared_external {
13177 let _ = std::fs::remove_file(pending);
13178 }
13179 return Err(crate::commit_log::fault_as_io(fault));
13180 }
13181 if let Some(state) = &context.state {
13182 state.enter_commit_critical();
13183 }
13184
13185 prepared_run_links.disarm();
13189
13190 let commit_seq = self
13194 .standalone_commit_log
13195 .append_transaction(
13196 &mut wal,
13197 txn_id,
13198 new_epoch,
13199 commit_ts,
13200 wal_records,
13201 &added_runs,
13202 )
13203 .map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
13204
13205 self.conflicts.record(&write_keys, new_epoch);
13210 (
13211 new_epoch,
13212 _epoch_guard,
13213 applies,
13214 committed_materialized_views,
13215 commit_seq,
13216 commit_ts,
13217 )
13218 };
13219 drop(commit_guard);
13220
13221 let receipt =
13223 self.await_durable_commit_with_ts(txn_id, commit_seq, new_epoch, commit_ts)?;
13224 drop(security_guard);
13225
13226 if let Some(request) = &idempotency_request {
13230 if let Err(error) =
13231 self.idempotency
13232 .complete(request, txn_id, new_epoch, receipt.commit_ts)
13233 {
13234 return Err(MongrelError::DurableCommit {
13235 epoch: new_epoch.0,
13236 message: format!("idempotency record was not durable: {error}"),
13237 });
13238 }
13239 if let Some(guard) = idempotency_guard.as_mut() {
13240 guard.disarm();
13241 }
13242 }
13243
13244 let publish_result: Result<()> = {
13246 let mut first_error = None;
13247 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
13248 for run in &spilled {
13249 spilled_by_table.entry(run.table_id).or_default().push(run);
13250 }
13251 let mut modified_tables = Vec::with_capacity(applies.len());
13252 for batch in applies {
13255 #[cfg(test)]
13256 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13257 let mut t = batch.handle.lock();
13258 for op in batch.ops {
13259 match op {
13260 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
13261 StagedOp::Delete(row_ids) => {
13262 for row_id in row_ids {
13263 t.apply_delete(row_id, new_epoch);
13264 }
13265 }
13266 StagedOp::Truncate => t.apply_truncate(new_epoch),
13267 }
13268 }
13269 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
13270 for run in runs {
13271 t.link_run(crate::manifest::RunRef {
13272 run_id: run.run_id,
13273 level: 0,
13274 epoch_created: new_epoch.0,
13275 row_count: run.row_count,
13276 });
13277 t.apply_run_metadata_prepared(&run.rows)?;
13278 if truncated_tables.contains(&batch.table_id) {
13279 t.set_flushed_epoch(new_epoch);
13284 }
13285 }
13286 }
13287 t.invalidate_pending_cache();
13288 drop(t);
13289 modified_tables.push(batch.handle);
13290 }
13291
13292 for handle in modified_tables {
13296 #[cfg(test)]
13297 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
13298 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
13299 first_error.get_or_insert(error);
13300 }
13301 }
13302 for (name, _, pending) in &prepared_external {
13303 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
13304 first_error.get_or_insert(error);
13305 }
13306 }
13307 if !committed_materialized_views.is_empty() {
13308 let mut next_catalog = self.catalog.read().clone();
13309 for definition in committed_materialized_views {
13310 if let Some(existing) = next_catalog
13311 .materialized_views
13312 .iter_mut()
13313 .find(|existing| existing.name == definition.name)
13314 {
13315 *existing = definition;
13316 } else {
13317 next_catalog.materialized_views.push(definition);
13318 }
13319 }
13320 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13321 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
13322 first_error.get_or_insert(error);
13323 }
13324 }
13325 match first_error {
13326 Some(error) => Err(error),
13327 None => Ok(()),
13328 }
13329 };
13330
13331 if has_changes {
13332 let _ = self.change_wake.send(());
13333 }
13334 self.finish_durable_publish(new_epoch, &mut _epoch_guard, &receipt, publish_result)?;
13335 if let Some(state) = &context.state {
13339 state.committed(receipt.clone());
13340 }
13341 mongreldb_fault::inject("txn.decision.after").map_err(|fault| {
13345 MongrelError::DurableCommit {
13346 epoch: new_epoch.0,
13347 message: fault.to_string(),
13348 }
13349 })?;
13350 Ok((new_epoch, committed_row_ids))
13351 }
13352
13353 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
13356 let e = self.epoch.visible();
13357 let g = self.snapshots.register(e);
13358 (Snapshot::at(e), g)
13359 }
13360
13361 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
13364 let e = self.epoch.visible();
13365 let g = self.snapshots.register_owned(e);
13366 (Snapshot::at(e), g)
13367 }
13368
13369 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
13374 let _guard = self.ddl_lock.lock();
13375 let current = self.epoch.visible();
13376 let (old_epochs, old_start) = self.snapshots.history_config();
13377 let earliest_already_guaranteed = if old_epochs == 0 {
13378 current
13379 } else {
13380 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
13381 };
13382 let start = if epochs == 0 {
13383 current
13384 } else {
13385 earliest_already_guaranteed
13386 };
13387 let published = std::cell::Cell::new(false);
13388 let result = write_history_retention(&self.root, epochs, start, || {
13389 self.snapshots.configure_history(epochs, start);
13390 published.set(true);
13391 });
13392 match result {
13393 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
13394 epoch: current.0,
13395 message: format!("history-retention publication was not durable: {error}"),
13396 }),
13397 result => result,
13398 }
13399 }
13400
13401 pub fn history_retention_epochs(&self) -> u64 {
13402 self.snapshots.history_config().0
13403 }
13404
13405 pub fn earliest_retained_epoch(&self) -> Epoch {
13406 let current = self.epoch.visible();
13407 self.snapshots.history_floor(current).unwrap_or(current)
13408 }
13409
13410 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
13413 let current = self.epoch.visible();
13414 if epoch > current {
13415 return Err(MongrelError::InvalidArgument(format!(
13416 "epoch {} is in the future; current epoch is {}",
13417 epoch.0, current.0
13418 )));
13419 }
13420 let earliest = self.earliest_retained_epoch();
13421 if epoch < earliest {
13422 return Err(MongrelError::InvalidArgument(format!(
13423 "epoch {} is no longer retained; earliest available epoch is {}",
13424 epoch.0, earliest.0
13425 )));
13426 }
13427 let guard = self.snapshots.register_owned(epoch);
13428 Ok((Snapshot::at(epoch), guard))
13429 }
13430
13431 pub fn table_names(&self) -> Vec<String> {
13433 self.catalog
13434 .read()
13435 .tables
13436 .iter()
13437 .filter(|t| matches!(t.state, TableState::Live))
13438 .map(|t| t.name.clone())
13439 .collect()
13440 }
13441
13442 pub fn close(&self) -> Result<()> {
13448 for name in self.table_names() {
13449 if let Ok(handle) = self.table(&name) {
13450 if let Err(e) = handle.lock().close() {
13451 eprintln!("[close] flush failed for {name}: {e}");
13452 }
13453 }
13454 }
13455 Ok(())
13456 }
13457
13458 pub fn compact(&self) -> Result<(usize, usize)> {
13465 self.require(&crate::auth::Permission::Ddl)?;
13466 let _operation = self.admit_operation()?;
13468 let mut compacted = 0;
13469 let mut skipped = 0;
13470 for name in self.table_names() {
13471 let Ok(handle) = self.table(&name) else {
13472 continue;
13473 };
13474 {
13475 let mut t = handle.lock();
13476 let before = t.run_count();
13477 if before < 2 && !t.should_compact() {
13478 skipped += 1;
13479 continue;
13480 }
13481 match t.compact() {
13482 Ok(()) => {
13483 let after = t.run_count();
13484 compacted += 1;
13485 eprintln!("[compact] {name}: {before} -> {after} runs");
13486 }
13487 Err(e) => {
13488 eprintln!("[compact] {name}: compaction failed: {e}");
13489 skipped += 1;
13490 }
13491 }
13492 }
13493 }
13494 Ok((compacted, skipped))
13495 }
13496
13497 pub fn compact_table(&self, name: &str) -> Result<bool> {
13500 self.require(&crate::auth::Permission::Ddl)?;
13501 let handle = self.table(name)?;
13502 let mut t = handle.lock();
13503 let before = t.run_count();
13504 if before < 2 {
13505 return Ok(false);
13506 }
13507 t.compact()?;
13508 Ok(t.run_count() < before)
13509 }
13510
13511 pub fn table(&self, name: &str) -> Result<TableHandle> {
13513 self.ensure_owner_process()?;
13514 let cat = self.catalog.read();
13515 let entry = cat
13516 .live(name)
13517 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13518 let id = entry.table_id;
13519 drop(cat);
13520 self.tables
13521 .read()
13522 .get(&id)
13523 .cloned()
13524 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
13525 }
13526
13527 pub fn has_ttl_tables(&self) -> bool {
13530 self.tables
13531 .read()
13532 .values()
13533 .any(|table| table.lock().ttl().is_some())
13534 }
13535
13536 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
13539 self.tables
13540 .read()
13541 .get(&id)
13542 .cloned()
13543 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
13544 }
13545
13546 pub(crate) fn table_auto_inc_would_allocate(
13552 &self,
13553 table_id: u64,
13554 cells: &[(u16, Value)],
13555 ) -> bool {
13556 let catalog = self.catalog.read();
13557 let Some(entry) = catalog
13558 .tables
13559 .iter()
13560 .find(|entry| entry.table_id == table_id)
13561 else {
13562 return false;
13563 };
13564 let Some(column) = entry.schema.auto_increment_column() else {
13565 return false;
13566 };
13567 matches!(
13568 cells.iter().find(|(id, _)| *id == column.id),
13569 None | Some((_, Value::Null))
13570 )
13571 }
13572
13573 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
13579 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
13580 return Err(MongrelError::InvalidArgument(format!(
13581 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
13582 )));
13583 }
13584 self.create_table_with_state(name, schema, TableState::Live)
13585 }
13586
13587 #[doc(hidden)]
13589 pub fn create_building_table(
13590 &self,
13591 build_name: &str,
13592 intended_name: &str,
13593 query_id: &str,
13594 schema: Schema,
13595 ) -> Result<u64> {
13596 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13597 || intended_name.is_empty()
13598 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13599 || query_id.is_empty()
13600 {
13601 return Err(MongrelError::InvalidArgument(
13602 "invalid CTAS building-table identity".into(),
13603 ));
13604 }
13605 self.create_table_with_state(
13606 build_name,
13607 schema,
13608 TableState::Building {
13609 intended_name: intended_name.to_string(),
13610 query_id: query_id.to_string(),
13611 created_at_unix_nanos: current_unix_nanos(),
13612 replaces_table_id: None,
13613 },
13614 )
13615 }
13616
13617 #[doc(hidden)]
13620 pub fn create_rebuilding_table(
13621 &self,
13622 build_name: &str,
13623 intended_name: &str,
13624 query_id: &str,
13625 schema: Schema,
13626 ) -> Result<u64> {
13627 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13628 || intended_name.is_empty()
13629 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13630 || query_id.is_empty()
13631 {
13632 return Err(MongrelError::InvalidArgument(
13633 "invalid rebuilding-table identity".into(),
13634 ));
13635 }
13636 let replaces_table_id = self
13637 .catalog
13638 .read()
13639 .live(intended_name)
13640 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
13641 .table_id;
13642 self.create_table_with_state(
13643 build_name,
13644 schema,
13645 TableState::Building {
13646 intended_name: intended_name.to_string(),
13647 query_id: query_id.to_string(),
13648 created_at_unix_nanos: current_unix_nanos(),
13649 replaces_table_id: Some(replaces_table_id),
13650 },
13651 )
13652 }
13653
13654 fn create_table_with_state(
13655 &self,
13656 name: &str,
13657 schema: Schema,
13658 state: TableState,
13659 ) -> Result<u64> {
13660 use crate::wal::DdlOp;
13661 use std::sync::atomic::Ordering;
13662
13663 self.require(&crate::auth::Permission::Ddl)?;
13664 if self.poisoned.load(Ordering::Relaxed) {
13665 return Err(MongrelError::Other(
13666 "database poisoned by fsync error".into(),
13667 ));
13668 }
13669 let _operation = self.admit_operation()?;
13671 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13672 let _g = self.ddl_lock.lock();
13673 let _security_write = self.security_write()?;
13674 self.require(&crate::auth::Permission::Ddl)?;
13675 {
13676 let cat = self.catalog.read();
13677 match &state {
13678 TableState::Live => {
13679 if cat.live(name).is_some() || cat.building_for(name).is_some() {
13680 return Err(MongrelError::InvalidArgument(format!(
13681 "table {name:?} already exists or is being built"
13682 )));
13683 }
13684 }
13685 TableState::Building {
13686 intended_name,
13687 replaces_table_id,
13688 ..
13689 } => {
13690 let target_matches = match replaces_table_id {
13691 Some(table_id) => cat
13692 .live(intended_name)
13693 .is_some_and(|entry| entry.table_id == *table_id),
13694 None => cat.live(intended_name).is_none(),
13695 };
13696 if !target_matches || cat.building_for(intended_name).is_some() {
13697 return Err(MongrelError::InvalidArgument(format!(
13698 "table {intended_name:?} changed or is already being built"
13699 )));
13700 }
13701 if cat.building(name).is_some() {
13702 return Err(MongrelError::InvalidArgument(format!(
13703 "building table {name:?} already exists"
13704 )));
13705 }
13706 }
13707 TableState::Dropped { .. } => {
13708 return Err(MongrelError::InvalidArgument(
13709 "cannot create a dropped table".into(),
13710 ));
13711 }
13712 }
13713 }
13714
13715 let commit_lock = Arc::clone(&self.commit_lock);
13720 let _c = commit_lock.lock();
13721 let live_create = matches!(state, TableState::Live);
13722 let legacy_table_id = if live_create {
13723 None
13724 } else {
13725 let mut cat = self.catalog.write();
13726 let id = cat.next_table_id;
13727 cat.next_table_id = id
13728 .checked_add(1)
13729 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
13730 Some(id)
13731 };
13732 let epoch = self.epoch.bump_assigned();
13733 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13734 let txn_id = self.alloc_txn_id()?;
13735
13736 let mut schema = schema;
13737 if let Some(table_id) = legacy_table_id {
13738 schema.schema_id = table_id;
13742 schema.validate_auto_increment()?;
13750 schema.validate_defaults()?;
13751 schema.validate_ai()?;
13752 for index in &schema.indexes {
13753 index.validate_options()?;
13754 }
13755 for constraint in &schema.constraints.checks {
13756 constraint.expr.validate()?;
13757 }
13758 }
13759
13760 let mut next_catalog = self.catalog.read().clone();
13761 let (table_id, schema) = match legacy_table_id {
13762 Some(table_id) => {
13763 next_catalog.tables.push(CatalogEntry {
13764 table_id,
13765 name: name.to_string(),
13766 schema: schema.clone(),
13767 state: state.clone(),
13768 created_epoch: epoch.0,
13769 });
13770 (table_id, schema)
13771 }
13772 None => {
13773 let command = crate::catalog_cmds::CatalogCommand::CreateTable {
13777 name: name.to_string(),
13778 schema,
13779 created_epoch: epoch.0,
13780 };
13781 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
13782 let crate::catalog_cmds::CatalogDelta::TableCreated { entry } = delta else {
13783 return Err(MongrelError::Other(
13784 "CreateTable resolved to an unexpected catalog delta".into(),
13785 ));
13786 };
13787 (entry.table_id, entry.schema)
13788 }
13789 };
13790 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
13791
13792 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
13795 let canonical_tdir = self.root.join(&table_relative);
13796 let table_root = Arc::new(
13797 self.durable_root
13798 .create_directory_all_pinned(&table_relative)?,
13799 );
13800 let tdir = table_root.io_path()?;
13801 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
13802 let ctx = SharedCtx {
13803 root_guard: Some(table_root),
13804 epoch: Arc::clone(&self.epoch),
13805 page_cache: Arc::clone(&self.page_cache),
13806 decoded_cache: Arc::clone(&self.decoded_cache),
13807 snapshots: Arc::clone(&self.snapshots),
13808 kek: self.kek.clone(),
13809 commit_lock: Arc::clone(&self.commit_lock),
13810 shared: Some(crate::engine::SharedWalCtx {
13811 wal: Arc::clone(&self.shared_wal),
13812 group: Arc::clone(&self.group),
13813 poisoned: Arc::clone(&self.poisoned),
13814 txn_ids: Arc::clone(&self.next_txn_id),
13815 change_wake: self.change_wake.clone(),
13816 lifecycle: Arc::clone(&self.lifecycle),
13817 }),
13818 table_name: Some(name.to_string()),
13819 auth: self.table_auth_checker(),
13820 read_only: self.read_only,
13821 };
13822 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
13823
13824 let schema_json = DdlOp::encode_schema(&schema)?;
13827 let ddl = match &state {
13828 TableState::Live => DdlOp::CreateTable {
13829 table_id,
13830 name: name.to_string(),
13831 schema_json,
13832 },
13833 TableState::Building {
13834 intended_name,
13835 query_id,
13836 created_at_unix_nanos,
13837 replaces_table_id,
13838 } => match replaces_table_id {
13839 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
13840 table_id,
13841 build_name: name.to_string(),
13842 intended_name: intended_name.clone(),
13843 query_id: query_id.clone(),
13844 created_at_unix_nanos: *created_at_unix_nanos,
13845 replaces_table_id: *replaces_table_id,
13846 schema_json,
13847 },
13848 None => DdlOp::CreateBuildingTable {
13849 table_id,
13850 build_name: name.to_string(),
13851 intended_name: intended_name.clone(),
13852 query_id: query_id.clone(),
13853 created_at_unix_nanos: *created_at_unix_nanos,
13854 schema_json,
13855 },
13856 },
13857 TableState::Dropped { .. } => {
13858 return Err(MongrelError::InvalidArgument(
13859 "cannot create a table in dropped state".into(),
13860 ));
13861 }
13862 };
13863 let commit_seq = {
13864 let mut wal = self.shared_wal.lock();
13865 let append: Result<u64> = (|| {
13866 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
13867 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
13868 wal.append_commit(txn_id, epoch, &[])
13869 })();
13870 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
13871 };
13872 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
13873 pending_table_dir.disarm();
13874
13875 self.tables
13878 .write()
13879 .insert(table_id, TableHandle::new(table));
13880 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
13881 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
13882 Ok(table_id)
13883 }
13884
13885 pub fn drop_table(&self, name: &str) -> Result<()> {
13887 self.drop_table_with_epoch(name).map(|_| ())
13888 }
13889
13890 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
13892 self.drop_table_with_state(name, false, None)
13893 }
13894
13895 pub fn drop_table_with_epoch_controlled<F>(
13896 &self,
13897 name: &str,
13898 mut before_commit: F,
13899 ) -> Result<Epoch>
13900 where
13901 F: FnMut() -> Result<()>,
13902 {
13903 self.drop_table_with_state(name, false, Some(&mut before_commit))
13904 }
13905
13906 #[doc(hidden)]
13908 pub fn discard_building_table(&self, name: &str) -> Result<()> {
13909 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
13910 return Err(MongrelError::InvalidArgument(
13911 "not a CTAS building table".into(),
13912 ));
13913 }
13914 self.drop_table_with_state(name, true, None).map(|_| ())
13915 }
13916
13917 fn drop_table_with_state(
13918 &self,
13919 name: &str,
13920 building: bool,
13921 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
13922 ) -> Result<Epoch> {
13923 use crate::wal::DdlOp;
13924 use std::sync::atomic::Ordering;
13925
13926 self.require(&crate::auth::Permission::Ddl)?;
13927 if self.poisoned.load(Ordering::Relaxed) {
13928 return Err(MongrelError::Other(
13929 "database poisoned by fsync error".into(),
13930 ));
13931 }
13932 let _operation = self.admit_operation()?;
13934 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
13935 let _g = self.ddl_lock.lock();
13936 let _security_write = self.security_write()?;
13937 self.require(&crate::auth::Permission::Ddl)?;
13938 let table_id = {
13939 let cat = self.catalog.read();
13940 if building {
13941 cat.building(name)
13942 } else {
13943 cat.live(name)
13944 }
13945 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
13946 .table_id
13947 };
13948
13949 let commit_lock = Arc::clone(&self.commit_lock);
13950 let _c = commit_lock.lock();
13951 let epoch = self.epoch.bump_assigned();
13952 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
13953 let txn_id = self.alloc_txn_id()?;
13954 let mut next_catalog = self.catalog.read().clone();
13955 if building {
13956 let entry = next_catalog
13959 .tables
13960 .iter_mut()
13961 .find(|t| t.table_id == table_id)
13962 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13963 entry.state = TableState::Dropped { at_epoch: epoch.0 };
13964 next_catalog.triggers.retain(|trigger| {
13965 !matches!(
13966 &trigger.trigger.target,
13967 TriggerTarget::Table(target) if target == name
13968 )
13969 });
13970 next_catalog
13971 .materialized_views
13972 .retain(|definition| definition.name != name);
13973 next_catalog
13974 .security
13975 .rls_tables
13976 .retain(|table| table != name);
13977 next_catalog
13978 .security
13979 .policies
13980 .retain(|policy| policy.table != name);
13981 next_catalog
13982 .security
13983 .masks
13984 .retain(|mask| mask.table != name);
13985 for role in &mut next_catalog.roles {
13986 role.permissions
13987 .retain(|permission| permission_table(permission) != Some(name));
13988 }
13989 advance_security_version(&mut next_catalog)?;
13990 } else {
13991 let command = crate::catalog_cmds::CatalogCommand::DropTable {
13995 name: name.to_string(),
13996 at_epoch: epoch.0,
13997 };
13998 self.apply_catalog_command_to(&mut next_catalog, command)?;
13999 }
14000 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14001 let commit_seq = {
14002 let mut wal = self.shared_wal.lock();
14003 if let Some(before_commit) = before_commit {
14004 before_commit()?;
14005 }
14006 let append: Result<u64> = (|| {
14007 wal.append(
14008 txn_id,
14009 table_id,
14010 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
14011 )?;
14012 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14013 wal.append_commit(txn_id, epoch, &[])
14014 })();
14015 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14016 };
14017 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14018
14019 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14020 self.tables.write().remove(&table_id);
14021 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14022 Ok(epoch)
14023 }
14024
14025 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
14034 self.rename_table_with_epoch(name, new_name).map(|_| ())
14035 }
14036
14037 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
14039 self.rename_table_with_epoch_inner(name, new_name, None)
14040 }
14041
14042 pub fn rename_table_with_epoch_controlled<F>(
14043 &self,
14044 name: &str,
14045 new_name: &str,
14046 mut before_commit: F,
14047 ) -> Result<Epoch>
14048 where
14049 F: FnMut() -> Result<()>,
14050 {
14051 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
14052 }
14053
14054 fn rename_table_with_epoch_inner(
14055 &self,
14056 name: &str,
14057 new_name: &str,
14058 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14059 ) -> Result<Epoch> {
14060 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14061 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14062 {
14063 return Err(MongrelError::InvalidArgument(
14064 "the CTAS building-table namespace is reserved".into(),
14065 ));
14066 }
14067 self.rename_table_with_state(name, new_name, false, None, before_commit)
14068 }
14069
14070 #[doc(hidden)]
14072 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14073 self.publish_building_table_inner(build_name, new_name, None)
14074 }
14075
14076 #[doc(hidden)]
14077 pub fn publish_building_table_controlled<F>(
14078 &self,
14079 build_name: &str,
14080 new_name: &str,
14081 mut before_commit: F,
14082 ) -> Result<Epoch>
14083 where
14084 F: FnMut() -> Result<()>,
14085 {
14086 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
14087 }
14088
14089 fn publish_building_table_inner(
14090 &self,
14091 build_name: &str,
14092 new_name: &str,
14093 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14094 ) -> Result<Epoch> {
14095 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14096 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14097 {
14098 return Err(MongrelError::InvalidArgument(
14099 "invalid CTAS publish identity".into(),
14100 ));
14101 }
14102 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
14103 }
14104
14105 #[doc(hidden)]
14107 pub fn publish_materialized_building_table(
14108 &self,
14109 build_name: &str,
14110 new_name: &str,
14111 definition: crate::catalog::MaterializedViewEntry,
14112 ) -> Result<Epoch> {
14113 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
14114 }
14115
14116 #[doc(hidden)]
14117 pub fn publish_materialized_building_table_controlled<F>(
14118 &self,
14119 build_name: &str,
14120 new_name: &str,
14121 definition: crate::catalog::MaterializedViewEntry,
14122 mut before_commit: F,
14123 ) -> Result<Epoch>
14124 where
14125 F: FnMut() -> Result<()>,
14126 {
14127 self.publish_materialized_building_table_inner(
14128 build_name,
14129 new_name,
14130 definition,
14131 Some(&mut before_commit),
14132 )
14133 }
14134
14135 fn publish_materialized_building_table_inner(
14136 &self,
14137 build_name: &str,
14138 new_name: &str,
14139 definition: crate::catalog::MaterializedViewEntry,
14140 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14141 ) -> Result<Epoch> {
14142 if definition.name != new_name || definition.query.trim().is_empty() {
14143 return Err(MongrelError::InvalidArgument(
14144 "invalid materialized-view publication".into(),
14145 ));
14146 }
14147 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
14148 }
14149
14150 #[doc(hidden)]
14152 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14153 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
14154 }
14155
14156 #[doc(hidden)]
14157 pub fn publish_rebuilding_table_controlled<F>(
14158 &self,
14159 build_name: &str,
14160 new_name: &str,
14161 mut before_commit: F,
14162 ) -> Result<Epoch>
14163 where
14164 F: FnMut() -> Result<()>,
14165 {
14166 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
14167 }
14168
14169 #[doc(hidden)]
14171 pub fn publish_materialized_rebuilding_table_controlled<F>(
14172 &self,
14173 build_name: &str,
14174 new_name: &str,
14175 definition: crate::catalog::MaterializedViewEntry,
14176 mut before_commit: F,
14177 ) -> Result<Epoch>
14178 where
14179 F: FnMut() -> Result<()>,
14180 {
14181 self.publish_rebuilding_table_inner(
14182 build_name,
14183 new_name,
14184 Some(definition),
14185 Some(&mut before_commit),
14186 )
14187 }
14188
14189 fn publish_rebuilding_table_inner(
14190 &self,
14191 build_name: &str,
14192 new_name: &str,
14193 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14194 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14195 ) -> Result<Epoch> {
14196 use crate::wal::DdlOp;
14197
14198 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14199 || new_name.is_empty()
14200 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14201 {
14202 return Err(MongrelError::InvalidArgument(
14203 "invalid rebuilding-table publish identity".into(),
14204 ));
14205 }
14206 if materialized_view.as_ref().is_some_and(|definition| {
14207 definition.name != new_name || definition.query.trim().is_empty()
14208 }) {
14209 return Err(MongrelError::InvalidArgument(
14210 "invalid materialized-view replacement".into(),
14211 ));
14212 }
14213 self.require(&crate::auth::Permission::Ddl)?;
14214 if self.poisoned.load(Ordering::Relaxed) {
14215 return Err(MongrelError::Other(
14216 "database poisoned by fsync error".into(),
14217 ));
14218 }
14219 let _operation = self.admit_operation()?;
14221 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14222 let _ddl = self.ddl_lock.lock();
14223 let _security_write = self.security_write()?;
14224 let (table_id, replaced_table_id) = {
14225 let catalog = self.catalog.read();
14226 let build = catalog.building(build_name).ok_or_else(|| {
14227 MongrelError::NotFound(format!("building table {build_name:?} not found"))
14228 })?;
14229 let replaced_table_id = match &build.state {
14230 TableState::Building {
14231 intended_name,
14232 replaces_table_id: Some(replaced_table_id),
14233 ..
14234 } if intended_name == new_name => *replaced_table_id,
14235 _ => {
14236 return Err(MongrelError::InvalidArgument(format!(
14237 "building table {build_name:?} is not a replacement for {new_name:?}"
14238 )))
14239 }
14240 };
14241 if catalog
14242 .live(new_name)
14243 .is_none_or(|entry| entry.table_id != replaced_table_id)
14244 {
14245 return Err(MongrelError::Conflict(format!(
14246 "table {new_name:?} changed while its replacement was built"
14247 )));
14248 }
14249 (build.table_id, replaced_table_id)
14250 };
14251
14252 let _commit = self.commit_lock.lock();
14253 let epoch = self.epoch.assigned().next();
14254 let txn_id = self.alloc_txn_id()?;
14255 let mut next_catalog = self.catalog.read().clone();
14256 apply_rebuilding_publish(
14257 &mut next_catalog,
14258 table_id,
14259 replaced_table_id,
14260 new_name,
14261 epoch.0,
14262 )?;
14263 if let Some(definition) = materialized_view.as_mut() {
14264 definition.last_refresh_epoch = epoch.0;
14265 }
14266 let materialized_view_json = materialized_view
14267 .as_ref()
14268 .map(DdlOp::encode_materialized_view)
14269 .transpose()?;
14270 if let Some(definition) = materialized_view {
14271 if let Some(existing) = next_catalog
14272 .materialized_views
14273 .iter_mut()
14274 .find(|existing| existing.name == definition.name)
14275 {
14276 *existing = definition;
14277 } else {
14278 next_catalog.materialized_views.push(definition);
14279 }
14280 }
14281 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14282 if let Some(before_commit) = before_commit {
14283 before_commit()?;
14284 }
14285 let assigned_epoch = self.epoch.bump_assigned();
14286 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
14287 if assigned_epoch != epoch {
14288 return Err(MongrelError::Conflict(
14289 "commit epoch changed while sequencer lock was held".into(),
14290 ));
14291 }
14292 let commit_seq = {
14293 let mut wal = self.shared_wal.lock();
14294 let append: Result<u64> = (|| {
14295 wal.append(
14296 txn_id,
14297 table_id,
14298 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
14299 table_id,
14300 replaced_table_id,
14301 new_name: new_name.to_string(),
14302 }),
14303 )?;
14304 if let Some(definition_json) = materialized_view_json {
14305 wal.append(
14306 txn_id,
14307 table_id,
14308 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14309 name: new_name.to_string(),
14310 definition_json,
14311 }),
14312 )?;
14313 }
14314 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14315 wal.append_commit(txn_id, epoch, &[])
14316 })();
14317 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14318 };
14319 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14320
14321 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14322 self.tables.write().remove(&replaced_table_id);
14323 if let Some(table) = self.tables.read().get(&table_id) {
14324 table.lock().set_catalog_name(new_name.to_string());
14325 }
14326 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
14327 Ok(epoch)
14328 }
14329
14330 fn rename_table_with_state(
14331 &self,
14332 name: &str,
14333 new_name: &str,
14334 building: bool,
14335 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14336 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14337 ) -> Result<Epoch> {
14338 use crate::wal::DdlOp;
14339 use std::sync::atomic::Ordering;
14340
14341 self.require(&crate::auth::Permission::Ddl)?;
14342 if self.poisoned.load(Ordering::Relaxed) {
14343 return Err(MongrelError::Other(
14344 "database poisoned by fsync error".into(),
14345 ));
14346 }
14347
14348 if name == new_name {
14351 return Ok(self.visible_epoch());
14352 }
14353 if new_name.is_empty() {
14354 return Err(MongrelError::InvalidArgument(
14355 "rename_table: new name must not be empty".into(),
14356 ));
14357 }
14358 let _operation = self.admit_operation()?;
14360 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14361 let _g = self.ddl_lock.lock();
14362 let _security_write = self.security_write()?;
14363 self.require(&crate::auth::Permission::Ddl)?;
14364 let table_id = {
14365 let cat = self.catalog.read();
14366 let src = if building {
14367 cat.building(name)
14368 } else {
14369 cat.live(name)
14370 }
14371 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14372 if building
14373 && !matches!(
14374 &src.state,
14375 TableState::Building { intended_name, .. } if intended_name == new_name
14376 )
14377 {
14378 return Err(MongrelError::InvalidArgument(format!(
14379 "building table {name:?} is not reserved for {new_name:?}"
14380 )));
14381 }
14382 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
14386 return Err(MongrelError::InvalidArgument(format!(
14387 "rename_table: a table named {new_name:?} already exists"
14388 )));
14389 }
14390 src.table_id
14391 };
14392
14393 let commit_lock = Arc::clone(&self.commit_lock);
14394 let _c = commit_lock.lock();
14395 let epoch = self.epoch.bump_assigned();
14396 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14397 let txn_id = self.alloc_txn_id()?;
14398 if let Some(definition) = materialized_view.as_mut() {
14399 definition.last_refresh_epoch = epoch.0;
14400 }
14401 let materialized_view_json = materialized_view
14402 .as_ref()
14403 .map(DdlOp::encode_materialized_view)
14404 .transpose()?;
14405 let mut next_catalog = self.catalog.read().clone();
14406 if building {
14407 let entry = next_catalog
14410 .tables
14411 .iter_mut()
14412 .find(|t| t.table_id == table_id)
14413 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14414 entry.name = new_name.to_string();
14415 entry.state = TableState::Live;
14416 for trigger in &mut next_catalog.triggers {
14417 if matches!(
14418 &trigger.trigger.target,
14419 TriggerTarget::Table(target) if target == name
14420 ) {
14421 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
14422 }
14423 }
14424 if let Some(definition) = next_catalog
14425 .materialized_views
14426 .iter_mut()
14427 .find(|definition| definition.name == name)
14428 {
14429 definition.name = new_name.to_string();
14430 }
14431 if let Some(definition) = materialized_view.take() {
14432 next_catalog.materialized_views.push(definition);
14433 }
14434 for table in &mut next_catalog.security.rls_tables {
14435 if table == name {
14436 *table = new_name.to_string();
14437 }
14438 }
14439 for policy in &mut next_catalog.security.policies {
14440 if policy.table == name {
14441 policy.table = new_name.to_string();
14442 }
14443 }
14444 for mask in &mut next_catalog.security.masks {
14445 if mask.table == name {
14446 mask.table = new_name.to_string();
14447 }
14448 }
14449 for role in &mut next_catalog.roles {
14450 for permission in &mut role.permissions {
14451 rename_permission_table(permission, name, new_name);
14452 }
14453 }
14454 advance_security_version(&mut next_catalog)?;
14455 } else {
14456 let command = crate::catalog_cmds::CatalogCommand::RenameTable {
14460 name: name.to_string(),
14461 new_name: new_name.to_string(),
14462 at_epoch: epoch.0,
14463 };
14464 self.apply_catalog_command_to(&mut next_catalog, command)?;
14465 }
14466 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14467 let ddl = if building {
14468 DdlOp::PublishBuildingTable {
14469 table_id,
14470 new_name: new_name.to_string(),
14471 }
14472 } else {
14473 DdlOp::RenameTable {
14474 table_id,
14475 new_name: new_name.to_string(),
14476 }
14477 };
14478 let commit_seq = {
14479 let mut wal = self.shared_wal.lock();
14480 if let Some(before_commit) = before_commit {
14481 before_commit()?;
14482 }
14483 let append: Result<u64> = (|| {
14484 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
14485 if let Some(definition_json) = materialized_view_json {
14486 wal.append(
14487 txn_id,
14488 table_id,
14489 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14490 name: new_name.to_string(),
14491 definition_json,
14492 }),
14493 )?;
14494 }
14495 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14496 wal.append_commit(txn_id, epoch, &[])
14497 })();
14498 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14499 };
14500 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14501
14502 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14503 if let Some(table) = self.tables.read().get(&table_id) {
14506 table.lock().set_catalog_name(new_name.to_string());
14507 }
14508 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14509 Ok(epoch)
14510 }
14511
14512 pub fn add_column(
14518 &self,
14519 table_name: &str,
14520 name: &str,
14521 ty: TypeId,
14522 flags: ColumnFlags,
14523 default_value: Option<crate::schema::DefaultExpr>,
14524 ) -> Result<ColumnDef> {
14525 self.add_column_with_id(table_name, name, ty, flags, default_value, None)
14526 }
14527
14528 pub fn add_column_with_id(
14530 &self,
14531 table_name: &str,
14532 name: &str,
14533 ty: TypeId,
14534 flags: ColumnFlags,
14535 default_value: Option<crate::schema::DefaultExpr>,
14536 requested_id: Option<u16>,
14537 ) -> Result<ColumnDef> {
14538 use crate::wal::DdlOp;
14539 use std::sync::atomic::Ordering;
14540
14541 self.require(&crate::auth::Permission::Ddl)?;
14542 if self.poisoned.load(Ordering::Relaxed) {
14543 return Err(MongrelError::Other(
14544 "database poisoned by fsync error".into(),
14545 ));
14546 }
14547 let _operation = self.admit_operation()?;
14548 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14549 let _ddl = self.ddl_lock.lock();
14550 let _security_write = self.security_write()?;
14551 self.require(&crate::auth::Permission::Ddl)?;
14552 let table_id = {
14553 let catalog = self.catalog.read();
14554 catalog
14555 .live(table_name)
14556 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
14557 .table_id
14558 };
14559 let handle =
14560 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
14561 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
14562 })?;
14563 let durable_epoch = std::cell::Cell::new(None);
14564 let result: Result<ColumnDef> = (|| {
14565 let mut table = handle.lock();
14566 let (column, prepared_schema) =
14567 table.prepare_add_column(name, ty, flags, default_value, requested_id)?;
14568 let command = crate::catalog_cmds::CatalogCommand::AddColumn {
14569 table: table_name.to_string(),
14570 column: column.clone(),
14571 };
14572 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
14573
14574 let commit_lock = Arc::clone(&self.commit_lock);
14575 let _commit = commit_lock.lock();
14576 let epoch = self.epoch.bump_assigned();
14577 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14578 let txn_id = self.alloc_txn_id()?;
14579 let column_json = DdlOp::encode_column(&column)?;
14580 let mut next_catalog = self.catalog.read().clone();
14581 let catalog_entry_index = next_catalog
14582 .tables
14583 .iter()
14584 .position(|entry| entry.table_id == table_id)
14585 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
14586 self.apply_catalog_command_to(&mut next_catalog, command)?;
14587 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
14588 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14589 let commit_seq = {
14590 let mut wal = self.shared_wal.lock();
14591 let append: Result<u64> = (|| {
14592 wal.append(
14593 txn_id,
14594 table_id,
14595 crate::wal::Op::Ddl(DdlOp::AlterTable {
14596 table_id,
14597 column_json,
14598 }),
14599 )?;
14600 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14601 wal.append_commit(txn_id, epoch, &[])
14602 })();
14603 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14604 };
14605 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14606 durable_epoch.set(Some(epoch));
14607
14608 table.apply_altered_schema_prepared(prepared_schema);
14609 let schema = table.schema().clone();
14610 let table_checkpoint = table.checkpoint_altered_schema();
14611 drop(table);
14612 next_catalog.tables[catalog_entry_index].schema = schema;
14613 let catalog_result =
14614 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
14615 *self.catalog.write() = next_catalog;
14616 self.publish_committed(&receipt, epoch)?;
14617 epoch_guard.disarm();
14618 if let Err(error) = table_checkpoint.and(catalog_result) {
14619 self.poisoned.store(true, Ordering::Relaxed);
14620 self.lifecycle.poison();
14621 return Err(MongrelError::DurableCommit {
14622 epoch: epoch.0,
14623 message: error.to_string(),
14624 });
14625 }
14626 Ok(column)
14627 })();
14628 result.map_err(|error| match (durable_epoch.get(), error) {
14629 (_, error @ MongrelError::DurableCommit { .. }) => error,
14630 (Some(epoch), error) => MongrelError::DurableCommit {
14631 epoch: epoch.0,
14632 message: error.to_string(),
14633 },
14634 (None, error) => error,
14635 })
14636 }
14637
14638 pub fn alter_column(
14639 &self,
14640 table_name: &str,
14641 column_name: &str,
14642 change: AlterColumn,
14643 ) -> Result<ColumnDef> {
14644 self.alter_column_with_epoch(table_name, column_name, change)
14645 .map(|(column, _)| column)
14646 }
14647
14648 pub fn alter_column_with_epoch(
14649 &self,
14650 table_name: &str,
14651 column_name: &str,
14652 change: AlterColumn,
14653 ) -> Result<(ColumnDef, Option<Epoch>)> {
14654 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
14655 }
14656
14657 pub fn alter_column_with_epoch_controlled<B, A>(
14662 &self,
14663 table_name: &str,
14664 column_name: &str,
14665 change: AlterColumn,
14666 control: &crate::ExecutionControl,
14667 mut before_commit: B,
14668 mut after_commit: A,
14669 ) -> Result<(ColumnDef, Option<Epoch>)>
14670 where
14671 B: FnMut() -> Result<()>,
14672 A: FnMut(Option<Epoch>) -> Result<()>,
14673 {
14674 self.alter_column_with_epoch_inner(
14675 table_name,
14676 column_name,
14677 change,
14678 Some(control),
14679 Some(&mut before_commit),
14680 Some(&mut after_commit),
14681 )
14682 }
14683
14684 #[allow(clippy::too_many_arguments)]
14685 fn alter_column_with_epoch_inner(
14686 &self,
14687 table_name: &str,
14688 column_name: &str,
14689 change: AlterColumn,
14690 control: Option<&crate::ExecutionControl>,
14691 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14692 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
14693 ) -> Result<(ColumnDef, Option<Epoch>)> {
14694 use crate::wal::DdlOp;
14695 use std::sync::atomic::Ordering;
14696
14697 self.require(&crate::auth::Permission::Ddl)?;
14698 commit_prepare_checkpoint(control, 0)?;
14699 if self.poisoned.load(Ordering::Relaxed) {
14700 return Err(MongrelError::Other(
14701 "database poisoned by fsync error".into(),
14702 ));
14703 }
14704 let _operation = self.admit_operation()?;
14706 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14707 let _g = self.ddl_lock.lock();
14708 let table_id = {
14709 let cat = self.catalog.read();
14710 cat.live(table_name)
14711 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
14712 .table_id
14713 };
14714 let handle =
14715 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
14716 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
14717 })?;
14718
14719 let backfill = {
14725 let table = handle.lock();
14726 let old = table
14727 .schema()
14728 .column(column_name)
14729 .cloned()
14730 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
14731 let next_flags = change.flags.unwrap_or(old.flags);
14732 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
14733 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
14734 && old.default_value.is_some()
14735 {
14736 let snapshot = Snapshot::at(self.epoch.visible());
14737 let mut updates = Vec::new();
14738 let rows = match control {
14739 Some(control) => table.visible_rows_controlled(snapshot, control)?,
14740 None => table.visible_rows(snapshot)?,
14741 };
14742 for (row_index, row) in rows.into_iter().enumerate() {
14743 commit_prepare_checkpoint(control, row_index)?;
14744 if row
14745 .columns
14746 .get(&old.id)
14747 .is_some_and(|value| !matches!(value, Value::Null))
14748 {
14749 continue;
14750 }
14751 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
14752 table.apply_defaults(&mut cells)?;
14753 updates.push((
14754 table_id,
14755 crate::txn::Staged::Update {
14756 row_id: row.row_id,
14757 new_row: cells,
14758 changed_columns: vec![old.id],
14759 },
14760 ));
14761 }
14762 updates
14763 } else {
14764 Vec::new()
14765 }
14766 };
14767 let durable_epoch = std::cell::Cell::new(None);
14768 let backfill_epoch = if backfill.is_empty() {
14769 None
14770 } else {
14771 let (principal, catalog_bound) = self.transaction_principal_snapshot();
14772 let txn_id = self.alloc_txn_id()?;
14773 let mut entered_fence = false;
14774 let commit_result = match (control, before_commit.as_deref_mut()) {
14775 (Some(control), Some(before_commit)) => self
14776 .commit_transaction_with_external_states_controlled(
14777 txn_id,
14778 self.epoch.visible(),
14779 backfill,
14780 Vec::new(),
14781 Vec::new(),
14782 principal.clone(),
14783 catalog_bound,
14784 None,
14785 crate::txn::TxnCommitContext::internal(),
14786 control,
14787 &mut || {
14788 before_commit()?;
14789 entered_fence = true;
14790 Ok(())
14791 },
14792 )
14793 .map(|(epoch, _)| epoch),
14794 _ => self
14795 .commit_transaction_with_external_states(
14796 txn_id,
14797 self.epoch.visible(),
14798 backfill,
14799 Vec::new(),
14800 Vec::new(),
14801 principal,
14802 catalog_bound,
14803 None,
14804 crate::txn::TxnCommitContext::internal(),
14805 )
14806 .map(|(epoch, _)| epoch),
14807 };
14808 let commit_result = if entered_fence {
14809 finish_controlled_commit_attempt(commit_result, &mut after_commit)
14810 } else {
14811 commit_result
14812 };
14813 match &commit_result {
14814 Ok(epoch) => durable_epoch.set(Some(*epoch)),
14815 Err(MongrelError::DurableCommit { epoch, .. }) => {
14816 durable_epoch.set(Some(Epoch(*epoch)));
14817 }
14818 Err(_) => {}
14819 }
14820 Some(commit_result?)
14821 };
14822 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
14823 let _security_write = self.security_write()?;
14824 self.require(&crate::auth::Permission::Ddl)?;
14825 if self
14826 .catalog
14827 .read()
14828 .live(table_name)
14829 .is_none_or(|entry| entry.table_id != table_id)
14830 {
14831 return Err(MongrelError::Conflict(format!(
14832 "table {table_name:?} changed during ALTER"
14833 )));
14834 }
14835 let mut table = handle.lock();
14836 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
14837 let renamed_column = (column.name != column_name).then(|| column.name.clone());
14838 let Some(prepared_schema) = prepared_schema else {
14839 return Ok((column, backfill_epoch));
14840 };
14841 let command = crate::catalog_cmds::CatalogCommand::AlterColumn {
14846 table: table_name.to_string(),
14847 column: column.clone(),
14848 };
14849 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
14850
14851 let commit_lock = Arc::clone(&self.commit_lock);
14852 let _c = commit_lock.lock();
14853 let epoch = self.epoch.bump_assigned();
14854 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14855 let txn_id = self.alloc_txn_id()?;
14856 let column_json = DdlOp::encode_column(&column)?;
14857 let mut next_catalog = self.catalog.read().clone();
14858 let catalog_entry_index = next_catalog
14859 .tables
14860 .iter()
14861 .position(|entry| entry.table_id == table_id)
14862 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
14863 if let Some(new_column_name) = &renamed_column {
14864 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
14865 commit_prepare_checkpoint(control, trigger_index)?;
14866 if matches!(
14867 &trigger.trigger.target,
14868 TriggerTarget::Table(target) if target == table_name
14869 ) {
14870 trigger.trigger = trigger.trigger.renamed_update_column(
14871 column_name,
14872 new_column_name.clone(),
14873 epoch.0,
14874 )?;
14875 }
14876 }
14877 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
14878 commit_prepare_checkpoint(control, role_index)?;
14879 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
14880 commit_prepare_checkpoint(control, permission_index)?;
14881 rename_permission_column(
14882 permission,
14883 table_name,
14884 column_name,
14885 new_column_name,
14886 );
14887 }
14888 }
14889 advance_security_version(&mut next_catalog)?;
14890 }
14891 self.apply_catalog_command_to(&mut next_catalog, command)?;
14897 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
14898 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14899 commit_prepare_checkpoint(control, 0)?;
14900 let mut entered_fence = false;
14901 if let Some(before_commit) = before_commit.as_deref_mut() {
14902 before_commit()?;
14903 entered_fence = true;
14904 }
14905 let commit_result: Result<Epoch> = (|| {
14906 let commit_seq = {
14907 let mut wal = self.shared_wal.lock();
14908 let append: Result<u64> = (|| {
14909 wal.append(
14910 txn_id,
14911 table_id,
14912 crate::wal::Op::Ddl(DdlOp::AlterTable {
14913 table_id,
14914 column_json,
14915 }),
14916 )?;
14917 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14918 wal.append_commit(txn_id, epoch, &[])
14919 })();
14920 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14921 };
14922 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14923 durable_epoch.set(Some(epoch));
14924
14925 table.apply_altered_schema_prepared(prepared_schema);
14926 let schema = table.schema().clone();
14927 let table_checkpoint = table.checkpoint_altered_schema();
14928 drop(table);
14929 next_catalog.tables[catalog_entry_index].schema = schema;
14930 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14931 let catalog_result =
14932 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
14933 let security_version = next_catalog.security_version;
14934 *self.catalog.write() = next_catalog;
14935 if renamed_column.is_some() {
14936 self.security_coordinator
14937 .version
14938 .store(security_version, Ordering::Release);
14939 }
14940 self.publish_committed(&receipt, epoch)?;
14941 _epoch_guard.disarm();
14942 if let Err(error) = table_checkpoint.and(catalog_result) {
14943 self.poisoned.store(true, Ordering::Relaxed);
14944 self.lifecycle.poison();
14945 return Err(MongrelError::DurableCommit {
14946 epoch: epoch.0,
14947 message: error.to_string(),
14948 });
14949 }
14950 Ok(epoch)
14951 })();
14952 let commit_result = if entered_fence {
14953 finish_controlled_commit_attempt(commit_result, &mut after_commit)
14954 } else {
14955 commit_result
14956 };
14957 let epoch = commit_result?;
14958 Ok((column, Some(epoch)))
14959 })();
14960 result.map_err(|error| match (durable_epoch.get(), error) {
14961 (_, error @ MongrelError::DurableCommit { .. }) => error,
14962 (Some(epoch), error) => MongrelError::DurableCommit {
14963 epoch: epoch.0,
14964 message: error.to_string(),
14965 },
14966 (None, error) => error,
14967 })
14968 }
14969
14970 pub fn set_table_ttl(
14973 &self,
14974 table_name: &str,
14975 column_name: &str,
14976 duration_nanos: u64,
14977 ) -> Result<crate::manifest::TtlPolicy> {
14978 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
14979 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
14980 }
14981
14982 #[doc(hidden)]
14984 pub fn set_building_table_ttl(
14985 &self,
14986 table_name: &str,
14987 column_name: &str,
14988 duration_nanos: u64,
14989 ) -> Result<crate::manifest::TtlPolicy> {
14990 let policy = self.replace_table_ttl_with_state(
14991 table_name,
14992 Some((column_name, duration_nanos)),
14993 true,
14994 )?;
14995 policy
14996 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
14997 }
14998
14999 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
15000 self.replace_table_ttl(table_name, None)?;
15001 Ok(())
15002 }
15003
15004 fn replace_table_ttl(
15005 &self,
15006 table_name: &str,
15007 requested: Option<(&str, u64)>,
15008 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15009 self.replace_table_ttl_with_state(table_name, requested, false)
15010 }
15011
15012 fn replace_table_ttl_with_state(
15013 &self,
15014 table_name: &str,
15015 requested: Option<(&str, u64)>,
15016 building: bool,
15017 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15018 use crate::wal::DdlOp;
15019 use std::sync::atomic::Ordering;
15020
15021 self.require(&crate::auth::Permission::Ddl)?;
15022 if self.poisoned.load(Ordering::Relaxed) {
15023 return Err(MongrelError::Other(
15024 "database poisoned by fsync error".into(),
15025 ));
15026 }
15027
15028 let _g = self.ddl_lock.lock();
15029 let _security_write = self.security_write()?;
15030 self.require(&crate::auth::Permission::Ddl)?;
15031 let table_id = {
15032 let cat = self.catalog.read();
15033 if building {
15034 cat.building(table_name)
15035 } else {
15036 cat.live(table_name)
15037 }
15038 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15039 .table_id
15040 };
15041 let handle =
15042 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15043 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15044 })?;
15045 let mut table = handle.lock();
15046 let policy = match requested {
15047 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
15048 None => None,
15049 };
15050 if table.ttl() == policy {
15051 return Ok(policy);
15052 }
15053
15054 let commit_lock = Arc::clone(&self.commit_lock);
15055 let _c = commit_lock.lock();
15056 let epoch = self.epoch.bump_assigned();
15057 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15058 let txn_id = self.alloc_txn_id()?;
15059 let policy_json = DdlOp::encode_ttl(policy)?;
15060 let mut next_catalog = self.catalog.read().clone();
15061 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15062 let commit_seq = {
15063 let mut wal = self.shared_wal.lock();
15064 let append: Result<u64> = (|| {
15065 wal.append(
15066 txn_id,
15067 table_id,
15068 crate::wal::Op::Ddl(DdlOp::SetTtl {
15069 table_id,
15070 policy_json,
15071 }),
15072 )?;
15073 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15074 wal.append_commit(txn_id, epoch, &[])
15075 })();
15076 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15077 };
15078 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15079
15080 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
15081 drop(table);
15082 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
15083 publish_error.get_or_insert(error);
15084 }
15085 self.finish_durable_publish(
15086 epoch,
15087 &mut _epoch_guard,
15088 &receipt,
15089 publish_error.map_or(Ok(()), Err),
15090 )?;
15091 Ok(policy)
15092 }
15093
15094 pub fn gc(&self) -> Result<usize> {
15100 let control = crate::ExecutionControl::new(None);
15101 self.gc_controlled(&control, || true)
15102 }
15103
15104 #[doc(hidden)]
15107 pub fn gc_controlled<F>(
15108 &self,
15109 control: &crate::ExecutionControl,
15110 before_publish: F,
15111 ) -> Result<usize>
15112 where
15113 F: FnOnce() -> bool,
15114 {
15115 self.gc_controlled_with_receipt(control, before_publish)
15116 .map(|(reclaimed, _)| reclaimed)
15117 }
15118
15119 #[doc(hidden)]
15122 pub fn gc_controlled_with_receipt<F>(
15123 &self,
15124 control: &crate::ExecutionControl,
15125 before_publish: F,
15126 ) -> Result<(usize, Option<MaintenanceReceipt>)>
15127 where
15128 F: FnOnce() -> bool,
15129 {
15130 enum Candidate {
15131 Directory(PathBuf),
15132 File(PathBuf),
15133 }
15134
15135 self.require(&crate::auth::Permission::Ddl)?;
15136 let _operation = self.admit_operation()?;
15138 let _ddl = self.ddl_lock.lock();
15139 self.require(&crate::auth::Permission::Ddl)?;
15140 control.checkpoint()?;
15141 let maintenance_epoch = self.epoch.visible();
15142 let min_active = self.snapshots.min_active(maintenance_epoch).0;
15143 let mut candidates = Vec::new();
15144
15145 let cat = self.catalog.read();
15147 for (entry_index, entry) in cat.tables.iter().enumerate() {
15148 if entry_index % 256 == 0 {
15149 control.checkpoint()?;
15150 }
15151 if let TableState::Dropped { at_epoch } = entry.state {
15152 if at_epoch <= min_active {
15153 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
15154 if tdir.exists() {
15155 candidates.push(Candidate::Directory(tdir));
15156 }
15157 }
15158 }
15159 }
15160 drop(cat);
15161
15162 let cat = self.catalog.read();
15167 for (entry_index, entry) in cat.tables.iter().enumerate() {
15168 if entry_index % 256 == 0 {
15169 control.checkpoint()?;
15170 }
15171 if !matches!(entry.state, TableState::Live) {
15172 continue;
15173 }
15174 let txn_dir = self
15175 .root
15176 .join(TABLES_DIR)
15177 .join(entry.table_id.to_string())
15178 .join("_txn");
15179 if !txn_dir.exists() {
15180 continue;
15181 }
15182 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
15183 if sub_index % 256 == 0 {
15184 control.checkpoint()?;
15185 }
15186 let sub = sub?;
15187 let name = sub.file_name();
15188 let Some(name) = name.to_str() else { continue };
15189 let is_active = name
15191 .parse::<u64>()
15192 .map(|id| self.active_spills.is_active(id))
15193 .unwrap_or(false);
15194 if is_active {
15195 continue;
15196 }
15197 candidates.push(Candidate::Directory(sub.path()));
15198 }
15199 }
15200 drop(cat);
15201
15202 let external_names = {
15203 let cat = self.catalog.read();
15204 cat.external_tables
15205 .iter()
15206 .map(|entry| entry.name.clone())
15207 .collect::<std::collections::HashSet<_>>()
15208 };
15209 let vtab_dir = self.root.join(VTAB_DIR);
15210 if vtab_dir.exists() {
15211 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
15212 if entry_index % 256 == 0 {
15213 control.checkpoint()?;
15214 }
15215 let entry = entry?;
15216 let name = entry.file_name();
15217 let Some(name) = name.to_str() else { continue };
15218 if external_names.contains(name) {
15219 continue;
15220 }
15221 let path = entry.path();
15222 if path.is_dir() {
15223 candidates.push(Candidate::Directory(path));
15224 } else {
15225 candidates.push(Candidate::File(path));
15226 }
15227 }
15228 }
15229
15230 let tables = self
15234 .tables
15235 .read()
15236 .iter()
15237 .map(|(table_id, handle)| (*table_id, handle.clone()))
15238 .collect::<Vec<_>>();
15239 let mut retiring = Vec::new();
15240 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
15241 if table_index % 256 == 0 {
15242 control.checkpoint()?;
15243 }
15244 let backup_pinned: HashSet<u128> = self
15245 .backup_pins
15246 .lock()
15247 .keys()
15248 .filter_map(|(pinned_table, run_id)| {
15249 (*pinned_table == *table_id).then_some(*run_id)
15250 })
15251 .collect();
15252 if handle
15253 .lock()
15254 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
15255 {
15256 retiring.push((handle.clone(), backup_pinned));
15257 }
15258 }
15259
15260 let all_durable = self.active_spills.is_idle()
15268 && tables.iter().all(|(_, handle)| {
15269 let g = handle.lock();
15270 g.memtable_len() == 0 && g.mutable_run_len() == 0
15271 });
15272 let retain = self
15273 .replication_wal_retention_segments
15274 .load(std::sync::atomic::Ordering::Relaxed);
15275 let reap_wal = all_durable
15276 && self
15277 .shared_wal
15278 .lock()
15279 .has_gc_segments_retain_recent(retain)?;
15280
15281 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
15282 return Ok((0, None));
15283 }
15284 control.checkpoint()?;
15285 if !before_publish() {
15286 return Err(MongrelError::Cancelled);
15287 }
15288
15289 let mut reclaimed = 0;
15290 for candidate in candidates {
15291 match candidate {
15292 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
15293 Candidate::File(path) => std::fs::remove_file(path)?,
15294 }
15295 reclaimed += 1;
15296 }
15297 for (handle, backup_pinned) in retiring {
15298 reclaimed += handle
15299 .lock()
15300 .reap_retiring(Epoch(min_active), &backup_pinned)?;
15301 }
15302 if reap_wal {
15303 reclaimed += self
15304 .shared_wal
15305 .lock()
15306 .gc_segments_retain_recent(u64::MAX, retain)?;
15307 }
15308
15309 Ok((
15310 reclaimed,
15311 Some(MaintenanceReceipt {
15312 epoch: maintenance_epoch,
15313 }),
15314 ))
15315 }
15316
15317 pub fn checkpoint(&self) -> Result<()> {
15335 self.checkpoint_controlled(|| Ok(()))
15336 }
15337
15338 #[doc(hidden)]
15341 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
15342 where
15343 F: FnOnce() -> Result<()>,
15344 {
15345 self.require(&crate::auth::Permission::Ddl)?;
15346 let _operation = self.admit_operation()?;
15348 let _replication = self.replication_barrier.write();
15352 let _ddl = self.ddl_lock.lock();
15353 let _security = self.security_coordinator.gate.read();
15354 self.require(&crate::auth::Permission::Ddl)?;
15355
15356 let mut handles = self
15357 .tables
15358 .read()
15359 .iter()
15360 .map(|(table_id, handle)| (*table_id, handle.clone()))
15361 .collect::<Vec<_>>();
15362 handles.sort_by_key(|(table_id, _)| *table_id);
15363 let mut tables = handles
15364 .iter()
15365 .map(|(table_id, handle)| (*table_id, handle.lock()))
15366 .collect::<Vec<_>>();
15367
15368 for (_, table) in &mut tables {
15370 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
15371 {
15372 table.force_flush()?;
15373 }
15374 }
15375
15376 for (_, table) in &mut tables {
15379 if table.run_count() >= 2 || table.should_compact() {
15380 table.compact()?;
15381 }
15382 }
15383
15384 before_wal_reset()?;
15385
15386 let maintenance_epoch = self.epoch.visible();
15388 let min_active = self.snapshots.min_active(maintenance_epoch);
15389 for (table_id, table) in &mut tables {
15390 let backup_pinned: HashSet<u128> = self
15391 .backup_pins
15392 .lock()
15393 .keys()
15394 .filter_map(|(pinned_table, run_id)| {
15395 (*pinned_table == *table_id).then_some(*run_id)
15396 })
15397 .collect();
15398 table.reap_retiring(min_active, &backup_pinned)?;
15399 }
15400
15401 self.shared_wal.lock().reset_after_checkpoint()?;
15404
15405 let catalog_snapshot = self.catalog.read().clone();
15407 for entry in &catalog_snapshot.tables {
15408 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
15409 crate::durable_file::remove_directory_all(
15410 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
15411 )?;
15412 }
15413 if !matches!(entry.state, TableState::Live) {
15414 continue;
15415 }
15416 let transaction_dir = self
15417 .root
15418 .join(TABLES_DIR)
15419 .join(entry.table_id.to_string())
15420 .join("_txn");
15421 if transaction_dir.is_dir() {
15422 for child in std::fs::read_dir(&transaction_dir)? {
15423 let child = child?;
15424 let active = child
15425 .file_name()
15426 .to_str()
15427 .and_then(|name| name.parse::<u64>().ok())
15428 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
15429 if !active {
15430 crate::durable_file::remove_directory_all(&child.path())?;
15431 }
15432 }
15433 }
15434 }
15435 let external_names = catalog_snapshot
15436 .external_tables
15437 .iter()
15438 .map(|entry| entry.name.as_str())
15439 .collect::<HashSet<_>>();
15440 let external_root = self.root.join(VTAB_DIR);
15441 if external_root.is_dir() {
15442 for entry in std::fs::read_dir(&external_root)? {
15443 let entry = entry?;
15444 let name = entry.file_name();
15445 if name
15446 .to_str()
15447 .is_some_and(|name| external_names.contains(name))
15448 {
15449 continue;
15450 }
15451 if entry.file_type()?.is_dir() {
15452 crate::durable_file::remove_directory_all(&entry.path())?;
15453 } else {
15454 std::fs::remove_file(entry.path())?;
15455 crate::durable_file::sync_directory(&external_root)?;
15456 }
15457 }
15458 }
15459
15460 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
15463 let visible = self.epoch.visible();
15464 for (_, table) in &tables {
15465 table.persist_manifest(visible)?;
15466 }
15467
15468 Ok(())
15469 }
15470 fn alloc_txn_id(&self) -> Result<u64> {
15471 self.ensure_owner_process()?;
15472 crate::txn::allocate_txn_id(&self.next_txn_id)
15473 }
15474
15475 pub fn allocate_lock_txn_id(&self) -> Result<u64> {
15479 self.alloc_txn_id()
15480 }
15481
15482 pub fn set_spill_threshold(&self, bytes: u64) {
15486 self.spill_threshold
15487 .store(bytes, std::sync::atomic::Ordering::Relaxed);
15488 }
15489
15490 #[doc(hidden)]
15494 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15495 *self.spill_hook.lock() = Some(Box::new(f));
15496 }
15497
15498 #[doc(hidden)]
15501 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15502 *self.security_commit_hook.lock() = Some(Box::new(f));
15503 }
15504
15505 #[doc(hidden)]
15508 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15509 *self.catalog_commit_hook.lock() = Some(Box::new(f));
15510 }
15511
15512 #[doc(hidden)]
15515 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15516 *self.backup_hook.lock() = Some(Box::new(f));
15517 }
15518
15519 #[doc(hidden)]
15523 pub fn __set_fk_lock_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15524 *self.fk_lock_hook.lock() = Some(Arc::new(f));
15525 }
15526
15527 #[doc(hidden)]
15529 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15530 *self.replication_hook.lock() = Some(Box::new(f));
15531 }
15532
15533 #[doc(hidden)]
15537 pub fn __wal_group_sync_count(&self) -> u64 {
15538 self.shared_wal.lock().group_sync_count()
15539 }
15540
15541 #[doc(hidden)]
15544 pub fn __poison(&self) {
15545 self.poisoned
15546 .store(true, std::sync::atomic::Ordering::Relaxed);
15547 }
15548
15549 pub fn check(&self) -> Vec<CheckIssue> {
15562 match self.check_inner(None) {
15563 Ok(issues) => issues,
15564 Err(error) => vec![CheckIssue {
15565 table_id: WAL_TABLE_ID,
15566 table_name: "shared WAL".into(),
15567 severity: "error".into(),
15568 description: error.to_string(),
15569 }],
15570 }
15571 }
15572
15573 #[doc(hidden)]
15575 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
15576 self.check_inner(Some(control))
15577 }
15578
15579 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
15580 let mut issues = Vec::new();
15581 let io_root = self.durable_root.io_path()?;
15582 let cat = self.catalog.read();
15583 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
15584 for (table_index, entry) in cat.tables.iter().enumerate() {
15585 if table_index % 256 == 0 {
15586 if let Some(control) = control {
15587 control.checkpoint()?;
15588 }
15589 }
15590 if !matches!(entry.state, TableState::Live) {
15591 continue;
15592 }
15593 let tdir = io_root.join(TABLES_DIR).join(entry.table_id.to_string());
15594 let mut err = |sev: &str, desc: String| {
15595 issues.push(CheckIssue {
15596 table_id: entry.table_id,
15597 table_name: entry.name.clone(),
15598 severity: sev.into(),
15599 description: desc,
15600 });
15601 };
15602 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
15603 Ok(m) => m,
15604 Err(e) => {
15605 err("error", format!("manifest read failed: {e}"));
15606 continue;
15607 }
15608 };
15609 if m.flushed_epoch > m.current_epoch {
15610 err(
15611 "error",
15612 format!(
15613 "flushed_epoch {} exceeds current_epoch {} (impossible)",
15614 m.flushed_epoch, m.current_epoch
15615 ),
15616 );
15617 }
15618
15619 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
15620 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
15621 for (run_index, rr) in m.runs.iter().enumerate() {
15622 if run_index % 256 == 0 {
15623 if let Some(control) = control {
15624 control.checkpoint()?;
15625 }
15626 }
15627 referenced.insert(rr.run_id);
15628 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
15629 if !run_path.exists() {
15630 err("error", format!("missing run file: r-{}.sr", rr.run_id));
15631 continue;
15632 }
15633 match crate::sorted_run::RunReader::open(
15634 &run_path,
15635 entry.schema.clone(),
15636 self.kek.clone(),
15637 ) {
15638 Ok(reader) => {
15639 if reader.row_count() as u64 != rr.row_count {
15640 err(
15641 "error",
15642 format!(
15643 "run r-{} row count mismatch: manifest {} vs run {}",
15644 rr.run_id,
15645 rr.row_count,
15646 reader.row_count()
15647 ),
15648 );
15649 }
15650 }
15651 Err(e) => {
15652 err(
15653 "error",
15654 format!("run r-{} integrity check failed: {e}", rr.run_id),
15655 );
15656 }
15657 }
15658 }
15659
15660 for r in &m.retiring {
15664 referenced.insert(r.run_id);
15665 }
15666
15667 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
15669 for (entry_index, ent) in rd.flatten().enumerate() {
15670 if entry_index % 256 == 0 {
15671 if let Some(control) = control {
15672 control.checkpoint()?;
15673 }
15674 }
15675 let p = ent.path();
15676 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
15677 continue;
15678 }
15679 let run_id = p
15680 .file_stem()
15681 .and_then(|s| s.to_str())
15682 .and_then(|s| s.strip_prefix("r-"))
15683 .and_then(|s| s.parse::<u128>().ok());
15684 if let Some(id) = run_id {
15685 if !referenced.contains(&id) {
15686 err(
15687 "warning",
15688 format!("orphan run file r-{id}.sr not referenced by the manifest"),
15689 );
15690 }
15691 }
15692 }
15693 }
15694 }
15695
15696 let external_names = cat
15697 .external_tables
15698 .iter()
15699 .map(|entry| entry.name.clone())
15700 .collect::<std::collections::HashSet<_>>();
15701 let vtab_dir = io_root.join(VTAB_DIR);
15702 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
15703 for (entry_index, entry) in entries.flatten().enumerate() {
15704 if entry_index % 256 == 0 {
15705 if let Some(control) = control {
15706 control.checkpoint()?;
15707 }
15708 }
15709 let name = entry.file_name();
15710 let Some(name) = name.to_str() else { continue };
15711 if !external_names.contains(name) {
15712 issues.push(CheckIssue {
15713 table_id: EXTERNAL_TABLE_ID,
15714 table_name: name.to_string(),
15715 severity: "warning".into(),
15716 description: format!(
15717 "orphan external table state entry {:?} not referenced by the catalog",
15718 entry.path()
15719 ),
15720 });
15721 }
15722 }
15723 }
15724
15725 if let Some(control) = control {
15732 control.checkpoint()?;
15733 }
15734 for (seg, msg) in self.shared_wal.lock().verify_segments() {
15735 issues.push(CheckIssue {
15736 table_id: WAL_TABLE_ID,
15737 table_name: "<wal>".into(),
15738 severity: "error".into(),
15739 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
15740 });
15741 }
15742 Ok(issues)
15743 }
15744
15745 pub fn doctor(&self) -> Result<Vec<u64>> {
15749 let control = crate::ExecutionControl::new(None);
15750 self.doctor_controlled(&control, || true)
15751 }
15752
15753 #[doc(hidden)]
15757 pub fn doctor_controlled<F>(
15758 &self,
15759 control: &crate::ExecutionControl,
15760 before_publish: F,
15761 ) -> Result<Vec<u64>>
15762 where
15763 F: FnOnce() -> bool,
15764 {
15765 self.doctor_controlled_with_receipt(control, before_publish)
15766 .map(|(quarantined, _)| quarantined)
15767 }
15768
15769 #[doc(hidden)]
15772 pub fn doctor_controlled_with_receipt<F>(
15773 &self,
15774 control: &crate::ExecutionControl,
15775 before_publish: F,
15776 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
15777 where
15778 F: FnOnce() -> bool,
15779 {
15780 let _ddl = self.ddl_lock.lock();
15783 let _security_write = self.security_write()?;
15784 let issues = self.check_inner(Some(control))?;
15785 let bad_tables: std::collections::HashSet<u64> = issues
15790 .iter()
15791 .filter(|i| {
15792 i.severity == "error"
15793 && i.table_id != WAL_TABLE_ID
15794 && i.table_id != EXTERNAL_TABLE_ID
15795 })
15796 .map(|i| i.table_id)
15797 .collect();
15798 if bad_tables.is_empty() {
15799 return Ok((Vec::new(), None));
15800 }
15801 let _commit = self.commit_lock.lock();
15802 control.checkpoint()?;
15803 if !before_publish() {
15804 return Err(MongrelError::Cancelled);
15805 }
15806 let maintenance_epoch = self.epoch.bump_assigned();
15807 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
15808
15809 let qdir = self.root.join("_quarantine");
15810 crate::durable_file::create_directory(&qdir)?;
15811 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
15812 bad_tables.sort_unstable();
15813
15814 let mut handles = self
15818 .tables
15819 .read()
15820 .iter()
15821 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
15822 .map(|(table_id, handle)| (*table_id, handle.clone()))
15823 .collect::<Vec<_>>();
15824 handles.sort_by_key(|(table_id, _)| *table_id);
15825 let mut table_guards = handles
15826 .iter()
15827 .map(|(table_id, handle)| (*table_id, handle.lock()))
15828 .collect::<Vec<_>>();
15829
15830 let mut next_catalog = self.catalog.read().clone();
15831 for table_id in &bad_tables {
15832 if let Some(entry) = next_catalog
15833 .tables
15834 .iter_mut()
15835 .find(|entry| entry.table_id == *table_id)
15836 {
15837 entry.state = TableState::Dropped {
15838 at_epoch: maintenance_epoch.0,
15839 };
15840 }
15841 }
15842 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
15843
15844 let txn_id = self.alloc_txn_id()?;
15845 let commit_seq = {
15846 let mut wal = self.shared_wal.lock();
15847 let append: Result<u64> = (|| {
15848 for table_id in &bad_tables {
15849 wal.append(
15850 txn_id,
15851 *table_id,
15852 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
15853 table_id: *table_id,
15854 }),
15855 )?;
15856 }
15857 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15858 wal.append_commit(txn_id, maintenance_epoch, &[])
15859 })();
15860 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
15861 };
15862 let receipt = self.await_durable_commit(txn_id, commit_seq, maintenance_epoch)?;
15863 for (_, table) in &mut table_guards {
15864 table.mark_unavailable_after_quarantine();
15865 }
15866 {
15867 let mut live_tables = self.tables.write();
15868 for table_id in &bad_tables {
15869 live_tables.remove(table_id);
15870 }
15871 }
15872 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
15873 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, &receipt, checkpoint)?;
15874
15875 drop(table_guards);
15879 drop(handles);
15880
15881 for table_id in &bad_tables {
15885 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
15886 if source.exists() {
15887 let destination = qdir.join(table_id.to_string());
15888 if let Err(error) = crate::durable_file::rename(&source, &destination) {
15889 return Err(MongrelError::DurableCommit {
15890 epoch: maintenance_epoch.0,
15891 message: format!(
15892 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
15893 ),
15894 });
15895 }
15896 }
15897 }
15898 Ok((
15899 bad_tables,
15900 Some(MaintenanceReceipt {
15901 epoch: maintenance_epoch,
15902 }),
15903 ))
15904 }
15905
15906 #[allow(dead_code)]
15908 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
15909 self.kek.as_ref()
15910 }
15911
15912 #[allow(dead_code)]
15914 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
15915 &self.epoch
15916 }
15917
15918 #[allow(dead_code)]
15920 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
15921 &self.snapshots
15922 }
15923}
15924
15925fn external_state_dir(root: &Path, name: &str) -> PathBuf {
15926 root.join(VTAB_DIR).join(name)
15927}
15928
15929fn append_catalog_snapshot(
15930 wal: &mut crate::wal::SharedWal,
15931 txn_id: u64,
15932 catalog: &Catalog,
15933) -> Result<()> {
15934 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
15935 wal.append(
15936 txn_id,
15937 WAL_TABLE_ID,
15938 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
15939 )?;
15940 Ok(())
15941}
15942
15943fn filter_ignored_staging(
15944 staging: Vec<(u64, crate::txn::Staged)>,
15945 ignored_indices: &std::collections::BTreeSet<usize>,
15946) -> Vec<(u64, crate::txn::Staged)> {
15947 if ignored_indices.is_empty() {
15948 return staging;
15949 }
15950 staging
15951 .into_iter()
15952 .enumerate()
15953 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
15954 .collect()
15955}
15956
15957fn external_state_file(root: &Path, name: &str) -> PathBuf {
15958 external_state_dir(root, name).join("state.json")
15959}
15960
15961fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
15962 let path = external_state_file(root, name);
15963 match std::fs::read(path) {
15964 Ok(bytes) => Ok(bytes),
15965 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
15966 Err(e) => Err(e.into()),
15967 }
15968}
15969
15970fn current_external_state_bytes(
15971 root: &Path,
15972 external_states: &[(String, Vec<u8>)],
15973 name: &str,
15974) -> Result<Vec<u8>> {
15975 for (table, state) in external_states.iter().rev() {
15976 if table == name {
15977 return Ok(state.clone());
15978 }
15979 }
15980 read_external_state_file(root, name)
15981}
15982
15983fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
15984 let mut out = external_states;
15985 dedup_external_states_in_place(&mut out);
15986 out
15987}
15988
15989fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
15990 let mut seen = std::collections::HashSet::new();
15991 let mut out = Vec::with_capacity(external_states.len());
15992 for (name, state) in std::mem::take(external_states).into_iter().rev() {
15993 if seen.insert(name.clone()) {
15994 out.push((name, state));
15995 }
15996 }
15997 out.reverse();
15998 *external_states = out;
15999}
16000
16001fn prepare_external_state_file(
16002 root: &Path,
16003 name: &str,
16004 state: &[u8],
16005 txn_id: u64,
16006) -> Result<PathBuf> {
16007 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
16008 let dir = external_state_dir(root, name);
16009 crate::durable_file::create_directory(&dir)?;
16010 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
16011 {
16012 let mut file = std::fs::OpenOptions::new()
16013 .create_new(true)
16014 .write(true)
16015 .open(&pending)?;
16016 file.write_all(state)?;
16017 file.sync_all()?;
16018 }
16019 Ok(pending)
16020}
16021
16022fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
16023 let path = external_state_file(root, name);
16024 crate::durable_file::replace(pending, &path)?;
16025 Ok(())
16026}
16027
16028fn write_external_state_file(
16029 durable: &crate::durable_file::DurableRoot,
16030 name: &str,
16031 state: &[u8],
16032) -> Result<()> {
16033 let directory = Path::new(VTAB_DIR).join(name);
16034 durable.create_directory_all(&directory)?;
16035 durable.write_atomic(directory.join("state.json"), state)?;
16036 Ok(())
16037}
16038
16039fn validate_recovered_data_table(
16040 catalog: &Catalog,
16041 tables: &HashMap<u64, TableHandle>,
16042 table_id: u64,
16043 commit_epoch: u64,
16044 offset: u64,
16045) -> Result<bool> {
16046 let entry = catalog
16047 .tables
16048 .iter()
16049 .find(|entry| entry.table_id == table_id)
16050 .ok_or_else(|| MongrelError::CorruptWal {
16051 offset,
16052 reason: format!("committed record references unknown table {table_id}"),
16053 })?;
16054 if commit_epoch < entry.created_epoch {
16055 return Err(MongrelError::CorruptWal {
16056 offset,
16057 reason: format!(
16058 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16059 entry.created_epoch
16060 ),
16061 });
16062 }
16063 match entry.state {
16064 TableState::Dropped { at_epoch } => {
16065 let abandoned_build_boundary =
16070 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16071 if commit_epoch >= at_epoch && !abandoned_build_boundary {
16072 Err(MongrelError::CorruptWal {
16073 offset,
16074 reason: format!(
16075 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16076 ),
16077 })
16078 } else {
16079 Ok(false)
16080 }
16081 }
16082 TableState::Live | TableState::Building { .. } => {
16083 if tables.contains_key(&table_id) {
16084 Ok(true)
16085 } else {
16086 Err(MongrelError::CorruptWal {
16087 offset,
16088 reason: format!("live table {table_id} has no mounted recovery handle"),
16089 })
16090 }
16091 }
16092 }
16093}
16094
16095type RecoveryTableStage = (
16096 Vec<crate::memtable::Row>,
16097 Vec<(crate::rowid::RowId, Epoch)>,
16098 Option<Epoch>,
16099 Epoch,
16100);
16101
16102#[derive(Clone)]
16103struct RecoveryValidationTable {
16104 schema: Schema,
16105 flushed_epoch: u64,
16106}
16107
16108fn validate_shared_wal_recovery_plan(
16109 durable_root: &crate::durable_file::DurableRoot,
16110 catalog: &Catalog,
16111 recovered_table_ids: &HashSet<u64>,
16112 reconciled_table_ids: &HashSet<u64>,
16113 meta_dek: Option<&[u8; META_DEK_LEN]>,
16114 kek: Option<Arc<crate::encryption::Kek>>,
16115 records: &[crate::wal::Record],
16116) -> Result<()> {
16117 use crate::wal::{DdlOp, Op};
16118
16119 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
16120 for entry in &catalog.tables {
16121 if !matches!(entry.state, TableState::Live) {
16122 continue;
16123 }
16124 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
16125 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
16126 Ok(manifest) => Some(manifest),
16127 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
16128 Err(error) => return Err(error),
16129 };
16130 let flushed_epoch = if let Some(manifest) = manifest {
16131 if manifest.table_id != entry.table_id {
16132 return Err(MongrelError::Conflict(format!(
16133 "catalog table {} storage identity mismatch",
16134 entry.table_id
16135 )));
16136 }
16137 if (manifest.schema_id != entry.schema.schema_id
16138 && !reconciled_table_ids.contains(&entry.table_id))
16139 || manifest.flushed_epoch > manifest.current_epoch
16140 || manifest.global_idx_epoch > manifest.current_epoch
16141 || manifest.next_row_id == u64::MAX
16142 || manifest.auto_inc_next < 0
16143 || manifest.auto_inc_next == i64::MAX
16144 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
16145 {
16146 return Err(MongrelError::InvalidArgument(format!(
16147 "table {} manifest counters or schema identity are invalid",
16148 entry.table_id
16149 )));
16150 }
16151 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
16152 crate::global_idx::read_durable_for(
16153 durable_root,
16154 &relative_dir,
16155 entry.table_id,
16156 &entry.schema,
16157 idx_dek.as_deref(),
16158 )?;
16159 let mut run_ids = HashSet::new();
16160 let mut maximum_row_id = None::<u64>;
16161 for run in &manifest.runs {
16162 if run.run_id >= u64::MAX as u128
16163 || run.epoch_created > manifest.current_epoch
16164 || !run_ids.insert(run.run_id)
16165 {
16166 return Err(MongrelError::InvalidArgument(format!(
16167 "table {} manifest contains an invalid or duplicate run id",
16168 entry.table_id
16169 )));
16170 }
16171 let relative = relative_dir
16172 .join(crate::engine::RUNS_DIR)
16173 .join(format!("r-{}.sr", run.run_id as u64));
16174 let file = durable_root.open_regular(&relative)?;
16175 let mut reader = crate::sorted_run::RunReader::open_file(
16176 file,
16177 entry.schema.clone(),
16178 kek.clone(),
16179 )?;
16180 let header = reader.header();
16181 if header.run_id != run.run_id
16182 || header.level != run.level
16183 || header.row_count != run.row_count
16184 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
16185 || header.is_uniform_epoch() && header.epoch_created != 0
16186 || header.schema_id > entry.schema.schema_id
16187 {
16188 return Err(MongrelError::InvalidArgument(format!(
16189 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
16190 entry.table_id,
16191 run.run_id,
16192 header.run_id,
16193 header.level,
16194 header.row_count,
16195 header.epoch_created,
16196 header.schema_id,
16197 run.run_id,
16198 run.level,
16199 run.row_count,
16200 run.epoch_created,
16201 entry.schema.schema_id,
16202 )));
16203 }
16204 if header.row_count != 0 {
16205 maximum_row_id = Some(
16206 maximum_row_id
16207 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
16208 );
16209 }
16210 reader.validate_all_pages()?;
16211 }
16212 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
16213 return Err(MongrelError::InvalidArgument(format!(
16214 "table {} next_row_id does not advance beyond persisted rows",
16215 entry.table_id
16216 )));
16217 }
16218 for run in &manifest.retiring {
16219 if run.run_id >= u64::MAX as u128
16220 || run.retire_epoch > manifest.current_epoch
16221 || !run_ids.insert(run.run_id)
16222 {
16223 return Err(MongrelError::InvalidArgument(format!(
16224 "table {} manifest contains an invalid or aliased retired run",
16225 entry.table_id
16226 )));
16227 }
16228 }
16229 manifest.flushed_epoch
16230 } else {
16231 if !recovered_table_ids.contains(&entry.table_id) {
16232 return Err(MongrelError::NotFound(format!(
16233 "live table {} manifest is missing",
16234 entry.table_id
16235 )));
16236 }
16237 0
16238 };
16239 tables.insert(
16240 entry.table_id,
16241 RecoveryValidationTable {
16242 schema: entry.schema.clone(),
16243 flushed_epoch,
16244 },
16245 );
16246 }
16247
16248 let committed = records
16249 .iter()
16250 .filter_map(|record| match record.op {
16251 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
16252 _ => None,
16253 })
16254 .collect::<HashMap<_, _>>();
16255 let mut run_ids = HashSet::new();
16256 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
16257 for record in records {
16258 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
16259 continue;
16260 };
16261 match &record.op {
16262 Op::Put { table_id, rows } => {
16263 let table = validate_recovery_data_table_plan(
16264 catalog,
16265 &tables,
16266 *table_id,
16267 commit_epoch,
16268 record.seq.0,
16269 )?;
16270 let decoded: Vec<crate::memtable::Row> =
16271 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
16272 offset: record.seq.0,
16273 reason: format!(
16274 "committed Put payload for transaction {} could not be decoded: {error}",
16275 record.txn_id
16276 ),
16277 })?;
16278 if let Some(table) = table {
16279 for row in &decoded {
16280 if !recovered_row_ids
16281 .entry(*table_id)
16282 .or_default()
16283 .insert(row.row_id.0)
16284 {
16285 return Err(MongrelError::CorruptWal {
16286 offset: record.seq.0,
16287 reason: format!(
16288 "committed WAL repeats recovered row id {} for table {table_id}",
16289 row.row_id.0
16290 ),
16291 });
16292 }
16293 validate_recovered_row(&table.schema, row)?;
16294 }
16295 }
16296 }
16297 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
16298 validate_recovery_data_table_plan(
16299 catalog,
16300 &tables,
16301 *table_id,
16302 commit_epoch,
16303 record.seq.0,
16304 )?;
16305 }
16306 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
16307 Op::Ddl(DdlOp::ResetExternalTableState {
16308 name,
16309 generation_epoch,
16310 }) => {
16311 if *generation_epoch != commit_epoch {
16312 return Err(MongrelError::CorruptWal {
16313 offset: record.seq.0,
16314 reason: format!(
16315 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
16316 ),
16317 });
16318 }
16319 validate_recovered_external_name(name)?;
16320 }
16321 Op::TxnCommit { added_runs, .. } => {
16322 for added in added_runs {
16323 let Some(table) = validate_recovery_data_table_plan(
16324 catalog,
16325 &tables,
16326 added.table_id,
16327 commit_epoch,
16328 record.seq.0,
16329 )?
16330 else {
16331 continue;
16332 };
16333 if added.run_id >= u64::MAX as u128
16334 || !run_ids.insert((added.table_id, added.run_id))
16335 {
16336 return Err(MongrelError::CorruptWal {
16337 offset: record.seq.0,
16338 reason: format!(
16339 "duplicate or invalid recovered run {} for table {}",
16340 added.run_id, added.table_id
16341 ),
16342 });
16343 }
16344 if commit_epoch <= table.flushed_epoch {
16345 continue;
16346 }
16347 validate_planned_spilled_run(
16348 durable_root,
16349 record.txn_id,
16350 commit_epoch,
16351 added,
16352 &table.schema,
16353 kek.clone(),
16354 )?;
16355 }
16356 }
16357 _ => {}
16358 }
16359 }
16360 Ok(())
16361}
16362
16363fn validate_recovery_data_table_plan<'a>(
16364 catalog: &Catalog,
16365 tables: &'a HashMap<u64, RecoveryValidationTable>,
16366 table_id: u64,
16367 commit_epoch: u64,
16368 offset: u64,
16369) -> Result<Option<&'a RecoveryValidationTable>> {
16370 let entry = catalog
16371 .tables
16372 .iter()
16373 .find(|entry| entry.table_id == table_id)
16374 .ok_or_else(|| MongrelError::CorruptWal {
16375 offset,
16376 reason: format!("committed record references unknown table {table_id}"),
16377 })?;
16378 if commit_epoch < entry.created_epoch {
16379 return Err(MongrelError::CorruptWal {
16380 offset,
16381 reason: format!(
16382 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16383 entry.created_epoch
16384 ),
16385 });
16386 }
16387 match entry.state {
16388 TableState::Dropped { at_epoch } => {
16389 let abandoned =
16390 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16391 if commit_epoch >= at_epoch && !abandoned {
16392 return Err(MongrelError::CorruptWal {
16393 offset,
16394 reason: format!(
16395 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16396 ),
16397 });
16398 }
16399 Ok(None)
16400 }
16401 TableState::Live => {
16402 tables
16403 .get(&table_id)
16404 .map(Some)
16405 .ok_or_else(|| MongrelError::CorruptWal {
16406 offset,
16407 reason: format!("live table {table_id} has no recovery plan"),
16408 })
16409 }
16410 TableState::Building { .. } => Err(MongrelError::CorruptWal {
16411 offset,
16412 reason: format!("building table {table_id} was not normalized before recovery"),
16413 }),
16414 }
16415}
16416
16417fn validate_planned_spilled_run(
16418 root: &crate::durable_file::DurableRoot,
16419 txn_id: u64,
16420 commit_epoch: u64,
16421 added: &crate::wal::AddedRun,
16422 schema: &Schema,
16423 kek: Option<Arc<crate::encryption::Kek>>,
16424) -> Result<()> {
16425 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
16426 let destination = table
16427 .join(crate::engine::RUNS_DIR)
16428 .join(format!("r-{}.sr", added.run_id as u64));
16429 let pending = table
16430 .join("_txn")
16431 .join(txn_id.to_string())
16432 .join(format!("r-{}.sr", added.run_id as u64));
16433 let file = match root.open_regular(&destination) {
16434 Ok(file) => file,
16435 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
16436 root.open_regular(&pending).map_err(|pending_error| {
16437 if pending_error.kind() == std::io::ErrorKind::NotFound {
16438 MongrelError::CorruptWal {
16439 offset: commit_epoch,
16440 reason: format!(
16441 "committed spilled run {} for transaction {txn_id} is missing",
16442 added.run_id
16443 ),
16444 }
16445 } else {
16446 pending_error.into()
16447 }
16448 })?
16449 }
16450 Err(error) => return Err(error.into()),
16451 };
16452 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
16453 let header = reader.header();
16454 if header.run_id != added.run_id
16455 || header.content_hash != added.content_hash
16456 || header.row_count != added.row_count
16457 || header.level != added.level
16458 || header.min_row_id != added.min_row_id
16459 || header.max_row_id != added.max_row_id
16460 || header.schema_id != schema.schema_id
16461 || !header.is_uniform_epoch()
16462 || header.epoch_created != 0
16463 {
16464 return Err(MongrelError::CorruptWal {
16465 offset: commit_epoch,
16466 reason: format!(
16467 "committed spilled run {} metadata differs from WAL",
16468 added.run_id
16469 ),
16470 });
16471 }
16472 reader.validate_all_pages()?;
16473 Ok(())
16474}
16475
16476#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16496pub enum StagedTxnWrite {
16497 Put {
16502 table_id: u64,
16504 rows: Vec<u8>,
16506 },
16507 Delete {
16509 table_id: u64,
16511 row_ids: Vec<u64>,
16513 },
16514}
16515
16516impl StagedTxnWrite {
16517 pub fn encode(&self) -> Result<Vec<u8>> {
16519 Ok(bincode::serialize(self)?)
16520 }
16521
16522 pub fn decode(bytes: &[u8]) -> Result<Self> {
16524 Ok(bincode::deserialize(bytes)?)
16525 }
16526}
16527
16528pub fn translate_records_for_replication(
16565 records: &[crate::wal::Record],
16566) -> Result<Vec<crate::wal::Record>> {
16567 use crate::wal::Op;
16568
16569 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
16572 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
16573 })?;
16574 if records.iter().any(|record| record.txn_id != txn_id) {
16575 return Err(MongrelError::InvalidArgument(
16576 "replicated transaction payload mixes transaction ids".into(),
16577 ));
16578 }
16579 let commits = records
16580 .iter()
16581 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
16582 .count();
16583 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
16584 return Err(MongrelError::InvalidArgument(
16585 "replicated transaction payload must end in exactly one commit marker".into(),
16586 ));
16587 }
16588
16589 let mut spilled_rows: HashMap<u64, Vec<crate::memtable::Row>> = HashMap::new();
16593 for record in records {
16594 if let Op::SpilledRows { table_id, rows } = &record.op {
16595 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(rows).map_err(|error| {
16596 MongrelError::InvalidArgument(format!(
16597 "spilled row payload for table {table_id} cannot decode for replication: \
16598 {error}"
16599 ))
16600 })?;
16601 spilled_rows.entry(*table_id).or_default().extend(chunk);
16602 }
16603 }
16604
16605 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
16609 unreachable!("one trailing commit marker validated above");
16610 };
16611 for run in added_runs {
16612 let rows = spilled_rows.get(&run.table_id).ok_or_else(|| {
16613 MongrelError::InvalidArgument(format!(
16614 "commit links spilled run {} for table {} but carries no logical row records \
16615 for it",
16616 run.run_id, run.table_id
16617 ))
16618 })?;
16619 let covered = rows
16620 .iter()
16621 .filter(|row| row.row_id.0 >= run.min_row_id && row.row_id.0 <= run.max_row_id)
16622 .count() as u64;
16623 if covered != run.row_count {
16624 return Err(MongrelError::InvalidArgument(format!(
16625 "commit links spilled run {} for table {} ({} rows in [{}, {}]) but the logical \
16626 row records cover {} rows",
16627 run.run_id, run.table_id, run.row_count, run.min_row_id, run.max_row_id, covered
16628 )));
16629 }
16630 }
16631
16632 let translated = records
16635 .iter()
16636 .map(|record| {
16637 let op = match &record.op {
16638 Op::SpilledRows { table_id, rows } => Op::Put {
16639 table_id: *table_id,
16640 rows: rows.clone(),
16641 },
16642 Op::TxnCommit { epoch, .. } => Op::TxnCommit {
16643 epoch: *epoch,
16644 added_runs: Vec::new(),
16645 },
16646 op => op.clone(),
16647 };
16648 crate::wal::Record::new(record.seq, record.txn_id, op)
16649 })
16650 .collect();
16651 Ok(translated)
16652}
16653
16654fn recover_shared_wal(
16655 durable_root: &crate::durable_file::DurableRoot,
16656 tables: &HashMap<u64, TableHandle>,
16657 catalog: &Catalog,
16658 epoch: &EpochAuthority,
16659 records: &[crate::wal::Record],
16660) -> Result<()> {
16661 use crate::memtable::Row;
16662 use crate::wal::{DdlOp, Op};
16663
16664 let mut committed: HashMap<u64, u64> = HashMap::new();
16666 let mut spilled_to_link: Vec<(
16667 u64, u64, Vec<crate::wal::AddedRun>,
16670 )> = Vec::new();
16671 for r in records {
16672 if let Op::TxnCommit {
16673 epoch: ce,
16674 ref added_runs,
16675 } = r.op
16676 {
16677 committed.insert(r.txn_id, ce);
16678 if !added_runs.is_empty() {
16679 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
16680 }
16681 }
16682 }
16683 for record in records {
16684 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
16685 continue;
16686 };
16687 match &record.op {
16688 Op::Put { table_id, .. }
16689 | Op::Delete { table_id, .. }
16690 | Op::TruncateTable { table_id } => {
16691 validate_recovered_data_table(
16692 catalog,
16693 tables,
16694 *table_id,
16695 commit_epoch,
16696 record.seq.0,
16697 )?;
16698 }
16699 Op::TxnCommit { added_runs, .. } => {
16700 for run in added_runs {
16701 validate_recovered_data_table(
16702 catalog,
16703 tables,
16704 run.table_id,
16705 commit_epoch,
16706 record.seq.0,
16707 )?;
16708 }
16709 }
16710 _ => {}
16711 }
16712 }
16713 let truncated_transactions: HashSet<(u64, u64)> = records
16714 .iter()
16715 .filter_map(|record| {
16716 committed.get(&record.txn_id)?;
16717 match record.op {
16718 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
16719 _ => None,
16720 }
16721 })
16722 .collect();
16723
16724 enum ExternalRecoveryAction {
16726 Write { name: String, state: Vec<u8> },
16727 Reset { name: String },
16728 }
16729 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
16730 let mut external_actions = Vec::new();
16731 let mut max_epoch = epoch.visible().0;
16732 for r in records.iter().cloned() {
16733 let Some(&ce) = committed.get(&r.txn_id) else {
16734 continue; };
16736 let commit_epoch = Epoch(ce);
16737 max_epoch = max_epoch.max(ce);
16738 match r.op {
16739 Op::Put { table_id, rows } => {
16740 let skip = tables
16742 .get(&table_id)
16743 .map(|h| h.lock().flushed_epoch() >= ce)
16744 .unwrap_or(true);
16745 if skip {
16746 continue;
16747 }
16748 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
16749 MongrelError::CorruptWal {
16750 offset: r.seq.0,
16751 reason: format!(
16752 "committed Put payload for transaction {} could not be decoded: {error}",
16753 r.txn_id
16754 ),
16755 }
16756 })?;
16757 let rows: Vec<Row> = rows
16760 .into_iter()
16761 .map(|mut row| {
16762 row.committed_epoch = commit_epoch;
16763 row
16764 })
16765 .collect();
16766 let entry = stage
16767 .entry(table_id)
16768 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
16769 entry.0.extend(rows);
16770 entry.3 = commit_epoch;
16771 }
16772 Op::Delete { table_id, row_ids } => {
16773 let skip = tables
16774 .get(&table_id)
16775 .map(|h| h.lock().flushed_epoch() >= ce)
16776 .unwrap_or(true);
16777 if skip {
16778 continue;
16779 }
16780 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
16781 let entry = stage
16782 .entry(table_id)
16783 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
16784 entry.1.extend(dels);
16785 entry.3 = commit_epoch;
16786 }
16787 Op::TruncateTable { table_id } => {
16788 let skip = tables
16789 .get(&table_id)
16790 .map(|h| h.lock().flushed_epoch() >= ce)
16791 .unwrap_or(true);
16792 if skip {
16793 continue;
16794 }
16795 stage.insert(
16796 table_id,
16797 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
16798 );
16799 }
16800 Op::ExternalTableState { name, state } => {
16801 let current_generation = catalog
16802 .external_tables
16803 .iter()
16804 .find(|entry| entry.name == name)
16805 .map(|entry| entry.created_epoch);
16806 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
16807 validate_recovered_external_name(&name)?;
16808 external_actions.push(ExternalRecoveryAction::Write { name, state });
16809 }
16810 }
16811 Op::Ddl(DdlOp::ResetExternalTableState {
16812 name,
16813 generation_epoch,
16814 }) => {
16815 if generation_epoch != ce {
16816 return Err(MongrelError::CorruptWal {
16817 offset: r.seq.0,
16818 reason: format!(
16819 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
16820 ),
16821 });
16822 }
16823 validate_recovered_external_name(&name)?;
16824 external_actions.push(ExternalRecoveryAction::Reset { name });
16825 }
16826 Op::Flush { .. }
16827 | Op::TxnCommit { .. }
16828 | Op::TxnAbort
16829 | Op::Ddl(_)
16830 | Op::BeforeImage { .. }
16831 | Op::CommitTimestamp { .. }
16832 | Op::SpilledRows { .. } => {}
16833 }
16834 }
16835 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
16836 added_runs.retain(|added| {
16837 tables
16838 .get(&added.table_id)
16839 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
16840 });
16841 }
16842 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
16843 validate_recovery_table_stages(tables, &stage)?;
16844 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
16845
16846 for action in external_actions {
16850 match action {
16851 ExternalRecoveryAction::Write { name, state } => {
16852 write_external_state_file(durable_root, &name, &state)?;
16853 }
16854 ExternalRecoveryAction::Reset { name } => {
16855 durable_root.create_directory_all(VTAB_DIR)?;
16856 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
16857 }
16858 }
16859 }
16860 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
16861 let Some(handle) = tables.get(&table_id) else {
16862 continue;
16863 };
16864 let mut t = handle.lock();
16865 if let Some(epoch) = truncate_epoch {
16866 t.apply_truncate(epoch);
16867 }
16868 t.recover_apply(rows, deletes)?;
16869 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
16873 t.live_count = rows.len() as u64;
16874 t.persist_manifest(table_epoch.max(epoch.visible()))?;
16878 }
16879
16880 for (txn_id, ce, added_runs) in &spilled_to_link {
16884 for ar in added_runs {
16885 let Some(handle) = tables.get(&ar.table_id) else {
16886 continue;
16887 };
16888 let mut t = handle.lock();
16889 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
16890 let destination = table_dir
16891 .join(crate::engine::RUNS_DIR)
16892 .join(format!("r-{}.sr", ar.run_id));
16893 match durable_root.open_regular(&destination) {
16894 Ok(_) => {}
16895 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
16896 let pending = table_dir
16897 .join("_txn")
16898 .join(txn_id.to_string())
16899 .join(format!("r-{}.sr", ar.run_id));
16900 durable_root.rename_file_new(&pending, &destination)?;
16901 }
16902 Err(error) => return Err(error.into()),
16903 }
16904 let linked = t.recover_spilled_run(crate::manifest::RunRef {
16910 run_id: ar.run_id,
16911 level: ar.level,
16912 epoch_created: *ce,
16913 row_count: ar.row_count,
16914 });
16915 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
16916 if replaced {
16917 t.set_flushed_epoch(Epoch(*ce));
16918 }
16919 if linked || replaced {
16920 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
16921 }
16922 }
16923 }
16924
16925 epoch.advance_recovered(Epoch(max_epoch));
16926 Ok(())
16927}
16928
16929fn reconcile_recovered_table_metadata(
16930 tables: &HashMap<u64, TableHandle>,
16931 epoch: Epoch,
16932) -> Result<()> {
16933 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
16934 table_ids.sort_unstable();
16935 let mut plans = Vec::with_capacity(table_ids.len());
16936 for table_id in &table_ids {
16937 let handle = tables.get(table_id).ok_or_else(|| {
16938 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
16939 })?;
16940 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
16941 }
16942 for (table_id, plan) in plans {
16945 let handle = tables.get(&table_id).ok_or_else(|| {
16946 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
16947 })?;
16948 handle.lock().apply_recovered_metadata(plan, epoch)?;
16949 }
16950 Ok(())
16951}
16952
16953fn validate_recovered_external_name(name: &str) -> Result<()> {
16954 if name.is_empty()
16955 || !name.chars().all(|character| {
16956 character.is_ascii_alphanumeric() || character == '_' || character == '-'
16957 })
16958 {
16959 return Err(MongrelError::CorruptWal {
16960 offset: 0,
16961 reason: format!("unsafe recovered external-table name {name:?}"),
16962 });
16963 }
16964 Ok(())
16965}
16966
16967fn validate_recovery_table_stages(
16968 tables: &HashMap<u64, TableHandle>,
16969 stages: &HashMap<u64, RecoveryTableStage>,
16970) -> Result<()> {
16971 for (table_id, (rows, _, _, _)) in stages {
16972 let handle = tables
16973 .get(table_id)
16974 .ok_or_else(|| MongrelError::CorruptWal {
16975 offset: *table_id,
16976 reason: format!("recovery stage references unmounted table {table_id}"),
16977 })?;
16978 let table = handle.lock();
16979 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
16982 for row in rows {
16983 validate_recovered_row(table.schema(), row)?;
16984 }
16985 }
16986 Ok(())
16987}
16988
16989fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
16990 if row.deleted || row.row_id.0 == u64::MAX {
16991 return Err(MongrelError::CorruptWal {
16992 offset: row.row_id.0,
16993 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
16994 });
16995 }
16996 let cells = row
16997 .columns
16998 .iter()
16999 .map(|(column, value)| (*column, value.clone()))
17000 .collect::<Vec<_>>();
17001 schema
17002 .validate_persisted_values(&cells)
17003 .map_err(|error| MongrelError::CorruptWal {
17004 offset: row.row_id.0,
17005 reason: format!("recovered row violates table schema: {error}"),
17006 })?;
17007 if schema.auto_increment_column().is_some_and(|column| {
17008 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
17009 }) {
17010 return Err(MongrelError::CorruptWal {
17011 offset: row.row_id.0,
17012 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
17013 });
17014 }
17015 Ok(())
17016}
17017
17018fn validate_recovery_spilled_runs(
17019 root: &crate::durable_file::DurableRoot,
17020 tables: &HashMap<u64, TableHandle>,
17021 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
17022) -> Result<()> {
17023 let mut identities = HashSet::new();
17024 for (txn_id, commit_epoch, added_runs) in spilled {
17025 for added in added_runs {
17026 if added.run_id >= u64::MAX as u128 {
17027 return Err(MongrelError::CorruptWal {
17028 offset: *commit_epoch,
17029 reason: format!(
17030 "recovered run id {} exceeds the on-disk namespace",
17031 added.run_id
17032 ),
17033 });
17034 }
17035 let Some(handle) = tables.get(&added.table_id) else {
17036 continue;
17037 };
17038 if !identities.insert((added.table_id, added.run_id)) {
17039 return Err(MongrelError::CorruptWal {
17040 offset: *commit_epoch,
17041 reason: format!(
17042 "duplicate recovered run {} for table {}",
17043 added.run_id, added.table_id
17044 ),
17045 });
17046 }
17047 let table = handle.lock();
17048 validate_planned_spilled_run(
17049 root,
17050 *txn_id,
17051 *commit_epoch,
17052 added,
17053 table.schema(),
17054 table.kek(),
17055 )?;
17056 }
17057 }
17058 Ok(())
17059}
17060
17061fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
17062 match condition {
17063 ProcedureCondition::Pk { .. } => {
17064 if schema.primary_key().is_none() {
17065 return Err(MongrelError::InvalidArgument(
17066 "procedure condition Pk references a table without a primary key".into(),
17067 ));
17068 }
17069 }
17070 ProcedureCondition::BitmapEq { column_id, .. }
17071 | ProcedureCondition::BitmapIn { column_id, .. }
17072 | ProcedureCondition::Range { column_id, .. }
17073 | ProcedureCondition::RangeF64 { column_id, .. }
17074 | ProcedureCondition::IsNull { column_id }
17075 | ProcedureCondition::IsNotNull { column_id }
17076 | ProcedureCondition::FmContains { column_id, .. } => {
17077 validate_column_id(*column_id, schema)?;
17078 }
17079 }
17080 Ok(())
17081}
17082
17083fn bind_procedure_args(
17084 procedure: &StoredProcedure,
17085 mut args: HashMap<String, crate::Value>,
17086) -> Result<HashMap<String, crate::Value>> {
17087 let mut out = HashMap::new();
17088 for param in &procedure.params {
17089 let value = match args.remove(¶m.name) {
17090 Some(value) => value,
17091 None => param.default.clone().ok_or_else(|| {
17092 MongrelError::InvalidArgument(format!(
17093 "missing required procedure parameter {:?}",
17094 param.name
17095 ))
17096 })?,
17097 };
17098 if !param.nullable && matches!(value, crate::Value::Null) {
17099 return Err(MongrelError::InvalidArgument(format!(
17100 "procedure parameter {:?} must not be NULL",
17101 param.name
17102 )));
17103 }
17104 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
17105 return Err(MongrelError::InvalidArgument(format!(
17106 "procedure parameter {:?} has wrong type",
17107 param.name
17108 )));
17109 }
17110 out.insert(param.name.clone(), value);
17111 }
17112 if let Some(extra) = args.keys().next() {
17113 return Err(MongrelError::InvalidArgument(format!(
17114 "unknown procedure parameter {extra:?}"
17115 )));
17116 }
17117 Ok(out)
17118}
17119
17120fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
17121 matches!(
17122 (value, ty),
17123 (crate::Value::Bool(_), crate::TypeId::Bool)
17124 | (crate::Value::Int64(_), crate::TypeId::Int8)
17125 | (crate::Value::Int64(_), crate::TypeId::Int16)
17126 | (crate::Value::Int64(_), crate::TypeId::Int32)
17127 | (crate::Value::Int64(_), crate::TypeId::Int64)
17128 | (crate::Value::Int64(_), crate::TypeId::UInt8)
17129 | (crate::Value::Int64(_), crate::TypeId::UInt16)
17130 | (crate::Value::Int64(_), crate::TypeId::UInt32)
17131 | (crate::Value::Int64(_), crate::TypeId::UInt64)
17132 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
17133 | (crate::Value::Int64(_), crate::TypeId::Date32)
17134 | (crate::Value::Float64(_), crate::TypeId::Float32)
17135 | (crate::Value::Float64(_), crate::TypeId::Float64)
17136 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
17137 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
17138 | (
17139 crate::Value::GeneratedEmbedding(_),
17140 crate::TypeId::Embedding { .. }
17141 )
17142 )
17143}
17144
17145fn eval_cells(
17146 cells: &[crate::procedure::ProcedureCell],
17147 args: &HashMap<String, crate::Value>,
17148 outputs: &HashMap<String, ProcedureCallOutput>,
17149) -> Result<Vec<(u16, crate::Value)>> {
17150 cells
17151 .iter()
17152 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
17153 .collect()
17154}
17155
17156fn eval_condition(
17157 condition: &ProcedureCondition,
17158 args: &HashMap<String, crate::Value>,
17159 outputs: &HashMap<String, ProcedureCallOutput>,
17160) -> Result<crate::Condition> {
17161 Ok(match condition {
17162 ProcedureCondition::Pk { value } => {
17163 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
17164 }
17165 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
17166 column_id: *column_id,
17167 value: eval_value(value, args, outputs)?.encode_key(),
17168 },
17169 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
17170 column_id: *column_id,
17171 values: values
17172 .iter()
17173 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
17174 .collect::<Result<Vec<_>>>()?,
17175 },
17176 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
17177 column_id: *column_id,
17178 lo: expect_i64(eval_value(lo, args, outputs)?)?,
17179 hi: expect_i64(eval_value(hi, args, outputs)?)?,
17180 },
17181 ProcedureCondition::RangeF64 {
17182 column_id,
17183 lo,
17184 lo_inclusive,
17185 hi,
17186 hi_inclusive,
17187 } => crate::Condition::RangeF64 {
17188 column_id: *column_id,
17189 lo: expect_f64(eval_value(lo, args, outputs)?)?,
17190 lo_inclusive: *lo_inclusive,
17191 hi: expect_f64(eval_value(hi, args, outputs)?)?,
17192 hi_inclusive: *hi_inclusive,
17193 },
17194 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
17195 column_id: *column_id,
17196 },
17197 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
17198 column_id: *column_id,
17199 },
17200 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
17201 column_id: *column_id,
17202 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
17203 },
17204 })
17205}
17206
17207fn eval_value(
17208 value: &ProcedureValue,
17209 args: &HashMap<String, crate::Value>,
17210 outputs: &HashMap<String, ProcedureCallOutput>,
17211) -> Result<crate::Value> {
17212 match value {
17213 ProcedureValue::Literal(value) => Ok(value.clone()),
17214 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
17215 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17216 }),
17217 ProcedureValue::StepScalar(id) => match outputs.get(id) {
17218 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
17219 _ => Err(MongrelError::InvalidArgument(format!(
17220 "procedure step {id:?} did not return a scalar"
17221 ))),
17222 },
17223 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
17224 Err(MongrelError::InvalidArgument(
17225 "row-valued procedure reference cannot be used as a scalar".into(),
17226 ))
17227 }
17228 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
17229 "structured procedure value cannot be used as a scalar cell".into(),
17230 )),
17231 }
17232}
17233
17234fn eval_return_output(
17235 value: &ProcedureValue,
17236 args: &HashMap<String, crate::Value>,
17237 outputs: &HashMap<String, ProcedureCallOutput>,
17238) -> Result<ProcedureCallOutput> {
17239 match value {
17240 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
17241 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
17242 args.get(name).cloned().ok_or_else(|| {
17243 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17244 })?,
17245 )),
17246 ProcedureValue::StepRows(id)
17247 | ProcedureValue::StepRow(id)
17248 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
17249 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
17250 }),
17251 ProcedureValue::Object(fields) => {
17252 let mut out = Vec::with_capacity(fields.len());
17253 for (name, value) in fields {
17254 out.push((name.clone(), eval_return_output(value, args, outputs)?));
17255 }
17256 Ok(ProcedureCallOutput::Object(out))
17257 }
17258 ProcedureValue::Array(values) => {
17259 let mut out = Vec::with_capacity(values.len());
17260 for value in values {
17261 out.push(eval_return_output(value, args, outputs)?);
17262 }
17263 Ok(ProcedureCallOutput::Array(out))
17264 }
17265 }
17266}
17267
17268fn expect_i64(value: crate::Value) -> Result<i64> {
17269 match value {
17270 crate::Value::Int64(value) => Ok(value),
17271 _ => Err(MongrelError::InvalidArgument(
17272 "procedure value must be Int64".into(),
17273 )),
17274 }
17275}
17276
17277fn expect_f64(value: crate::Value) -> Result<f64> {
17278 match value {
17279 crate::Value::Float64(value) => Ok(value),
17280 _ => Err(MongrelError::InvalidArgument(
17281 "procedure value must be Float64".into(),
17282 )),
17283 }
17284}
17285
17286fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
17287 match value {
17288 crate::Value::Bytes(value) => Ok(value),
17289 _ => Err(MongrelError::InvalidArgument(
17290 "procedure value must be Bytes".into(),
17291 )),
17292 }
17293}
17294
17295fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
17296 if schema.columns.iter().any(|c| c.id == column_id) {
17297 Ok(())
17298 } else {
17299 Err(MongrelError::InvalidArgument(format!(
17300 "unknown column id {column_id}"
17301 )))
17302 }
17303}
17304
17305fn trigger_matches_event(
17306 trigger: &StoredTrigger,
17307 event: &WriteEvent,
17308 cat: &Catalog,
17309) -> Result<bool> {
17310 if trigger.event != event.kind {
17311 return Ok(false);
17312 }
17313 let TriggerTarget::Table(target) = &trigger.target else {
17314 return Ok(false);
17315 };
17316 if target != &event.table {
17317 return Ok(false);
17318 }
17319 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
17320 let schema = &cat
17321 .live(target)
17322 .ok_or_else(|| {
17323 MongrelError::InvalidArgument(format!(
17324 "trigger {:?} references unknown table {target:?}",
17325 trigger.name
17326 ))
17327 })?
17328 .schema;
17329 let mut watched = Vec::with_capacity(trigger.update_of.len());
17330 for name in &trigger.update_of {
17331 let col = schema.column(name).ok_or_else(|| {
17332 MongrelError::InvalidArgument(format!(
17333 "trigger {:?} references unknown UPDATE OF column {name:?}",
17334 trigger.name
17335 ))
17336 })?;
17337 watched.push(col.id);
17338 }
17339 if !event
17340 .changed_columns
17341 .iter()
17342 .any(|column_id| watched.contains(column_id))
17343 {
17344 return Ok(false);
17345 }
17346 }
17347 Ok(true)
17348}
17349
17350fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
17351 let mut ids = std::collections::BTreeSet::new();
17352 if let Some(old) = old {
17353 ids.extend(old.columns.keys().copied());
17354 }
17355 if let Some(new) = new {
17356 ids.extend(new.columns.keys().copied());
17357 }
17358 ids.into_iter()
17359 .filter(|id| {
17360 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
17361 })
17362 .collect()
17363}
17364
17365fn eval_trigger_cells(
17366 cells: &[crate::trigger::TriggerCell],
17367 event: &WriteEvent,
17368 selected: Option<&TriggerRowImage>,
17369) -> Result<Vec<(u16, Value)>> {
17370 cells
17371 .iter()
17372 .map(|cell| {
17373 Ok((
17374 cell.column_id,
17375 eval_trigger_value(&cell.value, event, selected)?,
17376 ))
17377 })
17378 .collect()
17379}
17380
17381fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
17382 match expr {
17383 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
17384 Value::Bool(value) => Ok(value),
17385 Value::Null => Ok(false),
17386 other => Err(MongrelError::InvalidArgument(format!(
17387 "trigger WHEN value must be boolean, got {other:?}"
17388 ))),
17389 },
17390 TriggerExpr::Eq { left, right } => Ok(values_equal(
17391 &eval_trigger_value(left, event, None)?,
17392 &eval_trigger_value(right, event, None)?,
17393 )),
17394 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
17395 &eval_trigger_value(left, event, None)?,
17396 &eval_trigger_value(right, event, None)?,
17397 )),
17398 TriggerExpr::Lt { left, right } => match value_order(
17399 &eval_trigger_value(left, event, None)?,
17400 &eval_trigger_value(right, event, None)?,
17401 ) {
17402 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
17403 None => Ok(false),
17404 },
17405 TriggerExpr::Lte { left, right } => match value_order(
17406 &eval_trigger_value(left, event, None)?,
17407 &eval_trigger_value(right, event, None)?,
17408 ) {
17409 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
17410 None => Ok(false),
17411 },
17412 TriggerExpr::Gt { left, right } => match value_order(
17413 &eval_trigger_value(left, event, None)?,
17414 &eval_trigger_value(right, event, None)?,
17415 ) {
17416 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
17417 None => Ok(false),
17418 },
17419 TriggerExpr::Gte { left, right } => match value_order(
17420 &eval_trigger_value(left, event, None)?,
17421 &eval_trigger_value(right, event, None)?,
17422 ) {
17423 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
17424 None => Ok(false),
17425 },
17426 TriggerExpr::IsNull(value) => Ok(matches!(
17427 eval_trigger_value(value, event, None)?,
17428 Value::Null
17429 )),
17430 TriggerExpr::IsNotNull(value) => Ok(!matches!(
17431 eval_trigger_value(value, event, None)?,
17432 Value::Null
17433 )),
17434 TriggerExpr::And { left, right } => {
17435 if !eval_trigger_expr(left, event)? {
17436 Ok(false)
17437 } else {
17438 Ok(eval_trigger_expr(right, event)?)
17439 }
17440 }
17441 TriggerExpr::Or { left, right } => {
17442 if eval_trigger_expr(left, event)? {
17443 Ok(true)
17444 } else {
17445 Ok(eval_trigger_expr(right, event)?)
17446 }
17447 }
17448 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
17449 }
17450}
17451
17452fn eval_trigger_condition(
17453 condition: &TriggerCondition,
17454 event: &WriteEvent,
17455 selected: &TriggerRowImage,
17456 schema: &Schema,
17457) -> Result<bool> {
17458 match condition {
17459 TriggerCondition::Pk { value } => {
17460 let pk = schema.primary_key().ok_or_else(|| {
17461 MongrelError::InvalidArgument(
17462 "trigger condition Pk references a table without a primary key".into(),
17463 )
17464 })?;
17465 let lhs = eval_trigger_value(value, event, Some(selected))?;
17466 Ok(values_equal(
17467 &lhs,
17468 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
17469 ))
17470 }
17471 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
17472 selected.columns.get(column_id).unwrap_or(&Value::Null),
17473 &eval_trigger_value(value, event, Some(selected))?,
17474 )),
17475 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
17476 selected.columns.get(column_id).unwrap_or(&Value::Null),
17477 &eval_trigger_value(value, event, Some(selected))?,
17478 )),
17479 TriggerCondition::Lt { column_id, value } => match value_order(
17480 selected.columns.get(column_id).unwrap_or(&Value::Null),
17481 &eval_trigger_value(value, event, Some(selected))?,
17482 ) {
17483 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
17484 None => Ok(false),
17485 },
17486 TriggerCondition::Lte { column_id, value } => match value_order(
17487 selected.columns.get(column_id).unwrap_or(&Value::Null),
17488 &eval_trigger_value(value, event, Some(selected))?,
17489 ) {
17490 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
17491 None => Ok(false),
17492 },
17493 TriggerCondition::Gt { column_id, value } => match value_order(
17494 selected.columns.get(column_id).unwrap_or(&Value::Null),
17495 &eval_trigger_value(value, event, Some(selected))?,
17496 ) {
17497 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
17498 None => Ok(false),
17499 },
17500 TriggerCondition::Gte { column_id, value } => match value_order(
17501 selected.columns.get(column_id).unwrap_or(&Value::Null),
17502 &eval_trigger_value(value, event, Some(selected))?,
17503 ) {
17504 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
17505 None => Ok(false),
17506 },
17507 TriggerCondition::IsNull { column_id } => Ok(matches!(
17508 selected.columns.get(column_id),
17509 None | Some(Value::Null)
17510 )),
17511 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
17512 selected.columns.get(column_id),
17513 None | Some(Value::Null)
17514 )),
17515 TriggerCondition::And { left, right } => {
17516 if !eval_trigger_condition(left, event, selected, schema)? {
17517 Ok(false)
17518 } else {
17519 Ok(eval_trigger_condition(right, event, selected, schema)?)
17520 }
17521 }
17522 TriggerCondition::Or { left, right } => {
17523 if eval_trigger_condition(left, event, selected, schema)? {
17524 Ok(true)
17525 } else {
17526 Ok(eval_trigger_condition(right, event, selected, schema)?)
17527 }
17528 }
17529 TriggerCondition::Not(condition) => {
17530 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
17531 }
17532 }
17533}
17534
17535fn eval_trigger_value(
17536 value: &TriggerValue,
17537 event: &WriteEvent,
17538 selected: Option<&TriggerRowImage>,
17539) -> Result<Value> {
17540 match value {
17541 TriggerValue::Literal(value) => Ok(value.clone()),
17542 TriggerValue::NewColumn(column_id) => event
17543 .new
17544 .as_ref()
17545 .and_then(|row| row.columns.get(column_id))
17546 .cloned()
17547 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
17548 TriggerValue::OldColumn(column_id) => event
17549 .old
17550 .as_ref()
17551 .and_then(|row| row.columns.get(column_id))
17552 .cloned()
17553 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
17554 TriggerValue::SelectedColumn(column_id) => selected
17555 .and_then(|row| row.columns.get(column_id))
17556 .cloned()
17557 .ok_or_else(|| {
17558 MongrelError::InvalidArgument("SELECTED column is not available".into())
17559 }),
17560 }
17561}
17562
17563fn values_equal(left: &Value, right: &Value) -> bool {
17564 match (left, right) {
17565 (Value::Null, Value::Null) => true,
17566 (Value::Bool(a), Value::Bool(b)) => a == b,
17567 (Value::Int64(a), Value::Int64(b)) => a == b,
17568 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
17569 (Value::Bytes(a), Value::Bytes(b)) => a == b,
17570 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => {
17571 let a = a.as_embedding().unwrap();
17572 let b = b.as_embedding().unwrap();
17573 a.len() == b.len()
17574 && a.iter()
17575 .zip(b.iter())
17576 .all(|(a, b)| a.to_bits() == b.to_bits())
17577 }
17578 _ => false,
17579 }
17580}
17581
17582fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
17583 match (left, right) {
17584 (Value::Null, _) | (_, Value::Null) => None,
17585 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
17586 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
17587 (Value::Int64(a), Value::Float64(b)) => {
17590 let af = *a as f64;
17591 Some(af.total_cmp(b))
17592 }
17593 (Value::Float64(a), Value::Int64(b)) => {
17596 let bf = *b as f64;
17597 Some(a.total_cmp(&bf))
17598 }
17599 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
17600 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
17601 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => None,
17602 _ => None,
17603 }
17604}
17605
17606fn trigger_message(value: Value) -> String {
17607 match value {
17608 Value::Null => "NULL".into(),
17609 Value::Bool(value) => value.to_string(),
17610 Value::Int64(value) => value.to_string(),
17611 Value::Float64(value) => value.to_string(),
17612 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
17613 Value::Embedding(value) => format!("{value:?}"),
17614 Value::GeneratedEmbedding(value) => format!("{:?}", value.vector),
17615 Value::Decimal(value) => value.to_string(),
17616 Value::Interval {
17617 months,
17618 days,
17619 nanos,
17620 } => format!("{months}m {days}d {nanos}ns"),
17621 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
17622 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
17623 }
17624}
17625
17626fn validate_trigger_step<'a>(
17627 step: &TriggerStep,
17628 cat: &'a Catalog,
17629 target_schema: &Schema,
17630 event: TriggerEvent,
17631 select_schemas: &mut HashMap<String, &'a Schema>,
17632) -> Result<()> {
17633 match step {
17634 TriggerStep::SetNew { cells } => {
17635 if event == TriggerEvent::Delete {
17636 return Err(MongrelError::InvalidArgument(
17637 "SetNew trigger step is not valid for DELETE triggers".into(),
17638 ));
17639 }
17640 for cell in cells {
17641 validate_column_id(cell.column_id, target_schema)?;
17642 validate_trigger_value(&cell.value, target_schema, event)?;
17643 }
17644 }
17645 TriggerStep::Insert { table, cells } => {
17646 let schema = trigger_write_schema(cat, table, "insert")?;
17647 for cell in cells {
17648 validate_column_id(cell.column_id, schema)?;
17649 validate_trigger_value(&cell.value, target_schema, event)?;
17650 }
17651 }
17652 TriggerStep::UpdateByPk { table, pk, cells } => {
17653 let schema = trigger_write_schema(cat, table, "update")?;
17654 if schema.primary_key().is_none() {
17655 return Err(MongrelError::InvalidArgument(format!(
17656 "trigger update_by_pk references table {table:?} without a primary key"
17657 )));
17658 }
17659 validate_trigger_value(pk, target_schema, event)?;
17660 for cell in cells {
17661 validate_column_id(cell.column_id, schema)?;
17662 validate_trigger_value(&cell.value, target_schema, event)?;
17663 }
17664 }
17665 TriggerStep::DeleteByPk { table, pk } => {
17666 let schema = trigger_write_schema(cat, table, "delete")?;
17667 if schema.primary_key().is_none() {
17668 return Err(MongrelError::InvalidArgument(format!(
17669 "trigger delete_by_pk references table {table:?} without a primary key"
17670 )));
17671 }
17672 validate_trigger_value(pk, target_schema, event)?;
17673 }
17674 TriggerStep::Select {
17675 id,
17676 table,
17677 conditions,
17678 } => {
17679 let schema = trigger_read_schema(cat, table)?;
17680 for condition in conditions {
17681 validate_trigger_condition(condition, schema, target_schema, event)?;
17682 }
17683 if select_schemas.contains_key(id) {
17684 return Err(MongrelError::InvalidArgument(format!(
17685 "duplicate select id {id:?} in trigger program"
17686 )));
17687 }
17688 select_schemas.insert(id.clone(), schema);
17689 }
17690 TriggerStep::Foreach { id, steps } => {
17691 if !select_schemas.contains_key(id) {
17692 return Err(MongrelError::InvalidArgument(format!(
17693 "foreach references unknown select id {id:?}"
17694 )));
17695 }
17696 let mut inner_select_schemas = select_schemas.clone();
17697 for step in steps {
17698 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
17699 }
17700 }
17701 TriggerStep::DeleteWhere { table, conditions } => {
17702 let schema = trigger_write_schema(cat, table, "delete")?;
17703 for condition in conditions {
17704 validate_trigger_condition(condition, schema, target_schema, event)?;
17705 }
17706 }
17707 TriggerStep::UpdateWhere {
17708 table,
17709 conditions,
17710 cells,
17711 } => {
17712 let schema = trigger_write_schema(cat, table, "update")?;
17713 for condition in conditions {
17714 validate_trigger_condition(condition, schema, target_schema, event)?;
17715 }
17716 for cell in cells {
17717 validate_column_id(cell.column_id, schema)?;
17718 validate_trigger_value(&cell.value, target_schema, event)?;
17719 }
17720 }
17721 TriggerStep::Raise { message, .. } => {
17722 validate_trigger_value(message, target_schema, event)?
17723 }
17724 }
17725 Ok(())
17726}
17727
17728fn trigger_validation_error(error: MongrelError) -> MongrelError {
17729 match error {
17730 MongrelError::TriggerValidation(_) => error,
17731 MongrelError::InvalidArgument(message)
17732 | MongrelError::Conflict(message)
17733 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
17734 error => error,
17735 }
17736}
17737
17738fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
17739 if let Some(entry) = cat.live(table) {
17740 return Ok(&entry.schema);
17741 }
17742 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
17743 let allowed = match op {
17744 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
17745 "update" | "delete" => entry.capabilities.writable,
17746 _ => false,
17747 };
17748 if !allowed {
17749 return Err(MongrelError::InvalidArgument(format!(
17750 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
17751 entry.module
17752 )));
17753 }
17754 if !entry.capabilities.transaction_safe {
17755 return Err(MongrelError::InvalidArgument(format!(
17756 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
17757 entry.module
17758 )));
17759 }
17760 return Ok(&entry.declared_schema);
17761 }
17762 Err(MongrelError::InvalidArgument(format!(
17763 "trigger references unknown table {table:?}"
17764 )))
17765}
17766
17767fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
17768 if let Some(entry) = cat.live(table) {
17769 return Ok(&entry.schema);
17770 }
17771 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
17772 if entry.capabilities.trigger_safe {
17773 return Ok(&entry.declared_schema);
17774 }
17775 return Err(MongrelError::InvalidArgument(format!(
17776 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
17777 entry.module
17778 )));
17779 }
17780 Err(MongrelError::InvalidArgument(format!(
17781 "trigger references unknown table {table:?}"
17782 )))
17783}
17784
17785fn validate_trigger_condition(
17786 condition: &TriggerCondition,
17787 schema: &Schema,
17788 target_schema: &Schema,
17789 event: TriggerEvent,
17790) -> Result<()> {
17791 match condition {
17792 TriggerCondition::Pk { value } => {
17793 if schema.primary_key().is_none() {
17794 return Err(MongrelError::InvalidArgument(
17795 "trigger condition Pk references a table without a primary key".into(),
17796 ));
17797 }
17798 validate_trigger_value(value, target_schema, event)
17799 }
17800 TriggerCondition::Eq { column_id, value }
17801 | TriggerCondition::NotEq { column_id, value }
17802 | TriggerCondition::Lt { column_id, value }
17803 | TriggerCondition::Lte { column_id, value }
17804 | TriggerCondition::Gt { column_id, value }
17805 | TriggerCondition::Gte { column_id, value } => {
17806 validate_column_id(*column_id, schema)?;
17807 validate_trigger_value(value, target_schema, event)
17808 }
17809 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
17810 validate_column_id(*column_id, schema)
17811 }
17812 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
17813 validate_trigger_condition(left, schema, target_schema, event)?;
17814 validate_trigger_condition(right, schema, target_schema, event)
17815 }
17816 TriggerCondition::Not(condition) => {
17817 validate_trigger_condition(condition, schema, target_schema, event)
17818 }
17819 }
17820}
17821
17822fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
17823 match expr {
17824 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
17825 validate_trigger_value(value, schema, event)
17826 }
17827 TriggerExpr::Eq { left, right }
17828 | TriggerExpr::NotEq { left, right }
17829 | TriggerExpr::Lt { left, right }
17830 | TriggerExpr::Lte { left, right }
17831 | TriggerExpr::Gt { left, right }
17832 | TriggerExpr::Gte { left, right } => {
17833 validate_trigger_value(left, schema, event)?;
17834 validate_trigger_value(right, schema, event)
17835 }
17836 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
17837 validate_trigger_expr(left, schema, event)?;
17838 validate_trigger_expr(right, schema, event)
17839 }
17840 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
17841 }
17842}
17843
17844fn validate_trigger_value(
17845 value: &TriggerValue,
17846 schema: &Schema,
17847 event: TriggerEvent,
17848) -> Result<()> {
17849 match value {
17850 TriggerValue::Literal(_) => Ok(()),
17851 TriggerValue::NewColumn(id) => {
17852 if event == TriggerEvent::Delete {
17853 return Err(MongrelError::InvalidArgument(
17854 "DELETE triggers cannot reference NEW".into(),
17855 ));
17856 }
17857 validate_column_id(*id, schema)
17858 }
17859 TriggerValue::OldColumn(id) => {
17860 if event == TriggerEvent::Insert {
17861 return Err(MongrelError::InvalidArgument(
17862 "INSERT triggers cannot reference OLD".into(),
17863 ));
17864 }
17865 validate_column_id(*id, schema)
17866 }
17867 TriggerValue::SelectedColumn(_) => Ok(()),
17871 }
17872}
17873
17874const COMMIT_TS_LEDGER_CAP: usize = 10_000;
17879
17880fn commit_ts_ledger_from_recovery(
17888 records: &[crate::wal::Record],
17889) -> std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp> {
17890 use crate::wal::Op;
17891 let mut commits = HashMap::new();
17892 let mut timestamps = HashMap::new();
17893 for record in records {
17894 match &record.op {
17895 Op::TxnCommit { epoch, .. } => {
17896 commits.insert(record.txn_id, *epoch);
17897 }
17898 Op::CommitTimestamp { unix_nanos } => {
17899 timestamps.insert(record.txn_id, *unix_nanos);
17900 }
17901 _ => {}
17902 }
17903 }
17904 let mut ledger = std::collections::BTreeMap::new();
17905 for (txn_id, epoch) in commits {
17906 let Some(unix_nanos) = timestamps.get(&txn_id) else {
17907 continue;
17908 };
17909 ledger.insert(
17910 epoch,
17911 mongreldb_types::hlc::HlcTimestamp {
17912 physical_micros: unix_nanos / 1_000,
17913 logical: 0,
17914 node_tiebreaker: 0,
17915 },
17916 );
17917 }
17918 while ledger.len() > COMMIT_TS_LEDGER_CAP {
17919 ledger.pop_first();
17920 }
17921 ledger
17922}
17923
17924fn recover_ddl_from_wal(
17930 root: &Path,
17931 durable_root: Option<&crate::durable_file::DurableRoot>,
17932 target_catalog: &mut Catalog,
17933 meta_dek: Option<&[u8; META_DEK_LEN]>,
17934 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
17935 apply: bool,
17936 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
17937) -> Result<()> {
17938 use crate::wal::SharedWal;
17939 let records = match durable_root {
17940 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
17941 None => SharedWal::replay_with_dek(root, wal_dek)?,
17942 };
17943 recover_ddl_from_records(
17944 root,
17945 durable_root,
17946 target_catalog,
17947 meta_dek,
17948 apply,
17949 table_roots,
17950 &records,
17951 )
17952}
17953
17954fn recover_ddl_from_records(
17955 root: &Path,
17956 durable_root: Option<&crate::durable_file::DurableRoot>,
17957 target_catalog: &mut Catalog,
17958 meta_dek: Option<&[u8; META_DEK_LEN]>,
17959 apply: bool,
17960 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
17961 records: &[crate::wal::Record],
17962) -> Result<()> {
17963 use crate::wal::{DdlOp, Op};
17964
17965 let original_catalog = target_catalog.clone();
17966 let mut recovered_catalog = original_catalog.clone();
17967 let cat = &mut recovered_catalog;
17968 let mut created_table_ids = HashSet::<u64>::new();
17969 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
17970
17971 let mut committed: HashMap<u64, u64> = HashMap::new();
17972 for r in records {
17973 if let Op::TxnCommit { epoch: ce, .. } = r.op {
17974 committed.insert(r.txn_id, ce);
17975 }
17976 }
17977 let catalog_snapshot_txns = records
17978 .iter()
17979 .filter_map(|record| {
17980 (committed.contains_key(&record.txn_id)
17981 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
17982 .then_some(record.txn_id)
17983 })
17984 .collect::<HashSet<_>>();
17985
17986 let mut changed = false;
17987 let mut applied_catalog_epoch = cat.db_epoch;
17988 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
17989 for r in records.iter().cloned() {
17990 let Some(&ce) = committed.get(&r.txn_id) else {
17991 continue;
17992 };
17993 let txn_id = r.txn_id;
17994 match r.op {
17995 Op::Ddl(DdlOp::CreateTable {
17996 table_id,
17997 ref name,
17998 ref schema_json,
17999 }) => {
18000 if cat.tables.iter().any(|t| t.table_id == table_id) {
18001 continue;
18002 }
18003 let schema = DdlOp::decode_schema(schema_json)?;
18004 validate_recovered_schema(&schema)?;
18005 created_table_ids.insert(table_id);
18006 cat.tables.push(CatalogEntry {
18007 table_id,
18008 name: name.clone(),
18009 schema,
18010 state: TableState::Live,
18011 created_epoch: ce,
18012 });
18013 cat.next_table_id =
18014 cat.next_table_id
18015 .max(table_id.checked_add(1).ok_or_else(|| {
18016 MongrelError::Full("table id namespace exhausted".into())
18017 })?);
18018 changed = true;
18019 }
18020 Op::Ddl(DdlOp::CreateBuildingTable {
18021 table_id,
18022 ref build_name,
18023 ref intended_name,
18024 ref query_id,
18025 created_at_unix_nanos,
18026 ref schema_json,
18027 }) => {
18028 if cat.tables.iter().any(|table| table.table_id == table_id) {
18029 continue;
18030 }
18031 let schema = DdlOp::decode_schema(schema_json)?;
18032 validate_recovered_schema(&schema)?;
18033 created_table_ids.insert(table_id);
18034 cat.tables.push(CatalogEntry {
18035 table_id,
18036 name: build_name.clone(),
18037 schema,
18038 state: TableState::Building {
18039 intended_name: intended_name.clone(),
18040 query_id: query_id.clone(),
18041 created_at_unix_nanos,
18042 replaces_table_id: None,
18043 },
18044 created_epoch: ce,
18045 });
18046 cat.next_table_id =
18047 cat.next_table_id
18048 .max(table_id.checked_add(1).ok_or_else(|| {
18049 MongrelError::Full("table id namespace exhausted".into())
18050 })?);
18051 changed = true;
18052 }
18053 Op::Ddl(DdlOp::CreateRebuildingTable {
18054 table_id,
18055 ref build_name,
18056 ref intended_name,
18057 ref query_id,
18058 created_at_unix_nanos,
18059 replaces_table_id,
18060 ref schema_json,
18061 }) => {
18062 if cat.tables.iter().any(|table| table.table_id == table_id) {
18063 continue;
18064 }
18065 let schema = DdlOp::decode_schema(schema_json)?;
18066 validate_recovered_schema(&schema)?;
18067 created_table_ids.insert(table_id);
18068 cat.tables.push(CatalogEntry {
18069 table_id,
18070 name: build_name.clone(),
18071 schema,
18072 state: TableState::Building {
18073 intended_name: intended_name.clone(),
18074 query_id: query_id.clone(),
18075 created_at_unix_nanos,
18076 replaces_table_id: Some(replaces_table_id),
18077 },
18078 created_epoch: ce,
18079 });
18080 cat.next_table_id =
18081 cat.next_table_id
18082 .max(table_id.checked_add(1).ok_or_else(|| {
18083 MongrelError::Full("table id namespace exhausted".into())
18084 })?);
18085 changed = true;
18086 }
18087 Op::Ddl(DdlOp::DropTable { table_id }) => {
18088 let mut dropped_name = None;
18089 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18090 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18091 dropped_name = Some(entry.name.clone());
18092 entry.state = TableState::Dropped { at_epoch: ce };
18093 changed = true;
18094 }
18095 }
18096 if let Some(name) = dropped_name {
18097 let before = cat.materialized_views.len();
18098 cat.materialized_views
18099 .retain(|definition| definition.name != name);
18100 changed |= before != cat.materialized_views.len();
18101 cat.security.rls_tables.retain(|table| table != &name);
18102 cat.security.policies.retain(|policy| policy.table != name);
18103 cat.security.masks.retain(|mask| mask.table != name);
18104 for role in &mut cat.roles {
18105 role.permissions
18106 .retain(|permission| permission_table(permission) != Some(&name));
18107 }
18108 if !catalog_snapshot_txns.contains(&txn_id) {
18109 advance_security_version(cat)?;
18110 }
18111 }
18112 }
18113 Op::Ddl(DdlOp::PublishBuildingTable {
18114 table_id,
18115 ref new_name,
18116 }) => {
18117 if let Some(entry) = cat
18118 .tables
18119 .iter_mut()
18120 .find(|table| table.table_id == table_id)
18121 {
18122 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
18123 entry.name = new_name.clone();
18124 entry.state = TableState::Live;
18125 changed = true;
18126 }
18127 }
18128 }
18129 Op::Ddl(DdlOp::ReplaceBuildingTable {
18130 table_id,
18131 replaced_table_id,
18132 ref new_name,
18133 }) => {
18134 changed |=
18135 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
18136 }
18137 Op::Ddl(DdlOp::RenameTable {
18138 table_id,
18139 ref new_name,
18140 }) => {
18141 let mut old_name = None;
18142 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18143 if entry.name != *new_name {
18144 old_name = Some(entry.name.clone());
18145 entry.name = new_name.clone();
18146 changed = true;
18147 }
18148 }
18149 if let Some(old_name) = old_name {
18150 if let Some(definition) = cat
18151 .materialized_views
18152 .iter_mut()
18153 .find(|definition| definition.name == old_name)
18154 {
18155 definition.name = new_name.clone();
18156 }
18157 for table in &mut cat.security.rls_tables {
18158 if *table == old_name {
18159 *table = new_name.clone();
18160 }
18161 }
18162 for policy in &mut cat.security.policies {
18163 if policy.table == old_name {
18164 policy.table = new_name.clone();
18165 }
18166 }
18167 for mask in &mut cat.security.masks {
18168 if mask.table == old_name {
18169 mask.table = new_name.clone();
18170 }
18171 }
18172 for role in &mut cat.roles {
18173 for permission in &mut role.permissions {
18174 rename_permission_table(permission, &old_name, new_name);
18175 }
18176 }
18177 if !catalog_snapshot_txns.contains(&txn_id) {
18178 advance_security_version(cat)?;
18179 }
18180 }
18181 }
18185 Op::Ddl(DdlOp::AlterTable {
18186 table_id,
18187 ref column_json,
18188 }) => {
18189 let column = DdlOp::decode_column(column_json)?;
18190 let mut renamed = None;
18191 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18192 renamed = entry
18193 .schema
18194 .columns
18195 .iter()
18196 .find(|existing| existing.id == column.id && existing.name != column.name)
18197 .map(|existing| {
18198 (
18199 entry.name.clone(),
18200 existing.name.clone(),
18201 column.name.clone(),
18202 )
18203 });
18204 if apply_recovered_column_def(&mut entry.schema, column)? {
18205 validate_recovered_schema(&entry.schema)?;
18206 changed = true;
18207 }
18208 }
18209 if let Some((table, old_name, new_name)) = renamed {
18210 for role in &mut cat.roles {
18211 for permission in &mut role.permissions {
18212 rename_permission_column(permission, &table, &old_name, &new_name);
18213 }
18214 }
18215 if !catalog_snapshot_txns.contains(&txn_id) {
18216 advance_security_version(cat)?;
18217 }
18218 }
18219 }
18220 Op::Ddl(DdlOp::SetTtl {
18221 table_id,
18222 ref policy_json,
18223 }) => {
18224 let policy = DdlOp::decode_ttl(policy_json)?;
18225 let entry = cat
18226 .tables
18227 .iter()
18228 .find(|entry| entry.table_id == table_id)
18229 .ok_or_else(|| {
18230 MongrelError::Schema(format!(
18231 "recovered TTL references unknown table id {table_id}"
18232 ))
18233 })?;
18234 if let Some(policy) = policy {
18235 let valid = entry
18236 .schema
18237 .columns
18238 .iter()
18239 .find(|column| column.id == policy.column_id)
18240 .is_some_and(|column| {
18241 column.ty == TypeId::TimestampNanos
18242 && policy.duration_nanos > 0
18243 && policy.duration_nanos <= i64::MAX as u64
18244 });
18245 if !valid {
18246 return Err(MongrelError::Schema(format!(
18247 "invalid recovered TTL policy for table id {table_id}"
18248 )));
18249 }
18250 }
18251 ttl_updates.insert(table_id, (policy, ce));
18252 }
18253 Op::Ddl(DdlOp::SetMaterializedView {
18254 ref name,
18255 ref definition_json,
18256 }) => {
18257 let definition = DdlOp::decode_materialized_view(definition_json)?;
18258 if definition.name != *name {
18259 return Err(MongrelError::Schema(format!(
18260 "materialized view WAL name mismatch: {name:?}"
18261 )));
18262 }
18263 if cat.live(name).is_some() {
18264 if let Some(existing) = cat
18265 .materialized_views
18266 .iter_mut()
18267 .find(|existing| existing.name == *name)
18268 {
18269 if *existing != definition {
18270 *existing = definition;
18271 changed = true;
18272 }
18273 } else {
18274 cat.materialized_views.push(definition);
18275 changed = true;
18276 }
18277 }
18278 }
18279 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
18280 let security = DdlOp::decode_security(security_json)?;
18281 validate_security_catalog(cat, &security)?;
18282 if cat.security != security {
18283 cat.security = security;
18284 if !catalog_snapshot_txns.contains(&txn_id) {
18285 advance_security_version(cat)?;
18286 }
18287 changed = true;
18288 }
18289 }
18290 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
18291 let target = match key.as_str() {
18292 "user_version" => &mut cat.user_version,
18293 "application_id" => &mut cat.application_id,
18294 _ => {
18295 return Err(MongrelError::InvalidArgument(format!(
18296 "unsupported recovered SQL pragma {key:?}"
18297 )))
18298 }
18299 };
18300 if *target != Some(value) {
18301 *target = Some(value);
18302 cat.db_epoch = cat.db_epoch.max(ce);
18303 changed = true;
18304 }
18305 }
18306 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
18307 if ce <= applied_catalog_epoch {
18308 continue;
18309 }
18310 let snapshot = DdlOp::decode_catalog(catalog_json)?;
18311 if snapshot.db_epoch != ce {
18312 return Err(MongrelError::Schema(format!(
18313 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
18314 snapshot.db_epoch
18315 )));
18316 }
18317 validate_recovered_catalog(&snapshot)?;
18318 validate_catalog_transition(cat, &snapshot)?;
18319 *cat = snapshot;
18320 applied_catalog_epoch = ce;
18321 changed = true;
18322 }
18323 _ => {}
18324 }
18325 }
18326
18327 if cat.db_epoch < max_committed_epoch {
18328 cat.db_epoch = max_committed_epoch;
18329 changed = true;
18330 }
18331 changed |= repair_catalog_allocator_counters(cat)?;
18332
18333 validate_recovered_catalog(cat)?;
18334 let storage_reconciliation = validate_recovered_storage_plan(
18335 root,
18336 durable_root,
18337 cat,
18338 &created_table_ids,
18339 &ttl_updates,
18340 meta_dek,
18341 )?;
18342
18343 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
18344 if apply && (changed || needs_storage_apply) {
18345 for table_id in storage_reconciliation {
18346 let entry = cat
18347 .tables
18348 .iter()
18349 .find(|entry| entry.table_id == table_id)
18350 .ok_or_else(|| MongrelError::CorruptWal {
18351 offset: table_id,
18352 reason: "recovery storage plan lost its catalog table".into(),
18353 })?;
18354 ensure_recovered_table_storage(
18355 table_roots
18356 .and_then(|roots| roots.get(&table_id))
18357 .map(Arc::as_ref),
18358 durable_root,
18359 &root.join(TABLES_DIR).join(table_id.to_string()),
18360 table_id,
18361 &entry.schema,
18362 meta_dek,
18363 )?;
18364 }
18365 for (table_id, (policy, ttl_epoch)) in ttl_updates {
18366 let Some(entry) = cat.tables.iter().find(|entry| {
18367 entry.table_id == table_id
18368 && matches!(entry.state, TableState::Live | TableState::Building { .. })
18369 }) else {
18370 continue;
18371 };
18372 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
18373 {
18374 root.try_clone()?
18375 } else if let Some(root) = durable_root {
18376 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
18377 } else {
18378 crate::durable_file::DurableRoot::open(
18379 root.join(TABLES_DIR).join(table_id.to_string()),
18380 )?
18381 };
18382 let table_dir = table_root.io_path()?;
18383 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
18384 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
18385 manifest.ttl = policy;
18386 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
18387 manifest.schema_id = entry.schema.schema_id;
18388 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18389 }
18390 }
18391 if changed {
18392 match durable_root {
18393 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
18394 None => catalog::write_atomic(root, cat, meta_dek)?,
18395 }
18396 }
18397 }
18398 *target_catalog = recovered_catalog;
18399 Ok(())
18400}
18401
18402fn ensure_recovered_table_storage(
18403 pinned_table: Option<&crate::durable_file::DurableRoot>,
18404 durable_root: Option<&crate::durable_file::DurableRoot>,
18405 fallback_table_dir: &Path,
18406 table_id: u64,
18407 schema: &Schema,
18408 meta_dek: Option<&[u8; META_DEK_LEN]>,
18409) -> Result<()> {
18410 let table_root = if let Some(root) = pinned_table {
18411 root.try_clone()?
18412 } else if let Some(root) = durable_root {
18413 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
18414 match root.open_directory(&relative) {
18415 Ok(table) => table,
18416 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
18417 root.create_directory_all_pinned(relative)?
18418 }
18419 Err(error) => return Err(error.into()),
18420 }
18421 } else {
18422 crate::durable_file::create_directory_all(fallback_table_dir)?;
18423 crate::durable_file::DurableRoot::open(fallback_table_dir)?
18424 };
18425 let table_dir = table_root.io_path()?;
18426 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
18427 Ok(manifest) => {
18428 if manifest.table_id != table_id {
18429 return Err(MongrelError::Conflict(format!(
18430 "recovered table directory id mismatch: expected {table_id}, found {}",
18431 manifest.table_id
18432 )));
18433 }
18434 Some(manifest)
18435 }
18436 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
18437 Err(error) => return Err(error),
18438 };
18439
18440 table_root.create_directory_all(crate::engine::WAL_DIR)?;
18441 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
18442 crate::engine::write_schema(&table_dir, schema)?;
18443
18444 if let Some(mut manifest) = existing_manifest.take() {
18445 if manifest.schema_id != schema.schema_id {
18446 manifest.schema_id = schema.schema_id;
18447 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18448 }
18449 } else {
18450 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
18452 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18453 }
18454 Ok(())
18455}
18456
18457fn validate_recovered_schema(schema: &Schema) -> Result<()> {
18458 schema.validate_auto_increment()?;
18459 schema.validate_defaults()?;
18460 schema.validate_ai()?;
18461 let mut column_ids = HashSet::new();
18462 let mut column_names = HashSet::new();
18463 for column in &schema.columns {
18464 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
18465 return Err(MongrelError::Schema(
18466 "recovered schema contains duplicate columns".into(),
18467 ));
18468 }
18469 match &column.ty {
18470 TypeId::Decimal128 { precision, scale }
18471 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
18472 {
18473 return Err(MongrelError::Schema(format!(
18474 "column {:?} has invalid decimal precision or scale",
18475 column.name
18476 )));
18477 }
18478 TypeId::Enum { variants }
18479 if variants.is_empty()
18480 || variants.iter().any(String::is_empty)
18481 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
18482 {
18483 return Err(MongrelError::Schema(format!(
18484 "column {:?} has invalid enum variants",
18485 column.name
18486 )));
18487 }
18488 _ => {}
18489 }
18490 }
18491 let mut index_names = HashSet::new();
18492 for index in &schema.indexes {
18493 index.validate_options()?;
18494 if index.name.is_empty()
18495 || !index_names.insert(index.name.as_str())
18496 || schema
18497 .columns
18498 .iter()
18499 .all(|column| column.id != index.column_id)
18500 {
18501 return Err(MongrelError::Schema(format!(
18502 "recovered index {:?} references missing column {}",
18503 index.name, index.column_id
18504 )));
18505 }
18506 }
18507 let mut colocated = HashSet::new();
18508 for group in &schema.colocation {
18509 if group.is_empty()
18510 || group.iter().any(|id| !column_ids.contains(id))
18511 || group.iter().any(|id| !colocated.insert(*id))
18512 {
18513 return Err(MongrelError::Schema(
18514 "recovered schema contains invalid column co-location groups".into(),
18515 ));
18516 }
18517 }
18518
18519 let mut constraint_ids = HashSet::new();
18520 let mut constraint_names = HashSet::<String>::new();
18521 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
18522 if name.is_empty()
18523 || !constraint_ids.insert(id)
18524 || !constraint_names.insert(name.to_owned())
18525 {
18526 return Err(MongrelError::Schema(
18527 "recovered schema contains duplicate or empty constraint identities".into(),
18528 ));
18529 }
18530 Ok(())
18531 };
18532 for unique in &schema.constraints.uniques {
18533 validate_constraint_identity(unique.id, &unique.name)?;
18534 if unique.columns.is_empty()
18535 || unique.columns.iter().any(|id| !column_ids.contains(id))
18536 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
18537 {
18538 return Err(MongrelError::Schema(format!(
18539 "unique constraint {:?} has invalid columns",
18540 unique.name
18541 )));
18542 }
18543 }
18544 for foreign_key in &schema.constraints.foreign_keys {
18545 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
18546 if foreign_key.ref_table.is_empty()
18547 || foreign_key.columns.is_empty()
18548 || foreign_key.columns.len() != foreign_key.ref_columns.len()
18549 || foreign_key
18550 .columns
18551 .iter()
18552 .any(|id| !column_ids.contains(id))
18553 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
18554 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
18555 != foreign_key.ref_columns.len()
18556 {
18557 return Err(MongrelError::Schema(format!(
18558 "foreign key {:?} has invalid columns",
18559 foreign_key.name
18560 )));
18561 }
18562 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
18563 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
18564 && foreign_key.columns.iter().any(|id| {
18565 schema
18566 .columns
18567 .iter()
18568 .find(|column| column.id == *id)
18569 .is_none_or(|column| {
18570 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
18571 })
18572 })
18573 {
18574 return Err(MongrelError::Schema(format!(
18575 "foreign key {:?} uses SET NULL on a non-nullable column",
18576 foreign_key.name
18577 )));
18578 }
18579 }
18580 for check in &schema.constraints.checks {
18581 validate_constraint_identity(check.id, &check.name)?;
18582 check.expr.validate()?;
18583 validate_check_columns(&check.expr, &column_ids)?;
18584 }
18585 Ok(())
18586}
18587
18588fn validate_check_columns(
18589 expression: &crate::constraint::CheckExpr,
18590 column_ids: &HashSet<u16>,
18591) -> Result<()> {
18592 use crate::constraint::CheckExpr;
18593 match expression {
18594 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
18595 if column_ids.contains(id) {
18596 Ok(())
18597 } else {
18598 Err(MongrelError::Schema(format!(
18599 "check constraint references unknown column {id}"
18600 )))
18601 }
18602 }
18603 CheckExpr::Regex { col, .. } => {
18604 if column_ids.contains(col) {
18605 Ok(())
18606 } else {
18607 Err(MongrelError::Schema(format!(
18608 "check constraint references unknown column {col}"
18609 )))
18610 }
18611 }
18612 CheckExpr::Add(left, right)
18613 | CheckExpr::Sub(left, right)
18614 | CheckExpr::Mul(left, right)
18615 | CheckExpr::Div(left, right)
18616 | CheckExpr::Mod(left, right)
18617 | CheckExpr::Eq(left, right)
18618 | CheckExpr::Ne(left, right)
18619 | CheckExpr::Lt(left, right)
18620 | CheckExpr::Le(left, right)
18621 | CheckExpr::Gt(left, right)
18622 | CheckExpr::Ge(left, right)
18623 | CheckExpr::And(left, right)
18624 | CheckExpr::Or(left, right) => {
18625 validate_check_columns(left, column_ids)?;
18626 validate_check_columns(right, column_ids)
18627 }
18628 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
18629 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
18630 }
18631}
18632
18633fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
18634 for (name, prior, candidate) in [
18635 ("db_epoch", current.db_epoch, next.db_epoch),
18636 ("next_table_id", current.next_table_id, next.next_table_id),
18637 (
18638 "next_segment_no",
18639 current.next_segment_no,
18640 next.next_segment_no,
18641 ),
18642 ("next_user_id", current.next_user_id, next.next_user_id),
18643 (
18644 "security_version",
18645 current.security_version,
18646 next.security_version,
18647 ),
18648 ] {
18649 if candidate < prior {
18650 return Err(MongrelError::Schema(format!(
18651 "catalog snapshot rolls back {name} from {prior} to {candidate}"
18652 )));
18653 }
18654 }
18655 for prior in ¤t.tables {
18656 let Some(candidate) = next
18657 .tables
18658 .iter()
18659 .find(|entry| entry.table_id == prior.table_id)
18660 else {
18661 return Err(MongrelError::Schema(format!(
18662 "catalog snapshot removes table identity {}",
18663 prior.table_id
18664 )));
18665 };
18666 if candidate.created_epoch != prior.created_epoch
18667 || candidate.schema.schema_id < prior.schema.schema_id
18668 || matches!(prior.state, TableState::Dropped { .. })
18669 && !matches!(candidate.state, TableState::Dropped { .. })
18670 {
18671 return Err(MongrelError::Schema(format!(
18672 "catalog snapshot rolls back table identity {}",
18673 prior.table_id
18674 )));
18675 }
18676 }
18677 for prior in ¤t.users {
18678 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
18679 if candidate.username != prior.username
18680 || candidate.created_epoch != prior.created_epoch
18681 {
18682 return Err(MongrelError::Schema(format!(
18683 "catalog snapshot reuses user identity {}",
18684 prior.id
18685 )));
18686 }
18687 }
18688 }
18689 Ok(())
18690}
18691
18692fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
18693 let mut table_ids = HashSet::new();
18694 let mut active_names = HashSet::new();
18695 let mut max_table_id = None::<u64>;
18696 for entry in &catalog.tables {
18697 if !table_ids.insert(entry.table_id) {
18698 return Err(MongrelError::Schema(format!(
18699 "catalog contains duplicate table id {}",
18700 entry.table_id
18701 )));
18702 }
18703 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
18704 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
18705 return Err(MongrelError::Schema(format!(
18706 "catalog table {} has invalid name or creation epoch",
18707 entry.table_id
18708 )));
18709 }
18710 validate_recovered_schema(&entry.schema)?;
18711 match &entry.state {
18712 TableState::Live => {
18713 if !active_names.insert(entry.name.as_str()) {
18714 return Err(MongrelError::Schema(format!(
18715 "catalog contains duplicate active table name {:?}",
18716 entry.name
18717 )));
18718 }
18719 }
18720 TableState::Dropped { at_epoch } => {
18721 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
18722 return Err(MongrelError::Schema(format!(
18723 "catalog table {} has invalid drop epoch {at_epoch}",
18724 entry.table_id
18725 )));
18726 }
18727 }
18728 TableState::Building {
18729 intended_name,
18730 query_id,
18731 replaces_table_id,
18732 ..
18733 } => {
18734 if intended_name.is_empty() || query_id.is_empty() {
18735 return Err(MongrelError::Schema(format!(
18736 "building table {} has empty identity fields",
18737 entry.table_id
18738 )));
18739 }
18740 if !active_names.insert(entry.name.as_str()) {
18741 return Err(MongrelError::Schema(format!(
18742 "catalog contains duplicate active/building table name {:?}",
18743 entry.name
18744 )));
18745 }
18746 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
18747 return Err(MongrelError::Schema(
18748 "building table cannot replace itself".into(),
18749 ));
18750 }
18751 }
18752 }
18753 }
18754 if let Some(maximum) = max_table_id {
18755 let required = maximum
18756 .checked_add(1)
18757 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
18758 if catalog.next_table_id < required {
18759 return Err(MongrelError::Schema(format!(
18760 "catalog next_table_id {} precedes required {required}",
18761 catalog.next_table_id
18762 )));
18763 }
18764 }
18765 for entry in &catalog.tables {
18766 if let TableState::Building {
18767 replaces_table_id: Some(replaced),
18768 ..
18769 } = entry.state
18770 {
18771 if !table_ids.contains(&replaced) {
18772 return Err(MongrelError::Schema(format!(
18773 "building table {} replaces unknown table {replaced}",
18774 entry.table_id
18775 )));
18776 }
18777 }
18778 }
18779 for entry in &catalog.tables {
18780 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18781 validate_foreign_key_targets(catalog, &entry.schema)?;
18782 }
18783 }
18784
18785 let mut external_names = HashSet::new();
18786 for entry in &catalog.external_tables {
18787 entry.validate()?;
18788 validate_recovered_schema(&entry.declared_schema)?;
18789 if !entry.declared_schema.constraints.is_empty() {
18790 return Err(MongrelError::Schema(format!(
18791 "external table {:?} cannot carry engine-enforced constraints",
18792 entry.name
18793 )));
18794 }
18795 if entry.created_epoch > catalog.db_epoch
18796 || !external_names.insert(entry.name.as_str())
18797 || active_names.contains(entry.name.as_str())
18798 {
18799 return Err(MongrelError::Schema(format!(
18800 "invalid or duplicate external table {:?}",
18801 entry.name
18802 )));
18803 }
18804 }
18805
18806 let mut procedure_names = HashSet::new();
18807 for entry in &catalog.procedures {
18808 entry.procedure.validate()?;
18809 if entry.procedure.created_epoch > entry.procedure.updated_epoch
18810 || entry.procedure.updated_epoch > catalog.db_epoch
18811 || !procedure_names.insert(entry.procedure.name.as_str())
18812 {
18813 return Err(MongrelError::Schema(format!(
18814 "invalid or duplicate procedure {:?}",
18815 entry.procedure.name
18816 )));
18817 }
18818 validate_recovered_procedure_references(catalog, &entry.procedure)?;
18819 }
18820
18821 let mut trigger_names = HashSet::new();
18822 for entry in &catalog.triggers {
18823 entry.trigger.validate()?;
18824 if entry.trigger.created_epoch > entry.trigger.updated_epoch
18825 || entry.trigger.updated_epoch > catalog.db_epoch
18826 || !trigger_names.insert(entry.trigger.name.as_str())
18827 {
18828 return Err(MongrelError::Schema(format!(
18829 "invalid or duplicate trigger {:?}",
18830 entry.trigger.name
18831 )));
18832 }
18833 validate_recovered_trigger_references(catalog, &entry.trigger)?;
18834 }
18835
18836 let mut views = HashSet::new();
18837 for view in &catalog.materialized_views {
18838 let target = catalog.live(&view.name).ok_or_else(|| {
18839 MongrelError::Schema(format!(
18840 "materialized view {:?} has no live table",
18841 view.name
18842 ))
18843 })?;
18844 if view.name.is_empty()
18845 || view.query.trim().is_empty()
18846 || view.last_refresh_epoch > catalog.db_epoch
18847 || !views.insert(view.name.as_str())
18848 {
18849 return Err(MongrelError::Schema(format!(
18850 "materialized view {:?} has no unique live table",
18851 view.name
18852 )));
18853 }
18854 if let Some(incremental) = &view.incremental {
18855 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
18856 MongrelError::Schema(format!(
18857 "materialized view {:?} references missing source {:?}",
18858 view.name, incremental.source_table
18859 ))
18860 })?;
18861 if source.table_id != incremental.source_table_id
18862 || source
18863 .schema
18864 .columns
18865 .iter()
18866 .all(|column| column.id != incremental.group_column)
18867 {
18868 return Err(MongrelError::Schema(format!(
18869 "materialized view {:?} has invalid incremental source",
18870 view.name
18871 )));
18872 }
18873 let target_ids = target
18874 .schema
18875 .columns
18876 .iter()
18877 .map(|column| column.id)
18878 .collect::<HashSet<_>>();
18879 let mut output_ids = HashSet::new();
18880 let count_outputs = incremental
18881 .outputs
18882 .iter()
18883 .filter(|output| {
18884 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
18885 })
18886 .count();
18887 if incremental.checkpoint_event_id.is_empty()
18888 || !target_ids.contains(&incremental.group_output_column)
18889 || !target_ids.contains(&incremental.count_output_column)
18890 || incremental.outputs.is_empty()
18891 || count_outputs != 1
18892 || incremental.outputs.iter().any(|output| {
18893 !target_ids.contains(&output.output_column)
18894 || output.output_column == incremental.group_output_column
18895 || !output_ids.insert(output.output_column)
18896 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
18897 && output.output_column != incremental.count_output_column
18898 || match output.kind {
18899 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
18900 source
18901 .schema
18902 .columns
18903 .iter()
18904 .all(|column| column.id != source_column)
18905 }
18906 crate::catalog::IncrementalAggregateKind::Count => false,
18907 }
18908 })
18909 {
18910 return Err(MongrelError::Schema(format!(
18911 "materialized view {:?} has invalid incremental outputs",
18912 view.name
18913 )));
18914 }
18915 }
18916 }
18917
18918 validate_security_catalog(catalog, &catalog.security)?;
18919 validate_recovered_auth_catalog(catalog)?;
18920 Ok(())
18921}
18922
18923fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
18924 let mut changed = false;
18925 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
18926 let required = maximum
18927 .checked_add(1)
18928 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
18929 if catalog.next_table_id < required {
18930 catalog.next_table_id = required;
18931 changed = true;
18932 }
18933 }
18934 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
18935 let required = maximum
18936 .checked_add(1)
18937 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
18938 if catalog.next_user_id < required {
18939 catalog.next_user_id = required;
18940 changed = true;
18941 }
18942 }
18943 Ok(changed)
18944}
18945
18946fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
18947 for foreign_key in &schema.constraints.foreign_keys {
18948 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
18949 MongrelError::Schema(format!(
18950 "foreign key {:?} references unknown live table {:?}",
18951 foreign_key.name, foreign_key.ref_table
18952 ))
18953 })?;
18954 let referenced_unique = parent
18955 .schema
18956 .constraints
18957 .uniques
18958 .iter()
18959 .any(|unique| unique.columns == foreign_key.ref_columns)
18960 || foreign_key.ref_columns.len() == 1
18961 && parent
18962 .schema
18963 .primary_key()
18964 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
18965 if !referenced_unique {
18966 return Err(MongrelError::Schema(format!(
18967 "foreign key {:?} does not reference a unique key",
18968 foreign_key.name
18969 )));
18970 }
18971 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
18972 let local = schema.columns.iter().find(|column| column.id == *local_id);
18973 let referenced = parent
18974 .schema
18975 .columns
18976 .iter()
18977 .find(|column| column.id == *parent_id);
18978 if local
18979 .zip(referenced)
18980 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
18981 {
18982 return Err(MongrelError::Schema(format!(
18983 "foreign key {:?} has missing or incompatible columns",
18984 foreign_key.name
18985 )));
18986 }
18987 }
18988 }
18989 Ok(())
18990}
18991
18992fn validate_recovered_procedure_references(
18993 catalog: &Catalog,
18994 procedure: &StoredProcedure,
18995) -> Result<()> {
18996 for step in &procedure.body.steps {
18997 let Some(table_name) = step.table() else {
18998 continue;
18999 };
19000 let schema = &catalog
19001 .live(table_name)
19002 .ok_or_else(|| {
19003 MongrelError::Schema(format!(
19004 "procedure {:?} references unknown table {table_name:?}",
19005 procedure.name
19006 ))
19007 })?
19008 .schema;
19009 match step {
19010 ProcedureStep::NativeQuery {
19011 conditions,
19012 projection,
19013 ..
19014 } => {
19015 for condition in conditions {
19016 validate_condition_columns(condition, schema)?;
19017 }
19018 for id in projection.iter().flatten() {
19019 validate_column_id(*id, schema)?;
19020 }
19021 }
19022 ProcedureStep::Put { cells, .. } => {
19023 for cell in cells {
19024 validate_column_id(cell.column_id, schema)?;
19025 }
19026 }
19027 ProcedureStep::Upsert {
19028 cells,
19029 update_cells,
19030 ..
19031 } => {
19032 for cell in cells.iter().chain(update_cells.iter().flatten()) {
19033 validate_column_id(cell.column_id, schema)?;
19034 }
19035 }
19036 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
19037 return Err(MongrelError::Schema(format!(
19038 "procedure {:?} deletes by primary key on table without one",
19039 procedure.name
19040 )));
19041 }
19042 ProcedureStep::DeleteByPk { .. }
19043 | ProcedureStep::DeleteRows { .. }
19044 | ProcedureStep::SqlQuery { .. } => {}
19045 }
19046 }
19047 Ok(())
19048}
19049
19050fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
19051 let target_schema = match &trigger.target {
19052 TriggerTarget::Table(name) => catalog
19053 .live(name)
19054 .ok_or_else(|| {
19055 MongrelError::Schema(format!(
19056 "trigger {:?} references unknown table {name:?}",
19057 trigger.name
19058 ))
19059 })?
19060 .schema
19061 .clone(),
19062 TriggerTarget::View(_) => Schema {
19063 columns: trigger.target_columns.clone(),
19064 ..Schema::default()
19065 },
19066 };
19067 for column in &trigger.update_of {
19068 if target_schema.column(column).is_none() {
19069 return Err(MongrelError::Schema(format!(
19070 "trigger {:?} references unknown UPDATE OF column {column:?}",
19071 trigger.name
19072 )));
19073 }
19074 }
19075 if let Some(expr) = &trigger.when {
19076 validate_trigger_expr(expr, &target_schema, trigger.event)?;
19077 }
19078 let mut selects = HashMap::new();
19079 for step in &trigger.program.steps {
19080 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
19081 return Err(MongrelError::Schema(
19082 "SetNew is only valid in BEFORE triggers".into(),
19083 ));
19084 }
19085 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
19086 }
19087 Ok(())
19088}
19089
19090fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
19091 let mut role_names = HashSet::new();
19092 for role in &catalog.roles {
19093 if role.name.is_empty()
19094 || role.created_epoch > catalog.db_epoch
19095 || !role_names.insert(role.name.as_str())
19096 {
19097 return Err(MongrelError::Schema(format!(
19098 "invalid or duplicate role {:?}",
19099 role.name
19100 )));
19101 }
19102 for permission in &role.permissions {
19103 if let Some(table) = permission_table(permission) {
19104 let schema = catalog
19105 .live(table)
19106 .map(|entry| &entry.schema)
19107 .or_else(|| {
19108 catalog
19109 .external_tables
19110 .iter()
19111 .find(|entry| entry.name == table)
19112 .map(|entry| &entry.declared_schema)
19113 })
19114 .ok_or_else(|| {
19115 MongrelError::Schema(format!(
19116 "role {:?} references unknown table {table:?}",
19117 role.name
19118 ))
19119 })?;
19120 let columns = match permission {
19121 crate::auth::Permission::SelectColumns { columns, .. }
19122 | crate::auth::Permission::InsertColumns { columns, .. }
19123 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
19124 _ => None,
19125 };
19126 if columns.is_some_and(|columns| {
19127 columns.is_empty()
19128 || columns.iter().any(|column| schema.column(column).is_none())
19129 }) {
19130 return Err(MongrelError::Schema(format!(
19131 "role {:?} contains invalid column permissions",
19132 role.name
19133 )));
19134 }
19135 }
19136 }
19137 }
19138 let mut user_ids = HashSet::new();
19139 let mut usernames = HashSet::new();
19140 let mut maximum_user_id = 0;
19141 for user in &catalog.users {
19142 maximum_user_id = maximum_user_id.max(user.id);
19143 if user.id == 0
19144 || user.username.is_empty()
19145 || user.password_hash.is_empty()
19146 || user.created_epoch > catalog.db_epoch
19147 || !user_ids.insert(user.id)
19148 || !usernames.insert(user.username.as_str())
19149 || user
19150 .roles
19151 .iter()
19152 .any(|role| !role_names.contains(role.as_str()))
19153 {
19154 return Err(MongrelError::Schema(format!(
19155 "invalid or duplicate user {:?}",
19156 user.username
19157 )));
19158 }
19159 }
19160 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
19161 return Err(MongrelError::Schema(
19162 "catalog next_user_id does not advance beyond existing user ids".into(),
19163 ));
19164 }
19165 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
19166 return Err(MongrelError::Schema(
19167 "authenticated catalog has no administrator".into(),
19168 ));
19169 }
19170 Ok(())
19171}
19172
19173fn validate_recovered_storage_plan(
19174 root: &Path,
19175 durable_root: Option<&crate::durable_file::DurableRoot>,
19176 catalog: &Catalog,
19177 created_table_ids: &HashSet<u64>,
19178 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
19179 meta_dek: Option<&[u8; META_DEK_LEN]>,
19180) -> Result<Vec<u64>> {
19181 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
19182 let mut reconcile = Vec::new();
19183 for entry in &catalog.tables {
19184 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19185 continue;
19186 }
19187 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19188 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
19189 let table_exists = match durable_root {
19190 Some(root) => match root.open_directory(&relative_dir) {
19191 Ok(_) => true,
19192 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19193 Err(error) => return Err(error.into()),
19194 },
19195 None => table_dir.is_dir(),
19196 };
19197 if !table_exists {
19198 if created_table_ids.contains(&entry.table_id) {
19199 reconcile.push(entry.table_id);
19200 continue;
19201 }
19202 return Err(MongrelError::NotFound(format!(
19203 "catalog table {} storage is missing",
19204 entry.table_id
19205 )));
19206 }
19207 let manifest_result = match durable_root {
19208 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
19209 None => crate::manifest::read(&table_dir, meta_dek),
19210 };
19211 let manifest = match manifest_result {
19212 Ok(manifest) => manifest,
19213 Err(MongrelError::Io(error))
19214 if created_table_ids.contains(&entry.table_id)
19215 && error.kind() == std::io::ErrorKind::NotFound =>
19216 {
19217 reconcile.push(entry.table_id);
19218 continue;
19219 }
19220 Err(error) => return Err(error),
19221 };
19222 if manifest.table_id != entry.table_id {
19223 return Err(MongrelError::Conflict(format!(
19224 "catalog table {} storage identity mismatch",
19225 entry.table_id
19226 )));
19227 }
19228 let schema_result = match durable_root {
19229 Some(root) => root
19230 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
19231 .map_err(MongrelError::from),
19232 None => crate::durable_file::open_regular_nofollow(
19233 &table_dir.join(crate::engine::SCHEMA_FILENAME),
19234 ),
19235 };
19236 let file = match schema_result {
19237 Ok(file) => file,
19238 Err(MongrelError::Io(error))
19239 if created_table_ids.contains(&entry.table_id)
19240 && error.kind() == std::io::ErrorKind::NotFound =>
19241 {
19242 reconcile.push(entry.table_id);
19243 continue;
19244 }
19245 Err(error) => return Err(error),
19246 };
19247 let length = file.metadata()?.len();
19248 if length > MAX_SCHEMA_BYTES {
19249 return Err(MongrelError::ResourceLimitExceeded {
19250 resource: "recovered schema bytes",
19251 requested: usize::try_from(length).unwrap_or(usize::MAX),
19252 limit: MAX_SCHEMA_BYTES as usize,
19253 });
19254 }
19255 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
19256 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
19257 if manifest.schema_id != entry.schema.schema_id
19258 || crate::wal::DdlOp::encode_schema(&disk_schema)?
19259 != crate::wal::DdlOp::encode_schema(&entry.schema)?
19260 {
19261 reconcile.push(entry.table_id);
19262 }
19263 }
19264 for table_id in ttl_updates.keys() {
19265 if !catalog.tables.iter().any(|entry| {
19266 entry.table_id == *table_id
19267 && matches!(entry.state, TableState::Live | TableState::Building { .. })
19268 }) {
19269 continue;
19270 }
19271 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
19272 let table_exists = match durable_root {
19273 Some(root) => match root.open_directory(&relative_dir) {
19274 Ok(_) => true,
19275 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19276 Err(error) => return Err(error.into()),
19277 },
19278 None => root.join(&relative_dir).is_dir(),
19279 };
19280 if !table_exists && !created_table_ids.contains(table_id) {
19281 return Err(MongrelError::NotFound(format!(
19282 "TTL recovery table {table_id} storage is missing"
19283 )));
19284 }
19285 }
19286 reconcile.sort_unstable();
19287 reconcile.dedup();
19288 Ok(reconcile)
19289}
19290
19291fn validate_catalog_table_storage(
19292 root: &crate::durable_file::DurableRoot,
19293 catalog: &Catalog,
19294 meta_dek: Option<&[u8; META_DEK_LEN]>,
19295) -> Result<()> {
19296 for entry in &catalog.tables {
19297 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19298 continue;
19299 }
19300 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19301 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
19302 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
19303 return Err(MongrelError::Conflict(format!(
19304 "catalog table {} storage identity mismatch",
19305 entry.table_id
19306 )));
19307 }
19308 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
19309 }
19310 Ok(())
19311}
19312
19313fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
19314 match schema.columns.iter_mut().find(|c| c.id == column.id) {
19315 Some(existing) if *existing == column => Ok(false),
19316 Some(existing) => {
19317 *existing = column;
19318 schema.schema_id = schema
19319 .schema_id
19320 .checked_add(1)
19321 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19322 Ok(true)
19323 }
19324 None => {
19325 schema.columns.push(column);
19326 schema.schema_id = schema
19327 .schema_id
19328 .checked_add(1)
19329 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19330 Ok(true)
19331 }
19332 }
19333}
19334
19335fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
19336 use crate::auth::Permission;
19337 match permission {
19338 Permission::Select { table }
19339 | Permission::Insert { table }
19340 | Permission::Update { table }
19341 | Permission::Delete { table }
19342 | Permission::SelectColumns { table, .. }
19343 | Permission::InsertColumns { table, .. }
19344 | Permission::UpdateColumns { table, .. } => Some(table),
19345 Permission::All | Permission::Ddl | Permission::Admin => None,
19346 }
19347}
19348
19349fn apply_rebuilding_publish(
19350 catalog: &mut Catalog,
19351 table_id: u64,
19352 replaced_table_id: u64,
19353 new_name: &str,
19354 epoch: u64,
19355) -> Result<bool> {
19356 let already_published = catalog.tables.iter().any(|entry| {
19357 entry.table_id == table_id
19358 && entry.name == new_name
19359 && matches!(entry.state, TableState::Live)
19360 }) && catalog.tables.iter().any(|entry| {
19361 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
19362 });
19363 if already_published {
19364 return Ok(false);
19365 }
19366 let schema = catalog
19367 .tables
19368 .iter()
19369 .find(|entry| entry.table_id == table_id)
19370 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
19371 .schema
19372 .clone();
19373 let replaced = catalog
19374 .tables
19375 .iter_mut()
19376 .find(|entry| entry.table_id == replaced_table_id)
19377 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
19378 replaced.state = TableState::Dropped { at_epoch: epoch };
19379 let replacement = catalog
19380 .tables
19381 .iter_mut()
19382 .find(|entry| entry.table_id == table_id)
19383 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
19384 replacement.name = new_name.to_string();
19385 replacement.state = TableState::Live;
19386
19387 for role in &mut catalog.roles {
19388 role.permissions.retain_mut(|permission| {
19389 retain_rebuilt_permission_columns(permission, new_name, &schema)
19390 });
19391 }
19392 for definition in &mut catalog.materialized_views {
19393 if let Some(incremental) = definition.incremental.as_mut() {
19394 if incremental.source_table == new_name
19395 && incremental.source_table_id == replaced_table_id
19396 {
19397 incremental.source_table_id = table_id;
19398 }
19399 }
19400 }
19401 advance_security_version(catalog)?;
19402 Ok(true)
19403}
19404
19405fn retain_rebuilt_permission_columns(
19406 permission: &mut crate::auth::Permission,
19407 target_table: &str,
19408 schema: &Schema,
19409) -> bool {
19410 use crate::auth::Permission;
19411 let columns = match permission {
19412 Permission::SelectColumns { table, columns }
19413 | Permission::InsertColumns { table, columns }
19414 | Permission::UpdateColumns { table, columns }
19415 if table == target_table =>
19416 {
19417 Some(columns)
19418 }
19419 _ => None,
19420 };
19421 if let Some(columns) = columns {
19422 columns.retain(|column| schema.column(column).is_some());
19423 !columns.is_empty()
19424 } else {
19425 true
19426 }
19427}
19428
19429fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
19430 use crate::auth::Permission;
19431 let table = match permission {
19432 Permission::Select { table }
19433 | Permission::Insert { table }
19434 | Permission::Update { table }
19435 | Permission::Delete { table }
19436 | Permission::SelectColumns { table, .. }
19437 | Permission::InsertColumns { table, .. }
19438 | Permission::UpdateColumns { table, .. } => Some(table),
19439 Permission::All | Permission::Ddl | Permission::Admin => None,
19440 };
19441 if let Some(table) = table.filter(|table| table.as_str() == old) {
19442 *table = new.to_string();
19443 }
19444}
19445
19446fn rename_permission_column(
19447 permission: &mut crate::auth::Permission,
19448 target_table: &str,
19449 old: &str,
19450 new: &str,
19451) {
19452 use crate::auth::Permission;
19453 let columns = match permission {
19454 Permission::SelectColumns { table, columns }
19455 | Permission::InsertColumns { table, columns }
19456 | Permission::UpdateColumns { table, columns }
19457 if table == target_table =>
19458 {
19459 Some(columns)
19460 }
19461 _ => None,
19462 };
19463 if let Some(column) = columns
19464 .into_iter()
19465 .flatten()
19466 .find(|column| column.as_str() == old)
19467 {
19468 *column = new.to_string();
19469 }
19470}
19471
19472pub(crate) fn validate_security_catalog(
19473 catalog: &Catalog,
19474 security: &crate::security::SecurityCatalog,
19475) -> Result<()> {
19476 let mut policy_names = HashSet::new();
19477 for table in &security.rls_tables {
19478 if catalog.live(table).is_none() {
19479 return Err(MongrelError::NotFound(format!(
19480 "RLS table {table:?} not found"
19481 )));
19482 }
19483 }
19484 for policy in &security.policies {
19485 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
19486 return Err(MongrelError::InvalidArgument(format!(
19487 "duplicate policy {:?} on {:?}",
19488 policy.name, policy.table
19489 )));
19490 }
19491 let schema = &catalog
19492 .live(&policy.table)
19493 .ok_or_else(|| {
19494 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
19495 })?
19496 .schema;
19497 if let Some(expression) = &policy.using {
19498 validate_security_expression(expression, schema)?;
19499 }
19500 if let Some(expression) = &policy.with_check {
19501 validate_security_expression(expression, schema)?;
19502 }
19503 }
19504 let mut mask_names = HashSet::new();
19505 for mask in &security.masks {
19506 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
19507 return Err(MongrelError::InvalidArgument(format!(
19508 "duplicate mask {:?} on {:?}",
19509 mask.name, mask.table
19510 )));
19511 }
19512 let column = catalog
19513 .live(&mask.table)
19514 .and_then(|entry| {
19515 entry
19516 .schema
19517 .columns
19518 .iter()
19519 .find(|column| column.id == mask.column)
19520 })
19521 .ok_or_else(|| {
19522 MongrelError::NotFound(format!(
19523 "mask column {} on {:?} not found",
19524 mask.column, mask.table
19525 ))
19526 })?;
19527 if matches!(
19528 mask.strategy,
19529 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
19530 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
19531 {
19532 return Err(MongrelError::InvalidArgument(format!(
19533 "mask {:?} requires a string/bytes column",
19534 mask.name
19535 )));
19536 }
19537 }
19538 Ok(())
19539}
19540
19541fn validate_security_expression(
19542 expression: &crate::security::SecurityExpr,
19543 schema: &Schema,
19544) -> Result<()> {
19545 use crate::security::SecurityExpr;
19546 match expression {
19547 SecurityExpr::True => Ok(()),
19548 SecurityExpr::ColumnEqCurrentUser { column }
19549 | SecurityExpr::ColumnEqValue { column, .. } => {
19550 if schema
19551 .columns
19552 .iter()
19553 .any(|candidate| candidate.id == *column)
19554 {
19555 Ok(())
19556 } else {
19557 Err(MongrelError::InvalidArgument(format!(
19558 "security expression references unknown column id {column}"
19559 )))
19560 }
19561 }
19562 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
19563 validate_security_expression(left, schema)?;
19564 validate_security_expression(right, schema)
19565 }
19566 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
19567 }
19568}
19569
19570fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
19572 let referenced = cat
19573 .tables
19574 .iter()
19575 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
19576 .map(|entry| entry.table_id)
19577 .collect::<HashSet<_>>();
19578 let tables_dir = root.join(TABLES_DIR);
19579 let entries = match std::fs::read_dir(&tables_dir) {
19580 Ok(entries) => entries,
19581 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
19582 Err(error) => return Err(error.into()),
19583 };
19584 for entry in entries {
19585 let entry = entry?;
19586 if !entry.file_type()?.is_dir() {
19587 continue;
19588 }
19589 let file_name = entry.file_name();
19590 let Some(name) = file_name.to_str() else {
19591 continue;
19592 };
19593 let Ok(table_id) = name.parse::<u64>() else {
19594 continue;
19595 };
19596 if name != table_id.to_string() {
19597 continue;
19598 }
19599 if !referenced.contains(&table_id) {
19600 crate::durable_file::remove_directory_all(&entry.path())?;
19601 }
19602 }
19603 Ok(())
19604}
19605
19606fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
19611 for entry in &cat.tables {
19612 let txn_dir = root
19613 .join(TABLES_DIR)
19614 .join(entry.table_id.to_string())
19615 .join("_txn");
19616 if txn_dir.exists() {
19617 let _ = std::fs::remove_dir_all(&txn_dir);
19618 }
19619 }
19620}
19621
19622#[cfg(test)]
19623mod write_permission_tests {
19624 use super::*;
19625 use crate::txn::Staged;
19626
19627 struct NoopExternalBridge;
19628
19629 impl ExternalTriggerBridge for NoopExternalBridge {
19630 fn apply_trigger_external_write(
19631 &self,
19632 _entry: &ExternalTableEntry,
19633 base_state: Vec<u8>,
19634 _op: ExternalTriggerWrite,
19635 ) -> Result<ExternalTriggerWriteResult> {
19636 Ok(ExternalTriggerWriteResult::new(base_state))
19637 }
19638 }
19639
19640 fn assert_txn_namespace_full<T>(result: Result<T>) {
19641 assert!(matches!(result, Err(MongrelError::Full(_))));
19642 }
19643
19644 #[test]
19645 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
19646 let directory = tempfile::tempdir().unwrap();
19647 let database = Database::create(directory.path()).unwrap();
19648 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
19649 *database.next_txn_id.lock() = generation << 32;
19650 let before = crate::wal::SharedWal::replay(directory.path())
19651 .unwrap()
19652 .len();
19653 let bridge = NoopExternalBridge;
19654
19655 assert_txn_namespace_full(database.begin().commit());
19656 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
19657 assert_txn_namespace_full(
19658 database
19659 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
19660 .commit(),
19661 );
19662 assert_txn_namespace_full(
19663 database
19664 .begin_with_external_trigger_bridge(&bridge)
19665 .commit(),
19666 );
19667 assert_txn_namespace_full(
19668 database
19669 .begin_with_external_trigger_bridge_as(&bridge, None)
19670 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
19671 );
19672
19673 assert_eq!(
19674 crate::wal::SharedWal::replay(directory.path())
19675 .unwrap()
19676 .len(),
19677 before
19678 );
19679 drop(database);
19680 Database::open(directory.path()).unwrap();
19681 }
19682
19683 #[test]
19684 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
19685 let directory = tempfile::tempdir().unwrap();
19686 let table_dir = directory.path().join("7");
19687 crate::durable_file::create_directory_all(&table_dir).unwrap();
19688 let original_schema = test_schema();
19689 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
19690 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
19691 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
19692 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
19693 let original_bytes = std::fs::read(&schema_path).unwrap();
19694
19695 let mut replacement_schema = original_schema;
19696 replacement_schema.schema_id += 1;
19697 assert!(matches!(
19698 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
19699 Err(MongrelError::Conflict(_))
19700 ));
19701
19702 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
19703 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
19704 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
19705 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
19706 }
19707
19708 #[test]
19709 fn catalog_table_missing_storage_fails_without_recreating_it() {
19710 let directory = tempfile::tempdir().unwrap();
19711 let table_dir = {
19712 let database = Database::create(directory.path()).unwrap();
19713 database.create_table("docs", test_schema()).unwrap();
19714 directory
19715 .path()
19716 .join(TABLES_DIR)
19717 .join(database.table_id("docs").unwrap().to_string())
19718 };
19719 std::fs::remove_dir_all(&table_dir).unwrap();
19720
19721 assert!(matches!(
19722 Database::open(directory.path()),
19723 Err(MongrelError::NotFound(_))
19724 ));
19725 assert!(!table_dir.exists());
19726 }
19727
19728 #[test]
19729 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
19730 let directory = tempfile::tempdir().unwrap();
19731 let database = std::sync::Arc::new(
19732 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
19733 );
19734 database.create_user("alice", "old-password").unwrap();
19735 let old_identity = database.user_identity("alice").unwrap();
19736 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
19737 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
19738 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
19739 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
19740
19741 std::thread::scope(|scope| {
19742 let authenticate = {
19743 let database = std::sync::Arc::clone(&database);
19744 scope.spawn(move || {
19745 database.authenticate_principal_inner("alice", "old-password", || {
19746 verified_tx.send(()).unwrap();
19747 resume_rx.recv().unwrap();
19748 })
19749 })
19750 };
19751 verified_rx.recv().unwrap();
19752 let mutate = {
19753 let database = std::sync::Arc::clone(&database);
19754 scope.spawn(move || {
19755 mutation_started_tx.send(()).unwrap();
19756 database.drop_user("alice").unwrap();
19757 database.create_user("alice", "new-password").unwrap();
19758 mutation_done_tx.send(()).unwrap();
19759 })
19760 };
19761 mutation_started_rx.recv().unwrap();
19762 assert!(mutation_done_rx
19763 .recv_timeout(std::time::Duration::from_millis(50))
19764 .is_err());
19765 resume_tx.send(()).unwrap();
19766 let principal = authenticate.join().unwrap().unwrap().unwrap();
19767 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
19768 mutate.join().unwrap();
19769 });
19770
19771 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
19772 assert!(database
19773 .authenticate_principal("alice", "old-password")
19774 .unwrap()
19775 .is_none());
19776 assert!(database
19777 .authenticate_principal("alice", "new-password")
19778 .unwrap()
19779 .is_some());
19780 }
19781
19782 #[test]
19783 fn homogeneous_batch_summarizes_to_one_permission_decision() {
19784 let staging = (0..10_050)
19785 .map(|_| {
19786 (
19787 7,
19788 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
19789 )
19790 })
19791 .collect::<Vec<_>>();
19792
19793 let needs = summarize_write_permissions(&staging);
19794 let table = needs.get(&7).unwrap();
19795 assert_eq!(needs.len(), 1);
19796 assert!(table.insert);
19797 assert_eq!(table.insert_columns, [1, 2]);
19798 assert!(!table.update);
19799 assert!(!table.delete);
19800 assert!(!table.truncate);
19801 }
19802
19803 #[test]
19804 fn mixed_writes_union_columns_and_preserve_empty_operations() {
19805 let staging = vec![
19806 (7, Staged::Put(vec![(2, Value::Int64(2))])),
19807 (7, Staged::Put(vec![(1, Value::Int64(1))])),
19808 (
19809 7,
19810 Staged::Update {
19811 row_id: RowId(1),
19812 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
19813 changed_columns: vec![2],
19814 },
19815 ),
19816 (7, Staged::Delete(RowId(2))),
19817 (8, Staged::Truncate),
19818 ];
19819
19820 let needs = summarize_write_permissions(&staging);
19821 let table = needs.get(&7).unwrap();
19822 assert_eq!(table.insert_columns, [1, 2]);
19823 assert!(table.update);
19824 assert_eq!(table.update_columns, [2]);
19825 assert!(table.delete);
19826 assert!(needs.get(&8).unwrap().truncate);
19827 }
19828
19829 #[test]
19830 fn final_permission_decisions_do_not_scale_with_rows() {
19831 let credentialless_dir = tempfile::tempdir().unwrap();
19832 let credentialless = Database::create(credentialless_dir.path()).unwrap();
19833 credentialless.create_table("docs", test_schema()).unwrap();
19834 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
19835 credentialless
19836 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
19837 .unwrap();
19838 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
19839
19840 let authenticated_dir = tempfile::tempdir().unwrap();
19841 let authenticated =
19842 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
19843 .unwrap();
19844 authenticated.create_table("docs", test_schema()).unwrap();
19845 let admin = authenticated.resolve_principal("admin").unwrap();
19846 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
19847 authenticated
19848 .validate_write_permissions(
19849 &puts(authenticated.table_id("docs").unwrap()),
19850 Some(&admin),
19851 None,
19852 )
19853 .unwrap();
19854 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
19855 }
19856
19857 #[test]
19858 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
19859 let dir = tempfile::tempdir().unwrap();
19860 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
19861 db.create_table("docs", test_schema()).unwrap();
19862 let admin = db.resolve_principal("admin").unwrap();
19863 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
19864
19865 let mut transaction = db.begin_as(Some(admin));
19866 transaction
19867 .delete_batch("docs", (0..100).map(RowId).collect())
19868 .unwrap();
19869 transaction.commit().unwrap();
19870
19871 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
19872 }
19873
19874 #[test]
19875 fn truncate_validation_checks_admin_once_for_all_tables() {
19876 let dir = tempfile::tempdir().unwrap();
19877 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
19878 db.create_table("first", test_schema()).unwrap();
19879 db.create_table("second", test_schema()).unwrap();
19880 let admin = db.resolve_principal("admin").unwrap();
19881 let staging = vec![
19882 (db.table_id("first").unwrap(), Staged::Truncate),
19883 (db.table_id("second").unwrap(), Staged::Truncate),
19884 ];
19885
19886 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
19887 db.validate_write_permissions(&staging, Some(&admin), None)
19888 .unwrap();
19889 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
19890 }
19891
19892 #[test]
19893 fn one_table_commit_batches_structural_work() {
19894 let dir = tempfile::tempdir().unwrap();
19895 let db = Database::create(dir.path()).unwrap();
19896 db.create_table("docs", test_schema()).unwrap();
19897 let table_id = db.table_id("docs").unwrap();
19898
19899 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
19900 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
19901 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
19902 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
19903 db.transaction(|transaction| {
19904 for id in 0..100 {
19905 transaction.put("docs", vec![(1, Value::Int64(id))])?;
19906 }
19907 Ok(())
19908 })
19909 .unwrap();
19910
19911 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
19912 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
19913 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
19914 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
19915
19916 let puts = crate::wal::SharedWal::replay(dir.path())
19917 .unwrap()
19918 .into_iter()
19919 .filter_map(|record| match record.op {
19920 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
19921 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
19922 .unwrap()
19923 .len(),
19924 ),
19925 _ => None,
19926 })
19927 .collect::<Vec<_>>();
19928 assert_eq!(puts, [100]);
19929
19930 let row_ids = db
19931 .table("docs")
19932 .unwrap()
19933 .lock()
19934 .visible_rows(db.snapshot().0)
19935 .unwrap()
19936 .into_iter()
19937 .take(2)
19938 .map(|row| row.row_id)
19939 .collect::<Vec<_>>();
19940 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
19941 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
19942 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
19943 db.transaction(|transaction| {
19944 for row_id in row_ids {
19945 transaction.delete("docs", row_id)?;
19946 }
19947 Ok(())
19948 })
19949 .unwrap();
19950 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
19951 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
19952 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
19953
19954 let deletes = crate::wal::SharedWal::replay(dir.path())
19955 .unwrap()
19956 .into_iter()
19957 .filter_map(|record| match record.op {
19958 crate::wal::Op::Delete {
19959 table_id: id,
19960 row_ids,
19961 } if id == table_id => Some(row_ids.len()),
19962 _ => None,
19963 })
19964 .collect::<Vec<_>>();
19965 assert_eq!(deletes, [2]);
19966 }
19967
19968 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
19969 (0..10_050)
19970 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
19971 .collect()
19972 }
19973
19974 fn test_schema() -> Schema {
19975 Schema {
19976 columns: vec![ColumnDef {
19977 id: 1,
19978 name: "id".into(),
19979 ty: TypeId::Int64,
19980 flags: crate::schema::ColumnFlags::empty()
19981 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
19982 default_value: None,
19983 embedding_source: None,
19984 }],
19985 ..Schema::default()
19986 }
19987 }
19988}
19989
19990#[cfg(test)]
19991mod cdc_bounds_tests {
19992 use super::*;
19993
19994 #[test]
19995 fn retained_byte_limit_rejects_without_allocating_payload() {
19996 let mut retained = 0;
19997 let error = charge_cdc_bytes(
19998 &mut retained,
19999 CDC_MAX_RETAINED_BYTES.saturating_add(1),
20000 "CDC retained bytes",
20001 )
20002 .unwrap_err();
20003 assert!(matches!(
20004 error,
20005 MongrelError::ResourceLimitExceeded {
20006 resource: "CDC retained bytes",
20007 ..
20008 }
20009 ));
20010 }
20011
20012 #[test]
20013 fn row_json_estimate_accounts_for_byte_array_expansion() {
20014 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
20015 .with_column(1, Value::Bytes(vec![0; 1024]));
20016 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
20017 }
20018}
20019
20020#[cfg(test)]
20021mod generation_metrics_tests {
20022 use super::*;
20023 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20024
20025 #[test]
20026 fn legacy_cow_fallback_is_measured() {
20027 let dir = tempfile::tempdir().unwrap();
20028 let table = Table::create(
20029 dir.path(),
20030 Schema {
20031 columns: vec![ColumnDef {
20032 id: 1,
20033 name: "id".into(),
20034 ty: TypeId::Int64,
20035 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20036 default_value: None,
20037 embedding_source: None,
20038 }],
20039 ..Schema::default()
20040 },
20041 1,
20042 )
20043 .unwrap();
20044 let handle = TableHandle::from_table(table);
20045 let held = match &handle.inner {
20046 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
20047 TableHandleInner::Direct(_) => unreachable!(),
20048 };
20049
20050 handle.lock().set_sync_byte_threshold(1);
20051
20052 let stats = handle.generation_stats();
20053 assert_eq!(stats.cow_clone_count, 1);
20054 assert!(stats.estimated_cow_clone_bytes > 0);
20055 drop(held);
20056 }
20057}
20058
20059#[cfg(test)]
20060mod trigger_engine_tests {
20061 use super::*;
20062
20063 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
20064 WriteEvent {
20065 table: "test".into(),
20066 kind: TriggerEvent::Insert,
20067 new: Some(TriggerRowImage {
20068 columns: new_cells.iter().cloned().collect(),
20069 }),
20070 old: Some(TriggerRowImage {
20071 columns: old_cells.iter().cloned().collect(),
20072 }),
20073 changed_columns: Vec::new(),
20074 op_indices: Vec::new(),
20075 put_idx: None,
20076 trigger_stack: Vec::new(),
20077 }
20078 }
20079
20080 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
20081 WriteEvent {
20082 table: "test".into(),
20083 kind: TriggerEvent::Insert,
20084 new: Some(TriggerRowImage {
20085 columns: new_cells.iter().cloned().collect(),
20086 }),
20087 old: None,
20088 changed_columns: Vec::new(),
20089 op_indices: Vec::new(),
20090 put_idx: None,
20091 trigger_stack: Vec::new(),
20092 }
20093 }
20094
20095 #[test]
20096 fn value_order_int64_vs_float64() {
20097 assert_eq!(
20098 value_order(&Value::Int64(5), &Value::Float64(5.0)),
20099 Some(std::cmp::Ordering::Equal)
20100 );
20101 assert_eq!(
20102 value_order(&Value::Int64(5), &Value::Float64(3.0)),
20103 Some(std::cmp::Ordering::Greater)
20104 );
20105 assert_eq!(
20106 value_order(&Value::Int64(2), &Value::Float64(3.0)),
20107 Some(std::cmp::Ordering::Less)
20108 );
20109 }
20110
20111 #[test]
20112 fn value_order_null_returns_none() {
20113 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
20114 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
20115 assert_eq!(value_order(&Value::Null, &Value::Null), None);
20116 }
20117
20118 #[test]
20119 fn value_order_cross_group_returns_none() {
20120 assert_eq!(
20121 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
20122 None
20123 );
20124 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
20125 assert_eq!(
20126 value_order(
20127 &Value::Embedding(vec![1.0, 2.0]),
20128 &Value::Embedding(vec![1.0, 2.0])
20129 ),
20130 None
20131 );
20132 }
20133
20134 #[test]
20135 fn eval_trigger_expr_ranges_and_booleans() {
20136 let expr = TriggerExpr::And {
20137 left: Box::new(TriggerExpr::Gt {
20138 left: TriggerValue::NewColumn(1),
20139 right: TriggerValue::Literal(Value::Int64(0)),
20140 }),
20141 right: Box::new(TriggerExpr::Lte {
20142 left: TriggerValue::NewColumn(1),
20143 right: TriggerValue::Literal(Value::Int64(100)),
20144 }),
20145 };
20146 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
20147 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
20148 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
20149
20150 let or_expr = TriggerExpr::Or {
20151 left: Box::new(TriggerExpr::Lt {
20152 left: TriggerValue::NewColumn(1),
20153 right: TriggerValue::Literal(Value::Int64(0)),
20154 }),
20155 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
20156 TriggerValue::OldColumn(2),
20157 )))),
20158 };
20159 assert!(eval_trigger_expr(
20160 &or_expr,
20161 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
20162 )
20163 .unwrap());
20164 assert!(!eval_trigger_expr(
20165 &or_expr,
20166 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
20167 )
20168 .unwrap());
20169
20170 assert!(eval_trigger_expr(
20171 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
20172 &event_insert(&[])
20173 )
20174 .unwrap());
20175 assert!(!eval_trigger_expr(
20176 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
20177 &event_insert(&[])
20178 )
20179 .unwrap());
20180 assert!(!eval_trigger_expr(
20181 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
20182 &event_insert(&[])
20183 )
20184 .unwrap());
20185 }
20186}
20187
20188#[cfg(test)]
20189mod core_resource_tests {
20190 use super::*;
20191
20192 fn int_pk_schema() -> Schema {
20193 Schema {
20194 columns: vec![ColumnDef {
20195 id: 1,
20196 name: "id".into(),
20197 ty: TypeId::Int64,
20198 flags: crate::schema::ColumnFlags::empty()
20199 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20200 default_value: None,
20201 embedding_source: None,
20202 }],
20203 ..Schema::default()
20204 }
20205 }
20206
20207 #[test]
20208 fn open_constructs_governor_spill_and_jobs_with_documented_defaults() {
20209 let dir = tempfile::tempdir().unwrap();
20210 let db = Database::create(dir.path()).unwrap();
20211 assert_eq!(
20212 db.memory_governor().max_bytes(),
20213 DEFAULT_MEMORY_BUDGET_BYTES
20214 );
20215 assert_eq!(
20216 db.spill_manager().config().global_bytes,
20217 DEFAULT_TEMP_DISK_BUDGET_BYTES
20218 );
20219 assert!(db.job_registry().list().is_empty());
20220 assert_eq!(
20222 db.resource_groups().len(),
20223 crate::resource::WorkloadClass::ALL.len()
20224 );
20225 assert!(db.resource_groups().get("control").is_some());
20226 assert!(db.embedding_providers().list_ids().is_empty());
20227 let err = db
20229 .embedding_providers()
20230 .embed(
20231 &crate::embedding::EmbeddingSource::SuppliedByApplication,
20232 &["text"],
20233 4,
20234 )
20235 .unwrap_err();
20236 assert!(matches!(
20237 err,
20238 crate::embedding::EmbeddingError::SuppliedByApplication
20239 ));
20240 }
20241
20242 #[test]
20243 fn lock_rows_for_update_acquires_exclusive_row_locks() {
20244 use crate::locks::LockKey;
20245 use crate::rowid::RowId;
20246
20247 let dir = tempfile::tempdir().unwrap();
20248 let db = Database::create(dir.path()).unwrap();
20249 let txn_id = db.allocate_lock_txn_id().unwrap();
20250 let rid = RowId(42);
20251 db.lock_rows_for_update(txn_id, 7, &[rid], None).unwrap();
20252 assert!(db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20253 db.release_txn_locks(txn_id);
20254 assert!(!db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20255 }
20256
20257 #[test]
20258 fn open_with_options_sizes_the_core_budgets() {
20259 let dir = tempfile::tempdir().unwrap();
20260 let db = Database::create(dir.path()).unwrap();
20261 drop(db);
20262 let db = Database::open_with_options(
20263 dir.path(),
20264 OpenOptions::default()
20265 .with_memory_budget_bytes(256 * 1024 * 1024)
20266 .with_temp_disk_budget_bytes(16 * 1024 * 1024),
20267 )
20268 .unwrap();
20269 assert_eq!(db.memory_governor().max_bytes(), 256 * 1024 * 1024);
20270 assert_eq!(db.spill_manager().config().global_bytes, 16 * 1024 * 1024);
20271 }
20272
20273 #[test]
20274 fn zero_budgets_are_rejected() {
20275 let dir = tempfile::tempdir().unwrap();
20276 let db = Database::create(dir.path()).unwrap();
20277 drop(db);
20278 let result = Database::open_with_options(
20279 dir.path(),
20280 OpenOptions::default().with_memory_budget_bytes(0),
20281 );
20282 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20283 let result = Database::open_with_options(
20284 dir.path(),
20285 OpenOptions::default().with_temp_disk_budget_bytes(0),
20286 );
20287 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20288 }
20289
20290 #[test]
20291 fn page_caches_reserve_under_the_governor() {
20292 let dir = tempfile::tempdir().unwrap();
20293 let db = Database::create(dir.path()).unwrap();
20294 db.create_table("t", int_pk_schema()).unwrap();
20295 let mut txn = db.begin();
20296 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20297 txn.commit().unwrap();
20298 let stats = db.memory_governor().stats();
20302 assert_eq!(
20303 stats.usage_for(crate::memory::MemoryClass::PageCache),
20304 db.page_cache.used_bytes()
20305 );
20306 assert_eq!(
20307 stats.usage_for(crate::memory::MemoryClass::DecodedCache),
20308 db.decoded_cache.used_bytes()
20309 );
20310 assert_eq!(
20311 db.memory_governor().reclaimable_bytes(),
20312 db.page_cache.used_bytes() + db.decoded_cache.used_bytes()
20313 );
20314 let _ = db.memory_governor().evict_reclaimable(1024 * 1024);
20316 }
20317
20318 #[test]
20319 fn job_registry_persists_across_reopen() {
20320 let dir = tempfile::tempdir().unwrap();
20321 let db = Database::create(dir.path()).unwrap();
20322 db.create_table("t", int_pk_schema()).unwrap();
20323 let job_id = db
20324 .job_registry()
20325 .submit(
20326 crate::jobs::JobKind::IndexBuild,
20327 crate::jobs::JobTarget {
20328 table: "t".to_string(),
20329 index: Some("idx".to_string()),
20330 },
20331 )
20332 .unwrap();
20333 drop(db);
20334 let db = Database::open(dir.path()).unwrap();
20335 let record = db.job_registry().get(job_id).expect("job survives reopen");
20336 assert_eq!(record.state, crate::jobs::JobState::Pending);
20337 }
20338
20339 #[test]
20340 fn spill_manager_open_sweeps_stale_temp_tree() {
20341 let dir = tempfile::tempdir().unwrap();
20342 let db = Database::create(dir.path()).unwrap();
20343 let stale = dir.path().join("temp").join("spill").join("q-deadbeef");
20344 std::fs::create_dir_all(&stale).unwrap();
20345 std::fs::write(stale.join("chunk-0"), b"stale").unwrap();
20346 drop(db);
20347 let db = Database::open(dir.path()).unwrap();
20348 assert!(
20349 !stale.exists(),
20350 "the startup sweep removes stale spill files (S1E-004)"
20351 );
20352 let session = db
20354 .spill_manager()
20355 .begin_query(
20356 mongreldb_types::ids::QueryId::from_bytes([7u8; 16]),
20357 1024 * 1024,
20358 )
20359 .unwrap();
20360 assert_eq!(session.used(), 0);
20361 }
20362}
20363
20364#[cfg(test)]
20365mod version_pin_tests {
20366 use super::*;
20367
20368 fn int_pk_schema() -> Schema {
20369 Schema {
20370 columns: vec![ColumnDef {
20371 id: 1,
20372 name: "id".into(),
20373 ty: TypeId::Int64,
20374 flags: crate::schema::ColumnFlags::empty()
20375 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20376 default_value: None,
20377 embedding_source: None,
20378 }],
20379 ..Schema::default()
20380 }
20381 }
20382
20383 fn pins_for(
20384 report: &[TablePinsReport],
20385 table: &str,
20386 source: crate::retention::PinSource,
20387 ) -> Option<crate::retention::PinInfo> {
20388 report
20389 .iter()
20390 .find(|entry| entry.table == table)
20391 .and_then(|entry| entry.pins.get(source).cloned())
20392 }
20393
20394 #[test]
20395 fn backup_boundary_registers_backup_pitr_pin() {
20396 let source = tempfile::tempdir().unwrap();
20397 let destination_parent = tempfile::tempdir().unwrap();
20398 let destination = destination_parent.path().join("backup");
20399 let db = Arc::new(Database::create(source.path()).unwrap());
20400 db.create_table("t", int_pk_schema()).unwrap();
20401 let mut txn = db.begin();
20402 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
20403 let boundary_epoch = txn.commit().unwrap();
20404
20405 let hold = Arc::new(std::sync::Barrier::new(2));
20406 let resume = Arc::new(std::sync::Barrier::new(2));
20407 db.__set_backup_hook({
20408 let hold = Arc::clone(&hold);
20409 let resume = Arc::clone(&resume);
20410 move || {
20411 hold.wait();
20412 resume.wait();
20413 }
20414 });
20415
20416 let backup = {
20417 let db = Arc::clone(&db);
20418 let destination = destination.clone();
20419 std::thread::spawn(move || db.hot_backup(destination))
20420 };
20421 hold.wait();
20422 let report = db.version_pins_report();
20425 let pin = pins_for(&report, "t", crate::retention::PinSource::BackupPitr)
20426 .expect("backup boundary must register a BackupPitr pin");
20427 assert_eq!(pin.oldest_epoch, boundary_epoch);
20428 assert!(pin.pin_count >= 1);
20429 resume.wait();
20430 backup.join().unwrap().unwrap();
20431
20432 let report = db.version_pins_report();
20433 assert!(
20434 pins_for(&report, "t", crate::retention::PinSource::BackupPitr).is_none(),
20435 "the BackupPitr pin releases when the backup finishes"
20436 );
20437 }
20438
20439 #[test]
20440 fn snapshot_and_read_generation_pins_surface_in_report() {
20441 let dir = tempfile::tempdir().unwrap();
20442 let db = Database::create(dir.path()).unwrap();
20443 db.create_table("t", int_pk_schema()).unwrap();
20444 let mut txn = db.begin();
20445 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20446 txn.commit().unwrap();
20447
20448 let (_snapshot, guard) = db.snapshot();
20449 let report = db.version_pins_report();
20450 assert!(
20451 pins_for(
20452 &report,
20453 "t",
20454 crate::retention::PinSource::TransactionSnapshot
20455 )
20456 .is_some(),
20457 "a database snapshot projects the TransactionSnapshot source"
20458 );
20459 drop(guard);
20460
20461 let handle = db.table("t").unwrap();
20462 let (generation, _snapshot) = handle.read_generation_with_context(None).unwrap();
20463 let report = db.version_pins_report();
20464 assert!(
20465 pins_for(&report, "t", crate::retention::PinSource::ReadGeneration).is_some(),
20466 "a cloned read generation registers a ReadGeneration pin"
20467 );
20468 drop(generation);
20469
20470 let report = db.version_pins_report();
20471 let entry = report.iter().find(|entry| entry.table == "t").unwrap();
20472 assert!(
20473 entry
20474 .pins
20475 .get(crate::retention::PinSource::BackupPitr)
20476 .is_none()
20477 && entry
20478 .pins
20479 .get(crate::retention::PinSource::Replication)
20480 .is_none()
20481 && entry
20482 .pins
20483 .get(crate::retention::PinSource::OnlineIndexBuild)
20484 .is_none(),
20485 "untaken sources stay absent from the report"
20486 );
20487 }
20488}
20489
20490#[cfg(test)]
20491mod lock_manager_tests {
20492 use super::*;
20493 use crate::locks::LockKey;
20494
20495 fn col(id: u16, name: &str, ty: TypeId, flags: crate::schema::ColumnFlags) -> ColumnDef {
20496 ColumnDef {
20497 id,
20498 name: name.into(),
20499 ty,
20500 flags,
20501 default_value: None,
20502 embedding_source: None,
20503 }
20504 }
20505
20506 fn unique_schema() -> Schema {
20507 let mut constraints = crate::constraint::TableConstraints::default();
20508 constraints
20509 .uniques
20510 .push(crate::constraint::UniqueConstraint {
20511 id: 1,
20512 name: "users_email_unique".into(),
20513 columns: vec![1],
20514 });
20515 Schema {
20516 columns: vec![
20517 col(
20518 0,
20519 "id",
20520 TypeId::Int64,
20521 crate::schema::ColumnFlags::empty()
20522 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20523 ),
20524 col(
20525 1,
20526 "email",
20527 TypeId::Bytes,
20528 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
20529 ),
20530 ],
20531 constraints,
20532 ..Schema::default()
20533 }
20534 }
20535
20536 fn parent_schema() -> Schema {
20537 Schema {
20538 columns: vec![col(
20539 0,
20540 "id",
20541 TypeId::Int64,
20542 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::PRIMARY_KEY),
20543 )],
20544 ..Schema::default()
20545 }
20546 }
20547
20548 fn child_schema() -> Schema {
20549 let mut constraints = crate::constraint::TableConstraints::default();
20550 constraints
20551 .foreign_keys
20552 .push(crate::constraint::ForeignKey {
20553 id: 1,
20554 name: "child_parent_fk".into(),
20555 columns: vec![1],
20556 ref_table: "parent".into(),
20557 ref_columns: vec![0],
20558 on_delete: crate::constraint::FkAction::Restrict,
20559 on_update: crate::constraint::FkAction::Restrict,
20560 });
20561 Schema {
20562 columns: vec![
20563 col(
20564 0,
20565 "id",
20566 TypeId::Int64,
20567 crate::schema::ColumnFlags::empty()
20568 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20569 ),
20570 col(
20571 1,
20572 "pid",
20573 TypeId::Int64,
20574 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
20575 ),
20576 ],
20577 constraints,
20578 ..Schema::default()
20579 }
20580 }
20581
20582 fn auto_inc_schema() -> Schema {
20583 Schema {
20584 columns: vec![col(
20585 0,
20586 "id",
20587 TypeId::Int64,
20588 crate::schema::ColumnFlags::empty()
20589 .with(crate::schema::ColumnFlags::PRIMARY_KEY)
20590 .with(crate::schema::ColumnFlags::AUTO_INCREMENT),
20591 )],
20592 ..Schema::default()
20593 }
20594 }
20595
20596 fn pk_lock_key(table_id: u64, value: i64) -> LockKey {
20597 let mut key = b"pk:".to_vec();
20598 key.extend_from_slice(&Value::Int64(value).encode_key());
20599 LockKey::key(table_id, key)
20600 }
20601
20602 #[test]
20603 fn unique_claims_serialize_concurrent_commits() {
20604 let dir = tempfile::tempdir().unwrap();
20605 let db = Arc::new(Database::create(dir.path()).unwrap());
20606 let table_id = db.create_table("users", unique_schema()).unwrap();
20607 let pk_key = pk_lock_key(table_id, 1);
20608 let entered = Arc::new(std::sync::Barrier::new(2));
20609 let resume = Arc::new(std::sync::Barrier::new(2));
20610 let parked = Arc::new(AtomicBool::new(false));
20611 db.__set_catalog_commit_hook({
20612 let entered = Arc::clone(&entered);
20613 let resume = Arc::clone(&resume);
20614 let parked = Arc::clone(&parked);
20615 move || {
20616 if !parked.swap(true, Ordering::SeqCst) {
20619 entered.wait();
20620 resume.wait();
20621 }
20622 }
20623 });
20624
20625 let mut txn_a = db.begin();
20626 txn_a
20627 .put(
20628 "users",
20629 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
20630 )
20631 .unwrap();
20632 let a_id = txn_a.txn_id();
20633 let (a_tx, a_rx) = std::sync::mpsc::channel();
20634 let (b_tx, b_rx) = std::sync::mpsc::channel();
20635 std::thread::scope(|scope| {
20636 scope.spawn(|| {
20637 a_tx.send(txn_a.commit()).unwrap();
20638 });
20639 entered.wait();
20640 assert!(
20642 db.lock_manager().holds(a_id, &pk_key),
20643 "primary-key claim must be held until the commit ends"
20644 );
20645 let mut uq_key = format!("uq{}:", 1).into_bytes();
20646 let cells_map: HashMap<u16, Value> = [(1u16, Value::Bytes(b"a@x".to_vec()))]
20647 .into_iter()
20648 .collect();
20649 uq_key.extend_from_slice(
20650 &crate::constraint::encode_composite_key(&[1], &cells_map).unwrap(),
20651 );
20652 assert!(
20653 db.lock_manager()
20654 .holds(a_id, &LockKey::key(table_id, uq_key)),
20655 "declared-unique claim must be held until the commit ends"
20656 );
20657
20658 let mut txn_b = db.begin();
20659 txn_b
20660 .put(
20661 "users",
20662 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"b@x".to_vec()))],
20663 )
20664 .unwrap();
20665 scope.spawn(|| {
20666 b_tx.send(txn_b.commit()).unwrap();
20667 });
20668 std::thread::sleep(std::time::Duration::from_millis(100));
20669 assert!(
20670 b_rx.try_recv().is_err(),
20671 "the concurrent claim must block until A ends its transaction"
20672 );
20673 resume.wait();
20674 assert!(a_rx.recv().unwrap().is_ok());
20675 let b_result = b_rx.recv().unwrap();
20676 assert!(
20677 matches!(b_result, Err(MongrelError::Conflict(_))),
20678 "the loser surfaces a conflict after serializing: {b_result:?}"
20679 );
20680 });
20681 assert!(
20682 !db.lock_manager().holds(a_id, &pk_key),
20683 "no phantom holds remain after the commit"
20684 );
20685 }
20686
20687 #[test]
20688 fn ddl_waits_for_inflight_dml_commit_on_schema_barrier() {
20689 let dir = tempfile::tempdir().unwrap();
20690 let db = Arc::new(Database::create(dir.path()).unwrap());
20691 db.create_table("parent", parent_schema()).unwrap();
20692 db.create_table("child", child_schema()).unwrap();
20693 let mut seed = db.begin();
20694 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
20695 seed.commit().unwrap();
20696
20697 let entered = Arc::new(std::sync::Barrier::new(2));
20698 let resume = Arc::new(std::sync::Barrier::new(2));
20699 db.__set_fk_lock_hook({
20700 let entered = Arc::clone(&entered);
20701 let resume = Arc::clone(&resume);
20702 move || {
20703 entered.wait();
20704 resume.wait();
20705 }
20706 });
20707
20708 let mut txn_a = db.begin();
20709 txn_a
20710 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(1))])
20711 .unwrap();
20712 let a_id = txn_a.txn_id();
20713 let (a_tx, a_rx) = std::sync::mpsc::channel();
20714 let (ddl_tx, ddl_rx) = std::sync::mpsc::channel();
20715 std::thread::scope(|scope| {
20716 scope.spawn(|| {
20717 a_tx.send(txn_a.commit()).unwrap();
20718 });
20719 entered.wait();
20720 assert!(
20722 db.lock_manager().holds(a_id, &LockKey::schema_barrier()),
20723 "DML holds the schema barrier Shared for its commit"
20724 );
20725 let db = Arc::clone(&db);
20726 scope.spawn(move || {
20727 ddl_tx.send(db.drop_table("parent")).unwrap();
20728 });
20729 std::thread::sleep(std::time::Duration::from_millis(100));
20730 assert!(
20731 ddl_rx.try_recv().is_err(),
20732 "DDL must wait on the Exclusive schema barrier while DML is in flight"
20733 );
20734 resume.wait();
20735 let a_result = a_rx.recv().unwrap();
20741 match &a_result {
20742 Ok(_) => {}
20743 Err(MongrelError::Conflict(message)) => {
20744 assert!(
20745 message.contains("security policy changed during write"),
20746 "unexpected commit conflict: {message}"
20747 );
20748 }
20749 other => panic!("unexpected commit outcome: {other:?}"),
20750 }
20751 assert!(ddl_rx.recv().unwrap().is_ok());
20752 });
20753 assert!(!db.lock_manager().holds(a_id, &LockKey::schema_barrier()));
20754 }
20755
20756 #[test]
20757 fn auto_increment_sequence_barrier_held_until_commit() {
20758 let dir = tempfile::tempdir().unwrap();
20759 let db = Database::create(dir.path()).unwrap();
20760 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
20761 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
20762 let entered = Arc::new(std::sync::Barrier::new(2));
20763 let resume = Arc::new(std::sync::Barrier::new(2));
20764 let parked = Arc::new(AtomicBool::new(false));
20765 db.__set_catalog_commit_hook({
20766 let entered = Arc::clone(&entered);
20767 let resume = Arc::clone(&resume);
20768 let parked = Arc::clone(&parked);
20769 move || {
20770 if !parked.swap(true, Ordering::SeqCst) {
20771 entered.wait();
20772 resume.wait();
20773 }
20774 }
20775 });
20776
20777 let mut txn_a = db.begin();
20778 txn_a.put("seq_t", vec![(0, Value::Null)]).unwrap();
20779 let a_id = txn_a.txn_id();
20780 assert!(
20782 db.lock_manager().holds(a_id, &barrier_key),
20783 "sequence allocation takes the barrier at stage time"
20784 );
20785 let (a_tx, a_rx) = std::sync::mpsc::channel();
20786 std::thread::scope(|scope| {
20787 scope.spawn(|| {
20788 a_tx.send(txn_a.commit()).unwrap();
20789 });
20790 entered.wait();
20791 assert!(
20792 db.lock_manager().holds(a_id, &barrier_key),
20793 "the barrier is held through the commit"
20794 );
20795 resume.wait();
20796 assert!(a_rx.recv().unwrap().is_ok());
20797 });
20798 assert!(
20799 !db.lock_manager().holds(a_id, &barrier_key),
20800 "the barrier releases when the commit ends"
20801 );
20802 }
20803
20804 #[test]
20805 fn fk_wait_for_cycle_surfaces_deadlock_victim() {
20806 let dir = tempfile::tempdir().unwrap();
20807 let db = Database::create(dir.path()).unwrap();
20808 db.create_table("parent", parent_schema()).unwrap();
20809 db.create_table("child", child_schema()).unwrap();
20810 let mut seed = db.begin();
20811 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
20812 seed.put("parent", vec![(0, Value::Int64(2))]).unwrap();
20813 seed.commit().unwrap();
20814 let (rid1, rid2) = {
20815 let handle = db.table("parent").unwrap();
20816 let table = handle.lock();
20817 let rid = |pk: i64| {
20818 table
20819 .lookup_pk(&Value::Int64(pk).encode_key())
20820 .expect("seeded parent row")
20821 };
20822 (rid(1), rid(2))
20823 };
20824
20825 let rendezvous = Arc::new(std::sync::Barrier::new(2));
20830 let calls = Arc::new(AtomicUsize::new(0));
20831 db.__set_fk_lock_hook({
20832 let rendezvous = Arc::clone(&rendezvous);
20833 let calls = Arc::clone(&calls);
20834 move || {
20835 if calls.fetch_add(1, Ordering::SeqCst) < 2 {
20836 rendezvous.wait();
20837 }
20838 }
20839 });
20840
20841 let mut txn_a = db.begin();
20844 txn_a.delete("parent", rid1).unwrap();
20845 txn_a
20846 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(2))])
20847 .unwrap();
20848 let mut txn_b = db.begin();
20849 txn_b.delete("parent", rid2).unwrap();
20850 txn_b
20851 .put("child", vec![(0, Value::Int64(101)), (1, Value::Int64(1))])
20852 .unwrap();
20853 let b_id = txn_b.txn_id();
20854
20855 let (a_tx, a_rx) = std::sync::mpsc::channel();
20856 let (b_tx, b_rx) = std::sync::mpsc::channel();
20857 std::thread::scope(|scope| {
20858 scope.spawn(|| {
20859 a_tx.send(txn_a.commit()).unwrap();
20860 });
20861 scope.spawn(|| {
20862 b_tx.send(txn_b.commit()).unwrap();
20863 });
20864 let a_result = a_rx.recv().unwrap();
20865 let b_result = b_rx.recv().unwrap();
20866 assert!(
20867 a_result.is_ok(),
20868 "the survivor commits once the victim releases: {a_result:?}"
20869 );
20870 match b_result {
20871 Err(MongrelError::Deadlock { victim, .. }) => {
20872 assert_eq!(victim, b_id, "the youngest transaction is the victim");
20873 }
20874 other => panic!("the victim must surface a deadlock, got {other:?}"),
20875 }
20876 });
20877 let fk_key = |table: &str, pk: i64| {
20879 let table_id = db.table_id(table).unwrap();
20880 let mut key = b"fk:".to_vec();
20881 key.extend_from_slice(&Value::Int64(pk).encode_key());
20882 LockKey::key(table_id, key)
20883 };
20884 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 2)));
20885 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 1)));
20886 }
20887
20888 #[test]
20889 fn locks_release_after_commit_rollback_and_failed_commit() {
20890 let dir = tempfile::tempdir().unwrap();
20891 let db = Database::create(dir.path()).unwrap();
20892 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
20893 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
20894
20895 let mut txn = db.begin();
20897 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
20898 let committed_id = txn.txn_id();
20899 txn.commit().unwrap();
20900 assert!(!db.lock_manager().holds(committed_id, &barrier_key));
20901
20902 let mut txn = db.begin();
20904 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
20905 let rolled_back_id = txn.txn_id();
20906 assert!(db.lock_manager().holds(rolled_back_id, &barrier_key));
20907 txn.rollback();
20908 assert!(
20909 !db.lock_manager().holds(rolled_back_id, &barrier_key),
20910 "rollback must not leave phantom holds"
20911 );
20912
20913 db.create_table("users", unique_schema()).unwrap();
20916 let users_id = db.table_id("users").unwrap();
20917 let mut seed = db.begin();
20918 seed.put(
20919 "users",
20920 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
20921 )
20922 .unwrap();
20923 seed.commit().unwrap();
20924 let mut txn = db.begin();
20925 txn.put(
20928 "users",
20929 vec![(0, Value::Int64(2)), (1, Value::Bytes(b"a@x".to_vec()))],
20930 )
20931 .unwrap();
20932 let failed_id = txn.txn_id();
20933 let result = txn.commit();
20934 assert!(matches!(result, Err(MongrelError::Conflict(_))));
20935 assert!(
20936 !db.lock_manager()
20937 .holds(failed_id, &pk_lock_key(users_id, 2)),
20938 "a failed commit must not leave phantom holds"
20939 );
20940 }
20941}
20942
20943#[cfg(test)]
20944mod lifecycle_tests {
20945 use super::*;
20946
20947 fn int_pk_schema() -> Schema {
20948 Schema {
20949 columns: vec![ColumnDef {
20950 id: 1,
20951 name: "id".into(),
20952 ty: TypeId::Int64,
20953 flags: crate::schema::ColumnFlags::empty()
20954 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20955 default_value: None,
20956 embedding_source: None,
20957 }],
20958 ..Schema::default()
20959 }
20960 }
20961
20962 #[test]
20963 fn poisoned_core_rejects_operations_with_typed_errors() {
20964 let dir = tempfile::tempdir().unwrap();
20965 let db = Database::create(dir.path()).unwrap();
20966 db.create_table("t", int_pk_schema()).unwrap();
20967 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Open);
20968
20969 db.poisoned.store(true, Ordering::Relaxed);
20974 db.lifecycle.poison();
20975 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Poisoned);
20976
20977 let error = db.gc().unwrap_err();
20980 assert!(
20981 matches!(error, MongrelError::Conflict(_)),
20982 "gc must reject on a poisoned core: {error:?}"
20983 );
20984 let error = db.compact().unwrap_err();
20985 assert!(
20986 matches!(error, MongrelError::Conflict(_)),
20987 "compact must reject on a poisoned core: {error:?}"
20988 );
20989 assert!(db.operation_guard().is_err());
20990 let error = db.create_table("t2", int_pk_schema()).unwrap_err();
20993 assert!(
20994 error.to_string().contains("database poisoned"),
20995 "the legacy poison error still wins where it existed: {error:?}"
20996 );
20997 let mut txn = db.begin();
20998 txn.put("t", vec![(1, Value::Int64(2))]).unwrap();
20999 assert!(txn
21000 .commit()
21001 .unwrap_err()
21002 .to_string()
21003 .contains("database poisoned"));
21004 }
21005
21006 #[test]
21007 fn shutdown_waits_for_operation_guards_to_drain() {
21008 let dir = tempfile::tempdir().unwrap();
21009 let db = Arc::new(Database::create(dir.path()).unwrap());
21010 db.create_table("t", int_pk_schema()).unwrap();
21011 let guard = db.operation_guard().unwrap();
21014 let (started_tx, started_rx) = std::sync::mpsc::channel();
21015 let (done_tx, done_rx) = std::sync::mpsc::channel();
21016 let shutdown_thread = std::thread::spawn(move || {
21017 started_tx.send(()).unwrap();
21018 let result = db.shutdown();
21019 let _ = done_tx.send(result);
21020 });
21021 started_rx.recv().unwrap();
21022 std::thread::sleep(std::time::Duration::from_millis(100));
21023 assert!(
21024 done_rx.try_recv().is_err(),
21025 "shutdown must wait for the outstanding guard to drain"
21026 );
21027 drop(guard);
21028 shutdown_thread.join().unwrap();
21029 assert!(
21030 done_rx.recv().unwrap().is_ok(),
21031 "shutdown completes once the guard drops"
21032 );
21033 }
21034}
21035
21036#[cfg(test)]
21037mod commit_ts_ledger_tests {
21038 use super::*;
21039
21040 fn int_pk_schema() -> Schema {
21041 Schema {
21042 columns: vec![ColumnDef {
21043 id: 1,
21044 name: "id".into(),
21045 ty: TypeId::Int64,
21046 flags: crate::schema::ColumnFlags::empty()
21047 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21048 default_value: None,
21049 embedding_source: None,
21050 }],
21051 ..Schema::default()
21052 }
21053 }
21054
21055 fn commit_one(db: &Database) -> (Epoch, mongreldb_types::hlc::HlcTimestamp) {
21056 let mut txn = db.begin();
21057 let handle = txn.state_handle();
21058 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
21059 let epoch = txn.commit().unwrap();
21060 let crate::txn::TransactionState::Committed(receipt) = handle.state() else {
21061 panic!("expected Committed, got {:?}", handle.state());
21062 };
21063 (epoch, receipt.commit_ts)
21064 }
21065
21066 #[test]
21067 fn commit_ts_for_epoch_returns_the_exact_receipt_within_one_open() {
21068 let dir = tempfile::tempdir().unwrap();
21069 let db = Database::create(dir.path()).unwrap();
21070 db.create_table("t", int_pk_schema()).unwrap();
21071
21072 let (epoch, commit_ts) = commit_one(&db);
21073 assert_eq!(db.commit_ts_for_epoch(epoch), Some(commit_ts));
21074 assert_eq!(db.commit_ts_for_epoch(Epoch(epoch.0 + 100)), None);
21076 }
21077
21078 #[test]
21079 fn commit_ts_for_epoch_survives_reopen_with_the_physical_component() {
21080 let dir = tempfile::tempdir().unwrap();
21081 let (epoch, commit_ts) = {
21082 let db = Database::create(dir.path()).unwrap();
21083 db.create_table("t", int_pk_schema()).unwrap();
21084 commit_one(&db)
21085 };
21086
21087 let db = Database::open(dir.path()).unwrap();
21088 let reconstructed = db
21089 .commit_ts_for_epoch(epoch)
21090 .expect("the durable WAL CommitTimestamp ledger reconstructs the epoch");
21091 assert_eq!(reconstructed.physical_micros, commit_ts.physical_micros);
21092 assert_eq!(reconstructed.logical, 0);
21095 assert_eq!(reconstructed.node_tiebreaker, 0);
21096 }
21097
21098 #[test]
21099 fn recovery_ledger_keeps_only_newest_epochs_and_ignores_aborted_txns() {
21100 use crate::wal::Op;
21101 let records = vec![
21102 crate::wal::Record::new(Epoch(1), 7, Op::CommitTimestamp { unix_nanos: 1_000 }),
21103 crate::wal::Record::new(
21104 Epoch(2),
21105 7,
21106 Op::TxnCommit {
21107 epoch: 41,
21108 added_runs: vec![],
21109 },
21110 ),
21111 crate::wal::Record::new(
21113 Epoch(3),
21114 8,
21115 Op::TxnCommit {
21116 epoch: 42,
21117 added_runs: vec![],
21118 },
21119 ),
21120 crate::wal::Record::new(Epoch(4), 9, Op::CommitTimestamp { unix_nanos: 9_000 }),
21122 ];
21123 let ledger = commit_ts_ledger_from_recovery(&records);
21124 assert_eq!(ledger.len(), 1);
21125 assert_eq!(
21126 ledger.get(&41),
21127 Some(&mongreldb_types::hlc::HlcTimestamp {
21128 physical_micros: 1,
21129 logical: 0,
21130 node_tiebreaker: 0,
21131 })
21132 );
21133 }
21134}
21135
21136#[cfg(test)]
21137mod stage2e_storage_mode_tests {
21138 use super::*;
21139 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21140 use crate::storage_mode::{StorageMode, STORAGE_MODE_FILENAME};
21141 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21142
21143 fn identity(seed: u8) -> (ClusterId, NodeId, DatabaseId) {
21144 (
21145 ClusterId::from_bytes([seed; 16]),
21146 NodeId::from_bytes([seed + 1; 16]),
21147 DatabaseId::from_bytes([seed + 2; 16]),
21148 )
21149 }
21150
21151 fn marker(root: &Path) -> Option<StorageMode> {
21152 let durable = crate::durable_file::DurableRoot::open(root).unwrap();
21153 crate::storage_mode::read(&durable).unwrap()
21154 }
21155
21156 fn simple_schema() -> Schema {
21157 Schema {
21158 columns: vec![ColumnDef {
21159 id: 1,
21160 name: "id".into(),
21161 ty: TypeId::Int64,
21162 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21163 default_value: None,
21164 embedding_source: None,
21165 }],
21166 ..Schema::default()
21167 }
21168 }
21169
21170 #[test]
21171 fn standalone_create_writes_marker_and_reopens() {
21172 let dir = tempfile::tempdir().unwrap();
21173 let root = dir.path().join("db");
21174 let db = Database::create(&root).unwrap();
21175 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21176 assert_eq!(db.storage_mode().unwrap(), Some(StorageMode::Standalone));
21177 drop(db);
21178 let db = Database::open(&root).unwrap();
21179 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21180 drop(db);
21181 }
21182
21183 #[test]
21184 fn legacy_database_without_marker_opens_and_gains_marker() {
21185 let dir = tempfile::tempdir().unwrap();
21186 let root = dir.path().join("db");
21187 let db = Database::create(&root).unwrap();
21188 drop(db);
21189 std::fs::remove_file(root.join(META_DIR).join(STORAGE_MODE_FILENAME)).unwrap();
21191 assert_eq!(marker(&root), None);
21192 let db = Database::open(&root).unwrap();
21193 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21194 drop(db);
21195 }
21196
21197 #[test]
21198 fn server_owned_standalone_opens_embedded() {
21199 let dir = tempfile::tempdir().unwrap();
21200 let root = dir.path().join("db");
21201 let db = Database::create(&root).unwrap();
21202 drop(db);
21203 let durable = crate::durable_file::DurableRoot::open(&root).unwrap();
21204 crate::storage_mode::rewrite(&durable, &StorageMode::ServerOwnedStandalone).unwrap();
21205 let db = Database::open(&root).unwrap();
21206 assert_eq!(marker(&root), Some(StorageMode::ServerOwnedStandalone));
21207 drop(db);
21208 }
21209
21210 #[test]
21211 fn cluster_replica_is_rejected_by_normal_opens() {
21212 let dir = tempfile::tempdir().unwrap();
21213 let root = dir.path().join("db");
21214 let (cluster_id, node_id, database_id) = identity(10);
21215 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21216 assert_eq!(
21217 marker(&root),
21218 Some(StorageMode::ClusterReplica {
21219 cluster_id,
21220 node_id,
21221 database_id,
21222 })
21223 );
21224 drop(db);
21225
21226 let error = Database::open(&root).unwrap_err();
21227 let message = error.to_string();
21228 assert!(
21229 matches!(error, MongrelError::InvalidArgument(_)),
21230 "unexpected error: {message}"
21231 );
21232 assert!(message.contains("cluster node runtime"), "{message}");
21233 assert!(message.contains(&cluster_id.to_hex()), "{message}");
21234 assert!(message.contains(&node_id.to_hex()), "{message}");
21235 assert!(message.contains(&database_id.to_hex()), "{message}");
21236
21237 let error = Database::open_with_options(&root, OpenOptions::default()).unwrap_err();
21238 assert!(error.to_string().contains("cluster node runtime"));
21239 assert_eq!(
21241 marker(&root),
21242 Some(StorageMode::ClusterReplica {
21243 cluster_id,
21244 node_id,
21245 database_id,
21246 })
21247 );
21248 }
21249
21250 #[test]
21251 fn offline_validation_opens_cluster_replica_read_only() {
21252 let dir = tempfile::tempdir().unwrap();
21253 let root = dir.path().join("db");
21254 let (cluster_id, node_id, database_id) = identity(20);
21255 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21256 drop(db);
21257
21258 let options = OpenOptions::default().with_offline_validation(true);
21259 let db = Database::open_with_options(&root, options).unwrap();
21260 assert!(db.is_read_only_replica());
21261 let error = db.create_table("t", simple_schema()).unwrap_err();
21262 assert!(matches!(error, MongrelError::ReadOnlyReplica));
21263 drop(db);
21264 assert_eq!(
21266 marker(&root),
21267 Some(StorageMode::ClusterReplica {
21268 cluster_id,
21269 node_id,
21270 database_id,
21271 })
21272 );
21273 }
21274
21275 #[test]
21276 fn cluster_runtime_open_requires_exact_identity() {
21277 let dir = tempfile::tempdir().unwrap();
21278 let root = dir.path().join("db");
21279 let (cluster_id, node_id, database_id) = identity(30);
21280 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21281 drop(db);
21282
21283 let error = Database::open_cluster_replica(&root, &StorageMode::Standalone).unwrap_err();
21285 assert!(matches!(error, MongrelError::InvalidArgument(_)));
21286 let wrong = StorageMode::ClusterReplica {
21288 cluster_id,
21289 node_id,
21290 database_id: DatabaseId::from_bytes([99; 16]),
21291 };
21292 let error = Database::open_cluster_replica(&root, &wrong).unwrap_err();
21293 assert!(error.to_string().contains("identity mismatch"), "{error}");
21294 let legacy = dir.path().join("legacy");
21296 let legacy_db = Database::create(&legacy).unwrap();
21297 drop(legacy_db);
21298 let expected = StorageMode::ClusterReplica {
21299 cluster_id,
21300 node_id,
21301 database_id,
21302 };
21303 let error = Database::open_cluster_replica(&legacy, &expected).unwrap_err();
21304 assert!(error.to_string().contains("identity mismatch"), "{error}");
21305
21306 let db = Database::open_cluster_replica(&root, &expected).unwrap();
21309 assert!(db.is_read_only_replica());
21310 let error = db.create_table("t", simple_schema()).unwrap_err();
21311 assert!(matches!(error, MongrelError::ReadOnlyReplica));
21312 drop(db);
21313 }
21314}
21315
21316#[cfg(test)]
21317mod stage2e_replicated_apply_tests {
21318 use super::*;
21319 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord, CatalogDelta};
21320 use crate::memtable::{Row, Value};
21321 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21322 use crate::wal::{Op, Record};
21323 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21324 use std::sync::Arc;
21325
21326 fn ids() -> (ClusterId, NodeId, DatabaseId) {
21327 (
21328 ClusterId::from_bytes([1; 16]),
21329 NodeId::from_bytes([2; 16]),
21330 DatabaseId::from_bytes([3; 16]),
21331 )
21332 }
21333
21334 fn expected_mode() -> crate::storage_mode::StorageMode {
21335 let (cluster_id, node_id, database_id) = ids();
21336 crate::storage_mode::StorageMode::ClusterReplica {
21337 cluster_id,
21338 node_id,
21339 database_id,
21340 }
21341 }
21342
21343 fn simple_schema() -> Schema {
21344 Schema {
21345 columns: vec![ColumnDef {
21346 id: 1,
21347 name: "id".into(),
21348 ty: TypeId::Int64,
21349 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21350 default_value: None,
21351 embedding_source: None,
21352 }],
21353 ..Schema::default()
21354 }
21355 }
21356
21357 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
21358 CatalogCommandRecord {
21359 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21360 catalog_version,
21361 command: CatalogCommand::CreateTable {
21362 name: name.to_string(),
21363 schema: simple_schema(),
21364 created_epoch: 1,
21365 },
21366 }
21367 }
21368
21369 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
21370 let rows: Vec<Row> = values
21371 .iter()
21372 .map(|value| {
21373 Row::new(crate::RowId(*value as u64), Epoch(epoch))
21376 .with_column(1, Value::Int64(*value))
21377 })
21378 .collect();
21379 vec![
21380 Record::new(
21381 Epoch(0),
21382 txn_id,
21383 Op::Put {
21384 table_id,
21385 rows: bincode::serialize(&rows).unwrap(),
21386 },
21387 ),
21388 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
21389 Record::new(
21390 Epoch(0),
21391 txn_id,
21392 Op::TxnCommit {
21393 epoch,
21394 added_runs: Vec::new(),
21395 },
21396 ),
21397 ]
21398 }
21399
21400 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
21401 let handle = db.table(table).unwrap();
21402 let rows = handle
21403 .lock()
21404 .visible_rows(crate::epoch::Snapshot::at(Epoch(u64::MAX)))
21405 .unwrap();
21406 let mut values: Vec<i64> = rows
21407 .iter()
21408 .map(|row| match row.columns.get(&1) {
21409 Some(Value::Int64(value)) => *value,
21410 other => panic!("unexpected column: {other:?}"),
21411 })
21412 .collect();
21413 values.sort_unstable();
21414 values
21415 }
21416
21417 #[test]
21418 fn catalog_command_mounts_table_and_replays_as_noop() {
21419 let dir = tempfile::tempdir().unwrap();
21420 let (cluster_id, node_id, database_id) = ids();
21421 let db =
21422 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
21423
21424 let record = create_table_record("items", 1);
21425 let delta = db.apply_replicated_catalog_command(&record).unwrap();
21426 assert!(matches!(delta, CatalogDelta::TableCreated { .. }));
21427 assert_eq!(db.table_names(), vec!["items".to_string()]);
21428 assert_eq!(db.catalog_version(), 1);
21429
21430 let delta = db.apply_replicated_catalog_command(&record).unwrap();
21432 assert!(matches!(delta, CatalogDelta::NoOp));
21433 assert_eq!(db.table_names().len(), 1);
21434 drop(db);
21435
21436 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
21438 assert_eq!(db.table_names(), vec!["items".to_string()]);
21439 assert_eq!(db.catalog_version(), 1);
21440 }
21441
21442 #[test]
21443 fn records_apply_rows_and_skip_replays_across_restart() {
21444 let dir = tempfile::tempdir().unwrap();
21445 let (cluster_id, node_id, database_id) = ids();
21446 let db =
21447 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
21448 db.apply_replicated_catalog_command(&create_table_record("items", 1))
21449 .unwrap();
21450
21451 let records = put_records(1, 0, 2, &[10, 20, 30]);
21452 assert!(db.apply_replicated_records(&records).unwrap());
21453 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
21454 assert_eq!(db.visible_epoch(), Epoch(2));
21455
21456 assert!(!db.apply_replicated_records(&records).unwrap());
21459 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
21460
21461 let later = put_records(2, 0, 3, &[40]);
21463 assert!(db.apply_replicated_records(&later).unwrap());
21464 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
21465 let db = Arc::new(db);
21466 db.shutdown().unwrap();
21467
21468 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
21471 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
21472 assert!(!db.apply_replicated_records(&later).unwrap());
21473 assert!(!db.apply_replicated_records(&records).unwrap());
21474 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
21475 }
21476
21477 #[test]
21478 fn spilled_run_commits_fail_closed_this_wave() {
21479 let dir = tempfile::tempdir().unwrap();
21480 let (cluster_id, node_id, database_id) = ids();
21481 let db =
21482 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
21483 db.apply_replicated_catalog_command(&create_table_record("items", 1))
21484 .unwrap();
21485 let mut records = put_records(1, 0, 2, &[10]);
21486 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
21487 panic!("put_records ends in TxnCommit");
21488 };
21489 added_runs.push(crate::wal::AddedRun {
21490 table_id: 0,
21491 run_id: 7,
21492 row_count: 1,
21493 level: 0,
21494 min_row_id: 1,
21495 max_row_id: 1,
21496 content_hash: [0; 32],
21497 });
21498 let error = db.apply_replicated_records(&records).unwrap_err();
21499 assert!(
21500 error.to_string().contains("spilled-run"),
21501 "unexpected error: {error}"
21502 );
21503 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
21504 }
21505
21506 #[test]
21507 fn records_without_commit_marker_fail_closed() {
21508 let dir = tempfile::tempdir().unwrap();
21509 let (cluster_id, node_id, database_id) = ids();
21510 let db =
21511 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
21512 db.apply_replicated_catalog_command(&create_table_record("items", 1))
21513 .unwrap();
21514 let mut records = put_records(1, 0, 2, &[10]);
21515 records.pop(); let error = db.apply_replicated_records(&records).unwrap_err();
21517 assert!(matches!(error, MongrelError::InvalidArgument(_)));
21518 assert!(db.apply_replicated_records(&[]).is_err());
21519 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
21520 }
21521}
21522
21523#[cfg(test)]
21524mod stage2c_spill_translation_tests {
21525 use super::*;
21526 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord};
21527 use crate::memtable::{Row, Value};
21528 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21529 use crate::wal::{Op, Record};
21530 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21531
21532 fn simple_schema() -> Schema {
21533 Schema {
21534 columns: vec![ColumnDef {
21535 id: 1,
21536 name: "id".into(),
21537 ty: TypeId::Int64,
21538 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21539 default_value: None,
21540 embedding_source: None,
21541 }],
21542 ..Schema::default()
21543 }
21544 }
21545
21546 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
21547 CatalogCommandRecord {
21548 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21549 catalog_version,
21550 command: CatalogCommand::CreateTable {
21551 name: name.to_string(),
21552 schema: simple_schema(),
21553 created_epoch: 1,
21554 },
21555 }
21556 }
21557
21558 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
21559 let handle = db.table(table).unwrap();
21560 let rows = handle
21561 .lock()
21562 .visible_rows(crate::epoch::Snapshot::at(Epoch(u64::MAX)))
21563 .unwrap();
21564 let mut values: Vec<i64> = rows
21565 .iter()
21566 .map(|row| match row.columns.get(&1) {
21567 Some(Value::Int64(value)) => *value,
21568 other => panic!("unexpected column: {other:?}"),
21569 })
21570 .collect();
21571 values.sort_unstable();
21572 values
21573 }
21574
21575 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
21576 let rows: Vec<Row> = values
21577 .iter()
21578 .map(|value| {
21579 Row::new(crate::RowId(*value as u64), Epoch(epoch))
21580 .with_column(1, Value::Int64(*value))
21581 })
21582 .collect();
21583 vec![
21584 Record::new(
21585 Epoch(0),
21586 txn_id,
21587 Op::Put {
21588 table_id,
21589 rows: bincode::serialize(&rows).unwrap(),
21590 },
21591 ),
21592 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
21593 Record::new(
21594 Epoch(0),
21595 txn_id,
21596 Op::TxnCommit {
21597 epoch,
21598 added_runs: Vec::new(),
21599 },
21600 ),
21601 ]
21602 }
21603
21604 fn added_run(
21605 table_id: u64,
21606 row_count: u64,
21607 min_row_id: u64,
21608 max_row_id: u64,
21609 ) -> crate::wal::AddedRun {
21610 crate::wal::AddedRun {
21611 table_id,
21612 run_id: 7,
21613 row_count,
21614 level: 0,
21615 min_row_id,
21616 max_row_id,
21617 content_hash: [0; 32],
21618 }
21619 }
21620
21621 fn spilled_commit_records(db: &Database) -> Vec<Record> {
21624 let records = crate::wal::SharedWal::replay_with_dek(&db.root, None).unwrap();
21625 let txn_id = records
21626 .iter()
21627 .find_map(|record| match &record.op {
21628 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty() => Some(record.txn_id),
21629 _ => None,
21630 })
21631 .expect("a spilled commit is present in the WAL");
21632 records
21633 .into_iter()
21634 .filter(|record| record.txn_id == txn_id)
21635 .collect()
21636 }
21637
21638 #[test]
21639 fn non_spilled_records_translate_byte_identical() {
21640 let records = put_records(1, 0, 2, &[10, 20, 30]);
21641 let translated = translate_records_for_replication(&records).unwrap();
21642 assert_eq!(
21643 bincode::serialize(&translated).unwrap(),
21644 bincode::serialize(&records).unwrap(),
21645 "a commit without spill links must pass through byte-identical"
21646 );
21647 }
21648
21649 #[test]
21650 fn translation_rejects_uncovered_or_malformed_spills() {
21651 let mut records = put_records(1, 0, 2, &[10]);
21653 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
21654 panic!("put_records ends in TxnCommit");
21655 };
21656 added_runs.push(added_run(0, 1, 10, 10));
21657 let error = translate_records_for_replication(&records).unwrap_err();
21658 assert!(
21659 error.to_string().contains("no logical row records"),
21660 "unexpected error: {error}"
21661 );
21662
21663 let mut records = put_records(1, 0, 2, &[10]);
21665 let spilled: Vec<Row> = (0..3_u64)
21666 .map(|value| {
21667 Row::new(crate::RowId(value), Epoch(2)).with_column(1, Value::Int64(value as i64))
21668 })
21669 .collect();
21670 records.insert(
21671 0,
21672 Record::new(
21673 Epoch(0),
21674 1,
21675 Op::SpilledRows {
21676 table_id: 0,
21677 rows: bincode::serialize(&spilled).unwrap(),
21678 },
21679 ),
21680 );
21681 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
21682 panic!("put_records ends in TxnCommit");
21683 };
21684 added_runs.push(added_run(0, 4, 0, 3));
21685 let error = translate_records_for_replication(&records).unwrap_err();
21686 assert!(
21687 error.to_string().contains("cover 3 rows"),
21688 "unexpected error: {error}"
21689 );
21690
21691 let mut records = put_records(1, 0, 2, &[10]);
21693 records.insert(
21694 0,
21695 Record::new(
21696 Epoch(0),
21697 1,
21698 Op::SpilledRows {
21699 table_id: 0,
21700 rows: vec![0xFF, 0x01, 0x02],
21701 },
21702 ),
21703 );
21704 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
21705 panic!("put_records ends in TxnCommit");
21706 };
21707 added_runs.push(added_run(0, 1, 0, 0));
21708 assert!(translate_records_for_replication(&records).is_err());
21709
21710 assert!(translate_records_for_replication(&[]).is_err());
21712 let mut mixed = put_records(1, 0, 2, &[10]);
21713 mixed[0].txn_id = 99;
21714 assert!(translate_records_for_replication(&mixed).is_err());
21715 let mut no_commit = put_records(1, 0, 2, &[10]);
21716 no_commit.pop();
21717 assert!(translate_records_for_replication(&no_commit).is_err());
21718 }
21719
21720 #[test]
21721 fn spilled_commit_translates_to_logical_rows_and_applies_on_replica() {
21722 let leader_dir = tempfile::tempdir().unwrap();
21724 let leader = Database::create(leader_dir.path()).unwrap();
21725 leader.create_table("t", simple_schema()).unwrap();
21726 leader.set_spill_threshold(1);
21727 let table_id = leader.table_id("t").unwrap();
21728 let values: Vec<i64> = (0..60).collect();
21729 leader
21730 .transaction(|txn| {
21731 for value in &values {
21732 txn.put("t", vec![(1, Value::Int64(*value))])?;
21733 }
21734 Ok(())
21735 })
21736 .unwrap();
21737 assert_eq!(visible_ids(&leader, "t"), values);
21738
21739 let records = spilled_commit_records(&leader);
21742 assert!(records
21743 .iter()
21744 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
21745 let Some(Op::TxnCommit { added_runs, epoch }) = records.last().map(|r| &r.op) else {
21746 panic!("a commit sequence ends in TxnCommit");
21747 };
21748 assert!(!added_runs.is_empty());
21749 let commit_epoch = *epoch;
21750
21751 let translated = translate_records_for_replication(&records).unwrap();
21754 assert!(records
21755 .iter()
21756 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
21757 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
21758 panic!("a commit sequence ends in TxnCommit");
21759 };
21760 assert!(!added_runs.is_empty(), "input records must be unchanged");
21761 assert_eq!(translated.len(), records.len());
21762 assert!(translated
21763 .iter()
21764 .all(|record| !matches!(record.op, Op::SpilledRows { .. })));
21765 assert!(translated
21766 .iter()
21767 .any(|record| matches!(record.op, Op::Put { .. })));
21768 let Some(Op::TxnCommit { added_runs, epoch }) = translated.last().map(|r| &r.op) else {
21769 panic!("a commit sequence ends in TxnCommit");
21770 };
21771 assert!(added_runs.is_empty(), "no added_runs may reach a replica");
21772 assert_eq!(*epoch, commit_epoch);
21773
21774 let replica_dir = tempfile::tempdir().unwrap();
21776 let replica = Database::create_cluster_replica(
21777 replica_dir.path(),
21778 ClusterId::from_bytes([1; 16]),
21779 NodeId::from_bytes([2; 16]),
21780 DatabaseId::from_bytes([3; 16]),
21781 )
21782 .unwrap();
21783 replica
21784 .apply_replicated_catalog_command(&create_table_record("t", 1))
21785 .unwrap();
21786 assert_eq!(replica.table_id("t").unwrap(), table_id);
21787 assert!(replica.apply_replicated_records(&translated).unwrap());
21788 assert_eq!(visible_ids(&replica, "t"), values);
21789 assert_eq!(visible_ids(&replica, "t"), visible_ids(&leader, "t"));
21790
21791 drop(leader);
21794 let leader = Database::open(leader_dir.path()).unwrap();
21795 assert_eq!(visible_ids(&leader, "t"), values);
21796 }
21797
21798 #[test]
21799 fn staged_txn_writes_validate_and_apply() {
21800 let dir = tempfile::tempdir().unwrap();
21801 let db = Database::create_cluster_replica(
21802 dir.path(),
21803 ClusterId::from_bytes([1; 16]),
21804 NodeId::from_bytes([2; 16]),
21805 DatabaseId::from_bytes([3; 16]),
21806 )
21807 .unwrap();
21808 db.apply_replicated_catalog_command(&create_table_record("t", 1))
21809 .unwrap();
21810 let table_id = db.table_id("t").unwrap();
21811
21812 assert!(db.validate_staged_txn_writes(&[vec![0xFF]]).is_err());
21814 let unknown_table = StagedTxnWrite::Put {
21815 table_id: 99,
21816 rows: bincode::serialize(&Vec::<Row>::new()).unwrap(),
21817 }
21818 .encode()
21819 .unwrap();
21820 assert!(db.validate_staged_txn_writes(&[unknown_table]).is_err());
21821 let good: Vec<Vec<u8>> = [10_i64, 20, 30]
21822 .iter()
21823 .map(|value| {
21824 let rows = vec![Row::new(crate::RowId(*value as u64), Epoch(0))
21825 .with_column(1, Value::Int64(*value))];
21826 StagedTxnWrite::Put {
21827 table_id,
21828 rows: bincode::serialize(&rows).unwrap(),
21829 }
21830 .encode()
21831 .unwrap()
21832 })
21833 .collect();
21834 db.validate_staged_txn_writes(&good).unwrap();
21835
21836 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
21839 physical_micros: 5_000,
21840 logical: 0,
21841 node_tiebreaker: 0,
21842 };
21843 assert!(db
21844 .apply_staged_txn_writes(1 << 63, &good, commit_ts)
21845 .unwrap());
21846 assert_eq!(visible_ids(&db, "t"), vec![10, 20, 30]);
21847 let delete = StagedTxnWrite::Delete {
21848 table_id,
21849 row_ids: vec![20],
21850 }
21851 .encode()
21852 .unwrap();
21853 assert!(db
21854 .apply_staged_txn_writes((1 << 63) + 1, &[delete], commit_ts)
21855 .unwrap());
21856 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
21857
21858 let db = Arc::new(db);
21861 db.shutdown().unwrap();
21862 let expected = crate::storage_mode::StorageMode::ClusterReplica {
21863 cluster_id: ClusterId::from_bytes([1; 16]),
21864 node_id: NodeId::from_bytes([2; 16]),
21865 database_id: DatabaseId::from_bytes([3; 16]),
21866 };
21867 let db = Database::open_cluster_replica(dir.path(), &expected).unwrap();
21868 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
21869 }
21870}