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::{BTreeSet, 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 commit_ts: None,
974 })
975 .collect::<Vec<_>>();
976 let mut total = 0;
977 let error = encode_spilled_row_chunks(&rows, &mut total, 32, None).unwrap_err();
978 assert!(matches!(
979 error,
980 MongrelError::ResourceLimitExceeded {
981 resource: "spilled WAL transaction bytes",
982 ..
983 }
984 ));
985 }
986}
987
988struct PreparedRunLinks {
993 links: Vec<(PathBuf, PathBuf)>,
994 armed: bool,
995}
996
997impl PreparedRunLinks {
998 fn prepare(spilled: &[SpilledRun]) -> Result<Self> {
999 let mut guard = Self {
1000 links: Vec::with_capacity(spilled.len()),
1001 armed: true,
1002 };
1003 for run in spilled {
1004 crate::durable_file::rename(&run.pending_path, &run.final_path)?;
1005 guard
1006 .links
1007 .push((run.pending_path.clone(), run.final_path.clone()));
1008 }
1009 Ok(guard)
1010 }
1011
1012 fn disarm(&mut self) {
1013 self.armed = false;
1014 for (pending, _) in &self.links {
1015 if let Some(parent) = pending.parent() {
1016 let _ = std::fs::remove_dir_all(parent);
1017 }
1018 }
1019 }
1020}
1021
1022impl Drop for PreparedRunLinks {
1023 fn drop(&mut self) {
1024 if !self.armed {
1025 return;
1026 }
1027 for (pending, final_path) in self.links.iter().rev() {
1028 let _ = std::fs::rename(final_path, pending);
1029 }
1030 }
1031}
1032
1033struct TableApplyBatch {
1034 table_id: u64,
1035 handle: TableHandle,
1036 ops: Vec<crate::txn::StagedOp>,
1037}
1038
1039#[derive(Debug, Clone)]
1040struct TriggerRowImage {
1041 columns: HashMap<u16, Value>,
1042}
1043
1044impl TriggerRowImage {
1045 fn from_row(row: crate::memtable::Row) -> Self {
1046 Self {
1047 columns: row.columns,
1048 }
1049 }
1050
1051 fn from_cells(cells: &[(u16, Value)]) -> Self {
1052 Self {
1053 columns: cells.iter().cloned().collect(),
1054 }
1055 }
1056}
1057
1058#[derive(Debug, Clone)]
1059struct WriteEvent {
1060 table: String,
1061 kind: TriggerEvent,
1062 old: Option<TriggerRowImage>,
1063 new: Option<TriggerRowImage>,
1064 changed_columns: Vec<u16>,
1065 op_indices: Vec<usize>,
1066 put_idx: Option<usize>,
1067 trigger_stack: Vec<String>,
1068}
1069
1070#[derive(Default)]
1071struct TriggerExpansion {
1072 before: Vec<(u64, crate::txn::Staged)>,
1073 before_stacks: Vec<Vec<String>>,
1074 before_external: Vec<ExternalTriggerWrite>,
1075 after: Vec<(u64, crate::txn::Staged)>,
1076 after_stacks: Vec<Vec<String>>,
1077 after_external: Vec<ExternalTriggerWrite>,
1078 ignored_indices: std::collections::BTreeSet<usize>,
1079}
1080
1081#[derive(Clone, PartialEq)]
1082struct TriggerCatalogBinding {
1083 triggers: Vec<TriggerEntry>,
1084 tables: Vec<(String, u64, u64)>,
1085 external_tables: Vec<(String, u64, u64)>,
1086}
1087
1088fn trigger_catalog_binding(catalog: &Catalog) -> Option<TriggerCatalogBinding> {
1089 let mut triggers = catalog
1090 .triggers
1091 .iter()
1092 .filter(|entry| entry.trigger.enabled)
1093 .cloned()
1094 .collect::<Vec<_>>();
1095 if triggers.is_empty() {
1096 return None;
1097 }
1098 triggers.sort_by(|left, right| left.trigger.name.cmp(&right.trigger.name));
1099 let mut tables = catalog
1100 .tables
1101 .iter()
1102 .filter(|entry| matches!(entry.state, TableState::Live))
1103 .map(|entry| (entry.name.clone(), entry.table_id, entry.schema.schema_id))
1104 .collect::<Vec<_>>();
1105 tables.sort_unstable();
1106 let mut external_tables = catalog
1107 .external_tables
1108 .iter()
1109 .map(|entry| {
1110 (
1111 entry.name.clone(),
1112 entry.created_epoch,
1113 entry.declared_schema.schema_id,
1114 )
1115 })
1116 .collect::<Vec<_>>();
1117 external_tables.sort_unstable();
1118 Some(TriggerCatalogBinding {
1119 triggers,
1120 tables,
1121 external_tables,
1122 })
1123}
1124
1125struct TriggerProgramOutput<'a> {
1126 added: &'a mut Vec<(u64, crate::txn::Staged)>,
1127 added_stacks: &'a mut Vec<Vec<String>>,
1128 added_external: &'a mut Vec<ExternalTriggerWrite>,
1129 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
1130}
1131
1132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1133enum TriggerProgramOutcome {
1134 Continue,
1135 Ignore,
1136}
1137
1138#[derive(Debug, Clone)]
1140pub struct CheckIssue {
1141 pub table_id: u64,
1142 pub table_name: String,
1143 pub severity: String,
1144 pub description: String,
1145}
1146
1147#[derive(Debug, Clone)]
1149pub struct AuthorizedReadSnapshot {
1150 pub table: String,
1151 pub table_snapshot: Snapshot,
1152 pub data_generation: u64,
1153 pub security_version: u64,
1154 pub allowed_row_ids: Option<HashSet<RowId>>,
1155}
1156
1157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1159pub struct AuthorizedReadStamp {
1160 pub table_id: u64,
1161 pub schema_id: u64,
1162 pub data_generation: u64,
1163 pub security_version: u64,
1164 pub snapshot: Snapshot,
1165}
1166
1167type RlsCacheKey = (String, u64, u64, String);
1168
1169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1171pub struct RlsCacheStats {
1172 pub entries: usize,
1173 pub bytes: usize,
1174 pub hits: u64,
1175 pub misses: u64,
1176 pub evictions: u64,
1177 pub build_nanos: u64,
1178 pub rows_evaluated: u64,
1179}
1180
1181const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
1182const CDC_MAX_WAL_RECORDS: usize = 1_000_000;
1183const CDC_MAX_WAL_REPLAY_BYTES: usize = 256 * 1024 * 1024;
1184const CDC_MAX_EVENTS: usize = 100_000;
1185const CDC_MAX_ROWS: usize = 1_000_000;
1186const CDC_MAX_INLINE_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
1187const CDC_MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
1188
1189fn charge_cdc_bytes(total: &mut usize, amount: usize, resource: &'static str) -> Result<()> {
1190 let requested = total.saturating_add(amount);
1191 if requested > CDC_MAX_RETAINED_BYTES {
1192 return Err(MongrelError::ResourceLimitExceeded {
1193 resource,
1194 requested,
1195 limit: CDC_MAX_RETAINED_BYTES,
1196 });
1197 }
1198 *total = requested;
1199 Ok(())
1200}
1201
1202fn cdc_row_storage_bytes(row: &crate::memtable::Row) -> usize {
1203 usize::try_from(row.estimated_bytes())
1204 .unwrap_or(usize::MAX)
1205 .saturating_add(std::mem::size_of::<crate::memtable::Row>())
1206}
1207
1208fn cdc_row_json_bytes(row: &crate::memtable::Row) -> usize {
1209 let value_slot = std::mem::size_of::<serde_json::Value>();
1210 row.columns.values().fold(512_usize, |bytes, value| {
1211 let values = match value {
1212 Value::Bytes(values) => values.len(),
1213 Value::Json(values) => values.len(),
1214 Value::Embedding(values) => values.len(),
1215 Value::GeneratedEmbedding(value) => value.vector.len(),
1216 _ => 1,
1217 };
1218 bytes.saturating_add(values.saturating_mul(value_slot))
1219 })
1220}
1221
1222fn cdc_rows_json_bytes(rows: &[crate::memtable::Row]) -> usize {
1223 rows.iter().fold(0_usize, |bytes, row| {
1224 bytes.saturating_add(cdc_row_json_bytes(row))
1225 })
1226}
1227
1228struct RlsCacheEntry {
1229 value: Arc<HashSet<RowId>>,
1230 bytes: usize,
1231 generation: u64,
1232}
1233
1234#[derive(Default)]
1235struct RlsCache {
1236 entries: HashMap<RlsCacheKey, RlsCacheEntry>,
1237 lru: BTreeSet<(u64, RlsCacheKey)>,
1238 next_generation: u64,
1239 bytes: usize,
1240 hits: u64,
1241 misses: u64,
1242 evictions: u64,
1243 build_nanos: u64,
1244 rows_evaluated: u64,
1245}
1246
1247impl RlsCache {
1248 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
1249 let value = self.entries.get(key).map(|entry| Arc::clone(&entry.value));
1250 if value.is_some() {
1251 self.hits = self.hits.saturating_add(1);
1252 self.touch(key);
1253 } else {
1254 self.misses = self.misses.saturating_add(1);
1255 }
1256 value
1257 }
1258
1259 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
1260 let bytes = key
1261 .0
1262 .len()
1263 .saturating_add(key.3.len())
1264 .saturating_add(
1265 value
1266 .capacity()
1267 .saturating_mul(std::mem::size_of::<RowId>() * 3),
1268 )
1269 .saturating_add(std::mem::size_of::<RlsCacheKey>());
1270 if bytes > RLS_CACHE_MAX_BYTES {
1271 return;
1272 }
1273 if let Some(old) = self.entries.remove(&key) {
1274 self.bytes = self.bytes.saturating_sub(old.bytes);
1275 self.lru.remove(&(old.generation, key.clone()));
1276 }
1277 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
1278 let Some((_, oldest)) = self.lru.pop_first() else {
1279 break;
1280 };
1281 if let Some(old) = self.entries.remove(&oldest) {
1282 self.bytes = self.bytes.saturating_sub(old.bytes);
1283 self.evictions = self.evictions.saturating_add(1);
1284 }
1285 }
1286 let generation = self.allocate_generation();
1287 self.bytes = self.bytes.saturating_add(bytes);
1288 self.lru.insert((generation, key.clone()));
1289 self.entries.insert(
1290 key,
1291 RlsCacheEntry {
1292 value,
1293 bytes,
1294 generation,
1295 },
1296 );
1297 }
1298
1299 fn touch(&mut self, key: &RlsCacheKey) {
1300 let Some(previous) = self.entries.get(key).map(|entry| entry.generation) else {
1301 return;
1302 };
1303 self.lru.remove(&(previous, key.clone()));
1304 let generation = self.allocate_generation();
1305 if let Some(entry) = self.entries.get_mut(key) {
1306 entry.generation = generation;
1307 }
1308 self.lru.insert((generation, key.clone()));
1309 }
1310
1311 fn allocate_generation(&mut self) -> u64 {
1312 if self.next_generation == u64::MAX {
1313 self.rebase_generations();
1314 }
1315 let generation = self.next_generation;
1316 self.next_generation += 1;
1317 generation
1318 }
1319
1320 fn rebase_generations(&mut self) {
1321 let ordered = std::mem::take(&mut self.lru);
1322 for (generation, (_, key)) in ordered.into_iter().enumerate() {
1323 let generation = generation as u64;
1324 if let Some(entry) = self.entries.get_mut(&key) {
1325 entry.generation = generation;
1326 self.lru.insert((generation, key));
1327 }
1328 }
1329 self.next_generation = self.lru.len() as u64;
1330 }
1331
1332 fn stats(&self) -> RlsCacheStats {
1333 RlsCacheStats {
1334 entries: self.entries.len(),
1335 bytes: self.bytes,
1336 hits: self.hits,
1337 misses: self.misses,
1338 evictions: self.evictions,
1339 build_nanos: self.build_nanos,
1340 rows_evaluated: self.rows_evaluated,
1341 }
1342 }
1343}
1344
1345#[cfg(test)]
1346mod rls_cache_tests {
1347 use super::*;
1348
1349 fn key(principal: &str) -> RlsCacheKey {
1350 ("table".into(), 1, 1, principal.into())
1351 }
1352
1353 fn rows(row_id: u64) -> Arc<HashSet<RowId>> {
1354 Arc::new(std::iter::once(RowId(row_id)).collect())
1355 }
1356
1357 #[test]
1358 fn hits_update_recency_without_growing_the_order_index() {
1359 let mut cache = RlsCache::default();
1360 let first = key("first");
1361 let second = key("second");
1362 cache.insert(first.clone(), rows(1));
1363 cache.insert(second.clone(), rows(2));
1364 assert_eq!(cache.lru.len(), cache.entries.len());
1365
1366 for _ in 0..1_000 {
1367 assert!(cache.get(&first).is_some());
1368 }
1369
1370 assert_eq!(cache.lru.len(), cache.entries.len());
1371 assert_eq!(
1372 cache.lru.first().map(|(_, candidate)| candidate),
1373 Some(&second)
1374 );
1375
1376 cache.insert(first.clone(), rows(3));
1377 assert_eq!(cache.lru.len(), cache.entries.len());
1378 let replacement = cache.get(&first).unwrap();
1379 assert_eq!(replacement.len(), 1);
1380 assert!(replacement.contains(&RowId(3)));
1381 }
1382}
1383
1384#[derive(Clone)]
1386pub struct TableHandle {
1387 inner: TableHandleInner,
1388 generation_metrics: Arc<TableGenerationMetrics>,
1389}
1390
1391#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1392pub struct TableGenerationStats {
1393 pub active_read_generations: usize,
1394 pub max_live_read_generations: usize,
1395 pub cow_clone_count: u64,
1396 pub cow_clone_nanos: u64,
1397 pub estimated_cow_clone_bytes: u64,
1398 pub writer_wait_nanos: u64,
1399}
1400
1401#[derive(Default)]
1402#[doc(hidden)]
1403pub struct TableGenerationMetrics {
1404 active_read_generations: AtomicUsize,
1405 max_live_read_generations: AtomicUsize,
1406 cow_clone_count: AtomicU64,
1407 cow_clone_nanos: AtomicU64,
1408 estimated_cow_clone_bytes: AtomicU64,
1409 writer_wait_nanos: AtomicU64,
1410}
1411
1412impl TableGenerationMetrics {
1413 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
1414 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
1415 self.max_live_read_generations
1416 .fetch_max(active, Ordering::Relaxed);
1417 Arc::new(TableReadGeneration {
1418 table,
1419 metrics: Arc::clone(self),
1420 })
1421 }
1422
1423 fn stats(&self) -> TableGenerationStats {
1424 TableGenerationStats {
1425 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
1426 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
1427 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
1428 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
1429 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
1430 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
1431 }
1432 }
1433}
1434
1435pub struct TableReadGeneration {
1437 table: Table,
1438 metrics: Arc<TableGenerationMetrics>,
1439}
1440
1441impl std::ops::Deref for TableReadGeneration {
1442 type Target = Table;
1443
1444 fn deref(&self) -> &Self::Target {
1445 &self.table
1446 }
1447}
1448
1449impl Drop for TableReadGeneration {
1450 fn drop(&mut self) {
1451 self.metrics
1452 .active_read_generations
1453 .fetch_sub(1, Ordering::Relaxed);
1454 }
1455}
1456
1457#[derive(Clone)]
1458enum TableHandleInner {
1459 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
1460 Direct(Arc<Mutex<Table>>),
1461}
1462
1463pub enum TableGuard<'a> {
1464 CopyOnWrite {
1465 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
1466 metrics: Arc<TableGenerationMetrics>,
1467 },
1468 Direct {
1469 table: parking_lot::MutexGuard<'a, Table>,
1470 },
1471}
1472
1473pub enum TableReadGuard<'a> {
1479 CopyOnWrite {
1480 table: parking_lot::RwLockReadGuard<'a, Arc<Table>>,
1481 },
1482 Direct {
1483 table: parking_lot::MutexGuard<'a, Table>,
1484 },
1485}
1486
1487impl TableHandle {
1488 fn new(table: Table) -> Self {
1489 Self {
1490 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
1491 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1492 }
1493 }
1494
1495 pub fn from_table(table: Table) -> Self {
1496 Self::new(table)
1497 }
1498
1499 pub fn read(&self) -> TableReadGuard<'_> {
1502 match &self.inner {
1503 TableHandleInner::CopyOnWrite(table) => TableReadGuard::CopyOnWrite {
1504 table: table.read(),
1505 },
1506 TableHandleInner::Direct(table) => TableReadGuard::Direct {
1507 table: table.lock(),
1508 },
1509 }
1510 }
1511
1512 pub fn lock(&self) -> TableGuard<'_> {
1513 let started = std::time::Instant::now();
1514 let guard = match &self.inner {
1515 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
1516 table: table.write(),
1517 metrics: Arc::clone(&self.generation_metrics),
1518 },
1519 TableHandleInner::Direct(table) => TableGuard::Direct {
1520 table: table.lock(),
1521 },
1522 };
1523 self.generation_metrics.writer_wait_nanos.fetch_add(
1524 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1525 Ordering::Relaxed,
1526 );
1527 guard
1528 }
1529
1530 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
1531 let started = std::time::Instant::now();
1532 let guard = match &self.inner {
1533 TableHandleInner::CopyOnWrite(table) => {
1534 table
1535 .try_write_for(timeout)
1536 .map(|table| TableGuard::CopyOnWrite {
1537 table,
1538 metrics: Arc::clone(&self.generation_metrics),
1539 })
1540 }
1541 TableHandleInner::Direct(table) => table
1542 .try_lock_for(timeout)
1543 .map(|table| TableGuard::Direct { table }),
1544 };
1545 self.generation_metrics.writer_wait_nanos.fetch_add(
1546 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1547 Ordering::Relaxed,
1548 );
1549 guard
1550 }
1551
1552 pub fn ptr_eq(&self, other: &Self) -> bool {
1553 match (&self.inner, &other.inner) {
1554 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1555 Arc::ptr_eq(left, right)
1556 }
1557 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1558 Arc::ptr_eq(left, right)
1559 }
1560 _ => false,
1561 }
1562 }
1563
1564 pub fn read_generation_with_context(
1565 &self,
1566 context: Option<&crate::query::AiExecutionContext>,
1567 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1568 let mut table = if let Some(context) = context {
1569 loop {
1570 context.checkpoint()?;
1571 let wait = context
1572 .remaining_duration()
1573 .unwrap_or(std::time::Duration::from_millis(5))
1574 .min(std::time::Duration::from_millis(5));
1575 if let Some(table) = self.try_lock_for(wait) {
1576 break table;
1577 }
1578 }
1579 } else {
1580 self.lock()
1581 };
1582 let snapshot = table.snapshot();
1583 let generation = table.clone_read_generation()?;
1584 Ok((self.generation_metrics.activate(generation), snapshot))
1585 }
1586
1587 pub fn generation_stats(&self) -> TableGenerationStats {
1588 self.generation_metrics.stats()
1589 }
1590}
1591
1592impl From<Arc<Mutex<Table>>> for TableHandle {
1593 fn from(table: Arc<Mutex<Table>>) -> Self {
1594 Self {
1595 inner: TableHandleInner::Direct(table),
1596 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1597 }
1598 }
1599}
1600
1601impl std::ops::Deref for TableGuard<'_> {
1602 type Target = Table;
1603
1604 fn deref(&self) -> &Self::Target {
1605 match self {
1606 Self::CopyOnWrite { table, .. } => table.as_ref(),
1607 Self::Direct { table } => table,
1608 }
1609 }
1610}
1611
1612impl std::ops::Deref for TableReadGuard<'_> {
1613 type Target = Table;
1614
1615 fn deref(&self) -> &Self::Target {
1616 match self {
1617 Self::CopyOnWrite { table } => table.as_ref(),
1618 Self::Direct { table } => table,
1619 }
1620 }
1621}
1622
1623impl std::ops::DerefMut for TableGuard<'_> {
1624 fn deref_mut(&mut self) -> &mut Self::Target {
1625 match self {
1626 Self::CopyOnWrite { table, metrics } => {
1627 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1628 let estimated_bytes = table.estimated_clone_bytes();
1629 let started = std::time::Instant::now();
1630 let table = Arc::make_mut(table);
1631 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1632 metrics.cow_clone_nanos.fetch_add(
1633 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1634 Ordering::Relaxed,
1635 );
1636 metrics
1637 .estimated_cow_clone_bytes
1638 .fetch_add(estimated_bytes, Ordering::Relaxed);
1639 table
1640 } else {
1641 Arc::make_mut(table)
1642 }
1643 }
1644 Self::Direct { table } => table,
1645 }
1646 }
1647}
1648
1649#[derive(Clone, Debug)]
1650pub struct ReadAuthorization {
1651 pub operation: crate::auth::ColumnOperation,
1652 pub columns: Vec<u16>,
1653 pub permissions: Vec<crate::auth::Permission>,
1654}
1655
1656#[derive(Default, Debug)]
1657struct TableWritePermissionNeeds {
1658 insert: bool,
1659 insert_columns: Vec<u16>,
1660 update: bool,
1661 update_columns: Vec<u16>,
1662 delete: bool,
1663 truncate: bool,
1664}
1665
1666#[cfg(test)]
1667thread_local! {
1668 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1669 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1670 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1671 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1672 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1673 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1674}
1675
1676fn summarize_write_permissions(
1677 staging: &[(u64, crate::txn::Staged)],
1678) -> HashMap<u64, TableWritePermissionNeeds> {
1679 use crate::txn::Staged;
1680
1681 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1682 for (table_id, operation) in staging {
1683 let table = needs.entry(*table_id).or_default();
1684 match operation {
1685 Staged::Put(cells) => {
1686 table.insert = true;
1687 table
1688 .insert_columns
1689 .extend(cells.iter().map(|(column, _)| *column));
1690 }
1691 Staged::Update {
1692 changed_columns, ..
1693 } => {
1694 table.update = true;
1695 table.update_columns.extend(changed_columns);
1696 }
1697 Staged::Delete(_) => table.delete = true,
1698 Staged::Truncate => table.truncate = true,
1699 }
1700 }
1701 for table in needs.values_mut() {
1702 table.insert_columns.sort_unstable();
1703 table.insert_columns.dedup();
1704 table.update_columns.sort_unstable();
1705 table.update_columns.dedup();
1706 }
1707 needs
1708}
1709
1710struct SecurityCoordinator {
1711 gate: RwLock<()>,
1713 version: AtomicU64,
1714}
1715
1716fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1717 static COORDINATORS: std::sync::OnceLock<
1718 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1719 > = std::sync::OnceLock::new();
1720
1721 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1722 let mut coordinators = COORDINATORS
1723 .get_or_init(|| Mutex::new(HashMap::new()))
1724 .lock();
1725 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1726 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1727 return coordinator;
1728 }
1729 let coordinator = Arc::new(SecurityCoordinator {
1730 gate: RwLock::new(()),
1731 version: AtomicU64::new(version),
1732 });
1733 coordinators.insert(root, Arc::downgrade(&coordinator));
1734 coordinator
1735}
1736
1737pub fn lock_table_with_context<'a>(
1738 handle: &'a TableHandle,
1739 context: Option<&crate::query::AiExecutionContext>,
1740) -> Result<TableGuard<'a>> {
1741 let Some(context) = context else {
1742 return Ok(handle.lock());
1743 };
1744 loop {
1745 context.checkpoint()?;
1746 let wait = context
1747 .remaining_duration()
1748 .unwrap_or(std::time::Duration::from_millis(5))
1749 .min(std::time::Duration::from_millis(5));
1750 if let Some(guard) = handle.try_lock_for(wait) {
1751 return Ok(guard);
1752 }
1753 }
1754}
1755
1756#[derive(Clone, Debug, Default)]
1762pub struct OpenOptions {
1763 pub lock_timeout_ms: u32,
1779 pub memory_budget_bytes: Option<u64>,
1784 pub temp_disk_budget_bytes: Option<u64>,
1789 pub offline_validation: bool,
1796 pub max_recovery_wal_bytes: Option<u64>,
1799 pub max_recovery_wal_records: Option<usize>,
1802}
1803
1804impl OpenOptions {
1805 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1808 self.lock_timeout_ms = ms;
1809 self
1810 }
1811
1812 pub fn with_memory_budget_bytes(mut self, bytes: u64) -> Self {
1814 self.memory_budget_bytes = Some(bytes);
1815 self
1816 }
1817
1818 pub fn with_temp_disk_budget_bytes(mut self, bytes: u64) -> Self {
1820 self.temp_disk_budget_bytes = Some(bytes);
1821 self
1822 }
1823
1824 pub fn with_offline_validation(mut self, offline_validation: bool) -> Self {
1827 self.offline_validation = offline_validation;
1828 self
1829 }
1830
1831 pub fn with_max_recovery_wal_bytes(mut self, bytes: u64) -> Self {
1833 self.max_recovery_wal_bytes = Some(bytes);
1834 self
1835 }
1836
1837 pub fn with_max_recovery_wal_records(mut self, records: usize) -> Self {
1839 self.max_recovery_wal_records = Some(records);
1840 self
1841 }
1842}
1843
1844#[derive(Debug, Clone)]
1848pub(crate) enum OpenModeGate {
1849 Normal,
1852 SharedCore,
1855 OfflineValidation,
1857 ClusterRuntime {
1861 cluster_id: mongreldb_types::ids::ClusterId,
1863 node_id: mongreldb_types::ids::NodeId,
1865 database_id: mongreldb_types::ids::DatabaseId,
1867 },
1868 Create(crate::storage_mode::StorageMode),
1870}
1871
1872pub const DEFAULT_MEMORY_BUDGET_BYTES: u64 = 1024 * 1024 * 1024;
1876
1877pub const DEFAULT_TEMP_DISK_BUDGET_BYTES: u64 = 4 * 1024 * 1024 * 1024;
1880
1881#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1885pub(crate) struct CoreResourceConfig {
1886 pub memory_budget_bytes: u64,
1887 pub temp_disk_budget_bytes: u64,
1888 pub wal_replay_limits: crate::wal::WalReplayLimits,
1889}
1890
1891impl Default for CoreResourceConfig {
1892 fn default() -> Self {
1893 Self {
1894 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
1895 temp_disk_budget_bytes: DEFAULT_TEMP_DISK_BUDGET_BYTES,
1896 wal_replay_limits: crate::wal::WalReplayLimits::from_env().unwrap_or_default(),
1897 }
1898 }
1899}
1900
1901impl CoreResourceConfig {
1902 fn from_options(options: &OpenOptions) -> Result<Self> {
1903 let config = Self {
1904 memory_budget_bytes: options
1905 .memory_budget_bytes
1906 .unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES),
1907 temp_disk_budget_bytes: options
1908 .temp_disk_budget_bytes
1909 .unwrap_or(DEFAULT_TEMP_DISK_BUDGET_BYTES),
1910 wal_replay_limits: crate::wal::WalReplayLimits::from_env()?.overlay(
1911 options.max_recovery_wal_bytes,
1912 options.max_recovery_wal_records,
1913 ),
1914 };
1915 if config.memory_budget_bytes == 0 {
1916 return Err(MongrelError::InvalidArgument(
1917 "memory_budget_bytes must be nonzero".into(),
1918 ));
1919 }
1920 if config.temp_disk_budget_bytes == 0 {
1921 return Err(MongrelError::InvalidArgument(
1922 "temp_disk_budget_bytes must be nonzero".into(),
1923 ));
1924 }
1925 Ok(config)
1926 }
1927}
1928
1929#[derive(Debug, Clone)]
1932pub struct TablePinsReport {
1933 pub table_id: u64,
1935 pub table: String,
1937 pub pins: crate::retention::PinsReport,
1939}
1940
1941pub struct DatabaseCore {
1954 root: PathBuf,
1955 durable_root: Arc<crate::durable_file::DurableRoot>,
1956 lifecycle: Arc<crate::core::LifecycleController>,
1959 registry: std::sync::OnceLock<crate::manager::CoreRegistration>,
1963 read_only: bool,
1965 catalog: RwLock<Catalog>,
1966 security_coordinator: Arc<SecurityCoordinator>,
1967 security_catalog_disk_reads: AtomicU64,
1968 rls_cache: Mutex<RlsCache>,
1969 epoch: Arc<EpochAuthority>,
1970 snapshots: Arc<SnapshotRegistry>,
1971 memory_governor: crate::memory::MemoryGovernor,
1976 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1977 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1978 spill_manager: crate::spill::SpillManager,
1982 resource_groups: crate::resource::ResourceGroupRegistry,
1986 embedding_providers: crate::embedding::EmbeddingProviderRegistry,
1990 pub(crate) service_principals: crate::service_principal::ServicePrincipalStore,
1994 job_registry: Arc<crate::jobs::JobRegistry>,
1997 commit_lock: Arc<Mutex<()>>,
1998 kms_rotation_lock: Mutex<()>,
1999 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
2004 next_txn_id: Arc<Mutex<u64>>,
2008 tables: RwLock<HashMap<u64, TableHandle>>,
2009 kek: Option<Arc<crate::encryption::Kek>>,
2010 ddl_lock: Mutex<()>,
2013 meta_dek: Option<[u8; META_DEK_LEN]>,
2014 spill_threshold: std::sync::atomic::AtomicU64,
2017 conflicts: crate::txn::ConflictIndex,
2020 active_txns: crate::txn::ActiveTxns,
2023 lock_manager: Arc<crate::locks::LockManager>,
2029 poisoned: Arc<std::sync::atomic::AtomicBool>,
2032 group: Arc<crate::txn::GroupCommit>,
2036 commit_log: RwLock<Arc<dyn mongreldb_log::CommitLog>>,
2040 standalone_commit_log: Arc<crate::commit_log::StandaloneCommitLog>,
2044 hlc: Arc<mongreldb_types::hlc::HlcClock>,
2050 idempotency: crate::txn::IdempotencyLedger,
2053 commit_ts_ledger: Mutex<std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp>>,
2061 active_spills: Arc<crate::retention::ActiveSpills>,
2064 replication_barrier: parking_lot::RwLock<()>,
2067 replication_wal_retention_segments: AtomicUsize,
2069 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
2072 #[doc(hidden)]
2076 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2077 #[doc(hidden)]
2079 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2080 #[doc(hidden)]
2083 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2084 #[doc(hidden)]
2087 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2088 #[doc(hidden)]
2094 fk_lock_hook: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
2095 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2096 trigger_recursive: AtomicBool,
2097 trigger_max_depth: AtomicU32,
2098 trigger_max_loop_iterations: AtomicU32,
2099 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
2102 change_wake: tokio::sync::broadcast::Sender<()>,
2105 _lock: Mutex<Option<ExclusiveDatabaseLease>>,
2109}
2110
2111pub struct Database {
2121 core: Arc<DatabaseCore>,
2122 principal: RwLock<Option<crate::auth::Principal>>,
2132 auth_state: crate::auth_state::AuthState,
2138 shared: bool,
2145}
2146
2147impl std::ops::Deref for Database {
2148 type Target = DatabaseCore;
2149
2150 fn deref(&self) -> &DatabaseCore {
2151 &self.core
2152 }
2153}
2154
2155struct RunPins {
2156 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
2157 runs: Vec<(u64, u128)>,
2158}
2159
2160struct TxnLockGuard<'a> {
2165 locks: &'a crate::locks::LockManager,
2166 txn_id: u64,
2167}
2168
2169impl Drop for TxnLockGuard<'_> {
2170 fn drop(&mut self) {
2171 self.locks.release_all(self.txn_id);
2172 }
2173}
2174
2175struct BackupFilePins {
2176 root: PathBuf,
2177}
2178
2179struct PendingTableDir {
2180 path: PathBuf,
2181 armed: bool,
2182}
2183
2184impl PendingTableDir {
2185 fn new(path: PathBuf) -> Self {
2186 Self { path, armed: true }
2187 }
2188
2189 fn disarm(&mut self) {
2190 self.armed = false;
2191 }
2192}
2193
2194impl Drop for PendingTableDir {
2195 fn drop(&mut self) {
2196 if self.armed {
2197 let _ = std::fs::remove_dir_all(&self.path);
2198 }
2199 }
2200}
2201
2202impl Drop for BackupFilePins {
2203 fn drop(&mut self) {
2204 let _ = std::fs::remove_dir_all(&self.root);
2205 }
2206}
2207
2208impl Drop for RunPins {
2209 fn drop(&mut self) {
2210 let mut pins = self.pins.lock();
2211 for run in &self.runs {
2212 if let Some(count) = pins.get_mut(run) {
2213 *count -= 1;
2214 if *count == 0 {
2215 pins.remove(run);
2216 }
2217 }
2218 }
2219 }
2220}
2221
2222#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2225pub struct ChangeEvent {
2226 pub id: Option<String>,
2227 pub channel: String,
2228 pub table_id: Option<u64>,
2229 pub table: String,
2230 pub op: String,
2231 pub epoch: u64,
2232 pub txn_id: Option<u64>,
2233 pub message: Option<String>,
2234 pub data: Option<serde_json::Value>,
2235}
2236
2237#[derive(Debug, Clone)]
2238pub struct CdcBatch {
2239 pub events: Vec<ChangeEvent>,
2240 pub current_epoch: u64,
2241 pub earliest_epoch: Option<u64>,
2242 pub gap: bool,
2243}
2244
2245impl std::fmt::Debug for Database {
2252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2253 let cat = self.catalog.read();
2254 let principal_guard = self.principal.read();
2255 let principal: &str = principal_guard
2256 .as_ref()
2257 .map(|p| p.username.as_str())
2258 .unwrap_or("<none>");
2259 f.debug_struct("Database")
2260 .field("root", &self.root)
2261 .field("db_epoch", &cat.db_epoch)
2262 .field("open_generation", &"sidecar")
2263 .field("tables", &cat.tables.len())
2264 .field("visible_epoch", &self.epoch.visible().0)
2265 .field("encrypted", &self.kek.is_some())
2266 .field("require_auth", &cat.require_auth)
2267 .field("principal", &principal)
2268 .finish()
2269 }
2270}
2271
2272impl std::fmt::Debug for DatabaseCore {
2275 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2276 let cat = self.catalog.read();
2277 f.debug_struct("DatabaseCore")
2278 .field("root", &self.root)
2279 .field("lifecycle", &self.lifecycle.state())
2280 .field("db_epoch", &cat.db_epoch)
2281 .field("tables", &cat.tables.len())
2282 .field("visible_epoch", &self.epoch.visible().0)
2283 .field("encrypted", &self.kek.is_some())
2284 .field("require_auth", &cat.require_auth)
2285 .finish()
2286 }
2287}
2288
2289impl DatabaseCore {
2290 fn ensure_owner_process(&self) -> Result<()> {
2291 let current_pid = std::process::id();
2292 let owner_pid = self
2293 ._lock
2294 .lock()
2295 .as_ref()
2296 .map(|lease| lease.owner_pid)
2297 .unwrap_or(current_pid);
2298 if current_pid == owner_pid {
2299 Ok(())
2300 } else {
2301 Err(MongrelError::ForkedProcess {
2302 owner_pid,
2303 current_pid,
2304 })
2305 }
2306 }
2307
2308 pub fn root(&self) -> &Path {
2310 self.durable_root.canonical_path()
2311 }
2312
2313 pub fn file_identity(&self) -> Result<crate::core::DatabaseFileIdentity> {
2315 crate::core::DatabaseFileIdentity::from_durable_root(&self.durable_root)
2316 }
2317
2318 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2320 self.lifecycle.state()
2321 }
2322
2323 pub fn is_open(&self) -> bool {
2325 self.lifecycle.is_open()
2326 }
2327
2328 pub fn operation_guard(self: &Arc<Self>) -> Result<crate::core::OperationGuard> {
2332 self.lifecycle.begin_operation()
2333 }
2334
2335 pub(crate) fn set_registry(
2339 &self,
2340 registration: crate::manager::CoreRegistration,
2341 ) -> Result<()> {
2342 self.registry
2343 .set(registration)
2344 .map_err(|_| MongrelError::Conflict("database core is already registry-bound".into()))
2345 }
2346
2347 pub fn shutdown(self: &Arc<Self>, drain_deadline: std::time::Duration) -> Result<()> {
2367 self.ensure_owner_process()?;
2368 let initiated = self.lifecycle.begin_shutdown();
2369 if !initiated {
2370 match self.lifecycle.state() {
2371 crate::core::LifecycleState::Closed => return Ok(()),
2372 crate::core::LifecycleState::Draining => {}
2375 state => {
2376 return Err(MongrelError::Conflict(format!(
2377 "database core cannot shut down from lifecycle state {state}"
2378 )))
2379 }
2380 }
2381 }
2382 if let Some(registration) = self.registry.get() {
2383 registration
2384 .manager
2385 .mark_closing(®istration.identity, self);
2386 }
2387 self.lifecycle.wait_drained(drain_deadline)?;
2388 let sync = self.shared_wal.lock().group_sync().map(|_| ());
2389 self.lifecycle.mark_closing();
2390 let lease = self._lock.lock().take();
2391 drop(lease);
2392 if let Some(registration) = self.registry.get() {
2393 registration
2394 .manager
2395 .entry_closed(®istration.identity, self);
2396 }
2397 self.lifecycle.mark_closed();
2398 sync
2399 }
2400}
2401
2402impl Database {
2403 pub fn open_metrics() -> DatabaseOpenMetrics {
2404 DatabaseOpenMetrics {
2405 lock_waits: DATABASE_OPEN_WAIT_COUNT.load(Ordering::Relaxed),
2406 failures: DATABASE_OPEN_FAILURE_COUNT.load(Ordering::Relaxed),
2407 }
2408 }
2409
2410 pub fn core(&self) -> Arc<DatabaseCore> {
2412 Arc::clone(&self.core)
2413 }
2414
2415 pub fn identity(&self) -> crate::handle::HandleIdentity {
2418 match self.principal.read().as_ref() {
2419 Some(principal) => crate::handle::HandleIdentity::CatalogUser {
2420 username: principal.username.clone(),
2421 user_id: principal.user_id,
2422 created_version: principal.created_epoch,
2423 },
2424 None => crate::handle::HandleIdentity::Credentialless,
2425 }
2426 }
2427
2428 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2430 self.core.lifecycle_state()
2431 }
2432
2433 pub fn operation_guard(&self) -> Result<crate::core::OperationGuard> {
2437 self.core.operation_guard()
2438 }
2439
2440 pub(crate) fn from_core(
2444 core: Arc<DatabaseCore>,
2445 principal: Option<crate::auth::Principal>,
2446 shared: bool,
2447 ) -> Self {
2448 let require_auth = core.catalog.read().require_auth;
2449 Self {
2450 core,
2451 principal: RwLock::new(principal.clone()),
2452 auth_state: crate::auth_state::AuthState::new(require_auth, principal),
2453 shared,
2454 }
2455 }
2456
2457 pub(crate) fn rebind_service_principal(
2461 &self,
2462 def: &crate::service_principal::ServicePrincipalDefinition,
2463 ) {
2464 let principal = crate::auth::Principal {
2465 user_id: 0,
2466 created_epoch: def.creation_version,
2467 username: format!("service:{}", def.token_id),
2468 is_admin: false,
2469 roles: Vec::new(),
2470 permissions: def.permissions.clone(),
2471 };
2472 *self.principal.write() = Some(principal.clone());
2473 self.auth_state.set_principal(Some(principal));
2474 }
2475
2476 pub(crate) fn into_core(self) -> Arc<DatabaseCore> {
2478 self.core
2479 }
2480
2481 pub(crate) fn open_for_shared_core(root: impl AsRef<Path>) -> Result<Self> {
2486 Self::open_inner_with_lock_timeout(
2487 root,
2488 None,
2489 None,
2490 0,
2491 CoreResourceConfig::default(),
2492 OpenModeGate::SharedCore,
2493 )
2494 }
2495
2496 pub fn shutdown(self: Arc<Self>) -> Result<()> {
2502 if self.shared {
2503 return Err(MongrelError::Conflict(
2504 "shared-core facades do not own storage; use DatabaseHandle::shutdown".into(),
2505 ));
2506 }
2507 match Arc::try_unwrap(self) {
2508 Ok(database) => {
2509 database.ensure_owner_process()?;
2510 database.core.shutdown(std::time::Duration::from_secs(30))?;
2514 drop(database);
2515 Ok(())
2516 }
2517 Err(database) => Err(MongrelError::DatabaseBusy {
2518 strong_handles: Arc::strong_count(&database),
2519 }),
2520 }
2521 }
2522
2523 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
2524 if let Ok(canonical) = root.canonicalize() {
2525 let lock_dir = canonical.parent().ok_or_else(|| {
2526 std::io::Error::new(
2527 std::io::ErrorKind::InvalidInput,
2528 "database root must have a parent directory",
2529 )
2530 })?;
2531 return Ok((canonical.clone(), lock_dir.to_path_buf()));
2532 }
2533
2534 let absolute = if root.is_absolute() {
2535 root.to_path_buf()
2536 } else {
2537 std::env::current_dir()?.join(root)
2538 };
2539 let mut cursor = absolute.as_path();
2540 let mut suffix = Vec::new();
2541 while !cursor.exists() {
2542 let name = cursor.file_name().ok_or_else(|| {
2543 std::io::Error::new(
2544 std::io::ErrorKind::NotFound,
2545 format!("no existing ancestor for database root {}", root.display()),
2546 )
2547 })?;
2548 suffix.push(name.to_os_string());
2549 cursor = cursor.parent().ok_or_else(|| {
2550 std::io::Error::new(
2551 std::io::ErrorKind::NotFound,
2552 format!("no existing ancestor for database root {}", root.display()),
2553 )
2554 })?;
2555 }
2556 let lock_dir = cursor.canonicalize()?;
2557 let mut canonical = lock_dir.clone();
2558 for component in suffix.iter().rev() {
2559 canonical.push(component);
2560 }
2561 Ok((canonical, lock_dir))
2562 }
2563
2564 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<ExclusiveDatabaseLease> {
2565 use std::hash::{Hash, Hasher};
2566
2567 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
2568 let reservation =
2569 OpenReservation::acquire(DatabaseOpenKey::IntendedPath(canonical_path.clone()), root)?;
2570
2571 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2572 canonical_path.hash(&mut hasher);
2573 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
2574 let file = std::fs::OpenOptions::new()
2575 .create(true)
2576 .truncate(false)
2577 .write(true)
2578 .open(lock_path)?;
2579 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2580 return Err(MongrelError::DatabaseLocked {
2581 path: root.to_path_buf(),
2582 message: error.to_string(),
2583 });
2584 }
2585 Ok(reservation.into_lease(file, canonical_path))
2586 }
2587
2588 fn acquire_legacy_database_lock(
2589 lock: &mut ExclusiveDatabaseLease,
2590 root: &Path,
2591 timeout_ms: u32,
2592 ) -> Result<()> {
2593 let durable_root = lock
2594 .durable_root
2595 .as_ref()
2596 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
2597 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
2598 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2599 return Err(MongrelError::DatabaseLocked {
2600 path: root.to_path_buf(),
2601 message: error.to_string(),
2602 });
2603 }
2604 lock.legacy_file = Some(file);
2605 Ok(())
2606 }
2607
2608 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
2609 if path.exists() {
2610 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
2611 }
2612 let mut ancestor = path;
2613 while !ancestor.exists() {
2614 ancestor = ancestor.parent().ok_or_else(|| {
2615 MongrelError::NotFound(format!(
2616 "no existing ancestor for database root {}",
2617 path.display()
2618 ))
2619 })?;
2620 }
2621 let relative = path.strip_prefix(ancestor).map_err(|error| {
2622 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
2623 })?;
2624 crate::durable_file::DurableRoot::open(ancestor)?
2625 .create_directory_all_pinned(relative)
2626 .map_err(Into::into)
2627 }
2628
2629 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2630 let requested_root = root.as_ref();
2631 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
2632 let root = lock.canonical_path.clone();
2633 Self::reject_existing_database(&root)?;
2634 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
2635 if durable_root.canonical_path() != lock.canonical_path {
2636 return Err(MongrelError::Conflict(
2637 "database root changed while it was being created".into(),
2638 ));
2639 }
2640 lock.claim_root_identity(&durable_root)?;
2641 durable_root.create_directory_all(META_DIR)?;
2642 lock.durable_root = Some(durable_root);
2643 let io_root = lock
2644 .durable_root
2645 .as_ref()
2646 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2647 .io_path()?;
2648 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
2649 Self::reject_existing_database(&io_root)?;
2650 Ok((io_root, lock))
2651 }
2652
2653 fn begin_open(
2654 root: impl AsRef<Path>,
2655 lock_timeout_ms: u32,
2656 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2657 let root = root.as_ref();
2658 let canonical_root = root.canonicalize().map_err(|error| {
2659 if error.kind() == std::io::ErrorKind::NotFound {
2660 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
2661 } else {
2662 error.into()
2663 }
2664 })?;
2665 let durable_root = crate::durable_file::DurableRoot::open(&canonical_root)?;
2666 Self::begin_open_durable(durable_root, lock_timeout_ms)
2667 }
2668
2669 fn begin_open_durable(
2670 durable_root: crate::durable_file::DurableRoot,
2671 lock_timeout_ms: u32,
2672 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2673 let io_root = durable_root.io_path()?;
2674 let current_root = io_root.canonicalize()?;
2675 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
2676 lock.claim_root_identity(&durable_root)?;
2677 lock.durable_root = Some(Arc::new(durable_root));
2678 let io_root = lock
2679 .durable_root
2680 .as_ref()
2681 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2682 .io_path()?;
2683 if lock
2684 .durable_root
2685 .as_ref()
2686 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2687 .open_directory(META_DIR)
2688 .is_err()
2689 {
2690 return Err(MongrelError::NotFound(format!(
2691 "no database metadata found at {:?}",
2692 current_root
2693 )));
2694 }
2695 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
2696 Ok((io_root, lock))
2697 }
2698
2699 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
2701 let (root, lock) = Self::begin_create(root)?;
2702 Self::create_inner(
2703 root,
2704 None,
2705 lock,
2706 crate::storage_mode::StorageMode::Standalone,
2707 )
2708 }
2709
2710 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2713 let (root, lock) = Self::begin_create(root)?;
2714 let salt = crate::encryption::random_salt()?;
2715 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2716 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2717 Self::create_inner(
2718 root,
2719 Some(kek),
2720 lock,
2721 crate::storage_mode::StorageMode::Standalone,
2722 )
2723 }
2724
2725 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2728 let (root, lock) = Self::begin_create(root)?;
2729 let salt = crate::encryption::random_salt()?;
2730 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2731 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2732 Self::create_inner(
2733 root,
2734 Some(kek),
2735 lock,
2736 crate::storage_mode::StorageMode::Standalone,
2737 )
2738 }
2739
2740 pub fn create_with_kms(
2743 root: impl AsRef<Path>,
2744 provider: &dyn crate::security_hardening::KeyManagementProvider,
2745 kms_key_id: &str,
2746 ) -> Result<Self> {
2747 let (root, lock) = Self::begin_create(root)?;
2748 let kek = create_kms_kek(&root, provider, kms_key_id)?;
2749 Self::create_inner(
2750 root,
2751 Some(kek),
2752 lock,
2753 crate::storage_mode::StorageMode::Standalone,
2754 )
2755 }
2756
2757 pub fn create_cluster_replica(
2765 root: impl AsRef<Path>,
2766 cluster_id: mongreldb_types::ids::ClusterId,
2767 node_id: mongreldb_types::ids::NodeId,
2768 database_id: mongreldb_types::ids::DatabaseId,
2769 ) -> Result<Self> {
2770 let (root, lock) = Self::begin_create(root)?;
2771 Self::create_inner(
2772 root,
2773 None,
2774 lock,
2775 crate::storage_mode::StorageMode::ClusterReplica {
2776 cluster_id,
2777 node_id,
2778 database_id,
2779 },
2780 )
2781 }
2782
2783 pub fn open_cluster_replica(
2790 root: impl AsRef<Path>,
2791 expected: &crate::storage_mode::StorageMode,
2792 ) -> Result<Self> {
2793 let Some((cluster_id, node_id, database_id)) = expected.cluster_identity() else {
2794 return Err(MongrelError::InvalidArgument(format!(
2795 "open_cluster_replica requires a ClusterReplica identity, got {expected:?}"
2796 )));
2797 };
2798 let (root, lock) = Self::begin_open(root, 0)?;
2799 Self::open_inner_locked(
2800 root,
2801 None,
2802 lock,
2803 CoreResourceConfig::default(),
2804 OpenModeGate::ClusterRuntime {
2805 cluster_id,
2806 node_id,
2807 database_id,
2808 },
2809 )
2810 }
2811
2812 fn create_inner(
2813 root: PathBuf,
2814 kek: Option<Arc<crate::encryption::Kek>>,
2815 lock: ExclusiveDatabaseLease,
2816 storage_mode: crate::storage_mode::StorageMode,
2817 ) -> Result<Self> {
2818 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2819 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2820 let cat = Catalog::empty();
2821 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2822 Self::finish_open(
2823 root,
2824 cat,
2825 kek,
2826 meta_dek,
2827 false,
2828 None,
2829 None,
2830 None,
2831 lock,
2832 CoreResourceConfig::default(),
2833 OpenModeGate::Create(storage_mode),
2834 )
2835 }
2836
2837 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
2839 Self::open_inner(root, None, None)
2840 }
2841
2842 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2844 let (root, lock) = Self::begin_open(root, 0)?;
2845 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2846 MongrelError::Other("database root descriptor was not pinned".into())
2847 })?)?;
2848 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2849 Self::open_inner_locked(
2850 root,
2851 Some(kek),
2852 lock,
2853 CoreResourceConfig::default(),
2854 OpenModeGate::Normal,
2855 )
2856 }
2857
2858 pub fn open_encrypted_with_options(
2861 root: impl AsRef<Path>,
2862 passphrase: &str,
2863 options: OpenOptions,
2864 ) -> Result<Self> {
2865 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2866 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2867 MongrelError::Other("database root descriptor was not pinned".into())
2868 })?)?;
2869 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2870 let gate = if options.offline_validation {
2871 OpenModeGate::OfflineValidation
2872 } else {
2873 OpenModeGate::Normal
2874 };
2875 Self::open_inner_locked(
2876 root,
2877 Some(kek),
2878 lock,
2879 CoreResourceConfig::from_options(&options)?,
2880 gate,
2881 )
2882 }
2883
2884 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2886 let (root, lock) = Self::begin_open(root, 0)?;
2887 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2888 MongrelError::Other("database root descriptor was not pinned".into())
2889 })?)?;
2890 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2891 Self::open_inner_locked(
2892 root,
2893 Some(kek),
2894 lock,
2895 CoreResourceConfig::default(),
2896 OpenModeGate::Normal,
2897 )
2898 }
2899
2900 pub fn open_with_kms(
2903 root: impl AsRef<Path>,
2904 provider: &dyn crate::security_hardening::KeyManagementProvider,
2905 ) -> Result<Self> {
2906 Self::open_with_kms_and_options(root, provider, OpenOptions::default())
2907 }
2908
2909 pub fn open_with_kms_and_options(
2911 root: impl AsRef<Path>,
2912 provider: &dyn crate::security_hardening::KeyManagementProvider,
2913 options: OpenOptions,
2914 ) -> Result<Self> {
2915 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2916 let kek = open_kms_kek(
2917 lock.durable_root.as_deref().ok_or_else(|| {
2918 MongrelError::Other("database root descriptor was not pinned".into())
2919 })?,
2920 provider,
2921 )?;
2922 let gate = if options.offline_validation {
2923 OpenModeGate::OfflineValidation
2924 } else {
2925 OpenModeGate::Normal
2926 };
2927 Self::open_inner_locked(
2928 root,
2929 Some(kek),
2930 lock,
2931 CoreResourceConfig::from_options(&options)?,
2932 gate,
2933 )
2934 }
2935
2936 pub fn rotate_kms_key(
2940 &self,
2941 provider: &dyn crate::security_hardening::KeyManagementProvider,
2942 new_kms_key_id: &str,
2943 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2944 let _rotation_guard = self.core.kms_rotation_lock.lock();
2945 if new_kms_key_id.is_empty() {
2946 return Err(MongrelError::InvalidArgument(
2947 "KMS key id must not be empty".into(),
2948 ));
2949 }
2950 let durable_root = Arc::clone(&self.core.durable_root);
2951 let root = durable_root.io_path()?;
2952 let envelope = read_kms_key_envelope(&durable_root)?;
2953 if envelope.provider_id != provider.provider_id() {
2954 return Err(MongrelError::Encryption(format!(
2955 "KMS provider mismatch: database requires {:?}, got {:?}",
2956 envelope.provider_id,
2957 provider.provider_id()
2958 )));
2959 }
2960 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2961 if let Some(record) = journal
2962 .load()
2963 .map_err(|error| MongrelError::Encryption(error.to_string()))?
2964 .filter(|record| record.phase != crate::security_hardening::KeyRotationPhase::Succeeded)
2965 {
2966 if record.to_key_id != new_kms_key_id {
2967 return Err(MongrelError::Conflict(format!(
2968 "KMS rotation to {:?} is already in progress",
2969 record.to_key_id
2970 )));
2971 }
2972 return self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record);
2973 }
2974 let now_micros = std::time::SystemTime::now()
2975 .duration_since(std::time::UNIX_EPOCH)
2976 .unwrap_or_default()
2977 .as_micros() as u64;
2978 let mut record = crate::security_hardening::KeyRotationRecord::new(
2979 envelope.wrapped_key.key_version.clone(),
2980 String::new(),
2981 now_micros,
2982 );
2983 record.from_key_id = envelope.wrapped_key.kms_key_id.clone();
2984 record.to_key_id = new_kms_key_id.to_owned();
2985 persist_key_rotation(&journal, &record)?;
2986 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
2987 }
2988
2989 pub fn retry_kms_key_rotation(
2991 &self,
2992 provider: &dyn crate::security_hardening::KeyManagementProvider,
2993 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2994 let _rotation_guard = self.core.kms_rotation_lock.lock();
2995 let durable_root = Arc::clone(&self.core.durable_root);
2996 let root = durable_root.io_path()?;
2997 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2998 let mut record = journal
2999 .load()
3000 .map_err(|error| MongrelError::Encryption(error.to_string()))?
3001 .ok_or_else(|| MongrelError::NotFound("KMS rotation journal".into()))?;
3002 if record.phase != crate::security_hardening::KeyRotationPhase::Failed {
3003 return Err(MongrelError::Conflict(
3004 "KMS rotation is not in Failed state".into(),
3005 ));
3006 }
3007 record.retry();
3008 persist_key_rotation(&journal, &record)?;
3009 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
3010 }
3011
3012 fn run_kms_key_rotation(
3013 &self,
3014 provider: &dyn crate::security_hardening::KeyManagementProvider,
3015 durable_root: &crate::durable_file::DurableRoot,
3016 root: &Path,
3017 journal: &crate::security_hardening::KeyRotationJournal,
3018 mut record: crate::security_hardening::KeyRotationRecord,
3019 ) -> Result<crate::security_hardening::KeyRotationRecord> {
3020 use crate::security_hardening::{KeyRotationPhase, KmsDatabaseKeyEnvelope};
3021 loop {
3022 match record.phase {
3023 KeyRotationPhase::Pending => advance_key_rotation(journal, &mut record)?,
3024 KeyRotationPhase::WrappingNewKey => {
3025 if record.staged_wrapped_key.is_none() {
3026 let current = read_kms_key_envelope(durable_root)?;
3027 let wrapped =
3028 match provider.rewrap_key(¤t.wrapped_key, &record.to_key_id) {
3029 Ok(wrapped) => wrapped,
3030 Err(error) => {
3031 return fail_key_rotation(journal, &mut record, error);
3032 }
3033 };
3034 record.to_version = wrapped.key_version.clone();
3035 record.staged_wrapped_key = Some(wrapped);
3036 persist_key_rotation(journal, &record)?;
3037 }
3038 advance_key_rotation(journal, &mut record)?;
3039 }
3040 KeyRotationPhase::DualRead => {
3041 let old_envelope = read_kms_key_envelope(durable_root)?;
3042 let new_envelope = KmsDatabaseKeyEnvelope {
3043 provider_id: provider.provider_id().to_owned(),
3044 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
3045 MongrelError::Encryption(
3046 "KMS rotation has no staged wrapped key".into(),
3047 )
3048 })?,
3049 };
3050 let old_raw = match unwrap_kms_database_key(provider, &old_envelope) {
3051 Ok(key) => key,
3052 Err(error) => {
3053 return fail_key_rotation(journal, &mut record, error);
3054 }
3055 };
3056 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
3057 Ok(key) => key,
3058 Err(error) => {
3059 return fail_key_rotation(journal, &mut record, error);
3060 }
3061 };
3062 if !bool::from(old_raw.as_slice().ct_eq(new_raw.as_slice())) {
3063 return fail_key_rotation(
3064 journal,
3065 &mut record,
3066 "KMS rewrap changed the database root key",
3067 );
3068 }
3069 advance_key_rotation(journal, &mut record)?;
3070 }
3071 KeyRotationPhase::Reencrypting => {
3072 advance_key_rotation(journal, &mut record)?;
3075 }
3076 KeyRotationPhase::Validating => {
3077 let new_envelope = KmsDatabaseKeyEnvelope {
3078 provider_id: provider.provider_id().to_owned(),
3079 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
3080 MongrelError::Encryption(
3081 "KMS rotation has no staged wrapped key".into(),
3082 )
3083 })?,
3084 };
3085 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
3086 Ok(key) => key,
3087 Err(error) => {
3088 return fail_key_rotation(journal, &mut record, error);
3089 }
3090 };
3091 let salt = read_encryption_salt(durable_root)?;
3092 let candidate = crate::encryption::Kek::from_raw_key(new_raw.as_ref(), &salt)?;
3093 let candidate_meta = candidate.derive_meta_key();
3094 let Some(active_meta) = self.core.meta_dek.as_ref() else {
3095 return fail_key_rotation(
3096 journal,
3097 &mut record,
3098 "KMS metadata exists on a plaintext database",
3099 );
3100 };
3101 if !bool::from(candidate_meta.as_ref().ct_eq(active_meta.as_slice())) {
3102 return fail_key_rotation(
3103 journal,
3104 &mut record,
3105 "KMS rewrap validation failed",
3106 );
3107 }
3108 advance_key_rotation(journal, &mut record)?;
3109 }
3110 KeyRotationPhase::Published => {
3111 let envelope = KmsDatabaseKeyEnvelope {
3112 provider_id: provider.provider_id().to_owned(),
3113 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
3114 MongrelError::Encryption(
3115 "KMS rotation has no staged wrapped key".into(),
3116 )
3117 })?,
3118 };
3119 write_kms_key_envelope(root, &envelope)?;
3120 advance_key_rotation(journal, &mut record)?;
3121 }
3122 KeyRotationPhase::RetiringOldKey => {
3123 advance_key_rotation(journal, &mut record)?;
3124 }
3125 KeyRotationPhase::Succeeded => return Ok(record),
3126 KeyRotationPhase::Failed => {
3127 return Err(MongrelError::Encryption(
3128 "failed KMS rotation must be explicitly retried".into(),
3129 ));
3130 }
3131 }
3132 }
3133 }
3134
3135 pub fn open_with_credentials(
3147 root: impl AsRef<Path>,
3148 username: &str,
3149 password: &str,
3150 ) -> Result<Self> {
3151 Self::open_inner_with_credentials(root, None, username, password)
3152 }
3153
3154 pub fn open_with_credentials_and_options(
3158 root: impl AsRef<Path>,
3159 username: &str,
3160 password: &str,
3161 options: OpenOptions,
3162 ) -> Result<Self> {
3163 let gate = if options.offline_validation {
3164 OpenModeGate::OfflineValidation
3165 } else {
3166 OpenModeGate::Normal
3167 };
3168 Self::open_inner_with_credentials_and_lock_timeout(
3169 root,
3170 None,
3171 username,
3172 password,
3173 options.lock_timeout_ms,
3174 CoreResourceConfig::from_options(&options)?,
3175 gate,
3176 )
3177 }
3178
3179 pub fn open_encrypted_with_credentials(
3182 root: impl AsRef<Path>,
3183 passphrase: &str,
3184 username: &str,
3185 password: &str,
3186 ) -> Result<Self> {
3187 let (root, lock) = Self::begin_open(root, 0)?;
3188 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3189 MongrelError::Other("database root descriptor was not pinned".into())
3190 })?)?;
3191 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3192 Self::open_inner_with_credentials_locked(
3193 root,
3194 Some(kek),
3195 username,
3196 password,
3197 lock,
3198 CoreResourceConfig::default(),
3199 OpenModeGate::Normal,
3200 )
3201 }
3202
3203 pub fn open_encrypted_with_credentials_and_options(
3207 root: impl AsRef<Path>,
3208 passphrase: &str,
3209 username: &str,
3210 password: &str,
3211 options: OpenOptions,
3212 ) -> Result<Self> {
3213 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
3214 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3215 MongrelError::Other("database root descriptor was not pinned".into())
3216 })?)?;
3217 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3218 let gate = if options.offline_validation {
3219 OpenModeGate::OfflineValidation
3220 } else {
3221 OpenModeGate::Normal
3222 };
3223 Self::open_inner_with_credentials_locked(
3224 root,
3225 Some(kek),
3226 username,
3227 password,
3228 lock,
3229 CoreResourceConfig::from_options(&options)?,
3230 gate,
3231 )
3232 }
3233
3234 pub fn open_with_kms_and_credentials(
3236 root: impl AsRef<Path>,
3237 provider: &dyn crate::security_hardening::KeyManagementProvider,
3238 username: &str,
3239 password: &str,
3240 ) -> Result<Self> {
3241 let (root, lock) = Self::begin_open(root, 0)?;
3242 let kek = open_kms_kek(
3243 lock.durable_root.as_deref().ok_or_else(|| {
3244 MongrelError::Other("database root descriptor was not pinned".into())
3245 })?,
3246 provider,
3247 )?;
3248 Self::open_inner_with_credentials_locked(
3249 root,
3250 Some(kek),
3251 username,
3252 password,
3253 lock,
3254 CoreResourceConfig::default(),
3255 OpenModeGate::Normal,
3256 )
3257 }
3258
3259 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
3266 let gate = if options.offline_validation {
3269 OpenModeGate::OfflineValidation
3270 } else {
3271 OpenModeGate::Normal
3272 };
3273 Self::open_inner_with_lock_timeout(
3274 root,
3275 None,
3276 None,
3277 options.lock_timeout_ms,
3278 CoreResourceConfig::from_options(&options)?,
3279 gate,
3280 )
3281 }
3282
3283 fn open_inner_with_lock_timeout(
3284 root: impl AsRef<Path>,
3285 kek: Option<Arc<crate::encryption::Kek>>,
3286 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3287 lock_timeout_ms: u32,
3288 resources: CoreResourceConfig,
3289 mode_gate: OpenModeGate,
3290 ) -> Result<Self> {
3291 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3292 Self::open_inner_locked(root, kek, lock, resources, mode_gate)
3293 }
3294
3295 fn open_inner_locked(
3296 root: PathBuf,
3297 kek: Option<Arc<crate::encryption::Kek>>,
3298 lock: ExclusiveDatabaseLease,
3299 resources: CoreResourceConfig,
3300 mode_gate: OpenModeGate,
3301 ) -> Result<Self> {
3302 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3303 let mut cat = catalog::read_durable(
3304 lock.durable_root.as_deref().ok_or_else(|| {
3305 MongrelError::Other("database root descriptor was not pinned".into())
3306 })?,
3307 meta_dek.as_ref(),
3308 )?
3309 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3310 let recovery_checkpoint = cat.clone();
3311
3312 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3315 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek_limited(
3316 lock.durable_root.as_deref().ok_or_else(|| {
3317 MongrelError::Other("database root descriptor was not pinned".into())
3318 })?,
3319 wal_dek.as_ref(),
3320 resources.wal_replay_limits,
3321 )?;
3322 recover_ddl_from_records(
3323 &root,
3324 Some(lock.durable_root.as_deref().ok_or_else(|| {
3325 MongrelError::Other("database root descriptor was not pinned".into())
3326 })?),
3327 &mut cat,
3328 meta_dek.as_ref(),
3329 false,
3330 None,
3331 &recovery_records,
3332 )?;
3333 Self::finish_open(
3334 root,
3335 cat,
3336 kek,
3337 meta_dek,
3338 true,
3339 Some(recovery_checkpoint),
3340 Some(recovery_records),
3341 None,
3342 lock,
3343 resources,
3344 mode_gate,
3345 )
3346 }
3347
3348 fn open_inner_with_credentials(
3355 root: impl AsRef<Path>,
3356 kek: Option<Arc<crate::encryption::Kek>>,
3357 username: &str,
3358 password: &str,
3359 ) -> Result<Self> {
3360 Self::open_inner_with_credentials_and_lock_timeout(
3361 root,
3362 kek,
3363 username,
3364 password,
3365 0,
3366 CoreResourceConfig::default(),
3367 OpenModeGate::Normal,
3368 )
3369 }
3370
3371 #[allow(clippy::too_many_arguments)]
3375 fn open_inner_with_credentials_and_lock_timeout(
3376 root: impl AsRef<Path>,
3377 kek: Option<Arc<crate::encryption::Kek>>,
3378 username: &str,
3379 password: &str,
3380 lock_timeout_ms: u32,
3381 resources: CoreResourceConfig,
3382 mode_gate: OpenModeGate,
3383 ) -> Result<Self> {
3384 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3385 Self::open_inner_with_credentials_locked(
3386 root, kek, username, password, lock, resources, mode_gate,
3387 )
3388 }
3389
3390 #[allow(clippy::too_many_arguments)]
3391 fn open_inner_with_credentials_locked(
3392 root: PathBuf,
3393 kek: Option<Arc<crate::encryption::Kek>>,
3394 username: &str,
3395 password: &str,
3396 lock: ExclusiveDatabaseLease,
3397 resources: CoreResourceConfig,
3398 mode_gate: OpenModeGate,
3399 ) -> Result<Self> {
3400 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3401 let mut cat = catalog::read_durable(
3402 lock.durable_root.as_deref().ok_or_else(|| {
3403 MongrelError::Other("database root descriptor was not pinned".into())
3404 })?,
3405 meta_dek.as_ref(),
3406 )?
3407 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3408 let recovery_checkpoint = cat.clone();
3409
3410 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3413 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek_limited(
3414 lock.durable_root.as_deref().ok_or_else(|| {
3415 MongrelError::Other("database root descriptor was not pinned".into())
3416 })?,
3417 wal_dek.as_ref(),
3418 resources.wal_replay_limits,
3419 )?;
3420 recover_ddl_from_records(
3421 &root,
3422 Some(lock.durable_root.as_deref().ok_or_else(|| {
3423 MongrelError::Other("database root descriptor was not pinned".into())
3424 })?),
3425 &mut cat,
3426 meta_dek.as_ref(),
3427 false,
3428 None,
3429 &recovery_records,
3430 )?;
3431
3432 if !cat.require_auth {
3435 return Err(MongrelError::AuthNotRequired);
3436 }
3437
3438 let user = cat
3443 .users
3444 .iter()
3445 .find(|u| u.username == username)
3446 .filter(|u| !u.password_hash.is_empty())
3447 .ok_or_else(|| MongrelError::InvalidCredentials {
3448 username: username.to_string(),
3449 })?;
3450 let password_ok = crate::auth::verify_password(password, &user.password_hash)
3451 .map_err(MongrelError::Other)?;
3452 if !password_ok {
3453 return Err(MongrelError::InvalidCredentials {
3454 username: username.to_string(),
3455 });
3456 }
3457
3458 let principal =
3460 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
3461 MongrelError::InvalidCredentials {
3462 username: username.to_string(),
3463 }
3464 })?;
3465
3466 Self::finish_open(
3467 root,
3468 cat,
3469 kek,
3470 meta_dek,
3471 true,
3472 Some(recovery_checkpoint),
3473 Some(recovery_records),
3474 Some(principal),
3475 lock,
3476 resources,
3477 mode_gate,
3478 )
3479 }
3480
3481 pub fn create_with_credentials(
3491 root: impl AsRef<Path>,
3492 admin_username: &str,
3493 admin_password: &str,
3494 ) -> Result<Self> {
3495 let (root, lock) = Self::begin_create(root)?;
3496 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
3497 }
3498
3499 pub fn create_encrypted_with_credentials(
3503 root: impl AsRef<Path>,
3504 passphrase: &str,
3505 admin_username: &str,
3506 admin_password: &str,
3507 ) -> Result<Self> {
3508 let (root, lock) = Self::begin_create(root)?;
3509 let salt = crate::encryption::random_salt()?;
3510 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
3511 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3512 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3513 }
3514
3515 pub fn create_with_kms_and_credentials(
3517 root: impl AsRef<Path>,
3518 provider: &dyn crate::security_hardening::KeyManagementProvider,
3519 kms_key_id: &str,
3520 admin_username: &str,
3521 admin_password: &str,
3522 ) -> Result<Self> {
3523 let (root, lock) = Self::begin_create(root)?;
3524 let kek = create_kms_kek(&root, provider, kms_key_id)?;
3525 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3526 }
3527
3528 fn create_inner_with_credentials(
3529 root: PathBuf,
3530 kek: Option<Arc<crate::encryption::Kek>>,
3531 admin_username: &str,
3532 admin_password: &str,
3533 lock: ExclusiveDatabaseLease,
3534 ) -> Result<Self> {
3535 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
3536 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3537
3538 let password_hash =
3540 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
3541 let scram_sha_256 =
3542 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
3543 let mysql_caching_sha2 =
3544 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
3545 let mut cat = Catalog::empty();
3546 cat.require_auth = true;
3547 cat.next_user_id = 2;
3548 cat.users.push(crate::auth::UserEntry {
3549 id: 1,
3550 username: admin_username.to_string(),
3551 password_hash,
3552 scram_sha_256: Some(scram_sha_256),
3553 mysql_caching_sha2: Some(mysql_caching_sha2),
3554 roles: Vec::new(),
3555 is_admin: true,
3556 created_epoch: 0,
3557 });
3558 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3559
3560 let admin_principal = crate::auth::Principal {
3563 user_id: 1,
3564 created_epoch: 0,
3565 username: admin_username.to_string(),
3566 is_admin: true,
3567 roles: Vec::new(),
3568 permissions: Vec::new(),
3569 };
3570 Self::finish_open(
3571 root,
3572 cat,
3573 kek,
3574 meta_dek,
3575 false,
3576 None,
3577 None,
3578 Some(admin_principal),
3579 lock,
3580 CoreResourceConfig::default(),
3581 OpenModeGate::Create(crate::storage_mode::StorageMode::Standalone),
3582 )
3583 }
3584
3585 fn reject_existing_database(root: &Path) -> Result<()> {
3586 if root.join(catalog::CATALOG_FILENAME).exists() {
3589 return Err(MongrelError::InvalidArgument(format!(
3590 "database already exists at {}; use Database::open() to open it, \
3591 or remove the directory first",
3592 root.display()
3593 )));
3594 }
3595 Ok(())
3596 }
3597
3598 fn open_inner(
3599 root: impl AsRef<Path>,
3600 kek: Option<Arc<crate::encryption::Kek>>,
3601 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3602 ) -> Result<Self> {
3603 Self::open_inner_with_lock_timeout(
3604 root,
3605 kek,
3606 None,
3607 0,
3608 CoreResourceConfig::default(),
3609 OpenModeGate::Normal,
3610 )
3611 }
3612
3613 pub(crate) fn open_offline_validation(root: impl AsRef<Path>) -> Result<Self> {
3616 Self::open_inner_with_lock_timeout(
3617 root,
3618 None,
3619 None,
3620 0,
3621 CoreResourceConfig::default(),
3622 OpenModeGate::OfflineValidation,
3623 )
3624 }
3625
3626 pub(crate) fn open_replica_recovery_durable(
3630 root: &crate::durable_file::DurableRoot,
3631 ) -> Result<Self> {
3632 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3633 Self::open_replica_recovery_inner(root, None, lock)
3634 }
3635
3636 pub(crate) fn open_encrypted_replica_recovery_durable(
3637 root: &crate::durable_file::DurableRoot,
3638 passphrase: &str,
3639 ) -> Result<Self> {
3640 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3641 let salt = read_encryption_salt(root)?;
3642 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3643 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
3644 }
3645
3646 fn open_replica_recovery_inner(
3647 root: PathBuf,
3648 kek: Option<Arc<crate::encryption::Kek>>,
3649 lock: ExclusiveDatabaseLease,
3650 ) -> Result<Self> {
3651 if !root.join(META_DIR).join("replica").is_file() {
3652 return Err(MongrelError::InvalidArgument(
3653 "recovery auth bypass requires a marked replica staging directory".into(),
3654 ));
3655 }
3656 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3657 let mut cat = catalog::read_durable(
3658 lock.durable_root.as_deref().ok_or_else(|| {
3659 MongrelError::Other("database root descriptor was not pinned".into())
3660 })?,
3661 meta_dek.as_ref(),
3662 )?
3663 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3664 let recovery_checkpoint = cat.clone();
3665 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3666 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek_limited(
3667 lock.durable_root.as_deref().ok_or_else(|| {
3668 MongrelError::Other("database root descriptor was not pinned".into())
3669 })?,
3670 wal_dek.as_ref(),
3671 crate::wal::WalReplayLimits::from_env()?,
3672 )?;
3673 recover_ddl_from_records(
3674 &root,
3675 Some(lock.durable_root.as_deref().ok_or_else(|| {
3676 MongrelError::Other("database root descriptor was not pinned".into())
3677 })?),
3678 &mut cat,
3679 meta_dek.as_ref(),
3680 false,
3681 None,
3682 &recovery_records,
3683 )?;
3684 let principal = if cat.require_auth {
3685 cat.users
3686 .iter()
3687 .find(|user| user.is_admin)
3688 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
3689 .ok_or_else(|| {
3690 MongrelError::Schema(
3691 "authenticated replica catalog has no recoverable admin".into(),
3692 )
3693 })?
3694 .into()
3695 } else {
3696 None
3697 };
3698 Self::finish_open(
3699 root,
3700 cat,
3701 kek,
3702 meta_dek,
3703 true,
3704 Some(recovery_checkpoint),
3705 Some(recovery_records),
3706 principal,
3707 lock,
3708 CoreResourceConfig::default(),
3709 OpenModeGate::Normal,
3710 )
3711 }
3712
3713 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
3725 use fs2::FileExt;
3726 if timeout_ms == 0 {
3727 return f.try_lock_exclusive();
3728 }
3729 let deadline =
3731 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
3732 let mut next_sleep = std::time::Duration::from_millis(1);
3733 let mut recorded_wait = false;
3734 loop {
3735 match f.try_lock_exclusive() {
3736 Ok(()) => return Ok(()),
3737 Err(e) if Self::is_file_lock_contended(&e) => {
3738 if !recorded_wait {
3739 DATABASE_OPEN_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
3740 recorded_wait = true;
3741 }
3742 let now = std::time::Instant::now();
3743 if now >= deadline {
3744 return Err(std::io::Error::new(
3745 std::io::ErrorKind::WouldBlock,
3746 format!("could not acquire database lock within {timeout_ms}ms"),
3747 ));
3748 }
3749 let remaining = deadline - now;
3750 let sleep = next_sleep.min(remaining);
3751 std::thread::sleep(sleep);
3752 next_sleep = next_sleep
3755 .saturating_mul(10)
3756 .min(std::time::Duration::from_millis(50));
3757 }
3758 Err(e) => return Err(e),
3759 }
3760 }
3761 }
3762
3763 fn is_file_lock_contended(error: &std::io::Error) -> bool {
3764 if error.kind() == std::io::ErrorKind::WouldBlock {
3765 return true;
3766 }
3767 #[cfg(windows)]
3768 {
3769 return error.raw_os_error() == Some(33);
3772 }
3773 #[cfg(not(windows))]
3774 false
3775 }
3776
3777 #[allow(clippy::too_many_arguments)]
3778 fn finish_open(
3779 root: PathBuf,
3780 cat: Catalog,
3781 kek: Option<Arc<crate::encryption::Kek>>,
3782 meta_dek: Option<[u8; META_DEK_LEN]>,
3783 existing: bool,
3784 recovery_checkpoint: Option<Catalog>,
3785 recovery_records: Option<Vec<crate::wal::Record>>,
3786 principal: Option<crate::auth::Principal>,
3787 lock: ExclusiveDatabaseLease,
3788 resources: CoreResourceConfig,
3789 mode_gate: OpenModeGate,
3790 ) -> Result<Self> {
3791 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
3792 MongrelError::Other("database root descriptor was not pinned".into())
3793 })?);
3794 let storage_mode = if existing {
3799 crate::storage_mode::read(&durable_root)?
3800 } else {
3801 None
3802 };
3803 match (&mode_gate, &storage_mode) {
3804 (OpenModeGate::Create(_), _) => {}
3805 (OpenModeGate::Normal | OpenModeGate::SharedCore, mode) => {
3806 crate::storage_mode::check_open(mode.as_ref(), false)?
3807 }
3808 (OpenModeGate::OfflineValidation, mode) => {
3809 crate::storage_mode::check_open(mode.as_ref(), true)?
3810 }
3811 (
3812 OpenModeGate::ClusterRuntime {
3813 cluster_id,
3814 node_id,
3815 database_id,
3816 },
3817 Some(actual),
3818 ) => {
3819 let expected = crate::storage_mode::StorageMode::ClusterReplica {
3820 cluster_id: *cluster_id,
3821 node_id: *node_id,
3822 database_id: *database_id,
3823 };
3824 if *actual != expected {
3825 return Err(
3826 crate::storage_mode::StorageModeError::IdentityMismatch(format!(
3827 "expected {expected:?}, marker holds {actual:?}"
3828 ))
3829 .into(),
3830 );
3831 }
3832 }
3833 (OpenModeGate::ClusterRuntime { .. }, None) => {
3834 return Err(crate::storage_mode::StorageModeError::IdentityMismatch(
3835 "expected a ClusterReplica marker; directory has none".into(),
3836 )
3837 .into());
3838 }
3839 }
3840 let read_only = match &mode_gate {
3841 OpenModeGate::OfflineValidation | OpenModeGate::ClusterRuntime { .. } => true,
3842 OpenModeGate::Create(mode) => mode.cluster_identity().is_some(),
3845 OpenModeGate::Normal | OpenModeGate::SharedCore => false,
3846 } || if existing {
3847 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
3848 Ok(_) => true,
3849 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
3850 Err(error) => return Err(error.into()),
3851 }
3852 } else {
3853 false
3854 };
3855 let recovered_catalog = cat;
3856 let mut cat = recovered_catalog.clone();
3857 let abandoned = if existing && !read_only {
3858 let abandoned = cat
3859 .tables
3860 .iter()
3861 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
3862 .map(|entry| entry.table_id)
3863 .collect::<Vec<_>>();
3864 for entry in &mut cat.tables {
3865 if abandoned.contains(&entry.table_id) {
3866 entry.state = TableState::Dropped {
3867 at_epoch: cat.db_epoch,
3868 };
3869 }
3870 }
3871 abandoned
3872 } else {
3873 Vec::new()
3874 };
3875 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3876 let recovery_records = match (existing, recovery_records) {
3877 (true, Some(records)) => records,
3878 (true, None) => {
3879 return Err(MongrelError::Other(
3880 "existing open has no validated WAL recovery plan".into(),
3881 ))
3882 }
3883 (false, _) => Vec::new(),
3884 };
3885 let (history_epochs, history_start) =
3886 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
3887 let open_generation = if existing {
3888 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
3889 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3890 })?;
3891 let recovered_table_ids = cat
3892 .tables
3893 .iter()
3894 .filter(|entry| {
3895 checkpoint
3896 .tables
3897 .iter()
3898 .all(|checkpoint| checkpoint.table_id != entry.table_id)
3899 })
3900 .map(|entry| entry.table_id)
3901 .collect::<HashSet<_>>();
3902 let reconciled_table_ids = cat
3903 .tables
3904 .iter()
3905 .filter(|entry| {
3906 checkpoint
3907 .tables
3908 .iter()
3909 .find(|checkpoint| checkpoint.table_id == entry.table_id)
3910 .is_some_and(|checkpoint| {
3911 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
3912 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
3913 })
3914 })
3915 .map(|entry| entry.table_id)
3916 .collect::<HashSet<_>>();
3917 validate_shared_wal_recovery_plan(
3918 &durable_root,
3919 &cat,
3920 &recovered_table_ids,
3921 &reconciled_table_ids,
3922 meta_dek.as_ref(),
3923 kek.clone(),
3924 &recovery_records,
3925 )?;
3926 let retained_generation = recovery_records
3927 .iter()
3928 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
3929 .map(|record| record.txn_id >> 32)
3930 .max()
3931 .unwrap_or(0);
3932 let head_generation =
3933 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
3934 let durable_floor = match head_generation {
3935 Some(head) if retained_generation > head => {
3936 return Err(MongrelError::CorruptWal {
3937 offset: retained_generation,
3938 reason: format!(
3939 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
3940 ),
3941 })
3942 }
3943 Some(head) => head,
3944 None => retained_generation,
3945 };
3946 let stored = catalog::read_generation(&durable_root)?;
3947 if stored.is_some_and(|generation| generation < durable_floor) {
3948 return Err(MongrelError::Other(format!(
3949 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
3950 )));
3951 }
3952 let bumped = stored
3953 .unwrap_or(durable_floor)
3954 .max(durable_floor)
3955 .checked_add(1)
3956 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
3957 if bumped > u32::MAX as u64 {
3958 return Err(MongrelError::Full(
3959 "open-generation namespace exhausted".into(),
3960 ));
3961 }
3962 bumped
3963 } else {
3964 0
3965 };
3966 let principal = if cat.require_auth && !matches!(&mode_gate, OpenModeGate::SharedCore) {
3967 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3968 Some(
3969 Self::resolve_bound_principal_from_catalog(&cat, supplied)
3970 .ok_or(MongrelError::AuthRequired)?,
3971 )
3972 } else {
3973 principal
3974 };
3975 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
3976 if existing {
3977 for entry in &cat.tables {
3978 if !matches!(entry.state, TableState::Live) {
3979 continue;
3980 }
3981 match durable_root
3982 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
3983 {
3984 Ok(root) => {
3985 table_roots.insert(entry.table_id, Arc::new(root));
3986 }
3987 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3988 Err(error) => return Err(error.into()),
3989 }
3990 }
3991 }
3992
3993 if existing {
3997 let mut applied = recovery_checkpoint.ok_or_else(|| {
3998 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3999 })?;
4000 recover_ddl_from_records(
4001 &root,
4002 Some(&durable_root),
4003 &mut applied,
4004 meta_dek.as_ref(),
4005 true,
4006 Some(&table_roots),
4007 &recovery_records,
4008 )?;
4009 let catalog_value = |catalog: &Catalog| {
4010 serde_json::to_value(catalog)
4011 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
4012 };
4013 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
4014 return Err(MongrelError::CorruptWal {
4015 offset: 0,
4016 reason: "validated and applied DDL recovery plans differ".into(),
4017 });
4018 }
4019 if catalog_value(&cat)? != catalog_value(&applied)? {
4020 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
4021 }
4022 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
4023 if !read_only {
4024 sweep_unreferenced_table_dirs(&root, &cat)?;
4025 }
4026 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
4027 Ok(()) => {}
4028 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
4029 Err(error) => return Err(error.into()),
4030 }
4031 }
4032
4033 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
4034 let snapshots = Arc::new(SnapshotRegistry::new());
4035 snapshots.configure_history(history_epochs, history_start);
4036 let memory_governor = crate::memory::MemoryGovernor::new(
4041 crate::memory::GovernorConfig::new(resources.memory_budget_bytes),
4042 )
4043 .map_err(|error| MongrelError::InvalidArgument(format!("memory governor: {error}")))?;
4044 let spill_manager = crate::spill::SpillManager::open(
4048 &durable_root,
4049 crate::spill::SpillConfig::new(resources.temp_disk_budget_bytes),
4050 meta_dek,
4051 )?;
4052 let job_registry = Arc::new(crate::jobs::JobRegistry::open(&root, meta_dek.as_ref())?);
4056 let page_cache = Arc::new(crate::cache::Sharded::new(
4057 crate::cache::CACHE_SHARDS,
4058 || {
4059 crate::cache::PageCache::new(
4060 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
4061 )
4062 .with_governor(
4063 memory_governor.clone(),
4064 crate::memory::MemoryClass::PageCache,
4065 )
4066 },
4067 ));
4068 memory_governor.register_reclaimable(&page_cache);
4069 let decoded_cache = Arc::new(crate::cache::Sharded::new(
4070 crate::cache::CACHE_SHARDS,
4071 || {
4072 crate::cache::DecodedPageCache::new(
4073 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
4074 )
4075 .with_governor(
4076 memory_governor.clone(),
4077 crate::memory::MemoryClass::DecodedCache,
4078 )
4079 },
4080 ));
4081 memory_governor.register_reclaimable(&decoded_cache);
4082 let commit_lock = Arc::new(Mutex::new(()));
4083 let shared_wal = Arc::new(Mutex::new(if existing {
4084 let (_, layout) =
4085 crate::wal::SharedWal::inspect_durable_layout(&durable_root, wal_dek.as_ref())?;
4086 let planned = crate::wal::WalOpenContinuation {
4087 fingerprint: layout.fingerprint(),
4088 last_seq: recovery_records
4089 .last()
4090 .map(|record| record.seq.0)
4091 .unwrap_or(cat.db_epoch),
4092 retained_generation: recovery_records
4093 .iter()
4094 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
4095 .map(|record| record.txn_id >> 32)
4096 .max()
4097 .unwrap_or(0),
4098 };
4099 crate::wal::SharedWal::open_durable_root_validated(
4100 Arc::clone(&durable_root),
4101 Epoch(cat.db_epoch),
4102 wal_dek.clone(),
4103 Some(&planned),
4104 )?
4105 } else {
4106 crate::wal::SharedWal::create_with_durable_root(
4107 Arc::clone(&durable_root),
4108 Epoch(cat.db_epoch),
4109 wal_dek.clone(),
4110 )?
4111 }));
4112 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
4116 let lifecycle = Arc::new(crate::core::LifecycleController::new());
4121 let group = Arc::new(
4122 crate::txn::GroupCommit::new(shared_wal.lock().durable_seq())
4123 .with_lifecycle(Arc::clone(&lifecycle)),
4124 );
4125 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
4126 let txn_ids = Arc::new(Mutex::new(1u64));
4130 let _ = abandoned;
4131 let hlc = Arc::new(mongreldb_types::hlc::HlcClock::new(
4138 0,
4139 std::time::Duration::from_millis(500),
4140 ));
4141 let idempotency = crate::txn::IdempotencyLedger::open(Arc::clone(&durable_root), meta_dek)?;
4144 let standalone_commit_log = Arc::new(crate::commit_log::StandaloneCommitLog::new(
4145 Arc::clone(&shared_wal),
4146 Arc::clone(&group),
4147 Arc::clone(&epoch),
4148 Arc::clone(&commit_lock),
4149 Arc::clone(&txn_ids),
4150 root.clone(),
4151 wal_dek.clone(),
4152 Arc::clone(&hlc),
4153 ));
4154 let commit_log: Arc<dyn mongreldb_log::CommitLog> = standalone_commit_log.clone();
4155
4156 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
4161 let security_coordinator = security_coordinator(&root, cat.security_version);
4162 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> =
4163 if matches!(&mode_gate, OpenModeGate::SharedCore) {
4164 None
4168 } else {
4169 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
4170 auth_state.clone(),
4171 )))
4172 };
4173
4174 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
4180 for entry in &cat.tables {
4181 if !matches!(entry.state, TableState::Live) {
4182 continue;
4183 }
4184 let table_root = match table_roots.remove(&entry.table_id) {
4185 Some(root) => root,
4186 None => Arc::new(
4187 durable_root
4188 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
4189 ),
4190 };
4191 let tdir = table_root.io_path()?;
4192 let ctx = SharedCtx {
4193 root_guard: Some(table_root),
4194 epoch: Arc::clone(&epoch),
4195 page_cache: Arc::clone(&page_cache),
4196 decoded_cache: Arc::clone(&decoded_cache),
4197 snapshots: Arc::clone(&snapshots),
4198 kek: kek.clone(),
4199 commit_lock: Arc::clone(&commit_lock),
4200 shared: Some(crate::engine::SharedWalCtx {
4201 wal: Arc::clone(&shared_wal),
4202 group: Arc::clone(&group),
4203 poisoned: Arc::clone(&poisoned),
4204 txn_ids: Arc::clone(&txn_ids),
4205 change_wake: change_wake.clone(),
4206 lifecycle: Arc::clone(&lifecycle),
4207 hlc: Arc::clone(&hlc),
4208 }),
4209 table_name: Some(entry.name.clone()),
4210 auth: auth_checker.clone(),
4211 read_only,
4212 };
4213 let t = Table::open_in(&tdir, ctx)?;
4214 tables.insert(entry.table_id, TableHandle::new(t));
4215 }
4216
4217 if existing {
4223 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
4224 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
4225 if read_only {
4226 crate::replication::reconcile_replica_epoch_durable(
4227 &durable_root,
4228 epoch.visible().0,
4229 )?;
4230 }
4231 sweep_pending_txn_dirs(&root, &cat);
4234 }
4235
4236 catalog::write_generation(&durable_root, open_generation)?;
4238 match &mode_gate {
4243 OpenModeGate::Create(mode) => crate::storage_mode::write(&durable_root, mode)?,
4244 _ if existing && storage_mode.is_none() => {
4245 crate::storage_mode::write(
4246 &durable_root,
4247 &crate::storage_mode::StorageMode::Standalone,
4248 )?;
4249 }
4250 _ => {}
4251 }
4252 shared_wal.lock().seal_open_generation(open_generation)?;
4253 crate::replication::replication_identity_durable(&durable_root)?;
4254 let next_txn_id = (open_generation << 32) | 1;
4255 *txn_ids.lock() = next_txn_id;
4257 let mut lock = lock;
4258 lock.mark_open()?;
4259
4260 lifecycle.mark_open();
4264 let core = DatabaseCore {
4265 root,
4266 durable_root,
4267 lifecycle,
4268 registry: std::sync::OnceLock::new(),
4269 read_only,
4270 catalog: RwLock::new(cat),
4271 security_coordinator,
4272 security_catalog_disk_reads: AtomicU64::new(0),
4273 rls_cache: Mutex::new(RlsCache::default()),
4274 epoch,
4275 snapshots,
4276 memory_governor,
4277 page_cache,
4278 decoded_cache,
4279 spill_manager,
4280 resource_groups: crate::resource::ResourceGroupRegistry::with_defaults(),
4281 embedding_providers: crate::embedding::EmbeddingProviderRegistry::new(),
4282 service_principals: crate::service_principal::ServicePrincipalStore::new(),
4283 job_registry,
4284 commit_lock,
4285 kms_rotation_lock: Mutex::new(()),
4286 shared_wal,
4287 next_txn_id: txn_ids,
4288 tables: RwLock::new(tables),
4289 kek,
4290 ddl_lock: Mutex::new(()),
4291 meta_dek,
4292 conflicts: crate::txn::ConflictIndex::new(),
4293 active_txns: crate::txn::ActiveTxns::new(),
4294 lock_manager: Arc::new(crate::locks::LockManager::new()),
4295 poisoned,
4296 group,
4297 commit_log: RwLock::new(commit_log),
4298 standalone_commit_log,
4299 hlc,
4300 idempotency,
4301 commit_ts_ledger: Mutex::new(commit_ts_ledger_from_recovery(&recovery_records)),
4302 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
4303 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
4304 replication_barrier: parking_lot::RwLock::new(()),
4305 replication_wal_retention_segments: AtomicUsize::new(0),
4306 backup_pins: Arc::new(Mutex::new(HashMap::new())),
4307 spill_hook: Mutex::new(None),
4308 security_commit_hook: Mutex::new(None),
4309 catalog_commit_hook: Mutex::new(None),
4310 backup_hook: Mutex::new(None),
4311 fk_lock_hook: Mutex::new(None),
4312 replication_hook: Mutex::new(None),
4313 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
4314 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
4315 trigger_max_loop_iterations: AtomicU32::new(
4316 TriggerConfig::default().max_loop_iterations,
4317 ),
4318 notify: {
4319 let (tx, _rx) = tokio::sync::broadcast::channel(256);
4320 tx
4321 },
4322 change_wake,
4323 _lock: Mutex::new(Some(lock)),
4324 };
4325 Ok(Self {
4326 core: Arc::new(core),
4327 principal: RwLock::new(principal),
4328 auth_state,
4329 shared: false,
4330 })
4331 }
4332
4333 pub fn visible_epoch(&self) -> Epoch {
4335 self.epoch.visible()
4336 }
4337
4338 pub fn catalog_version(&self) -> u64 {
4340 self.catalog.read().catalog_version
4341 }
4342
4343 pub fn storage_mode(&self) -> Result<Option<crate::storage_mode::StorageMode>> {
4347 Ok(crate::storage_mode::read(&self.durable_root)?)
4348 }
4349
4350 pub fn memory_governor(&self) -> &crate::memory::MemoryGovernor {
4353 &self.memory_governor
4354 }
4355
4356 pub fn resource_groups(&self) -> &crate::resource::ResourceGroupRegistry {
4359 &self.resource_groups
4360 }
4361
4362 pub fn embedding_providers(&self) -> &crate::embedding::EmbeddingProviderRegistry {
4366 &self.embedding_providers
4367 }
4368
4369 pub fn unregister_embedding_provider(&self, provider_id: &str) -> Result<bool> {
4371 let references = self
4372 .catalog
4373 .read()
4374 .tables
4375 .iter()
4376 .flat_map(|table| &table.schema.columns)
4377 .filter(|column| {
4378 column
4379 .embedding_source
4380 .as_ref()
4381 .and_then(crate::embedding::EmbeddingSource::provider_id)
4382 == Some(provider_id)
4383 })
4384 .count();
4385 self.embedding_providers
4386 .unregister_unreferenced(provider_id, references)
4387 .map_err(embedding_error)
4388 }
4389
4390 fn materialize_generated_embeddings(
4391 &self,
4392 staging: &mut [(u64, crate::txn::Staged)],
4393 control: Option<&crate::ExecutionControl>,
4394 ) -> Result<()> {
4395 let schemas = {
4396 let catalog = self.catalog.read();
4397 catalog
4398 .tables
4399 .iter()
4400 .map(|table| (table.table_id, table.schema.clone()))
4401 .collect::<HashMap<_, _>>()
4402 };
4403 let fallback_control = crate::ExecutionControl::new(None);
4404 let control = control.unwrap_or(&fallback_control);
4405 for (table_id, staged) in &mut *staging {
4406 let (cells, mut update_changed_columns) = match staged {
4407 crate::txn::Staged::Put(cells) => (cells, None),
4408 crate::txn::Staged::Update {
4409 new_row,
4410 changed_columns,
4411 ..
4412 } => (new_row, Some(changed_columns)),
4413 crate::txn::Staged::Delete(_) | crate::txn::Staged::Truncate => continue,
4414 };
4415 let schema = schemas.get(table_id).ok_or_else(|| {
4416 MongrelError::Schema(format!("table id {table_id} disappeared during embedding"))
4417 })?;
4418 for target in &schema.columns {
4419 let Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) =
4420 target.embedding_source.as_ref()
4421 else {
4422 continue;
4423 };
4424 let text = render_embedding_input(schema, spec, cells)?;
4425 let reservation_bytes = text
4426 .len()
4427 .saturating_add((spec.dimension as usize).saturating_mul(4));
4428 let _reservation = self
4429 .memory_governor
4430 .try_reserve(
4431 reservation_bytes as u64,
4432 crate::memory::MemoryClass::AiCandidates,
4433 )
4434 .map_err(|error| MongrelError::ResourceLimitExceeded {
4435 resource: "embedding memory",
4436 requested: reservation_bytes,
4437 limit: match error {
4438 crate::memory::MemoryError::Exhausted { available, .. } => {
4439 available as usize
4440 }
4441 _ => 0,
4442 },
4443 })?;
4444 let source =
4445 crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec: spec.clone() };
4446 let (registry_generation, provider) = self
4447 .embedding_providers
4448 .resolve_with_generation(&source)
4449 .map_err(embedding_error)?;
4450 let semantic_identity = provider.semantic_identity();
4451 self.ensure_ann_semantic_identity_compatible(
4452 *table_id,
4453 target.id,
4454 &semantic_identity,
4455 )?;
4456 let vectors = self
4457 .embedding_providers
4458 .embed_controlled(
4459 &source,
4460 &[text.as_str()],
4461 spec.dimension,
4462 control,
4463 "generated-column-write",
4464 crate::embedding::EmbeddingLimits::default(),
4465 )
4466 .map_err(embedding_error)?;
4467 cells.retain(|(column, _)| *column != target.id);
4468 cells.push((
4469 target.id,
4470 crate::memtable::Value::GeneratedEmbedding(Box::new(
4471 crate::embedding::GeneratedEmbeddingValue {
4472 vector: vectors.into_iter().next().ok_or_else(|| {
4473 MongrelError::Other("embedding provider returned no vector".into())
4474 })?,
4475 metadata: crate::embedding::GeneratedEmbeddingMetadata {
4476 provider_id: provider.provider_id().to_owned(),
4477 model_id: provider.model_id().to_owned(),
4478 model_version: provider.model_version().to_owned(),
4479 preprocessing_version: provider.preprocessing_version().to_owned(),
4480 source_fingerprint: Sha256::digest(text.as_bytes()).into(),
4481 status: crate::embedding::EmbeddingGenerationStatus::Ready,
4482 last_error_category: None,
4483 attempt_count: 1,
4484 semantic_identity,
4485 provider_registry_generation: registry_generation,
4486 },
4487 },
4488 )),
4489 ));
4490 if let Some(changed_columns) = update_changed_columns.as_deref_mut() {
4491 changed_columns.push(target.id);
4492 }
4493 }
4494 if let Some(changed_columns) = update_changed_columns {
4495 changed_columns.sort_unstable();
4496 changed_columns.dedup();
4497 }
4498 }
4499 self.validate_staged_generated_embedding_identities(staging)?;
4500 Ok(())
4501 }
4502
4503 fn ensure_ann_semantic_identity_compatible(
4504 &self,
4505 table_id: u64,
4506 column_id: u16,
4507 identity: &crate::embedding::EmbeddingProviderRef,
4508 ) -> Result<()> {
4509 let tables = self.tables.read();
4510 let Some(handle) = tables.get(&table_id) else {
4511 return Ok(());
4512 };
4513 let table = handle.lock();
4514 let Some(ann) = table.ann_index(column_id) else {
4515 return Ok(());
4516 };
4517 if let Some(existing) = ann.semantic_identity() {
4518 if existing != identity {
4519 return Err(embedding_error(
4520 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
4521 column_id,
4522 expected: existing.fingerprint_sha256(),
4523 got: identity.fingerprint_sha256(),
4524 },
4525 ));
4526 }
4527 }
4528 Ok(())
4529 }
4530
4531 fn validate_staged_generated_embedding_identities(
4532 &self,
4533 staging: &[(u64, crate::txn::Staged)],
4534 ) -> Result<()> {
4535 for (table_id, staged) in staging {
4536 let cells = match staged {
4537 crate::txn::Staged::Put(cells) => cells.as_slice(),
4538 crate::txn::Staged::Update { new_row, .. } => new_row.as_slice(),
4539 crate::txn::Staged::Delete(_) | crate::txn::Staged::Truncate => continue,
4540 };
4541 for (column_id, value) in cells {
4542 let Some(meta) = value.generated_embedding_metadata() else {
4543 continue;
4544 };
4545 self.ensure_ann_semantic_identity_compatible(
4546 *table_id,
4547 *column_id,
4548 &meta.semantic_identity,
4549 )?;
4550 }
4551 }
4552 Ok(())
4553 }
4554
4555 pub fn lock_rows_for_update(
4559 &self,
4560 txn_id: u64,
4561 table_id: u64,
4562 row_ids: &[crate::rowid::RowId],
4563 control: Option<&crate::ExecutionControl>,
4564 ) -> Result<()> {
4565 for &row_id in row_ids {
4566 self.acquire_txn_lock(
4567 txn_id,
4568 crate::locks::LockKey::row(table_id, row_id),
4569 crate::locks::LockMode::Exclusive,
4570 control,
4571 )?;
4572 }
4573 Ok(())
4574 }
4575
4576 pub fn spill_manager(&self) -> &crate::spill::SpillManager {
4580 &self.spill_manager
4581 }
4582
4583 pub fn job_registry(&self) -> &Arc<crate::jobs::JobRegistry> {
4585 &self.job_registry
4586 }
4587
4588 pub fn version_pins_report(&self) -> Vec<TablePinsReport> {
4593 let names: HashMap<u64, String> = self
4594 .catalog
4595 .read()
4596 .tables
4597 .iter()
4598 .map(|entry| (entry.table_id, entry.name.clone()))
4599 .collect();
4600 let handles: Vec<_> = self
4601 .tables
4602 .read()
4603 .iter()
4604 .map(|(table_id, handle)| (*table_id, handle.clone()))
4605 .collect();
4606 handles
4607 .into_iter()
4608 .map(|(table_id, handle)| {
4609 let pins = handle.lock().version_pins_report();
4610 TablePinsReport {
4611 table_id,
4612 table: names.get(&table_id).cloned().unwrap_or_default(),
4613 pins,
4614 }
4615 })
4616 .collect()
4617 }
4618
4619 pub fn hlc_gc_floor(&self) -> crate::epoch::GcFloor {
4627 let mut combined = crate::epoch::GcFloor::ZERO;
4628 if let Some(epoch) = self.snapshots.min_pinned() {
4630 if let Some(ts) = self.commit_ts_for_epoch(epoch) {
4631 combined.transaction_snapshot = ts;
4632 }
4633 }
4634 if let Some(epoch) = self.snapshots.history_floor(self.epoch.visible()) {
4635 if let Some(ts) = self.commit_ts_for_epoch(epoch) {
4636 combined.history_retention = ts;
4637 }
4638 }
4639 let handles: Vec<_> = self.tables.read().values().cloned().collect();
4641 for handle in handles {
4642 let table_floor = handle
4643 .lock()
4644 .hlc_gc_floor(|epoch| self.commit_ts_for_epoch(epoch));
4645 for (label, ts) in table_floor.sources() {
4646 if ts == mongreldb_types::hlc::HlcTimestamp::ZERO {
4647 continue;
4648 }
4649 let slot = match label {
4650 "transaction_snapshot" => &mut combined.transaction_snapshot,
4651 "history_retention" => &mut combined.history_retention,
4652 "backup_pitr" => &mut combined.backup_pitr,
4653 "replication" => &mut combined.replication,
4654 "read_generation" => &mut combined.read_generation,
4655 "online_index_build" => &mut combined.online_index_build,
4656 _ => continue,
4657 };
4658 if *slot == mongreldb_types::hlc::HlcTimestamp::ZERO || ts < *slot {
4659 *slot = ts;
4660 }
4661 }
4662 }
4663 combined
4664 }
4665
4666 pub fn lock_manager(&self) -> &Arc<crate::locks::LockManager> {
4668 &self.lock_manager
4669 }
4670
4671 pub fn release_txn_locks(&self, txn_id: u64) {
4676 self.lock_manager.release_all(txn_id);
4677 }
4678
4679 fn txn_lock_request(
4682 txn_id: u64,
4683 mode: crate::locks::LockMode,
4684 control: Option<&crate::ExecutionControl>,
4685 ) -> crate::locks::LockRequest {
4686 let control = control
4687 .cloned()
4688 .unwrap_or_else(|| crate::ExecutionControl::new(None));
4689 crate::locks::LockRequest::new(txn_id, mode, control)
4690 }
4691
4692 pub(crate) fn acquire_txn_lock(
4696 &self,
4697 txn_id: u64,
4698 key: crate::locks::LockKey,
4699 mode: crate::locks::LockMode,
4700 control: Option<&crate::ExecutionControl>,
4701 ) -> Result<()> {
4702 self.lock_manager
4703 .acquire(key, Self::txn_lock_request(txn_id, mode, control))
4704 .map_err(MongrelError::from)
4705 }
4706
4707 fn acquire_schema_barrier_exclusive(&self) -> Result<TxnLockGuard<'_>> {
4712 let txn_id = self.alloc_txn_id()?;
4713 self.acquire_txn_lock(
4714 txn_id,
4715 crate::locks::LockKey::schema_barrier(),
4716 crate::locks::LockMode::Exclusive,
4717 None,
4718 )?;
4719 Ok(TxnLockGuard {
4720 locks: &self.lock_manager,
4721 txn_id,
4722 })
4723 }
4724
4725 pub fn commit_log(&self) -> Arc<dyn mongreldb_log::CommitLog> {
4730 Arc::clone(&self.commit_log.read())
4731 }
4732
4733 pub fn bind_cluster_commit_log(
4740 &self,
4741 commit_log: Arc<dyn mongreldb_log::CommitLog>,
4742 ) -> Result<()> {
4743 match self.storage_mode()? {
4744 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4745 *self.commit_log.write() = commit_log;
4746 Ok(())
4747 }
4748 mode => Err(MongrelError::InvalidArgument(format!(
4749 "commit-log substitution requires ClusterReplica storage, got {mode:?}"
4750 ))),
4751 }
4752 }
4753
4754 pub fn detach_cluster_commit_log(&self) -> Result<()> {
4760 match self.storage_mode()? {
4761 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4762 let standalone: Arc<dyn mongreldb_log::CommitLog> =
4763 self.standalone_commit_log.clone();
4764 *self.commit_log.write() = standalone;
4765 Ok(())
4766 }
4767 mode => Err(MongrelError::InvalidArgument(format!(
4768 "commit-log detachment requires ClusterReplica storage, got {mode:?}"
4769 ))),
4770 }
4771 }
4772
4773 pub fn hlc(&self) -> &mongreldb_types::hlc::HlcClock {
4779 self.hlc_clock()
4780 }
4781
4782 pub(crate) fn hlc_clock(&self) -> &mongreldb_types::hlc::HlcClock {
4783 &self.hlc
4784 }
4785
4786 pub fn catalog_snapshot(&self) -> Catalog {
4788 self.catalog.read().clone()
4789 }
4790
4791 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
4793 let catalog = self.catalog.read();
4794 match key {
4795 "user_version" => Ok(catalog.user_version),
4796 "application_id" => Ok(catalog.application_id),
4797 _ => Err(MongrelError::InvalidArgument(format!(
4798 "unsupported persistent SQL pragma {key:?}"
4799 ))),
4800 }
4801 }
4802
4803 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
4806 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
4807 }
4808
4809 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
4810 &self,
4811 key: &str,
4812 value: i64,
4813 mut before_commit: F,
4814 ) -> Result<Option<Epoch>>
4815 where
4816 F: FnMut() -> Result<()>,
4817 {
4818 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
4819 }
4820
4821 fn set_sql_pragma_i64_with_epoch_inner(
4822 &self,
4823 key: &str,
4824 value: i64,
4825 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
4826 ) -> Result<Option<Epoch>> {
4827 use crate::wal::DdlOp;
4828
4829 self.require(&crate::auth::Permission::Ddl)?;
4830 if self.read_only {
4831 return Err(MongrelError::ReadOnlyReplica);
4832 }
4833 if self.poisoned.load(Ordering::Relaxed) {
4834 return Err(MongrelError::Other(
4835 "database poisoned by fsync error".into(),
4836 ));
4837 }
4838 let _ddl = self.ddl_lock.lock();
4839 let _security_write = self.security_write()?;
4840 self.require(&crate::auth::Permission::Ddl)?;
4841 let mut next_catalog = self.catalog.read().clone();
4842 let target = match key {
4843 "user_version" => &mut next_catalog.user_version,
4844 "application_id" => &mut next_catalog.application_id,
4845 _ => {
4846 return Err(MongrelError::InvalidArgument(format!(
4847 "unsupported persistent SQL pragma {key:?}"
4848 )))
4849 }
4850 };
4851 if *target == Some(value) {
4852 return Ok(None);
4853 }
4854 *target = Some(value);
4855
4856 let _commit = self.commit_lock.lock();
4857 let epoch = self.epoch.bump_assigned();
4858 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4859 let txn_id = self.alloc_txn_id()?;
4860 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4861 let commit_seq = {
4862 let mut wal = self.shared_wal.lock();
4863 if let Some(before_commit) = before_commit {
4864 before_commit()?;
4865 }
4866 let append: Result<u64> = (|| {
4867 wal.append(
4868 txn_id,
4869 WAL_TABLE_ID,
4870 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
4871 key: key.to_string(),
4872 value,
4873 }),
4874 )?;
4875 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4876 wal.append_commit(txn_id, epoch, &[])
4877 })();
4878 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4879 };
4880 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4881 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4882 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
4883 Ok(Some(epoch))
4884 }
4885
4886 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
4887 self.catalog
4888 .read()
4889 .materialized_views
4890 .iter()
4891 .find(|definition| definition.name == name)
4892 .cloned()
4893 }
4894
4895 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
4896 self.catalog.read().materialized_views.clone()
4897 }
4898
4899 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
4900 self.catalog.read().security.clone()
4901 }
4902
4903 pub fn security_active_for(&self, table: &str) -> bool {
4904 self.catalog.read().security.table_has_security(table)
4905 }
4906
4907 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
4908 if self.catalog.read().security_version == expected_version {
4909 return Ok(());
4910 }
4911 self.security_catalog_disk_reads
4912 .fetch_add(1, Ordering::Relaxed);
4913 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
4914 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
4915 let principal = self.principal.read().clone();
4916 let principal = if fresh.require_auth
4917 && principal
4918 .as_ref()
4919 .is_some_and(|principal| principal.user_id != 0)
4920 {
4921 principal
4922 .as_ref()
4923 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
4924 } else {
4925 principal
4926 };
4927 self.auth_state.set_require_auth(fresh.require_auth);
4928 *self.catalog.write() = fresh;
4929 *self.principal.write() = principal.clone();
4930 self.auth_state.set_principal(principal);
4931 Ok(())
4932 }
4933
4934 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
4935 let guard = self.security_coordinator.gate.write();
4936 let version = self.security_coordinator.version.load(Ordering::Acquire);
4937 self.refresh_security_catalog_if_stale(version)?;
4938 Ok(guard)
4939 }
4940
4941 pub(crate) fn admit_operation(&self) -> Result<crate::core::OperationGuard> {
4968 self.core.operation_guard()
4969 }
4970
4971 fn apply_catalog_command_to(
4977 &self,
4978 next_catalog: &mut Catalog,
4979 command: crate::catalog_cmds::CatalogCommand,
4980 ) -> Result<crate::catalog_cmds::CatalogDelta> {
4981 let record = crate::catalog_cmds::CatalogCommandRecord::next(next_catalog, command);
4982 next_catalog.apply_command(&record)
4983 }
4984
4985 pub fn apply_replicated_records(&self, records: &[crate::wal::Record]) -> Result<bool> {
5003 use crate::wal::Op;
5004 use std::sync::atomic::Ordering;
5005
5006 let _operation = self.admit_operation()?;
5007 if self.poisoned.load(Ordering::Relaxed) {
5008 return Err(MongrelError::Other(
5009 "database poisoned by fsync error".into(),
5010 ));
5011 }
5012 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
5015 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
5016 })?;
5017 if records.iter().any(|record| record.txn_id != txn_id) {
5018 return Err(MongrelError::InvalidArgument(
5019 "replicated transaction payload mixes transaction ids".into(),
5020 ));
5021 }
5022 let commits = records
5023 .iter()
5024 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
5025 .count();
5026 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
5027 return Err(MongrelError::InvalidArgument(
5028 "replicated transaction payload must end in exactly one commit marker".into(),
5029 ));
5030 }
5031 let commit_epoch = match records.last().map(|r| &r.op) {
5032 Some(Op::TxnCommit { epoch, .. }) => *epoch,
5033 _ => unreachable!("validated above"),
5034 };
5035 if let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) {
5044 if !added_runs.is_empty() {
5045 return Err(MongrelError::InvalidArgument(
5046 "replicated spilled-run commits are not appliable: the leader must translate \
5047 the commit through translate_records_for_replication before proposal"
5048 .into(),
5049 ));
5050 }
5051 }
5052 if commit_epoch <= self.epoch.visible().0 {
5056 return Ok(false);
5057 }
5058 {
5063 let mut wal = self.shared_wal.lock();
5064 for record in records {
5065 wal.append(record.txn_id, 0, record.op.clone())?;
5066 }
5067 if let Err(error) = wal.group_sync() {
5068 self.poisoned.store(true, Ordering::Relaxed);
5069 return Err(error);
5070 }
5071 }
5072 let replication_pins: Vec<crate::retention::PinGuard> = {
5075 let tables = self.tables.read();
5076 let floor = Epoch(self.epoch.visible().0);
5077 tables
5078 .values()
5079 .map(|handle| {
5080 let t = handle.lock();
5081 Arc::clone(t.pin_registry())
5082 .pin(crate::retention::PinSource::Replication, floor)
5083 })
5084 .collect()
5085 };
5086 let tables = self.tables.read().clone();
5087 let catalog = self.catalog.read().clone();
5088 recover_shared_wal(&self.durable_root, &tables, &catalog, &self.epoch, records)?;
5089 if let Some(Op::TxnCommit { epoch, .. }) = records.last().map(|r| &r.op) {
5094 let commit_ts = records
5095 .iter()
5096 .rev()
5097 .find_map(|record| match &record.op {
5098 Op::CommitTimestamp { unix_nanos } => {
5099 Some(mongreldb_types::hlc::HlcTimestamp {
5100 physical_micros: unix_nanos / 1_000,
5101 logical: 0,
5102 node_tiebreaker: 0,
5103 })
5104 }
5105 _ => None,
5106 })
5107 .unwrap_or_else(|| {
5108 self.hlc
5109 .now()
5110 .unwrap_or(mongreldb_types::hlc::HlcTimestamp {
5111 physical_micros: 0,
5112 logical: 0,
5113 node_tiebreaker: 0,
5114 })
5115 });
5116 self.record_commit_ts(Epoch(*epoch), commit_ts);
5117 }
5118 drop(replication_pins);
5119 Ok(true)
5120 }
5121
5122 pub fn validate_staged_txn_writes(&self, staged: &[Vec<u8>]) -> Result<()> {
5132 let tables = self.tables.read();
5133 for payload in staged {
5134 match StagedTxnWrite::decode(payload)? {
5135 StagedTxnWrite::Put { table_id, rows } => {
5136 let handle = tables.get(&table_id).ok_or_else(|| {
5137 MongrelError::InvalidArgument(format!(
5138 "staged write targets unmounted table {table_id}"
5139 ))
5140 })?;
5141 let rows: Vec<crate::memtable::Row> =
5142 bincode::deserialize(&rows).map_err(|error| {
5143 MongrelError::InvalidArgument(format!(
5144 "staged put payload for table {table_id} cannot decode: {error}"
5145 ))
5146 })?;
5147 let schema = handle.lock().schema().clone();
5148 for row in &rows {
5149 validate_recovered_row(&schema, row).map_err(|error| {
5150 MongrelError::InvalidArgument(format!(
5151 "staged row for table {table_id} is not appliable: {error}"
5152 ))
5153 })?;
5154 }
5155 }
5156 StagedTxnWrite::Delete { table_id, row_ids } => {
5157 if !tables.contains_key(&table_id) {
5158 return Err(MongrelError::InvalidArgument(format!(
5159 "staged delete targets unmounted table {table_id}"
5160 )));
5161 }
5162 if row_ids.contains(&u64::MAX) {
5163 return Err(MongrelError::InvalidArgument(format!(
5164 "staged delete for table {table_id} names an exhausted row id"
5165 )));
5166 }
5167 }
5168 }
5169 }
5170 Ok(())
5171 }
5172
5173 pub fn apply_staged_txn_writes(
5195 &self,
5196 txn_tag: u64,
5197 staged: &[Vec<u8>],
5198 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5199 ) -> Result<bool> {
5200 use crate::wal::Op;
5201
5202 let mut writes = Vec::with_capacity(staged.len());
5204 for payload in staged {
5205 writes.push(StagedTxnWrite::decode(payload)?);
5206 }
5207 let generation = *self.next_txn_id.lock() >> 32;
5213 let txn_id = (generation << 32) | (txn_tag & 0x7FFF_FFFF) | 0x8000_0000;
5214 let mut records = Vec::with_capacity(writes.len() + 2);
5215 for write in writes {
5216 let op = match write {
5217 StagedTxnWrite::Put { table_id, rows } => {
5218 let decoded: Vec<crate::memtable::Row> =
5225 bincode::deserialize(&rows).map_err(|e| {
5226 MongrelError::Other(format!(
5227 "staged txn put rows could not be decoded: {e}"
5228 ))
5229 })?;
5230 let stamped = bincode::serialize(&decoded).map_err(|e| {
5231 MongrelError::Other(format!("staged txn put rows serialize: {e}"))
5232 })?;
5233 Op::Put {
5234 table_id,
5235 rows: stamped,
5236 }
5237 }
5238 StagedTxnWrite::Delete { table_id, row_ids } => Op::Delete {
5239 table_id,
5240 row_ids: row_ids.into_iter().map(crate::RowId).collect(),
5241 },
5242 };
5243 records.push(crate::wal::Record::new(Epoch(0), txn_id, op));
5244 }
5245 let epoch = self.epoch.visible().0 + 1;
5246 let unix_nanos = commit_ts.physical_micros.saturating_mul(1_000);
5250 records.push(crate::wal::Record::new(
5251 Epoch(0),
5252 txn_id,
5253 Op::CommitTimestamp { unix_nanos },
5254 ));
5255 records.push(crate::wal::Record::new(
5256 Epoch(0),
5257 txn_id,
5258 Op::TxnCommit {
5259 epoch,
5260 added_runs: Vec::new(),
5261 },
5262 ));
5263 let applied = self.apply_replicated_records(&records)?;
5264 if applied && commit_ts != mongreldb_types::hlc::HlcTimestamp::ZERO {
5265 self.record_commit_ts(Epoch(epoch), commit_ts);
5268 }
5269 Ok(applied)
5270 }
5271
5272 pub fn apply_committed_transaction(
5279 &self,
5280 transaction_id: u64,
5281 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5282 operations: &[Vec<u8>],
5283 ) -> Result<bool> {
5284 self.apply_staged_txn_writes(transaction_id, operations, commit_ts)
5285 }
5286
5287 pub fn apply_replicated_catalog_command(
5296 &self,
5297 record: &crate::catalog_cmds::CatalogCommandRecord,
5298 ) -> Result<crate::catalog_cmds::CatalogDelta> {
5299 let _operation = self.admit_operation()?;
5300 let _g = self.ddl_lock.lock();
5301 let mut next_catalog = self.catalog.read().clone();
5302 let delta = next_catalog.apply_command(record)?;
5303 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
5304 return Ok(delta);
5305 }
5306 let referenced_epoch = match &delta {
5312 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => Some(entry.created_epoch),
5313 crate::catalog_cmds::CatalogDelta::TableDropped { at_epoch, .. }
5314 | crate::catalog_cmds::CatalogDelta::TableRenamed { at_epoch, .. } => Some(*at_epoch),
5315 _ => None,
5316 };
5317 if let Some(referenced) = referenced_epoch {
5318 self.epoch.advance_recovered(Epoch(referenced));
5319 next_catalog.db_epoch = next_catalog.db_epoch.max(referenced);
5320 }
5321 match &delta {
5322 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => {
5323 if !self.tables.read().contains_key(&entry.table_id) {
5328 self.mount_catalog_entry(entry)?;
5329 }
5330 }
5331 crate::catalog_cmds::CatalogDelta::TableDropped { table_id, .. } => {
5332 self.tables.write().remove(table_id);
5333 }
5334 _ => {}
5335 }
5336 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref())?;
5340 *self.catalog.write() = next_catalog;
5341 Ok(delta)
5342 }
5343
5344 fn mount_catalog_entry(&self, entry: &crate::catalog::CatalogEntry) -> Result<()> {
5349 let table_relative = Path::new(TABLES_DIR).join(entry.table_id.to_string());
5350 let table_root = Arc::new(
5351 self.durable_root
5352 .create_directory_all_pinned(&table_relative)?,
5353 );
5354 let tdir = table_root.io_path()?;
5355 let ctx = SharedCtx {
5356 root_guard: Some(table_root),
5357 epoch: Arc::clone(&self.epoch),
5358 page_cache: Arc::clone(&self.page_cache),
5359 decoded_cache: Arc::clone(&self.decoded_cache),
5360 snapshots: Arc::clone(&self.snapshots),
5361 kek: self.kek.clone(),
5362 commit_lock: Arc::clone(&self.commit_lock),
5363 shared: Some(crate::engine::SharedWalCtx {
5364 wal: Arc::clone(&self.shared_wal),
5365 group: Arc::clone(&self.group),
5366 poisoned: Arc::clone(&self.poisoned),
5367 txn_ids: Arc::clone(&self.next_txn_id),
5368 change_wake: self.change_wake.clone(),
5369 lifecycle: Arc::clone(&self.lifecycle),
5370 hlc: Arc::clone(&self.hlc),
5371 }),
5372 table_name: Some(entry.name.clone()),
5373 auth: self.table_auth_checker(),
5374 read_only: self.read_only,
5375 };
5376 let table = Table::create_in(&tdir, entry.schema.clone(), entry.table_id, ctx)?;
5377 self.tables
5378 .write()
5379 .insert(entry.table_id, TableHandle::new(table));
5380 Ok(())
5381 }
5382
5383 fn publish_catalog_candidate(
5384 &self,
5385 catalog: Catalog,
5386 epoch: Epoch,
5387 epoch_guard: &mut EpochGuard<'_>,
5388 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5389 ) -> Result<()> {
5390 self.publish_catalog_candidate_with_prelude(
5391 catalog,
5392 epoch,
5393 epoch_guard,
5394 before_publish,
5395 Vec::new(),
5396 )
5397 }
5398
5399 fn publish_catalog_candidate_with_prelude(
5400 &self,
5401 catalog: Catalog,
5402 epoch: Epoch,
5403 epoch_guard: &mut EpochGuard<'_>,
5404 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5405 prelude: Vec<(u64, crate::wal::Op)>,
5406 ) -> Result<()> {
5407 use crate::wal::DdlOp;
5408
5409 if self.read_only {
5410 return Err(MongrelError::ReadOnlyReplica);
5411 }
5412 if self.poisoned.load(Ordering::Relaxed) {
5413 return Err(MongrelError::Other(
5414 "database poisoned by fsync error".into(),
5415 ));
5416 }
5417 let _operation = self.admit_operation()?;
5419 if let Some(before_publish) = before_publish.as_mut() {
5420 (**before_publish)()?;
5421 }
5422 if catalog.db_epoch != epoch.0 {
5423 return Err(MongrelError::InvalidArgument(format!(
5424 "catalog epoch {} does not match commit epoch {}",
5425 catalog.db_epoch, epoch.0
5426 )));
5427 }
5428 {
5429 let current = self.catalog.read();
5430 validate_catalog_transition(¤t, &catalog)?;
5431 }
5432 validate_recovered_catalog(&catalog)?;
5433 let catalog_json = DdlOp::encode_catalog(&catalog)?;
5434 let txn_id = self.alloc_txn_id()?;
5435 let commit_seq = {
5436 let mut wal = self.shared_wal.lock();
5437 let append: Result<u64> = (|| {
5438 for (table_id, op) in prelude {
5439 wal.append(txn_id, table_id, op)?;
5440 }
5441 wal.append(
5442 txn_id,
5443 WAL_TABLE_ID,
5444 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
5445 )?;
5446 wal.append_commit(txn_id, epoch, &[])
5447 })();
5448 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5449 };
5450 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5451 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
5452 self.finish_durable_publish(epoch, epoch_guard, &receipt, checkpoint)
5453 }
5454
5455 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
5459 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
5460 let version = catalog.security_version;
5461 let principal = self.principal.read().clone();
5462 let principal = if catalog.require_auth
5463 && principal
5464 .as_ref()
5465 .is_some_and(|principal| principal.user_id != 0)
5466 {
5467 principal.as_ref().and_then(|principal| {
5468 Self::resolve_bound_principal_from_catalog(&catalog, principal)
5469 })
5470 } else {
5471 principal
5472 };
5473 *self.catalog.write() = catalog;
5474 self.security_coordinator
5475 .version
5476 .store(version, Ordering::Release);
5477 self.auth_state
5478 .set_require_auth(self.catalog.read().require_auth);
5479 *self.principal.write() = principal.clone();
5480 self.auth_state.set_principal(principal);
5481 checkpoint
5482 }
5483
5484 fn finish_durable_publish(
5485 &self,
5486 epoch: Epoch,
5487 epoch_guard: &mut EpochGuard<'_>,
5488 receipt: &mongreldb_log::CommitReceipt,
5489 post_step: Result<()>,
5490 ) -> Result<()> {
5491 if let Err(error) = self.publish_committed(receipt, epoch) {
5492 self.poisoned.store(true, Ordering::Relaxed);
5497 self.lifecycle.poison();
5498 return Err(MongrelError::DurableCommit {
5499 epoch: epoch.0,
5500 message: error.to_string(),
5501 });
5502 }
5503 epoch_guard.disarm();
5504 match post_step {
5505 Ok(()) => Ok(()),
5506 Err(error) => {
5507 self.poisoned.store(true, Ordering::Relaxed);
5508 self.lifecycle.poison();
5509 Err(MongrelError::DurableCommit {
5510 epoch: epoch.0,
5511 message: error.to_string(),
5512 })
5513 }
5514 }
5515 }
5516
5517 fn publish_committed(
5523 &self,
5524 receipt: &mongreldb_log::CommitReceipt,
5525 epoch: Epoch,
5526 ) -> Result<()> {
5527 debug_assert_eq!(
5528 receipt.log_position.index, epoch.0,
5529 "commit receipt position must match the published epoch"
5530 );
5531 mongreldb_fault::inject("commit.publish.before").map_err(crate::commit_log::fault_as_io)?;
5532 self.epoch.publish_in_order(epoch);
5533 mongreldb_fault::inject("commit.publish.after").map_err(crate::commit_log::fault_as_io)?;
5534 Ok(())
5535 }
5536
5537 fn await_durable_commit(
5546 &self,
5547 txn_id: u64,
5548 commit_seq: u64,
5549 epoch: Epoch,
5550 ) -> Result<mongreldb_log::CommitReceipt> {
5551 match self
5552 .standalone_commit_log
5553 .seal_transaction(txn_id, epoch, commit_seq, None)
5554 {
5555 Ok(receipt) => {
5556 self.record_commit_ts(epoch, receipt.commit_ts);
5557 Ok(receipt)
5558 }
5559 Err(error) => {
5560 self.poisoned.store(true, Ordering::Relaxed);
5561 self.lifecycle.poison();
5562 Err(MongrelError::CommitOutcomeUnknown {
5563 epoch: epoch.0,
5564 message: error.to_string(),
5565 })
5566 }
5567 }
5568 }
5569
5570 fn await_durable_commit_with_ts(
5575 &self,
5576 txn_id: u64,
5577 commit_seq: u64,
5578 epoch: Epoch,
5579 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5580 ) -> Result<mongreldb_log::CommitReceipt> {
5581 match self.standalone_commit_log.seal_transaction(
5582 txn_id,
5583 epoch,
5584 commit_seq,
5585 Some(commit_ts),
5586 ) {
5587 Ok(receipt) => {
5588 self.record_commit_ts(epoch, receipt.commit_ts);
5589 Ok(receipt)
5590 }
5591 Err(error) => {
5592 self.poisoned.store(true, Ordering::Relaxed);
5593 self.lifecycle.poison();
5594 Err(MongrelError::CommitOutcomeUnknown {
5595 epoch: epoch.0,
5596 message: error.to_string(),
5597 })
5598 }
5599 }
5600 }
5601
5602 fn record_commit_ts(&self, epoch: Epoch, commit_ts: mongreldb_types::hlc::HlcTimestamp) {
5607 let mut ledger = self.commit_ts_ledger.lock();
5608 ledger.insert(epoch.0, commit_ts);
5609 while ledger.len() > COMMIT_TS_LEDGER_CAP {
5610 ledger.pop_first();
5611 }
5612 }
5613
5614 pub fn commit_ts_for_epoch(&self, epoch: Epoch) -> Option<mongreldb_types::hlc::HlcTimestamp> {
5628 self.commit_ts_ledger.lock().get(&epoch.0).copied()
5629 }
5630
5631 pub fn epoch_at_or_before_commit_ts(
5637 &self,
5638 timestamp: mongreldb_types::hlc::HlcTimestamp,
5639 ) -> Option<Epoch> {
5640 self.commit_ts_ledger
5641 .lock()
5642 .iter()
5643 .rev()
5644 .find_map(|(epoch, commit_ts)| (*commit_ts <= timestamp).then_some(Epoch(*epoch)))
5645 }
5646
5647 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
5648 self.poisoned.store(true, Ordering::Relaxed);
5649 self.lifecycle.poison();
5650 MongrelError::CommitOutcomeUnknown {
5651 epoch: epoch.0,
5652 message: error.to_string(),
5653 }
5654 }
5655
5656 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
5658 self.set_security_catalog_as_with_epoch(security, None)
5659 .map(|_| ())
5660 }
5661
5662 pub fn set_security_catalog_as(
5664 &self,
5665 security: crate::security::SecurityCatalog,
5666 principal: Option<&crate::auth::Principal>,
5667 ) -> Result<()> {
5668 self.set_security_catalog_as_with_epoch(security, principal)
5669 .map(|_| ())
5670 }
5671
5672 pub fn set_security_catalog_as_with_epoch(
5674 &self,
5675 security: crate::security::SecurityCatalog,
5676 principal: Option<&crate::auth::Principal>,
5677 ) -> Result<Epoch> {
5678 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
5679 }
5680
5681 pub fn set_security_catalog_as_with_epoch_controlled<F>(
5684 &self,
5685 security: crate::security::SecurityCatalog,
5686 principal: Option<&crate::auth::Principal>,
5687 mut before_commit: F,
5688 ) -> Result<Epoch>
5689 where
5690 F: FnMut() -> Result<()>,
5691 {
5692 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
5693 }
5694
5695 fn set_security_catalog_as_with_epoch_inner(
5696 &self,
5697 security: crate::security::SecurityCatalog,
5698 principal: Option<&crate::auth::Principal>,
5699 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
5700 ) -> Result<Epoch> {
5701 use crate::wal::DdlOp;
5702 use std::sync::atomic::Ordering;
5703
5704 let command = crate::catalog_cmds::CatalogCommand::SetSecurityCatalog {
5708 security: security.clone(),
5709 };
5710 self.require_for(
5711 principal,
5712 &crate::catalog_cmds::required_permission(&command),
5713 )?;
5714 if self.poisoned.load(Ordering::Relaxed) {
5715 return Err(MongrelError::Other(
5716 "database poisoned by fsync error".into(),
5717 ));
5718 }
5719 let _operation = self.admit_operation()?;
5722 let _ddl = self.ddl_lock.lock();
5723 let _security_write = self.security_write()?;
5726 self.require_for(
5727 principal,
5728 &crate::catalog_cmds::required_permission(&command),
5729 )?;
5730 let mut next_catalog = self.catalog.read().clone();
5731 validate_security_catalog(&next_catalog, &security)?;
5732 let payload = DdlOp::encode_security(&security)?;
5733 let _commit = self.commit_lock.lock();
5734 let epoch = self.epoch.bump_assigned();
5735 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5736 let txn_id = self.alloc_txn_id()?;
5737 self.apply_catalog_command_to(&mut next_catalog, command)?;
5738 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
5739 let commit_seq = {
5740 let mut wal = self.shared_wal.lock();
5741 if let Some(before_commit) = before_commit {
5742 before_commit()?;
5743 }
5744 let append: Result<u64> = (|| {
5745 wal.append(
5746 txn_id,
5747 WAL_TABLE_ID,
5748 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
5749 security_json: payload,
5750 }),
5751 )?;
5752 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
5753 wal.append_commit(txn_id, epoch, &[])
5754 })();
5755 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5756 };
5757 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5758 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
5759 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
5760 Ok(epoch)
5761 }
5762
5763 pub fn require_for(
5764 &self,
5765 principal: Option<&crate::auth::Principal>,
5766 permission: &crate::auth::Permission,
5767 ) -> Result<()> {
5768 let Some(principal) = principal else {
5769 return self.require(permission);
5770 };
5771 let resolved;
5772 let principal = if principal.user_id != 0 {
5773 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5774 .ok_or(MongrelError::AuthRequired)?;
5775 &resolved
5776 } else {
5777 principal
5778 };
5779 #[cfg(test)]
5780 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5781 if principal.has_permission(permission) {
5782 Ok(())
5783 } else {
5784 Err(MongrelError::PermissionDenied {
5785 required: permission.clone(),
5786 principal: principal.username.clone(),
5787 })
5788 }
5789 }
5790
5791 fn require_exact_principal_current(
5795 &self,
5796 principal: Option<&crate::auth::Principal>,
5797 permission: &crate::auth::Permission,
5798 ) -> Result<()> {
5799 let catalog = self.catalog.read();
5800 if !catalog.require_auth {
5801 return Ok(());
5802 }
5803 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
5804 let current = if supplied.user_id == 0 {
5805 supplied.clone()
5806 } else {
5807 Self::resolve_bound_principal_from_catalog(&catalog, supplied)
5808 .ok_or(MongrelError::AuthRequired)?
5809 };
5810 if current.has_permission(permission) {
5811 Ok(())
5812 } else {
5813 Err(MongrelError::PermissionDenied {
5814 required: permission.clone(),
5815 principal: current.username,
5816 })
5817 }
5818 }
5819
5820 pub(crate) fn with_exact_principal_current<T, F>(
5821 &self,
5822 principal: Option<&crate::auth::Principal>,
5823 permission: &crate::auth::Permission,
5824 operation: F,
5825 ) -> Result<T>
5826 where
5827 F: FnOnce() -> Result<T>,
5828 {
5829 let _security = self.security_coordinator.gate.read();
5830 self.require_exact_principal_current(principal, permission)?;
5831 operation()
5832 }
5833
5834 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
5835 self.principal.read().clone()
5836 }
5837
5838 #[cfg(test)]
5839 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
5840 *self.principal.write() = principal.clone();
5841 self.auth_state.set_principal(principal);
5842 }
5843
5844 pub fn require_columns_for(
5845 &self,
5846 table: &str,
5847 operation: crate::auth::ColumnOperation,
5848 column_ids: &[u16],
5849 principal: Option<&crate::auth::Principal>,
5850 ) -> Result<()> {
5851 if principal.is_none() && !self.auth_state.require_auth() {
5852 return Ok(());
5853 }
5854 let cached = self.principal.read().clone();
5855 let principal = principal.or(cached.as_ref());
5856 let Some(principal) = principal else {
5857 let permission = match operation {
5858 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5859 table: table.to_string(),
5860 },
5861 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5862 table: table.to_string(),
5863 },
5864 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5865 table: table.to_string(),
5866 },
5867 };
5868 return self.require(&permission);
5869 };
5870 let catalog = self.catalog.read();
5871 let resolved;
5872 let principal = if principal.user_id != 0 {
5873 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
5874 .ok_or(MongrelError::AuthRequired)?;
5875 &resolved
5876 } else {
5877 principal
5878 };
5879 let schema = &catalog
5880 .live(table)
5881 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5882 .schema;
5883 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
5884 }
5885
5886 fn require_columns_for_principal(
5887 table: &str,
5888 schema: &Schema,
5889 operation: crate::auth::ColumnOperation,
5890 column_ids: &[u16],
5891 principal: &crate::auth::Principal,
5892 ) -> Result<()> {
5893 #[cfg(test)]
5894 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5895 match principal.column_access(table, operation) {
5896 crate::auth::ColumnAccess::All => Ok(()),
5897 crate::auth::ColumnAccess::Columns(allowed) => {
5898 let denied = column_ids.iter().find_map(|column_id| {
5899 schema
5900 .columns
5901 .iter()
5902 .find(|column| column.id == *column_id)
5903 .filter(|column| !allowed.contains(&column.name))
5904 });
5905 if denied.is_none() {
5906 Ok(())
5907 } else {
5908 Err(MongrelError::PermissionDenied {
5909 required: match operation {
5910 crate::auth::ColumnOperation::Select => {
5911 crate::auth::Permission::SelectColumns {
5912 table: table.to_string(),
5913 columns: denied
5914 .into_iter()
5915 .map(|column| column.name.clone())
5916 .collect(),
5917 }
5918 }
5919 crate::auth::ColumnOperation::Insert => {
5920 crate::auth::Permission::InsertColumns {
5921 table: table.to_string(),
5922 columns: denied
5923 .into_iter()
5924 .map(|column| column.name.clone())
5925 .collect(),
5926 }
5927 }
5928 crate::auth::ColumnOperation::Update => {
5929 crate::auth::Permission::UpdateColumns {
5930 table: table.to_string(),
5931 columns: denied
5932 .into_iter()
5933 .map(|column| column.name.clone())
5934 .collect(),
5935 }
5936 }
5937 },
5938 principal: principal.username.clone(),
5939 })
5940 }
5941 }
5942 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5943 required: match operation {
5944 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5945 table: table.to_string(),
5946 },
5947 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5948 table: table.to_string(),
5949 },
5950 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5951 table: table.to_string(),
5952 },
5953 },
5954 principal: principal.username.clone(),
5955 }),
5956 }
5957 }
5958
5959 pub fn select_column_ids_for(
5960 &self,
5961 table: &str,
5962 principal: Option<&crate::auth::Principal>,
5963 ) -> Result<Vec<u16>> {
5964 let catalog = self.catalog.read();
5965 let columns = catalog
5966 .live(table)
5967 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5968 .schema
5969 .columns
5970 .iter()
5971 .map(|column| (column.id, column.name.clone()))
5972 .collect::<Vec<_>>();
5973 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
5974 drop(catalog);
5975 let Some(principal) = principal.as_ref() else {
5976 self.require(&crate::auth::Permission::Select {
5977 table: table.to_string(),
5978 })?;
5979 return Ok(columns.iter().map(|(id, _)| *id).collect());
5980 };
5981 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
5982 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
5983 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
5984 .iter()
5985 .filter(|(_, name)| allowed.contains(name))
5986 .map(|(id, _)| *id)
5987 .collect()),
5988 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5989 required: crate::auth::Permission::Select {
5990 table: table.to_string(),
5991 },
5992 principal: principal.username.clone(),
5993 }),
5994 }
5995 }
5996
5997 pub fn secure_rows_for(
5998 &self,
5999 table: &str,
6000 rows: Vec<crate::memtable::Row>,
6001 principal: Option<&crate::auth::Principal>,
6002 ) -> Result<Vec<crate::memtable::Row>> {
6003 self.secure_rows_for_with_context(table, rows, principal, None)
6004 }
6005
6006 pub fn secure_rows_for_with_context(
6007 &self,
6008 table: &str,
6009 rows: Vec<crate::memtable::Row>,
6010 principal: Option<&crate::auth::Principal>,
6011 context: Option<&crate::query::AiExecutionContext>,
6012 ) -> Result<Vec<crate::memtable::Row>> {
6013 let (security, principal) = {
6014 let catalog = self.catalog.read();
6015 (
6016 catalog.security.clone(),
6017 self.principal_for_authorized_read(&catalog, principal, false)?,
6018 )
6019 };
6020 if !security.table_has_security(table) {
6021 return Ok(rows);
6022 }
6023 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
6024 let mut output = Vec::new();
6025 for mut row in rows {
6026 if let Some(context) = context {
6027 context.consume(1)?;
6028 }
6029 if security.row_allowed(
6030 table,
6031 crate::security::PolicyCommand::Select,
6032 &row,
6033 principal,
6034 false,
6035 ) {
6036 security.apply_masks(table, &mut row, principal);
6037 output.push(row);
6038 }
6039 }
6040 Ok(output)
6041 }
6042
6043 pub fn mask_search_hits_for(
6046 &self,
6047 table: &str,
6048 hits: &mut [crate::query::SearchHit],
6049 principal: Option<&crate::auth::Principal>,
6050 ) -> Result<()> {
6051 let (security, principal) = {
6052 let catalog = self.catalog.read();
6053 (
6054 catalog.security.clone(),
6055 self.principal_for_authorized_read(&catalog, principal, false)?,
6056 )
6057 };
6058 if !security.table_has_security(table) {
6059 return Ok(());
6060 }
6061 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
6062 for hit in hits {
6063 security.apply_masks_to_cells(table, &mut hit.cells, principal);
6064 }
6065 Ok(())
6066 }
6067
6068 pub fn mask_rows_for(
6070 &self,
6071 table: &str,
6072 rows: &mut [crate::memtable::Row],
6073 principal: Option<&crate::auth::Principal>,
6074 ) -> Result<()> {
6075 let (security, principal) = {
6076 let catalog = self.catalog.read();
6077 (
6078 catalog.security.clone(),
6079 self.principal_for_authorized_read(&catalog, principal, false)?,
6080 )
6081 };
6082 if !security.table_has_security(table) {
6083 return Ok(());
6084 }
6085 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
6086 for row in rows {
6087 security.apply_masks(table, row, principal);
6088 }
6089 Ok(())
6090 }
6091
6092 pub fn authorized_candidate_ids_for(
6094 &self,
6095 table: &str,
6096 principal: Option<&crate::auth::Principal>,
6097 ) -> Result<Option<std::collections::HashSet<RowId>>> {
6098 Ok(self
6099 .authorized_read_snapshot(table, principal)?
6100 .allowed_row_ids)
6101 }
6102
6103 fn allowed_row_ids_locked(
6104 &self,
6105 table_name: &str,
6106 table: &Table,
6107 table_snapshot: Snapshot,
6108 security_state: (&crate::security::SecurityCatalog, u64),
6109 principal: Option<&crate::auth::Principal>,
6110 context: Option<&crate::query::AiExecutionContext>,
6111 ) -> Result<Option<Arc<HashSet<RowId>>>> {
6112 let (security, security_version) = security_state;
6113 if !security.rls_enabled(table_name) {
6114 return Ok(None);
6115 }
6116 let authorization_started = std::time::Instant::now();
6117 let principal = principal.ok_or(MongrelError::AuthRequired)?;
6118 let mut roles = principal.roles.clone();
6119 roles.sort_unstable();
6120 let principal_key = format!(
6121 "{}:{}:{}:{}:{roles:?}",
6122 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
6123 );
6124 let cache_key = (
6125 table_name.to_string(),
6126 table.data_generation(),
6127 security_version,
6128 principal_key,
6129 );
6130 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
6131 crate::trace::QueryTrace::record(|trace| {
6132 trace.rls_cache_hit = true;
6133 trace.authorization_nanos = trace
6134 .authorization_nanos
6135 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
6136 });
6137 return Ok(Some(allowed));
6138 }
6139 if let Some(context) = context {
6140 context.checkpoint()?;
6141 }
6142 let started = std::time::Instant::now();
6144 let rows = table.visible_rows(table_snapshot)?;
6145 let rows_evaluated = rows.len() as u64;
6146 let mut allowed = HashSet::new();
6147 for chunk in rows.chunks(256) {
6148 if let Some(context) = context {
6149 context.consume(chunk.len())?;
6150 }
6151 allowed.extend(chunk.iter().filter_map(|row| {
6152 security
6153 .row_allowed(
6154 table_name,
6155 crate::security::PolicyCommand::Select,
6156 row,
6157 principal,
6158 false,
6159 )
6160 .then_some(row.row_id)
6161 }));
6162 }
6163 let allowed = Arc::new(allowed);
6164 let mut cache = self.rls_cache.lock();
6165 cache.build_nanos = cache
6166 .build_nanos
6167 .saturating_add(started.elapsed().as_nanos() as u64);
6168 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
6169 cache.insert(cache_key, Arc::clone(&allowed));
6170 crate::trace::QueryTrace::record(|trace| {
6171 trace.rls_rows_evaluated = trace
6172 .rls_rows_evaluated
6173 .saturating_add(rows_evaluated as usize);
6174 trace.authorization_nanos = trace
6175 .authorization_nanos
6176 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
6177 });
6178 Ok(Some(allowed))
6179 }
6180
6181 fn principal_for_authorized_read(
6182 &self,
6183 catalog: &Catalog,
6184 principal: Option<&crate::auth::Principal>,
6185 catalog_bound: bool,
6186 ) -> Result<Option<crate::auth::Principal>> {
6187 let principal = principal.cloned().or_else(|| self.principal.read().clone());
6188 let Some(principal) = principal else {
6189 return Ok(None);
6190 };
6191 if catalog_bound || principal.user_id != 0 {
6192 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
6193 .map(Some)
6194 .ok_or(MongrelError::AuthRequired);
6195 }
6196 Ok(Some(principal))
6197 }
6198
6199 pub fn with_authorized_read<T, F>(
6203 &self,
6204 table_name: &str,
6205 principal: Option<&crate::auth::Principal>,
6206 catalog_bound: bool,
6207 read: F,
6208 ) -> Result<T>
6209 where
6210 F: FnMut(
6211 &mut Table,
6212 Snapshot,
6213 Option<&HashSet<RowId>>,
6214 Option<&crate::auth::Principal>,
6215 ) -> Result<T>,
6216 {
6217 self.with_authorized_read_context(
6218 table_name,
6219 principal,
6220 catalog_bound,
6221 None,
6222 None,
6223 None,
6224 read,
6225 )
6226 }
6227
6228 #[allow(clippy::too_many_arguments)]
6229 pub fn with_authorized_read_context<T, F>(
6230 &self,
6231 table_name: &str,
6232 principal: Option<&crate::auth::Principal>,
6233 catalog_bound: bool,
6234 authorization: Option<&ReadAuthorization>,
6235 context: Option<&crate::query::AiExecutionContext>,
6236 snapshot_override: Option<Snapshot>,
6237 read: F,
6238 ) -> Result<T>
6239 where
6240 F: FnMut(
6241 &mut Table,
6242 Snapshot,
6243 Option<&HashSet<RowId>>,
6244 Option<&crate::auth::Principal>,
6245 ) -> Result<T>,
6246 {
6247 self.with_authorized_read_context_stamped(
6248 table_name,
6249 principal,
6250 catalog_bound,
6251 authorization,
6252 context,
6253 snapshot_override,
6254 read,
6255 )
6256 .map(|(result, _)| result)
6257 }
6258
6259 #[allow(clippy::too_many_arguments)]
6260 pub fn with_authorized_read_context_stamped<T, F>(
6261 &self,
6262 table_name: &str,
6263 principal: Option<&crate::auth::Principal>,
6264 catalog_bound: bool,
6265 authorization: Option<&ReadAuthorization>,
6266 context: Option<&crate::query::AiExecutionContext>,
6267 snapshot_override: Option<Snapshot>,
6268 mut read: F,
6269 ) -> Result<(T, AuthorizedReadStamp)>
6270 where
6271 F: FnMut(
6272 &mut Table,
6273 Snapshot,
6274 Option<&HashSet<RowId>>,
6275 Option<&crate::auth::Principal>,
6276 ) -> Result<T>,
6277 {
6278 if principal.is_none() && self.principal.read().is_some() {
6279 self.refresh_principal()?;
6280 }
6281 const RETRIES: usize = 3;
6282 let handle = self.table(table_name)?;
6283 for attempt in 0..RETRIES {
6284 crate::trace::QueryTrace::record(|trace| {
6285 trace.authorization_retries = attempt;
6286 });
6287 let (security, security_version, effective_principal) = {
6288 let catalog = self.catalog.read();
6289 (
6290 catalog.security.clone(),
6291 catalog.security_version,
6292 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6293 )
6294 };
6295 if let Some(authorization) = authorization {
6296 for permission in &authorization.permissions {
6297 self.require_for(effective_principal.as_ref(), permission)?;
6298 }
6299 self.require_columns_for(
6300 table_name,
6301 authorization.operation,
6302 &authorization.columns,
6303 effective_principal.as_ref(),
6304 )?;
6305 }
6306 let result = {
6307 let mut table = lock_table_with_context(&handle, context)?;
6308 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
6309 let allowed = self.allowed_row_ids_locked(
6310 table_name,
6311 &table,
6312 snapshot,
6313 (&security, security_version),
6314 effective_principal.as_ref(),
6315 context,
6316 )?;
6317 let stamp = AuthorizedReadStamp {
6318 table_id: table.table_id(),
6319 schema_id: table.schema().schema_id,
6320 data_generation: table.data_generation(),
6321 security_version,
6322 snapshot,
6323 };
6324 let result = read(
6325 &mut table,
6326 snapshot,
6327 allowed.as_deref(),
6328 effective_principal.as_ref(),
6329 )?;
6330 (result, stamp)
6331 };
6332 if let Some(context) = context {
6333 context.checkpoint()?;
6334 }
6335 if self.catalog.read().security_version == security_version {
6336 return Ok(result);
6337 }
6338 if attempt + 1 == RETRIES {
6339 return Err(MongrelError::Conflict(
6340 "security policy changed during scored read".into(),
6341 ));
6342 }
6343 }
6344 Err(MongrelError::Conflict(
6345 "authorization retry loop exhausted".into(),
6346 ))
6347 }
6348
6349 fn with_authorized_aggregate_table<T, F>(
6350 &self,
6351 table_name: &str,
6352 columns: &[u16],
6353 principal: Option<&crate::auth::Principal>,
6354 catalog_bound: bool,
6355 allow_table_security: bool,
6356 mut aggregate: F,
6357 ) -> Result<T>
6358 where
6359 F: FnMut(
6360 &mut Table,
6361 Option<&crate::security::CandidateAuthorization<'_>>,
6362 Option<&crate::auth::Principal>,
6363 u64,
6364 ) -> Result<T>,
6365 {
6366 if principal.is_none() && self.principal.read().is_some() {
6367 self.refresh_principal()?;
6368 }
6369 const RETRIES: usize = 3;
6370 let handle = self.table(table_name)?;
6371 for attempt in 0..RETRIES {
6372 let (security, security_version, effective_principal) = {
6373 let catalog = self.catalog.read();
6374 (
6375 catalog.security.clone(),
6376 catalog.security_version,
6377 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6378 )
6379 };
6380 self.require_columns_for(
6381 table_name,
6382 crate::auth::ColumnOperation::Select,
6383 columns,
6384 effective_principal.as_ref(),
6385 )?;
6386 if !allow_table_security && security.table_has_security(table_name) {
6387 return Err(MongrelError::InvalidArgument(
6388 "incremental aggregate is unsupported while RLS or column masks are active"
6389 .into(),
6390 ));
6391 }
6392 let result = {
6393 let mut table = handle.lock();
6394 let authorization = if security.rls_enabled(table_name) {
6395 Some(crate::security::CandidateAuthorization {
6396 table: table_name,
6397 security: &security,
6398 principal: effective_principal
6399 .as_ref()
6400 .ok_or(MongrelError::AuthRequired)?,
6401 })
6402 } else {
6403 None
6404 };
6405 aggregate(
6406 &mut table,
6407 authorization.as_ref(),
6408 effective_principal.as_ref(),
6409 security_version,
6410 )?
6411 };
6412 if self.catalog.read().security_version == security_version {
6413 return Ok(result);
6414 }
6415 if attempt + 1 == RETRIES {
6416 return Err(MongrelError::Conflict(
6417 "security policy changed during aggregate read".into(),
6418 ));
6419 }
6420 }
6421 Err(MongrelError::Conflict(
6422 "aggregate authorization retry loop exhausted".into(),
6423 ))
6424 }
6425
6426 pub fn with_authorized_scored_read_context<T, F>(
6430 &self,
6431 table_name: &str,
6432 principal: Option<&crate::auth::Principal>,
6433 catalog_bound: bool,
6434 authorization: Option<&ReadAuthorization>,
6435 context: Option<&crate::query::AiExecutionContext>,
6436 mut read: F,
6437 ) -> Result<T>
6438 where
6439 F: FnMut(
6440 &mut Table,
6441 Snapshot,
6442 Option<&crate::security::CandidateAuthorization<'_>>,
6443 Option<&crate::auth::Principal>,
6444 ) -> Result<T>,
6445 {
6446 self.with_authorized_scored_read_context_at(
6447 table_name,
6448 principal,
6449 catalog_bound,
6450 authorization,
6451 context,
6452 None,
6453 |table, snapshot, authorization, principal| {
6454 let mut table = table.clone();
6455 read(&mut table, snapshot, authorization, principal)
6456 },
6457 )
6458 }
6459
6460 #[allow(clippy::too_many_arguments)]
6461 pub fn with_authorized_scored_read_context_at<T, F>(
6462 &self,
6463 table_name: &str,
6464 principal: Option<&crate::auth::Principal>,
6465 catalog_bound: bool,
6466 authorization: Option<&ReadAuthorization>,
6467 context: Option<&crate::query::AiExecutionContext>,
6468 snapshot_override: Option<Snapshot>,
6469 read: F,
6470 ) -> Result<T>
6471 where
6472 F: FnMut(
6473 &Table,
6474 Snapshot,
6475 Option<&crate::security::CandidateAuthorization<'_>>,
6476 Option<&crate::auth::Principal>,
6477 ) -> Result<T>,
6478 {
6479 self.with_authorized_scored_read_context_at_stamped(
6480 table_name,
6481 principal,
6482 catalog_bound,
6483 authorization,
6484 context,
6485 snapshot_override,
6486 read,
6487 )
6488 .map(|(result, _)| result)
6489 }
6490
6491 #[allow(clippy::too_many_arguments)]
6492 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
6493 &self,
6494 table_name: &str,
6495 principal: Option<&crate::auth::Principal>,
6496 catalog_bound: bool,
6497 authorization: Option<&ReadAuthorization>,
6498 context: Option<&crate::query::AiExecutionContext>,
6499 snapshot_override: Option<Snapshot>,
6500 mut read: F,
6501 ) -> Result<(T, AuthorizedReadStamp)>
6502 where
6503 F: FnMut(
6504 &Table,
6505 Snapshot,
6506 Option<&crate::security::CandidateAuthorization<'_>>,
6507 Option<&crate::auth::Principal>,
6508 ) -> Result<T>,
6509 {
6510 if principal.is_none() && self.principal.read().is_some() {
6511 self.refresh_principal()?;
6512 }
6513 const RETRIES: usize = 3;
6514 let handle = self.table(table_name)?;
6515 for attempt in 0..RETRIES {
6516 if let Some(context) = context {
6517 context.checkpoint()?;
6518 }
6519 crate::trace::QueryTrace::record(|trace| {
6520 trace.authorization_retries = attempt;
6521 });
6522 let (security, security_version, effective_principal) = {
6523 let catalog = self.catalog.read();
6524 (
6525 catalog.security.clone(),
6526 catalog.security_version,
6527 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6528 )
6529 };
6530 if let Some(authorization) = authorization {
6531 for permission in &authorization.permissions {
6532 self.require_for(effective_principal.as_ref(), permission)?;
6533 }
6534 self.require_columns_for(
6535 table_name,
6536 authorization.operation,
6537 &authorization.columns,
6538 effective_principal.as_ref(),
6539 )?;
6540 }
6541 let result = {
6542 let (table, snapshot, _snapshot_guard, _run_pins) =
6543 self.scored_read_generation(&handle, context, snapshot_override)?;
6544 let candidate_authorization = if security.rls_enabled(table_name) {
6545 Some(crate::security::CandidateAuthorization {
6546 table: table_name,
6547 security: &security,
6548 principal: effective_principal
6549 .as_ref()
6550 .ok_or(MongrelError::AuthRequired)?,
6551 })
6552 } else {
6553 None
6554 };
6555 let stamp = AuthorizedReadStamp {
6556 table_id: table.table_id(),
6557 schema_id: table.schema().schema_id,
6558 data_generation: table.data_generation(),
6559 security_version,
6560 snapshot,
6561 };
6562 let result = read(
6563 table.as_ref(),
6564 snapshot,
6565 candidate_authorization.as_ref(),
6566 effective_principal.as_ref(),
6567 )?;
6568 (result, stamp)
6569 };
6570 if let Some(context) = context {
6571 context.checkpoint()?;
6572 }
6573 if self.catalog.read().security_version == security_version {
6574 return Ok(result);
6575 }
6576 if attempt + 1 == RETRIES {
6577 return Err(MongrelError::Conflict(
6578 "security policy changed during scored read".into(),
6579 ));
6580 }
6581 }
6582 Err(MongrelError::Conflict(
6583 "scored-read authorization retry loop exhausted".into(),
6584 ))
6585 }
6586
6587 fn scored_read_generation(
6588 &self,
6589 handle: &TableHandle,
6590 context: Option<&crate::query::AiExecutionContext>,
6591 snapshot_override: Option<Snapshot>,
6592 ) -> Result<(
6593 Arc<TableReadGeneration>,
6594 Snapshot,
6595 crate::retention::OwnedSnapshotGuard,
6596 RunPins,
6597 )> {
6598 let mut table = if let Some(context) = context {
6599 loop {
6600 context.checkpoint()?;
6601 let wait = context
6602 .remaining_duration()
6603 .unwrap_or(std::time::Duration::from_millis(5))
6604 .min(std::time::Duration::from_millis(5));
6605 if let Some(table) = handle.try_lock_for(wait) {
6606 break table;
6607 }
6608 }
6609 } else {
6610 handle.lock()
6611 };
6612 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
6613 self.snapshot_at_owned(snapshot.epoch)?
6614 } else {
6615 let snapshot = table.snapshot();
6616 let guard = self.snapshots.register_owned(snapshot.epoch);
6617 (snapshot, guard)
6618 };
6619 let table_id = table.table_id();
6620 let run_keys: Vec<_> = table
6621 .active_run_ids()
6622 .map(|run_id| (table_id, run_id))
6623 .collect();
6624 let generation = handle
6625 .generation_metrics
6626 .activate(table.clone_read_generation()?);
6627 let run_pins = self.pin_runs(&run_keys);
6628 Ok((generation, snapshot, snapshot_guard, run_pins))
6629 }
6630
6631 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
6632 let mut pins = self.backup_pins.lock();
6633 for run in runs {
6634 *pins.entry(*run).or_insert(0) += 1;
6635 }
6636 drop(pins);
6637 RunPins {
6638 pins: Arc::clone(&self.backup_pins),
6639 runs: runs.to_vec(),
6640 }
6641 }
6642
6643 pub fn query_for_current_principal(
6647 &self,
6648 table_name: &str,
6649 query: &crate::query::Query,
6650 projection: Option<&[u16]>,
6651 ) -> Result<Vec<crate::memtable::Row>> {
6652 let condition_columns = crate::query::condition_columns(&query.conditions);
6653 let catalog_bound = self
6654 .principal
6655 .read()
6656 .as_ref()
6657 .is_some_and(|principal| principal.user_id != 0);
6658 self.with_authorized_read(
6659 table_name,
6660 None,
6661 catalog_bound,
6662 |table, snapshot, allowed, principal| {
6663 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6664 self.require_columns_for(
6665 table_name,
6666 crate::auth::ColumnOperation::Select,
6667 &condition_columns,
6668 principal,
6669 )?;
6670 if let Some(projection) = projection {
6671 self.require_columns_for(
6672 table_name,
6673 crate::auth::ColumnOperation::Select,
6674 projection,
6675 principal,
6676 )?;
6677 }
6678 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
6679 let projection =
6680 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6681 for row in &mut rows {
6682 row.columns.retain(|column, _| {
6683 allowed_columns.contains(column)
6684 && projection
6685 .as_ref()
6686 .is_none_or(|projection| projection.contains(column))
6687 });
6688 }
6689 self.secure_rows_for(table_name, rows, principal)
6690 },
6691 )
6692 }
6693
6694 pub fn query_for_current_principal_controlled(
6698 &self,
6699 table_name: &str,
6700 query: &crate::query::Query,
6701 projection: Option<&[u16]>,
6702 control: &crate::ExecutionControl,
6703 ) -> Result<Vec<crate::memtable::Row>> {
6704 let catalog_bound = self
6705 .principal
6706 .read()
6707 .as_ref()
6708 .is_some_and(|principal| principal.user_id != 0);
6709 self.query_for_principal_controlled(
6710 table_name,
6711 query,
6712 projection,
6713 None,
6714 catalog_bound,
6715 control,
6716 )
6717 }
6718
6719 pub fn query_as_principal_controlled(
6723 &self,
6724 table_name: &str,
6725 query: &crate::query::Query,
6726 projection: Option<&[u16]>,
6727 principal: Option<&crate::auth::Principal>,
6728 control: &crate::ExecutionControl,
6729 ) -> Result<Vec<crate::memtable::Row>> {
6730 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6731 self.query_for_principal_controlled(
6732 table_name,
6733 query,
6734 projection,
6735 principal,
6736 catalog_bound,
6737 control,
6738 )
6739 }
6740
6741 fn query_for_principal_controlled(
6742 &self,
6743 table_name: &str,
6744 query: &crate::query::Query,
6745 projection: Option<&[u16]>,
6746 principal: Option<&crate::auth::Principal>,
6747 catalog_bound: bool,
6748 control: &crate::ExecutionControl,
6749 ) -> Result<Vec<crate::memtable::Row>> {
6750 control.checkpoint()?;
6751 let context = crate::query::AiExecutionContext::with_control(
6752 control.clone(),
6753 usize::MAX,
6754 crate::query::MAX_FUSED_CANDIDATES,
6755 );
6756 let condition_columns = crate::query::condition_columns(&query.conditions);
6757 self.with_authorized_read_context(
6758 table_name,
6759 principal,
6760 catalog_bound,
6761 None,
6762 Some(&context),
6763 None,
6764 |table, snapshot, allowed, principal| {
6765 control.checkpoint()?;
6766 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6767 self.require_columns_for(
6768 table_name,
6769 crate::auth::ColumnOperation::Select,
6770 &condition_columns,
6771 principal,
6772 )?;
6773 if let Some(projection) = projection {
6774 self.require_columns_for(
6775 table_name,
6776 crate::auth::ColumnOperation::Select,
6777 projection,
6778 principal,
6779 )?;
6780 }
6781 let rows =
6782 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
6783 let projection =
6784 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6785 let mut projected = Vec::with_capacity(rows.len());
6786 for (index, mut row) in rows.into_iter().enumerate() {
6787 if index & 255 == 0 {
6788 control.checkpoint()?;
6789 }
6790 row.columns.retain(|column, _| {
6791 allowed_columns.contains(column)
6792 && projection
6793 .as_ref()
6794 .is_none_or(|projection| projection.contains(column))
6795 });
6796 projected.push(row);
6797 }
6798 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
6799 },
6800 )
6801 }
6802
6803 pub fn approx_aggregate_for_current_principal(
6806 &self,
6807 table_name: &str,
6808 conditions: &[crate::query::Condition],
6809 column: Option<u16>,
6810 agg: crate::engine::ApproxAgg,
6811 z: f64,
6812 ) -> Result<Option<crate::engine::ApproxResult>> {
6813 if !z.is_finite() || z <= 0.0 {
6814 return Err(MongrelError::InvalidArgument(
6815 "z must be finite and > 0".into(),
6816 ));
6817 }
6818 let mut columns = crate::query::condition_columns(conditions);
6819 columns.extend(column);
6820 columns.sort_unstable();
6821 columns.dedup();
6822 self.with_authorized_aggregate_table(
6823 table_name,
6824 &columns,
6825 None,
6826 true,
6827 true,
6828 |table, authorization, _, _| {
6829 table.approx_aggregate_with_candidate_authorization(
6830 conditions,
6831 column,
6832 agg,
6833 z,
6834 authorization,
6835 )
6836 },
6837 )
6838 }
6839
6840 pub fn incremental_aggregate_for_current_principal(
6844 &self,
6845 table_name: &str,
6846 conditions: &[crate::query::Condition],
6847 column: Option<u16>,
6848 agg: crate::engine::NativeAgg,
6849 ) -> Result<crate::engine::IncrementalAggResult> {
6850 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
6851 }
6852
6853 pub fn incremental_aggregate_for_principal(
6857 &self,
6858 table_name: &str,
6859 conditions: &[crate::query::Condition],
6860 column: Option<u16>,
6861 agg: crate::engine::NativeAgg,
6862 principal: Option<&crate::auth::Principal>,
6863 catalog_bound: bool,
6864 ) -> Result<crate::engine::IncrementalAggResult> {
6865 let mut columns = crate::query::condition_columns(conditions);
6866 columns.extend(column);
6867 columns.sort_unstable();
6868 columns.dedup();
6869 self.with_authorized_aggregate_table(
6870 table_name,
6871 &columns,
6872 principal,
6873 catalog_bound,
6874 false,
6875 |table, _, principal, security_version| {
6876 let cache_key = incremental_aggregate_cache_key(
6877 table_name,
6878 conditions,
6879 column,
6880 agg,
6881 principal,
6882 security_version,
6883 );
6884 table.aggregate_incremental(cache_key, conditions, column, agg)
6885 },
6886 )
6887 }
6888
6889 pub fn get_for_current_principal(
6892 &self,
6893 table_name: &str,
6894 row_id: RowId,
6895 ) -> Result<Option<crate::memtable::Row>> {
6896 self.with_authorized_read(
6897 table_name,
6898 None,
6899 true,
6900 |table, snapshot, allowed, principal| {
6901 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6902 let Some(row) = table.get(row_id, snapshot) else {
6903 return Ok(None);
6904 };
6905 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
6906 return Ok(None);
6907 }
6908 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6909 if let Some(row) = rows.first_mut() {
6910 row.columns
6911 .retain(|column, _| allowed_columns.contains(column));
6912 }
6913 Ok(rows.pop())
6914 },
6915 )
6916 }
6917
6918 pub fn ann_rerank_for_current_principal(
6921 &self,
6922 table_name: &str,
6923 request: &crate::query::AnnRerankRequest,
6924 ) -> Result<Vec<crate::query::AnnRerankHit>> {
6925 self.with_authorized_scored_read_context_at(
6926 table_name,
6927 None,
6928 true,
6929 Some(&ReadAuthorization {
6930 operation: crate::auth::ColumnOperation::Select,
6931 columns: vec![request.column_id],
6932 permissions: Vec::new(),
6933 }),
6934 None,
6935 None,
6936 |table, snapshot, authorization, principal| {
6937 self.require_columns_for(
6938 table_name,
6939 crate::auth::ColumnOperation::Select,
6940 &[request.column_id],
6941 principal,
6942 )?;
6943 table.ann_rerank_at_with_candidate_authorization_on_generation(
6944 request,
6945 snapshot,
6946 authorization,
6947 None,
6948 )
6949 },
6950 )
6951 }
6952
6953 pub fn search_for_current_principal(
6957 &self,
6958 table_name: &str,
6959 request: &crate::query::SearchRequest,
6960 ) -> Result<Vec<crate::query::SearchHit>> {
6961 let principal = self.principal_snapshot();
6962 self.search_for_principal_with_context(table_name, request, principal.as_ref(), None)
6963 }
6964
6965 pub fn search_for_principal_with_context(
6971 &self,
6972 table_name: &str,
6973 request: &crate::query::SearchRequest,
6974 principal: Option<&crate::auth::Principal>,
6975 context: Option<&crate::query::AiExecutionContext>,
6976 ) -> Result<Vec<crate::query::SearchHit>> {
6977 let mut columns = crate::query::condition_columns(&request.must);
6978 for named in &request.retrievers {
6979 columns.push(named.retriever.column_id());
6980 }
6981 if let Some(crate::query::Rerank::ExactVector {
6982 embedding_column, ..
6983 }) = &request.rerank
6984 {
6985 columns.push(*embedding_column);
6986 }
6987 if let Some(proj) = &request.projection {
6988 columns.extend(proj);
6989 }
6990 columns.sort_unstable();
6991 columns.dedup();
6992 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6993 self.with_authorized_scored_read_context_at(
6994 table_name,
6995 principal,
6996 catalog_bound,
6997 Some(&ReadAuthorization {
6998 operation: crate::auth::ColumnOperation::Select,
6999 columns: columns.clone(),
7000 permissions: Vec::new(),
7001 }),
7002 context,
7003 None,
7004 |table, snapshot, authorization, principal| {
7005 self.require_columns_for(
7006 table_name,
7007 crate::auth::ColumnOperation::Select,
7008 &columns,
7009 principal,
7010 )?;
7011 let hits = table.search_at_with_candidate_authorization_on_generation(
7012 request,
7013 snapshot,
7014 authorization,
7015 context,
7016 )?;
7017 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
7018 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
7019 for mut hit in hits {
7020 let row = crate::memtable::Row {
7021 row_id: hit.row_id,
7022 committed_epoch: crate::Epoch(0),
7023 columns: hit
7024 .cells
7025 .iter()
7026 .cloned()
7027 .collect::<std::collections::HashMap<u16, crate::Value>>(),
7028 deleted: false,
7029 commit_ts: None,
7030 };
7031 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
7032 if let Some(mut row) = rows.pop() {
7033 row.columns
7034 .retain(|column, _| allowed_columns.contains(column));
7035 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
7036 hit.cells.sort_by_key(|(column, _)| *column);
7037 secured.push(hit);
7038 }
7039 }
7040 Ok(secured)
7041 },
7042 )
7043 }
7044
7045 pub fn retrieve_text(
7048 &self,
7049 table: &str,
7050 embedding_column: u16,
7051 text: &str,
7052 search_options: crate::embedding::TextSearchOptions,
7053 ) -> Result<crate::embedding::TextRetrieveResult> {
7054 let principal = self.principal_snapshot();
7055 self.retrieve_text_for_principal(
7056 table,
7057 embedding_column,
7058 text,
7059 search_options,
7060 principal.as_ref(),
7061 )
7062 }
7063
7064 pub fn retrieve_text_for_principal(
7065 &self,
7066 table: &str,
7067 embedding_column: u16,
7068 text: &str,
7069 search_options: crate::embedding::TextSearchOptions,
7070 principal: Option<&crate::auth::Principal>,
7071 ) -> Result<crate::embedding::TextRetrieveResult> {
7072 if search_options.k == 0 {
7073 return Err(MongrelError::InvalidArgument(
7074 "retrieve_text k must be greater than zero".into(),
7075 ));
7076 }
7077 let catalog_bound = principal.is_some_and(|p| p.user_id != 0);
7078 let (semantic_identity, registry_generation, expected_dim) = {
7079 let handle = self.table(table)?;
7080 let mut g = handle.lock();
7081 g.ensure_indexes_complete()?;
7082 let schema = g.schema().clone();
7083 let column = schema
7084 .columns
7085 .iter()
7086 .find(|c| c.id == embedding_column)
7087 .ok_or_else(|| {
7088 MongrelError::ColumnNotFound(format!(
7089 "embedding column {embedding_column} not found on table {table}"
7090 ))
7091 })?;
7092 let dim = match column.ty {
7093 crate::schema::TypeId::Embedding { dim } => dim,
7094 _ => {
7095 return Err(MongrelError::InvalidArgument(format!(
7096 "column {embedding_column} is not an embedding column"
7097 )))
7098 }
7099 };
7100 if let Some(identity) = g
7101 .ann_index(embedding_column)
7102 .and_then(|a| a.semantic_identity().cloned())
7103 {
7104 let (generation, provider) = self
7105 .embedding_providers
7106 .resolve_by_semantic_identity(&identity)
7107 .map_err(embedding_error)?;
7108 if provider.dimension() != dim {
7109 return Err(embedding_error(
7110 crate::embedding::EmbeddingError::DimensionMismatch {
7111 expected: dim,
7112 got: provider.dimension(),
7113 },
7114 ));
7115 }
7116 (identity, generation, dim)
7117 } else {
7118 let source = column.embedding_source.clone().ok_or_else(|| {
7119 embedding_error(crate::embedding::EmbeddingError::NoActiveAnnIdentity(
7120 embedding_column,
7121 ))
7122 })?;
7123 let (generation, provider) = self
7124 .embedding_providers
7125 .resolve_with_generation(&source)
7126 .map_err(embedding_error)?;
7127 let identity = provider.semantic_identity();
7128 if identity.dimension != dim {
7129 return Err(embedding_error(
7130 crate::embedding::EmbeddingError::DimensionMismatch {
7131 expected: dim,
7132 got: identity.dimension,
7133 },
7134 ));
7135 }
7136 (identity, generation, dim)
7137 }
7138 };
7139 let (_g, provider) = self
7140 .embedding_providers
7141 .resolve_by_semantic_identity(&semantic_identity)
7142 .map_err(embedding_error)?;
7143 let control = crate::ExecutionControl::new(None);
7144 let response = provider
7145 .embed(crate::embedding::EmbeddingRequest {
7146 texts: &[text],
7147 control: &control,
7148 trace_id: "retrieve-text",
7149 })
7150 .map_err(embedding_error)?;
7151 crate::embedding::validate_response(
7152 provider.as_ref(),
7153 1,
7154 expected_dim,
7155 crate::embedding::EmbeddingLimits::default(),
7156 &response.vectors,
7157 )
7158 .map_err(embedding_error)?;
7159 let query = response.vectors.into_iter().next().ok_or_else(|| {
7160 MongrelError::Other("embedding provider returned no query vector".into())
7161 })?;
7162 if provider.semantic_identity() != semantic_identity {
7163 return Err(embedding_error(
7164 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7165 column_id: embedding_column,
7166 expected: semantic_identity.fingerprint_sha256(),
7167 got: provider.semantic_identity().fingerprint_sha256(),
7168 },
7169 ));
7170 }
7171 let retriever = crate::query::Retriever::Ann {
7172 column_id: embedding_column,
7173 query,
7174 k: search_options.k,
7175 };
7176 let hits = self.with_authorized_scored_read_context_at(
7177 table,
7178 principal,
7179 catalog_bound,
7180 Some(&ReadAuthorization {
7181 operation: crate::auth::ColumnOperation::Select,
7182 columns: vec![embedding_column],
7183 permissions: Vec::new(),
7184 }),
7185 None,
7186 None,
7187 |table_guard, snapshot, authorization, principal| {
7188 self.require_columns_for(
7189 table,
7190 crate::auth::ColumnOperation::Select,
7191 &[embedding_column],
7192 principal,
7193 )?;
7194 if let Some(ann) = table_guard.ann_index(embedding_column) {
7195 if let Some(active) = ann.semantic_identity() {
7196 if active != &semantic_identity {
7197 return Err(embedding_error(
7198 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7199 column_id: embedding_column,
7200 expected: semantic_identity.fingerprint_sha256(),
7201 got: active.fingerprint_sha256(),
7202 },
7203 ));
7204 }
7205 }
7206 }
7207 table_guard.retrieve_at_with_candidate_authorization_on_generation(
7208 &retriever,
7209 snapshot,
7210 authorization,
7211 None,
7212 )
7213 },
7214 )?;
7215 Ok(crate::embedding::TextRetrieveResult {
7216 hits,
7217 provenance: crate::embedding::TextRetrieveProvenance {
7218 semantic_identity,
7219 provider_registry_generation: registry_generation,
7220 query_source_fingerprint: Sha256::digest(text.as_bytes()).into(),
7221 embedding_column,
7222 },
7223 })
7224 }
7225
7226 pub fn authorized_read_snapshot(
7229 &self,
7230 table: &str,
7231 principal: Option<&crate::auth::Principal>,
7232 ) -> Result<AuthorizedReadSnapshot> {
7233 let (security, security_version, effective_principal) = {
7234 let catalog = self.catalog.read();
7235 (
7236 catalog.security.clone(),
7237 catalog.security_version,
7238 self.principal_for_authorized_read(&catalog, principal, false)?,
7239 )
7240 };
7241 let handle = self.table(table)?;
7242 let (table_snapshot, data_generation, allowed_row_ids) = {
7243 let table_handle = handle.lock();
7244 let table_snapshot = table_handle.snapshot();
7245 let data_generation = table_handle.data_generation();
7246 let allowed = self.allowed_row_ids_locked(
7247 table,
7248 &table_handle,
7249 table_snapshot,
7250 (&security, security_version),
7251 effective_principal.as_ref(),
7252 None,
7253 )?;
7254 (
7255 table_snapshot,
7256 data_generation,
7257 allowed.map(|allowed| (*allowed).clone()),
7258 )
7259 };
7260 Ok(AuthorizedReadSnapshot {
7261 table: table.to_string(),
7262 table_snapshot,
7263 data_generation,
7264 security_version,
7265 allowed_row_ids,
7266 })
7267 }
7268
7269 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
7270 if self.catalog.read().security_version != snapshot.security_version {
7271 return false;
7272 }
7273 self.table(&snapshot.table)
7274 .ok()
7275 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
7276 }
7277
7278 pub fn rls_cache_stats(&self) -> RlsCacheStats {
7279 self.rls_cache.lock().stats()
7280 }
7281
7282 pub fn rows_for(
7284 &self,
7285 table: &str,
7286 principal: Option<&crate::auth::Principal>,
7287 ) -> Result<Vec<crate::memtable::Row>> {
7288 if principal.is_none() && self.principal.read().is_some() {
7289 self.refresh_principal()?;
7290 }
7291 let allowed = self.select_column_ids_for(table, principal)?;
7292 let handle = self.table(table)?;
7293 let rows = {
7294 let table = handle.lock();
7295 table.visible_rows(table.snapshot())?
7296 };
7297 let mut rows = self.secure_rows_for(table, rows, principal)?;
7298 for row in &mut rows {
7299 row.columns.retain(|column, _| allowed.contains(column));
7300 }
7301 Ok(rows)
7302 }
7303
7304 pub fn rows_at_epoch_for_current_principal(
7307 &self,
7308 table_name: &str,
7309 snapshot: Snapshot,
7310 ) -> Result<Vec<crate::memtable::Row>> {
7311 self.with_authorized_read_context(
7312 table_name,
7313 None,
7314 true,
7315 Some(&ReadAuthorization {
7316 operation: crate::auth::ColumnOperation::Select,
7317 columns: Vec::new(),
7318 permissions: Vec::new(),
7319 }),
7320 None,
7321 Some(snapshot),
7322 |table, snapshot, allowed, principal| {
7323 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
7324 let mut rows = table.visible_rows(snapshot)?;
7325 if let Some(allowed) = allowed {
7326 rows.retain(|row| allowed.contains(&row.row_id));
7327 }
7328 rows = self.secure_rows_for(table_name, rows, principal)?;
7329 for row in &mut rows {
7330 row.columns
7331 .retain(|column, _| allowed_columns.contains(column));
7332 }
7333 Ok(rows)
7334 },
7335 )
7336 }
7337
7338 pub fn count_for(
7340 &self,
7341 table: &str,
7342 principal: Option<&crate::auth::Principal>,
7343 ) -> Result<u64> {
7344 if principal.is_none() && self.principal.read().is_some() {
7345 self.refresh_principal()?;
7346 }
7347 self.select_column_ids_for(table, principal)?;
7348 if self.security_active_for(table) {
7349 Ok(self.rows_for(table, principal)?.len() as u64)
7350 } else {
7351 Ok(self.table(table)?.lock().count())
7352 }
7353 }
7354
7355 pub fn put_for(
7357 &self,
7358 table: &str,
7359 mut cells: Vec<(u16, crate::memtable::Value)>,
7360 principal: Option<&crate::auth::Principal>,
7361 ) -> Result<RowId> {
7362 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
7363 self.require_columns_for(
7364 table,
7365 crate::auth::ColumnOperation::Insert,
7366 &columns,
7367 principal,
7368 )?;
7369 let handle = self.table(table)?;
7370 let mut table_handle = handle.lock();
7371 table_handle.fill_auto_inc(&mut cells)?;
7372 table_handle.apply_defaults(&mut cells)?;
7373 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
7374 row.columns.extend(cells.iter().cloned());
7375 self.check_row_policy_for(
7376 table,
7377 crate::security::PolicyCommand::Insert,
7378 &row,
7379 true,
7380 principal,
7381 )?;
7382 table_handle.put(cells)
7383 }
7384
7385 pub fn check_row_policy_for(
7386 &self,
7387 table: &str,
7388 command: crate::security::PolicyCommand,
7389 row: &crate::memtable::Row,
7390 check_new: bool,
7391 principal: Option<&crate::auth::Principal>,
7392 ) -> Result<()> {
7393 let security = self.catalog.read().security.clone();
7394 if !security.rls_enabled(table) {
7395 return Ok(());
7396 }
7397 let cached = self.principal.read().clone();
7398 let principal = principal
7399 .or(cached.as_ref())
7400 .ok_or(MongrelError::AuthRequired)?;
7401 if security.row_allowed(table, command, row, principal, check_new) {
7402 return Ok(());
7403 }
7404 let required = match command {
7405 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
7406 table: table.to_string(),
7407 },
7408 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
7409 table: table.to_string(),
7410 },
7411 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
7412 table: table.to_string(),
7413 },
7414 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
7415 crate::auth::Permission::Delete {
7416 table: table.to_string(),
7417 }
7418 }
7419 };
7420 Err(MongrelError::PermissionDenied {
7421 required,
7422 principal: principal.username.clone(),
7423 })
7424 }
7425
7426 pub fn set_materialized_view(
7429 &self,
7430 definition: crate::catalog::MaterializedViewEntry,
7431 ) -> Result<()> {
7432 self.set_materialized_view_with_epoch(definition)
7433 .map(|_| ())
7434 }
7435
7436 pub fn set_materialized_view_with_epoch(
7438 &self,
7439 definition: crate::catalog::MaterializedViewEntry,
7440 ) -> Result<Epoch> {
7441 use crate::wal::DdlOp;
7442 use std::sync::atomic::Ordering;
7443
7444 let command = crate::catalog_cmds::CatalogCommand::CreateMaterializedView {
7445 definition: definition.clone(),
7446 };
7447 self.require(&crate::catalog_cmds::required_permission(&command))?;
7448 if self.poisoned.load(Ordering::Relaxed) {
7449 return Err(MongrelError::Other(
7450 "database poisoned by fsync error".into(),
7451 ));
7452 }
7453 if definition.name.is_empty() || definition.query.trim().is_empty() {
7454 return Err(MongrelError::InvalidArgument(
7455 "materialized view name and query must not be empty".into(),
7456 ));
7457 }
7458 let _operation = self.admit_operation()?;
7461 let _ddl = self.ddl_lock.lock();
7462 let _security_write = self.security_write()?;
7463 self.require(&crate::catalog_cmds::required_permission(&command))?;
7464 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7468 let table_id = self
7469 .catalog
7470 .read()
7471 .live(&definition.name)
7472 .ok_or_else(|| {
7473 MongrelError::NotFound(format!(
7474 "materialized view table {:?} not found",
7475 definition.name
7476 ))
7477 })?
7478 .table_id;
7479 let definition_json = DdlOp::encode_materialized_view(&definition)?;
7480 let _commit = self.commit_lock.lock();
7481 let epoch = self.epoch.bump_assigned();
7482 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7483 let txn_id = self.alloc_txn_id()?;
7484 let mut next_catalog = self.catalog.read().clone();
7485 self.apply_catalog_command_to(&mut next_catalog, command)?;
7486 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
7487 let commit_seq = {
7488 let mut wal = self.shared_wal.lock();
7489 let append: Result<u64> = (|| {
7490 wal.append(
7491 txn_id,
7492 table_id,
7493 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
7494 name: definition.name.clone(),
7495 definition_json,
7496 }),
7497 )?;
7498 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
7499 wal.append_commit(txn_id, epoch, &[])
7500 })();
7501 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
7502 };
7503 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
7504
7505 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
7506 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
7507 Ok(epoch)
7508 }
7509
7510 pub fn root(&self) -> &Path {
7512 self.durable_root.canonical_path()
7513 }
7514
7515 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
7518 Arc::clone(&self.durable_root)
7519 }
7520
7521 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
7525 self.kek
7526 .as_deref()
7527 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
7528 }
7529
7530 pub fn is_read_only_replica(&self) -> bool {
7531 self.read_only
7532 }
7533
7534 pub fn ensure_consistent_read(&self) -> Result<()> {
7538 self.ensure_owner_process()?;
7539 if self.poisoned.load(Ordering::Relaxed) {
7540 return Err(MongrelError::Other(
7541 "database poisoned by post-commit failure; reopen required".into(),
7542 ));
7543 }
7544 Ok(())
7545 }
7546
7547 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
7548 self.replication_wal_retention_segments
7549 .store(segments, std::sync::atomic::Ordering::Relaxed);
7550 }
7551
7552 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
7557 let admin = crate::auth::Permission::Admin;
7558 self.require(&admin)?;
7559 let _operation = self.admit_operation()?;
7561 let operation_principal = self.principal_snapshot();
7562 let _barrier = self.replication_barrier.write();
7563 let _ddl = self.ddl_lock.lock();
7564 let _security = self.security_coordinator.gate.read();
7565 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7566 let mut handles: Vec<_> = self
7567 .tables
7568 .read()
7569 .iter()
7570 .map(|(id, handle)| (*id, handle.clone()))
7571 .collect();
7572 handles.sort_by_key(|(id, _)| *id);
7573 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7574 let _commit = self.commit_lock.lock();
7575 let mut wal = self.shared_wal.lock();
7576 wal.group_sync()?;
7577 let io_root = self.durable_root.io_path()?;
7578 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7579 let records = crate::wal::SharedWal::replay_with_dek(&io_root, wal_dek.as_ref())?;
7580 let epoch = records
7581 .iter()
7582 .filter_map(|record| match &record.op {
7583 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
7584 _ => None,
7585 })
7586 .max()
7587 .unwrap_or(0)
7588 .max(self.epoch.committed().0);
7589 let files = crate::replication::capture_files(&io_root)?;
7590 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7591 drop(wal);
7592 Ok(crate::replication::ReplicationSnapshot::new(
7593 source_id, epoch, files,
7594 ))
7595 }
7596
7597 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
7605 let control = crate::ExecutionControl::new(None);
7606 self.hot_backup_controlled(destination, &control, || true)
7607 }
7608
7609 pub(crate) fn hot_backup_to_durable_child(
7610 &self,
7611 parent: &crate::durable_file::DurableRoot,
7612 child: &Path,
7613 control: &crate::ExecutionControl,
7614 ) -> Result<crate::backup::BackupReport> {
7615 let mut components = child.components();
7616 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
7617 || components.next().is_some()
7618 {
7619 return Err(MongrelError::InvalidArgument(
7620 "durable backup child must be one relative path component".into(),
7621 ));
7622 }
7623 let destination_name = child.file_name().ok_or_else(|| {
7624 MongrelError::InvalidArgument("durable backup child has no filename".into())
7625 })?;
7626 let source_root = self.durable_root.io_path()?;
7627 let prepared = prepare_backup_destination_in(&source_root, parent, destination_name)?;
7628 self.hot_backup_prepared(prepared, control, || true)
7629 }
7630
7631 #[doc(hidden)]
7634 pub fn hot_backup_controlled<F>(
7635 &self,
7636 destination: impl AsRef<Path>,
7637 control: &crate::ExecutionControl,
7638 before_publish: F,
7639 ) -> Result<crate::backup::BackupReport>
7640 where
7641 F: FnOnce() -> bool,
7642 {
7643 let source_root = self.durable_root.io_path()?;
7644 let prepared = prepare_backup_destination(&source_root, destination.as_ref())?;
7645 self.hot_backup_prepared(prepared, control, before_publish)
7646 }
7647
7648 fn hot_backup_prepared<F>(
7649 &self,
7650 mut prepared: PreparedBackupDestination,
7651 control: &crate::ExecutionControl,
7652 before_publish: F,
7653 ) -> Result<crate::backup::BackupReport>
7654 where
7655 F: FnOnce() -> bool,
7656 {
7657 let admin = crate::auth::Permission::Admin;
7658 self.require(&admin)?;
7659 let _operation = self.admit_operation()?;
7662 let operation_principal = self.principal_snapshot();
7663 control.checkpoint()?;
7664 let destination = prepared.destination_path.clone();
7665 let mut before_publish = Some(before_publish);
7666
7667 let outcome = (|| {
7668 control.checkpoint()?;
7669 let barrier = self.replication_barrier.write();
7670 let ddl = self.ddl_lock.lock();
7671 let security = self.security_coordinator.gate.read();
7672 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7673 let mut handles: Vec<_> = self
7674 .tables
7675 .read()
7676 .iter()
7677 .map(|(id, handle)| (*id, handle.clone()))
7678 .collect();
7679 handles.sort_by_key(|(id, _)| *id);
7680 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7681 let commit = self.commit_lock.lock();
7682 let mut wal = self.shared_wal.lock();
7683 wal.group_sync()?;
7684 let epoch = self.epoch.committed().0;
7685 let boundary_unix_nanos = current_unix_nanos();
7686 let source_root = self.durable_root.io_path()?;
7687
7688 let pin_nonce = std::time::SystemTime::now()
7689 .duration_since(std::time::UNIX_EPOCH)
7690 .unwrap_or_default()
7691 .as_nanos();
7692 let file_pin_root = source_root
7693 .join(META_DIR)
7694 .join("backup-pins")
7695 .join(format!("{}-{pin_nonce}", std::process::id()));
7696 std::fs::create_dir_all(&file_pin_root)?;
7697 let _file_pins = BackupFilePins {
7698 root: file_pin_root.clone(),
7699 };
7700 let mut run_files = Vec::new();
7701 for (index, (table_id, _)) in handles.iter().enumerate() {
7702 if index % 256 == 0 {
7703 control.checkpoint()?;
7704 }
7705 let table = &table_guards[index];
7706 for (run_index, run) in table.run_refs().iter().enumerate() {
7707 if run_index % 256 == 0 {
7708 control.checkpoint()?;
7709 }
7710 let source = table.run_path(run.run_id as u64);
7711 let relative = Path::new(TABLES_DIR)
7712 .join(table_id.to_string())
7713 .join(crate::engine::RUNS_DIR)
7714 .join(format!("r-{}.sr", run.run_id));
7715 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
7716 if std::fs::hard_link(&source, &pinned).is_err() {
7717 crate::backup::copy_file_synced(&source, &pinned)?;
7718 }
7719 run_files.push(((*table_id, run.run_id), pinned, relative));
7720 }
7721 }
7722 crate::durable_file::sync_directory(&file_pin_root)?;
7723 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
7724 {
7725 let mut pins = self.backup_pins.lock();
7726 for key in &run_keys {
7727 *pins.entry(*key).or_insert(0) += 1;
7728 }
7729 }
7730 let _run_pins = RunPins {
7731 pins: Arc::clone(&self.backup_pins),
7732 runs: run_keys,
7733 };
7734 let _registry_pins: Vec<crate::retention::PinGuard> = table_guards
7741 .iter()
7742 .map(|table| {
7743 table
7744 .pin_registry()
7745 .pin(crate::retention::PinSource::BackupPitr, Epoch(epoch))
7746 })
7747 .collect();
7748 let deferred: HashSet<_> = run_files
7749 .iter()
7750 .map(|(_, _, relative)| relative.clone())
7751 .collect();
7752 let mut copied = Vec::new();
7753 copy_backup_boundary(
7754 &source_root,
7755 prepared.stage.as_deref().ok_or_else(|| {
7756 MongrelError::Other("backup staging root was already released".into())
7757 })?,
7758 &deferred,
7759 &mut copied,
7760 Some(control),
7761 )?;
7762
7763 drop(wal);
7764 drop(commit);
7765 drop(table_guards);
7766 drop(security);
7767 drop(ddl);
7768 drop(barrier);
7769
7770 if let Some(hook) = self.backup_hook.lock().as_ref() {
7771 hook();
7772 }
7773 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
7774 if index % 256 == 0 {
7775 control.checkpoint()?;
7776 }
7777 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
7778 prepared
7779 .stage
7780 .as_deref()
7781 .ok_or_else(|| {
7782 MongrelError::Other("backup staging root was already released".into())
7783 })?
7784 .copy_new_from(&relative, &mut source)?;
7785 copied.push(relative);
7786 }
7787
7788 let manifest = crate::backup::BackupManifest::create_controlled_durable(
7789 prepared.stage.as_deref().ok_or_else(|| {
7790 MongrelError::Other("backup staging root was already released".into())
7791 })?,
7792 epoch,
7793 &copied,
7794 control,
7795 )?;
7796 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
7797 MongrelError::Other("backup staging root was already released".into())
7798 })?)?;
7799 control.checkpoint()?;
7800 let publish = before_publish.take().ok_or_else(|| {
7801 MongrelError::Other("backup publication callback already consumed".into())
7802 })?;
7803 if !publish() {
7804 return Err(MongrelError::Cancelled);
7805 }
7806 let final_security = self.security_coordinator.gate.read();
7807 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7808 drop(prepared.stage.take().ok_or_else(|| {
7812 MongrelError::Other("backup staging root was already released".into())
7813 })?);
7814 let published = std::cell::Cell::new(false);
7815 if let Err(error) = prepared.parent.rename_directory_new_with_after(
7816 Path::new(&prepared.stage_name),
7817 &prepared.parent,
7818 Path::new(&prepared.destination_name),
7819 || published.set(true),
7820 ) {
7821 if published.get() {
7822 return Err(MongrelError::CommitOutcomeUnknown {
7823 epoch,
7824 message: format!("backup publication was not durable: {error}"),
7825 });
7826 }
7827 return Err(error.into());
7828 }
7829 drop(final_security);
7830 Ok(crate::backup::BackupReport {
7831 destination,
7832 epoch,
7833 boundary_unix_nanos,
7834 files: manifest.files.len(),
7835 bytes: manifest.total_bytes(),
7836 })
7837 })();
7838
7839 if outcome.is_err() {
7840 drop(prepared.stage.take());
7841 let _ = prepared
7842 .parent
7843 .remove_directory_all(Path::new(&prepared.stage_name));
7844 }
7845 outcome
7846 }
7847
7848 pub fn replication_batch_since(
7851 &self,
7852 since_epoch: u64,
7853 ) -> Result<crate::replication::ReplicationBatch> {
7854 use crate::wal::Op;
7855
7856 let admin = crate::auth::Permission::Admin;
7857 self.require(&admin)?;
7858 let _operation = self.admit_operation()?;
7860 let operation_principal = self.principal_snapshot();
7861
7862 let mut wal = self.shared_wal.lock();
7863 wal.group_sync()?;
7864 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7865 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
7866 drop(wal);
7867
7868 let commits: HashMap<u64, u64> = records
7869 .iter()
7870 .filter_map(|record| match &record.op {
7871 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
7872 _ => None,
7873 })
7874 .collect();
7875 let earliest_epoch = commits.values().copied().min();
7876 let current_epoch = commits
7877 .values()
7878 .copied()
7879 .max()
7880 .unwrap_or(0)
7881 .max(self.epoch.committed().0);
7882 let selected: HashSet<u64> = commits
7883 .iter()
7884 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
7885 .collect();
7886 let retention_gap = since_epoch < current_epoch
7887 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
7888 let spilled = records.iter().any(|record| {
7889 selected.contains(&record.txn_id)
7890 && matches!(
7891 &record.op,
7892 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
7893 )
7894 });
7895 let records = records
7896 .into_iter()
7897 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
7898 .filter(|record| selected.contains(&record.txn_id))
7899 .collect();
7900 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7901 let batch = crate::replication::ReplicationBatch::complete_for_source(
7902 source_id,
7903 since_epoch,
7904 current_epoch,
7905 earliest_epoch,
7906 retention_gap,
7907 spilled,
7908 records,
7909 )?;
7910 if let Some(hook) = self.replication_hook.lock().as_ref() {
7911 hook();
7912 }
7913 let _security = self.security_coordinator.gate.read();
7914 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7915 Ok(batch)
7916 }
7917
7918 pub fn append_replication_batch(
7922 &self,
7923 batch: &crate::replication::ReplicationBatch,
7924 ) -> Result<u64> {
7925 use crate::wal::Op;
7926
7927 if !self.read_only {
7928 return Err(MongrelError::InvalidArgument(
7929 "replication batches may only target a marked replica".into(),
7930 ));
7931 }
7932 let _operation = self.admit_operation()?;
7934 let current = crate::replication::replica_epoch(&self.root)?;
7935 if batch.is_source_bound() {
7936 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
7937 if batch.source_id != source_id {
7938 return Err(MongrelError::Conflict(
7939 "replication batch source does not match follower binding".into(),
7940 ));
7941 }
7942 }
7943 if batch.requires_snapshot {
7944 return Err(MongrelError::Conflict(
7945 "replication snapshot required for this batch".into(),
7946 ));
7947 }
7948 batch.validate_proof()?;
7949 if batch.from_epoch != current {
7950 if batch.from_epoch < current && batch.current_epoch == current {
7951 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7952 let _wal = self.shared_wal.lock();
7953 let existing: HashSet<(u64, u64)> =
7954 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7955 .into_iter()
7956 .filter_map(|record| match record.op {
7957 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7958 _ => None,
7959 })
7960 .collect();
7961 let already_applied = batch.records.iter().all(|record| match &record.op {
7962 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
7963 _ => true,
7964 });
7965 if already_applied {
7966 return Ok(current);
7967 }
7968 }
7969 return Err(MongrelError::Conflict(format!(
7970 "replication batch starts at epoch {}, follower is at epoch {current}",
7971 batch.from_epoch
7972 )));
7973 }
7974 if batch.current_epoch < current {
7975 return Err(MongrelError::InvalidArgument(format!(
7976 "replication batch current epoch {} precedes follower epoch {current}",
7977 batch.current_epoch
7978 )));
7979 }
7980 let records = &batch.records;
7981 let mut commits = HashMap::new();
7982 let mut commit_epochs = HashSet::new();
7983 let mut commit_timestamps = HashMap::new();
7984 for record in records {
7985 match &record.op {
7986 Op::TxnCommit { epoch, added_runs } => {
7987 if !added_runs.is_empty() {
7988 return Err(MongrelError::Conflict(
7989 "replication snapshot required for spilled-run transaction".into(),
7990 ));
7991 }
7992 if commits.insert(record.txn_id, *epoch).is_some() {
7993 return Err(MongrelError::InvalidArgument(format!(
7994 "duplicate commit for replication transaction {}",
7995 record.txn_id
7996 )));
7997 }
7998 if *epoch <= current || *epoch > batch.current_epoch {
7999 return Err(MongrelError::InvalidArgument(format!(
8000 "replication commit epoch {epoch} is outside ({current}, {}]",
8001 batch.current_epoch
8002 )));
8003 }
8004 if !commit_epochs.insert(*epoch) {
8005 return Err(MongrelError::InvalidArgument(format!(
8006 "duplicate replication commit epoch {epoch}"
8007 )));
8008 }
8009 }
8010 Op::CommitTimestamp { unix_nanos } => {
8011 commit_timestamps.insert(record.txn_id, *unix_nanos);
8012 }
8013 _ => {}
8014 }
8015 }
8016 for record in records {
8017 if record.txn_id == crate::wal::SYSTEM_TXN_ID
8018 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
8019 {
8020 return Err(MongrelError::InvalidArgument(
8021 "replication batch contains a non-committed record".into(),
8022 ));
8023 }
8024 if !commits.contains_key(&record.txn_id) {
8025 return Err(MongrelError::InvalidArgument(format!(
8026 "incomplete replication transaction {}",
8027 record.txn_id
8028 )));
8029 }
8030 }
8031 let target_epoch = commits
8032 .values()
8033 .copied()
8034 .filter(|epoch| *epoch > current)
8035 .max()
8036 .unwrap_or(current);
8037 if target_epoch != batch.current_epoch {
8038 return Err(MongrelError::InvalidArgument(format!(
8039 "replication batch ends at epoch {target_epoch}, expected {}",
8040 batch.current_epoch
8041 )));
8042 }
8043 let mut selected: HashSet<u64> = commits
8044 .iter()
8045 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
8046 .collect();
8047 if selected.is_empty() {
8048 return Ok(current);
8049 }
8050 let mut wal = self.shared_wal.lock();
8051 wal.group_sync()?;
8052 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
8053 let existing: HashSet<(u64, u64)> =
8054 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
8055 .into_iter()
8056 .filter_map(|record| match record.op {
8057 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
8058 _ => None,
8059 })
8060 .collect();
8061 selected.retain(|txn_id| {
8062 commits
8063 .get(txn_id)
8064 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
8065 });
8066 for record in records {
8067 if !selected.contains(&record.txn_id) {
8068 continue;
8069 }
8070 match &record.op {
8071 Op::TxnCommit { epoch, added_runs } => {
8072 let timestamp = commit_timestamps
8073 .get(&record.txn_id)
8074 .copied()
8075 .unwrap_or_else(current_unix_nanos);
8076 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
8077 }
8078 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
8079 op => {
8080 wal.append(record.txn_id, 0, op.clone())?;
8081 }
8082 }
8083 }
8084 if !selected.is_empty() {
8085 wal.group_sync()?;
8086 }
8087 drop(wal);
8088
8089 let mut recovered_catalog = self.catalog.read().clone();
8093 if let Err(error) = recover_ddl_from_wal(
8094 &self.root,
8095 Some(&self.durable_root),
8096 &mut recovered_catalog,
8097 self.meta_dek.as_ref(),
8098 wal_dek.as_ref(),
8099 true,
8100 None,
8101 ) {
8102 return Err(MongrelError::DurableCommit {
8103 epoch: target_epoch,
8104 message: format!(
8105 "replication WAL is durable but catalog checkpoint failed: {error}"
8106 ),
8107 });
8108 }
8109 let _security = self.security_coordinator.gate.write();
8110 let old_security_version = self.catalog.read().security_version;
8111 let security_changed = old_security_version != recovered_catalog.security_version
8112 || self.catalog.read().require_auth != recovered_catalog.require_auth;
8113 let require_auth = recovered_catalog.require_auth;
8114 let principal = if security_changed {
8115 None
8116 } else {
8117 self.principal.read().as_ref().and_then(|principal| {
8118 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
8119 })
8120 };
8121 if require_auth {
8122 self.auth_state.set_require_auth(true);
8123 }
8124 self.auth_state.set_principal(principal.clone());
8125 *self.principal.write() = principal;
8126 let security_version = recovered_catalog.security_version;
8127 *self.catalog.write() = recovered_catalog;
8128 self.security_coordinator
8129 .version
8130 .store(security_version, Ordering::Release);
8131 if !require_auth {
8132 self.auth_state.set_require_auth(false);
8133 }
8134 if let Err(error) =
8135 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
8136 {
8137 return Err(MongrelError::DurableCommit {
8138 epoch: target_epoch,
8139 message: format!(
8140 "replication WAL and catalog are durable but follower watermark failed: {error}"
8141 ),
8142 });
8143 }
8144 Ok(target_epoch)
8145 }
8146
8147 pub fn table_id(&self, name: &str) -> Result<u64> {
8150 let cat = self.catalog.read();
8151 cat.live(name)
8152 .map(|e| e.table_id)
8153 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
8154 }
8155
8156 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
8161 let catalog = self.catalog.read();
8162 catalog
8163 .live(name)
8164 .map(|entry| (entry.table_id, entry.schema.schema_id))
8165 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
8166 }
8167
8168 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
8169 self.catalog
8170 .read()
8171 .building(name)
8172 .map(|entry| entry.table_id)
8173 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
8174 }
8175
8176 pub fn procedures(&self) -> Vec<StoredProcedure> {
8177 self.catalog
8178 .read()
8179 .procedures
8180 .iter()
8181 .map(|p| p.procedure.clone())
8182 .collect()
8183 }
8184
8185 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
8186 self.catalog
8187 .read()
8188 .procedures
8189 .iter()
8190 .find(|p| p.procedure.name == name)
8191 .map(|p| p.procedure.clone())
8192 }
8193
8194 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
8195 self.create_procedure_inner(procedure, None)
8196 }
8197
8198 pub fn create_procedure_controlled<F>(
8199 &self,
8200 procedure: StoredProcedure,
8201 mut before_publish: F,
8202 ) -> Result<StoredProcedure>
8203 where
8204 F: FnMut() -> Result<()>,
8205 {
8206 self.create_procedure_inner(procedure, Some(&mut before_publish))
8207 }
8208
8209 fn create_procedure_inner(
8210 &self,
8211 mut procedure: StoredProcedure,
8212 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8213 ) -> Result<StoredProcedure> {
8214 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8215 procedure: procedure.clone(),
8216 };
8217 self.require(&crate::catalog_cmds::required_permission(&command))?;
8218 let _g = self.ddl_lock.lock();
8219 let _security_write = self.security_write()?;
8220 self.require(&crate::catalog_cmds::required_permission(&command))?;
8221 procedure.validate()?;
8222 self.validate_procedure_references(&procedure)?;
8223 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8227 let commit_lock = Arc::clone(&self.commit_lock);
8228 let _c = commit_lock.lock();
8229 let epoch = self.epoch.bump_assigned();
8230 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8231 procedure.created_epoch = epoch.0;
8232 procedure.updated_epoch = epoch.0;
8233 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8234 procedure: procedure.clone(),
8235 };
8236 let mut next_catalog = self.catalog.read().clone();
8237 self.apply_catalog_command_to(&mut next_catalog, command)?;
8238 next_catalog.db_epoch = epoch.0;
8239 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8240 Ok(procedure)
8241 }
8242
8243 pub fn create_or_replace_procedure(
8244 &self,
8245 procedure: StoredProcedure,
8246 ) -> Result<StoredProcedure> {
8247 self.create_or_replace_procedure_inner(procedure, None)
8248 }
8249
8250 pub fn create_or_replace_procedure_controlled<F>(
8251 &self,
8252 procedure: StoredProcedure,
8253 mut before_publish: F,
8254 ) -> Result<StoredProcedure>
8255 where
8256 F: FnMut() -> Result<()>,
8257 {
8258 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
8259 }
8260
8261 fn create_or_replace_procedure_inner(
8262 &self,
8263 procedure: StoredProcedure,
8264 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8265 ) -> Result<StoredProcedure> {
8266 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8267 procedure: procedure.clone(),
8268 };
8269 self.require(&crate::catalog_cmds::required_permission(&command))?;
8270 let _g = self.ddl_lock.lock();
8271 let _security_write = self.security_write()?;
8272 self.require(&crate::catalog_cmds::required_permission(&command))?;
8273 procedure.validate()?;
8274 self.validate_procedure_references(&procedure)?;
8275 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8279 let commit_lock = Arc::clone(&self.commit_lock);
8280 let _c = commit_lock.lock();
8281 let epoch = self.epoch.bump_assigned();
8282 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8283 let mut next_catalog = self.catalog.read().clone();
8284 let replaced = match next_catalog
8289 .procedures
8290 .iter()
8291 .position(|p| p.procedure.name == procedure.name)
8292 {
8293 Some(idx) => next_catalog.procedures[idx]
8294 .procedure
8295 .replaced(procedure.clone(), epoch.0)?,
8296 None => {
8297 let mut next = procedure;
8298 next.created_epoch = epoch.0;
8299 next.updated_epoch = epoch.0;
8300 next
8301 }
8302 };
8303 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8304 procedure: replaced.clone(),
8305 };
8306 self.apply_catalog_command_to(&mut next_catalog, command)?;
8307 next_catalog.db_epoch = epoch.0;
8308 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8309 Ok(replaced)
8310 }
8311
8312 pub fn drop_procedure(&self, name: &str) -> Result<()> {
8313 self.drop_procedure_with_epoch(name).map(|_| ())
8314 }
8315
8316 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
8317 self.drop_procedure_with_epoch_inner(name, None)
8318 }
8319
8320 pub fn drop_procedure_with_epoch_controlled<F>(
8321 &self,
8322 name: &str,
8323 mut before_publish: F,
8324 ) -> Result<Epoch>
8325 where
8326 F: FnMut() -> Result<()>,
8327 {
8328 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
8329 }
8330
8331 fn drop_procedure_with_epoch_inner(
8332 &self,
8333 name: &str,
8334 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8335 ) -> Result<Epoch> {
8336 let command = crate::catalog_cmds::CatalogCommand::DropProcedure {
8337 name: name.to_string(),
8338 };
8339 self.require(&crate::catalog_cmds::required_permission(&command))?;
8340 let _g = self.ddl_lock.lock();
8341 let _security_write = self.security_write()?;
8342 self.require(&crate::catalog_cmds::required_permission(&command))?;
8343 let commit_lock = Arc::clone(&self.commit_lock);
8344 let _c = commit_lock.lock();
8345 let epoch = self.epoch.bump_assigned();
8346 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8347 let mut next_catalog = self.catalog.read().clone();
8348 self.apply_catalog_command_to(&mut next_catalog, command)?;
8349 next_catalog.db_epoch = epoch.0;
8350 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8351 Ok(epoch)
8352 }
8353
8354 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
8359 self.catalog.read().users.clone()
8360 }
8361
8362 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
8365 self.catalog
8366 .read()
8367 .users
8368 .iter()
8369 .find(|user| user.username == username)
8370 .map(|user| (user.id, user.created_epoch))
8371 }
8372
8373 pub fn user_scram_verifier(
8375 &self,
8376 username: &str,
8377 ) -> Option<crate::security_hardening::ScramVerifier> {
8378 self.catalog
8379 .read()
8380 .users
8381 .iter()
8382 .find(|user| user.username == username)
8383 .and_then(|user| user.scram_sha_256.clone())
8384 }
8385
8386 pub fn security_version(&self) -> u64 {
8389 self.catalog.read().security_version
8390 }
8391
8392 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
8394 self.catalog.read().roles.clone()
8395 }
8396
8397 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
8399 self.require(&crate::auth::Permission::Admin)?;
8400 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
8401 let scram = crate::auth::scram_verifier(password).map_err(MongrelError::Other)?;
8402 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(password);
8403 self.create_user_with_password_hash_inner(username, hash, Some((scram, mysql)), None)
8404 }
8405
8406 pub fn create_user_with_password_hash(
8408 &self,
8409 username: &str,
8410 hash: String,
8411 ) -> Result<crate::auth::UserEntry> {
8412 self.create_user_with_password_hash_inner(username, hash, None, None)
8413 }
8414
8415 pub fn create_user_with_password_hash_controlled<F>(
8416 &self,
8417 username: &str,
8418 hash: String,
8419 mut before_publish: F,
8420 ) -> Result<crate::auth::UserEntry>
8421 where
8422 F: FnMut() -> Result<()>,
8423 {
8424 self.create_user_with_password_hash_inner(username, hash, None, Some(&mut before_publish))
8425 }
8426
8427 fn create_user_with_password_hash_inner(
8428 &self,
8429 username: &str,
8430 hash: String,
8431 verifiers: Option<(
8432 crate::security_hardening::ScramVerifier,
8433 crate::auth::MysqlCachingSha2Verifier,
8434 )>,
8435 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8436 ) -> Result<crate::auth::UserEntry> {
8437 let command = match &verifiers {
8441 Some((scram_sha_256, mysql_caching_sha2)) => {
8442 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8443 username: username.to_string(),
8444 password_hash: hash.clone(),
8445 scram_sha_256: scram_sha_256.clone(),
8446 mysql_caching_sha2: mysql_caching_sha2.clone(),
8447 is_admin: false,
8448 created_epoch: 0,
8449 }
8450 }
8451 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8452 username: username.to_string(),
8453 password_hash: hash.clone(),
8454 is_admin: false,
8455 created_epoch: 0,
8456 },
8457 };
8458 self.require(&crate::catalog_cmds::required_permission(&command))?;
8459 let _ddl = self.ddl_lock.lock();
8460 let _security_write = self.security_write()?;
8461 self.require(&crate::catalog_cmds::required_permission(&command))?;
8462 let _commit = self.commit_lock.lock();
8463 let epoch = self.epoch.bump_assigned();
8464 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8465 let command = match verifiers {
8466 Some((scram_sha_256, mysql_caching_sha2)) => {
8467 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8468 username: username.to_string(),
8469 password_hash: hash,
8470 scram_sha_256,
8471 mysql_caching_sha2,
8472 is_admin: false,
8473 created_epoch: epoch.0,
8474 }
8475 }
8476 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8477 username: username.to_string(),
8478 password_hash: hash,
8479 is_admin: false,
8480 created_epoch: epoch.0,
8481 },
8482 };
8483 let mut next_catalog = self.catalog.read().clone();
8484 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8485 let crate::catalog_cmds::CatalogDelta::UserUpserted(entry) = delta else {
8486 return Err(MongrelError::Other(
8487 "CreateUser resolved to an unexpected catalog delta".into(),
8488 ));
8489 };
8490 next_catalog.db_epoch = epoch.0;
8491 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8492 Ok(entry)
8493 }
8494
8495 pub fn drop_user(&self, username: &str) -> Result<()> {
8497 self.drop_user_with_epoch(username).map(|_| ())
8498 }
8499
8500 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
8501 self.drop_user_with_epoch_inner(username, None)
8502 }
8503
8504 pub fn drop_user_with_epoch_controlled<F>(
8505 &self,
8506 username: &str,
8507 mut before_publish: F,
8508 ) -> Result<Epoch>
8509 where
8510 F: FnMut() -> Result<()>,
8511 {
8512 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
8513 }
8514
8515 fn drop_user_with_epoch_inner(
8516 &self,
8517 username: &str,
8518 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8519 ) -> Result<Epoch> {
8520 let command = crate::catalog_cmds::CatalogCommand::DropUser {
8521 username: username.to_string(),
8522 };
8523 self.require(&crate::catalog_cmds::required_permission(&command))?;
8524 let _ddl = self.ddl_lock.lock();
8525 let _security_write = self.security_write()?;
8526 self.require(&crate::catalog_cmds::required_permission(&command))?;
8527 let _commit = self.commit_lock.lock();
8528 let epoch = self.epoch.bump_assigned();
8529 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8530 let mut next_catalog = self.catalog.read().clone();
8531 self.apply_catalog_command_to(&mut next_catalog, command)?;
8532 next_catalog.db_epoch = epoch.0;
8533 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8534 Ok(epoch)
8535 }
8536
8537 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
8539 self.alter_user_password_with_epoch(username, new_password)
8540 .map(|_| ())
8541 }
8542
8543 pub fn alter_user_password_with_epoch(
8544 &self,
8545 username: &str,
8546 new_password: &str,
8547 ) -> Result<Epoch> {
8548 self.require(&crate::auth::Permission::Admin)?;
8549 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
8550 let scram = crate::auth::scram_verifier(new_password).map_err(MongrelError::Other)?;
8551 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(new_password);
8552 self.alter_user_password_hash_with_epoch_inner(username, hash, Some((scram, mysql)), None)
8553 }
8554
8555 pub fn alter_user_password_hash_with_epoch(
8556 &self,
8557 username: &str,
8558 hash: String,
8559 ) -> Result<Epoch> {
8560 self.alter_user_password_hash_with_epoch_inner(username, hash, None, None)
8561 }
8562
8563 pub fn alter_user_password_hash_with_epoch_controlled<F>(
8564 &self,
8565 username: &str,
8566 hash: String,
8567 mut before_publish: F,
8568 ) -> Result<Epoch>
8569 where
8570 F: FnMut() -> Result<()>,
8571 {
8572 self.alter_user_password_hash_with_epoch_inner(
8573 username,
8574 hash,
8575 None,
8576 Some(&mut before_publish),
8577 )
8578 }
8579
8580 fn alter_user_password_hash_with_epoch_inner(
8581 &self,
8582 username: &str,
8583 hash: String,
8584 verifiers: Option<(
8585 crate::security_hardening::ScramVerifier,
8586 crate::auth::MysqlCachingSha2Verifier,
8587 )>,
8588 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8589 ) -> Result<Epoch> {
8590 let command = match verifiers {
8591 Some((scram_sha_256, mysql_caching_sha2)) => {
8592 crate::catalog_cmds::CatalogCommand::AlterUserPasswordWithAuthVerifiers {
8593 username: username.to_string(),
8594 password_hash: hash,
8595 scram_sha_256,
8596 mysql_caching_sha2,
8597 }
8598 }
8599 None => crate::catalog_cmds::CatalogCommand::AlterUserPassword {
8600 username: username.to_string(),
8601 password_hash: hash,
8602 },
8603 };
8604 self.require(&crate::catalog_cmds::required_permission(&command))?;
8605 let _ddl = self.ddl_lock.lock();
8606 let _security_write = self.security_write()?;
8607 self.require(&crate::catalog_cmds::required_permission(&command))?;
8608 let _commit = self.commit_lock.lock();
8609 let epoch = self.epoch.bump_assigned();
8610 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8611 let mut next_catalog = self.catalog.read().clone();
8612 self.apply_catalog_command_to(&mut next_catalog, command)?;
8613 next_catalog.db_epoch = epoch.0;
8614 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8615 Ok(epoch)
8616 }
8617
8618 pub fn verify_user(
8621 &self,
8622 username: &str,
8623 password: &str,
8624 ) -> Result<Option<crate::auth::UserEntry>> {
8625 let cat = self.catalog.read();
8626 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
8627 return Ok(None);
8628 };
8629 if user.password_hash.is_empty() {
8630 return Ok(None);
8631 }
8632 let ok = crate::auth::verify_password(password, &user.password_hash)
8633 .map_err(MongrelError::Other)?;
8634 if ok {
8635 Ok(Some(user.clone()))
8636 } else {
8637 Ok(None)
8638 }
8639 }
8640
8641 pub fn authenticate_principal(
8645 &self,
8646 username: &str,
8647 password: &str,
8648 ) -> Result<Option<crate::auth::Principal>> {
8649 self.authenticate_principal_inner(username, password, || {})
8650 }
8651
8652 fn authenticate_principal_inner<F>(
8653 &self,
8654 username: &str,
8655 password: &str,
8656 after_verify: F,
8657 ) -> Result<Option<crate::auth::Principal>>
8658 where
8659 F: FnOnce(),
8660 {
8661 let catalog = self.catalog.read();
8662 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
8663 return Ok(None);
8664 };
8665 if user.password_hash.is_empty()
8666 || !crate::auth::verify_password(password, &user.password_hash)
8667 .map_err(MongrelError::Other)?
8668 {
8669 return Ok(None);
8670 }
8671 after_verify();
8672 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
8673 }
8674
8675 pub fn authenticate_mysql_caching_sha2_principal(
8678 &self,
8679 username: &str,
8680 nonce: &[u8],
8681 proof: &[u8],
8682 ) -> Option<crate::auth::Principal> {
8683 let catalog = self.catalog.read();
8684 let user = catalog
8685 .users
8686 .iter()
8687 .find(|user| user.username == username)?;
8688 if !user.mysql_caching_sha2.as_ref()?.verify(nonce, proof) {
8689 return None;
8690 }
8691 Self::resolve_user_principal_from_catalog(&catalog, user)
8692 }
8693
8694 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
8696 self.set_user_admin_with_epoch(username, is_admin)
8697 .map(|_| ())
8698 }
8699
8700 pub fn set_user_admin_with_epoch(
8701 &self,
8702 username: &str,
8703 is_admin: bool,
8704 ) -> Result<Option<Epoch>> {
8705 self.set_user_admin_with_epoch_inner(username, is_admin, None)
8706 }
8707
8708 pub fn set_user_admin_with_epoch_controlled<F>(
8709 &self,
8710 username: &str,
8711 is_admin: bool,
8712 mut before_publish: F,
8713 ) -> Result<Option<Epoch>>
8714 where
8715 F: FnMut() -> Result<()>,
8716 {
8717 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
8718 }
8719
8720 fn set_user_admin_with_epoch_inner(
8721 &self,
8722 username: &str,
8723 is_admin: bool,
8724 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8725 ) -> Result<Option<Epoch>> {
8726 let command = crate::catalog_cmds::CatalogCommand::SetUserAdmin {
8727 username: username.to_string(),
8728 is_admin,
8729 };
8730 self.require(&crate::catalog_cmds::required_permission(&command))?;
8731 let _ddl = self.ddl_lock.lock();
8732 let _security_write = self.security_write()?;
8733 self.require(&crate::catalog_cmds::required_permission(&command))?;
8734 let _commit = self.commit_lock.lock();
8735 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8739 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8740 return Ok(None);
8741 }
8742 let epoch = self.epoch.bump_assigned();
8743 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8744 let mut next_catalog = self.catalog.read().clone();
8745 self.apply_catalog_command_to(&mut next_catalog, command)?;
8746 next_catalog.db_epoch = epoch.0;
8747 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8748 Ok(Some(epoch))
8749 }
8750
8751 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
8753 self.create_role_inner(name, None)
8754 }
8755
8756 pub fn create_role_controlled<F>(
8757 &self,
8758 name: &str,
8759 mut before_publish: F,
8760 ) -> Result<crate::auth::RoleEntry>
8761 where
8762 F: FnMut() -> Result<()>,
8763 {
8764 self.create_role_inner(name, Some(&mut before_publish))
8765 }
8766
8767 fn create_role_inner(
8768 &self,
8769 name: &str,
8770 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8771 ) -> Result<crate::auth::RoleEntry> {
8772 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8773 name: name.to_string(),
8774 created_epoch: 0, };
8776 self.require(&crate::catalog_cmds::required_permission(&command))?;
8777 let _ddl = self.ddl_lock.lock();
8778 let _security_write = self.security_write()?;
8779 self.require(&crate::catalog_cmds::required_permission(&command))?;
8780 let _commit = self.commit_lock.lock();
8781 let epoch = self.epoch.bump_assigned();
8782 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8783 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8784 name: name.to_string(),
8785 created_epoch: epoch.0,
8786 };
8787 let mut next_catalog = self.catalog.read().clone();
8788 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8789 let crate::catalog_cmds::CatalogDelta::RoleUpserted(entry) = delta else {
8790 return Err(MongrelError::Other(
8791 "CreateRole resolved to an unexpected catalog delta".into(),
8792 ));
8793 };
8794 next_catalog.db_epoch = epoch.0;
8795 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8796 Ok(entry)
8797 }
8798
8799 pub fn drop_role(&self, name: &str) -> Result<()> {
8801 self.drop_role_with_epoch(name).map(|_| ())
8802 }
8803
8804 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
8805 self.drop_role_with_epoch_inner(name, None)
8806 }
8807
8808 pub fn drop_role_with_epoch_controlled<F>(
8809 &self,
8810 name: &str,
8811 mut before_publish: F,
8812 ) -> Result<Epoch>
8813 where
8814 F: FnMut() -> Result<()>,
8815 {
8816 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
8817 }
8818
8819 fn drop_role_with_epoch_inner(
8820 &self,
8821 name: &str,
8822 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8823 ) -> Result<Epoch> {
8824 let command = crate::catalog_cmds::CatalogCommand::DropRole {
8825 name: name.to_string(),
8826 };
8827 self.require(&crate::catalog_cmds::required_permission(&command))?;
8828 let _ddl = self.ddl_lock.lock();
8829 let _security_write = self.security_write()?;
8830 self.require(&crate::catalog_cmds::required_permission(&command))?;
8831 let _commit = self.commit_lock.lock();
8832 let epoch = self.epoch.bump_assigned();
8833 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8834 let mut next_catalog = self.catalog.read().clone();
8835 self.apply_catalog_command_to(&mut next_catalog, command)?;
8836 next_catalog.db_epoch = epoch.0;
8837 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8838 Ok(epoch)
8839 }
8840
8841 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
8843 self.grant_role_with_epoch(username, role_name).map(|_| ())
8844 }
8845
8846 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8847 self.grant_role_with_epoch_inner(username, role_name, None)
8848 }
8849
8850 pub fn grant_role_with_epoch_controlled<F>(
8851 &self,
8852 username: &str,
8853 role_name: &str,
8854 mut before_publish: F,
8855 ) -> Result<Option<Epoch>>
8856 where
8857 F: FnMut() -> Result<()>,
8858 {
8859 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8860 }
8861
8862 fn grant_role_with_epoch_inner(
8863 &self,
8864 username: &str,
8865 role_name: &str,
8866 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8867 ) -> Result<Option<Epoch>> {
8868 let command = crate::catalog_cmds::CatalogCommand::GrantRole {
8869 username: username.to_string(),
8870 role: role_name.to_string(),
8871 };
8872 self.require(&crate::catalog_cmds::required_permission(&command))?;
8873 let _ddl = self.ddl_lock.lock();
8874 let _security_write = self.security_write()?;
8875 self.require(&crate::catalog_cmds::required_permission(&command))?;
8876 let _commit = self.commit_lock.lock();
8877 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8881 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8882 return Ok(None);
8883 }
8884 let epoch = self.epoch.bump_assigned();
8885 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8886 let mut next_catalog = self.catalog.read().clone();
8887 self.apply_catalog_command_to(&mut next_catalog, command)?;
8888 next_catalog.db_epoch = epoch.0;
8889 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8890 Ok(Some(epoch))
8891 }
8892
8893 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
8895 self.revoke_role_with_epoch(username, role_name).map(|_| ())
8896 }
8897
8898 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8899 self.revoke_role_with_epoch_inner(username, role_name, None)
8900 }
8901
8902 pub fn revoke_role_with_epoch_controlled<F>(
8903 &self,
8904 username: &str,
8905 role_name: &str,
8906 mut before_publish: F,
8907 ) -> Result<Option<Epoch>>
8908 where
8909 F: FnMut() -> Result<()>,
8910 {
8911 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8912 }
8913
8914 fn revoke_role_with_epoch_inner(
8915 &self,
8916 username: &str,
8917 role_name: &str,
8918 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8919 ) -> Result<Option<Epoch>> {
8920 let command = crate::catalog_cmds::CatalogCommand::RevokeRole {
8921 username: username.to_string(),
8922 role: role_name.to_string(),
8923 };
8924 self.require(&crate::catalog_cmds::required_permission(&command))?;
8925 let _ddl = self.ddl_lock.lock();
8926 let _security_write = self.security_write()?;
8927 self.require(&crate::catalog_cmds::required_permission(&command))?;
8928 let _commit = self.commit_lock.lock();
8929 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8933 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8934 return Ok(None);
8935 }
8936 let epoch = self.epoch.bump_assigned();
8937 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8938 let mut next_catalog = self.catalog.read().clone();
8939 self.apply_catalog_command_to(&mut next_catalog, command)?;
8940 next_catalog.db_epoch = epoch.0;
8941 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8942 Ok(Some(epoch))
8943 }
8944
8945 pub fn grant_permission(
8947 &self,
8948 role_name: &str,
8949 permission: crate::auth::Permission,
8950 ) -> Result<()> {
8951 self.grant_permission_with_epoch(role_name, permission)
8952 .map(|_| ())
8953 }
8954
8955 pub fn grant_permission_with_epoch(
8956 &self,
8957 role_name: &str,
8958 permission: crate::auth::Permission,
8959 ) -> Result<Option<Epoch>> {
8960 self.grant_permission_with_epoch_inner(role_name, permission, None)
8961 }
8962
8963 pub fn grant_permission_with_epoch_controlled<F>(
8964 &self,
8965 role_name: &str,
8966 permission: crate::auth::Permission,
8967 mut before_publish: F,
8968 ) -> Result<Option<Epoch>>
8969 where
8970 F: FnMut() -> Result<()>,
8971 {
8972 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8973 }
8974
8975 fn grant_permission_with_epoch_inner(
8976 &self,
8977 role_name: &str,
8978 permission: crate::auth::Permission,
8979 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8980 ) -> Result<Option<Epoch>> {
8981 let command = crate::catalog_cmds::CatalogCommand::GrantPermission {
8982 role: role_name.to_string(),
8983 permission,
8984 };
8985 self.require(&crate::catalog_cmds::required_permission(&command))?;
8986 let _ddl = self.ddl_lock.lock();
8987 let _security_write = self.security_write()?;
8988 self.require(&crate::catalog_cmds::required_permission(&command))?;
8989 let _commit = self.commit_lock.lock();
8990 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8994 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8995 return Ok(None);
8996 }
8997 let epoch = self.epoch.bump_assigned();
8998 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8999 let mut next_catalog = self.catalog.read().clone();
9000 self.apply_catalog_command_to(&mut next_catalog, command)?;
9001 next_catalog.db_epoch = epoch.0;
9002 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9003 Ok(Some(epoch))
9004 }
9005
9006 pub fn revoke_permission(
9008 &self,
9009 role_name: &str,
9010 permission: crate::auth::Permission,
9011 ) -> Result<()> {
9012 self.revoke_permission_with_epoch(role_name, permission)
9013 .map(|_| ())
9014 }
9015
9016 pub fn revoke_permission_with_epoch(
9017 &self,
9018 role_name: &str,
9019 permission: crate::auth::Permission,
9020 ) -> Result<Option<Epoch>> {
9021 self.revoke_permission_with_epoch_inner(role_name, permission, None)
9022 }
9023
9024 pub fn revoke_permission_with_epoch_controlled<F>(
9025 &self,
9026 role_name: &str,
9027 permission: crate::auth::Permission,
9028 mut before_publish: F,
9029 ) -> Result<Option<Epoch>>
9030 where
9031 F: FnMut() -> Result<()>,
9032 {
9033 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
9034 }
9035
9036 fn revoke_permission_with_epoch_inner(
9037 &self,
9038 role_name: &str,
9039 permission: crate::auth::Permission,
9040 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9041 ) -> Result<Option<Epoch>> {
9042 let command = crate::catalog_cmds::CatalogCommand::RevokePermission {
9043 role: role_name.to_string(),
9044 permission,
9045 };
9046 self.require(&crate::catalog_cmds::required_permission(&command))?;
9047 let _ddl = self.ddl_lock.lock();
9048 let _security_write = self.security_write()?;
9049 self.require(&crate::catalog_cmds::required_permission(&command))?;
9050 let _commit = self.commit_lock.lock();
9051 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9055 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
9056 return Ok(None);
9057 }
9058 let epoch = self.epoch.bump_assigned();
9059 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9060 let mut next_catalog = self.catalog.read().clone();
9061 self.apply_catalog_command_to(&mut next_catalog, command)?;
9062 next_catalog.db_epoch = epoch.0;
9063 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9064 Ok(Some(epoch))
9065 }
9066
9067 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
9070 let cat = self.catalog.read();
9071 Self::resolve_principal_from_catalog(&cat, username)
9072 }
9073
9074 pub fn resolve_current_principal(
9077 &self,
9078 principal: &crate::auth::Principal,
9079 ) -> Option<crate::auth::Principal> {
9080 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
9081 }
9082
9083 fn resolve_principal_from_catalog(
9088 cat: &Catalog,
9089 username: &str,
9090 ) -> Option<crate::auth::Principal> {
9091 let user = cat.users.iter().find(|u| u.username == username)?;
9092 Self::resolve_user_principal_from_catalog(cat, user)
9093 }
9094
9095 fn resolve_bound_principal_from_catalog(
9096 cat: &Catalog,
9097 principal: &crate::auth::Principal,
9098 ) -> Option<crate::auth::Principal> {
9099 let user = cat.users.iter().find(|user| {
9100 user.id == principal.user_id
9101 && user.created_epoch == principal.created_epoch
9102 && user.username == principal.username
9103 })?;
9104 Self::resolve_user_principal_from_catalog(cat, user)
9105 }
9106
9107 fn resolve_user_principal_from_catalog(
9108 cat: &Catalog,
9109 user: &crate::auth::UserEntry,
9110 ) -> Option<crate::auth::Principal> {
9111 let mut permissions = Vec::new();
9112 for role_name in &user.roles {
9113 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
9114 permissions.extend(role.permissions.iter().cloned());
9115 }
9116 }
9117 Some(crate::auth::Principal {
9118 user_id: user.id,
9119 created_epoch: user.created_epoch,
9120 username: user.username.clone(),
9121 is_admin: user.is_admin,
9122 roles: user.roles.clone(),
9123 permissions,
9124 })
9125 }
9126
9127 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
9129 match self.resolve_principal(username) {
9130 Some(p) => p.has_permission(permission),
9131 None => false,
9132 }
9133 }
9134
9135 pub fn require_auth_enabled(&self) -> bool {
9139 self.catalog.read().require_auth
9140 }
9141
9142 pub fn principal(&self) -> Option<crate::auth::Principal> {
9146 self.principal.read().clone()
9147 }
9148
9149 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
9155 if self.shared {
9156 return None;
9157 }
9158 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
9159 self.auth_state.clone(),
9160 )))
9161 }
9162
9163 pub fn refresh_principal(&self) -> Result<()> {
9177 let previous = match self.principal.read().clone() {
9178 Some(principal) => principal,
9179 None => return Ok(()),
9180 };
9181 if previous.user_id == 0 {
9185 return Ok(());
9186 }
9187 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
9188 self.refresh_security_catalog_if_stale(observed_version)?;
9189 let cat = self.catalog.read();
9190 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
9191 Some(p) => {
9192 *self.principal.write() = Some(p.clone());
9193 self.auth_state.set_principal(Some(p));
9197 Ok(())
9198 }
9199 None => Err(MongrelError::InvalidCredentials {
9200 username: previous.username,
9201 }),
9202 }
9203 }
9204
9205 pub fn security_catalog_disk_read_count(&self) -> u64 {
9208 self.security_catalog_disk_reads.load(Ordering::Relaxed)
9209 }
9210
9211 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
9223 if self.shared {
9224 return Err(MongrelError::Conflict(
9230 "enable_auth requires an exclusive Database owner; shared cores reject \
9231 auth-mode transitions"
9232 .into(),
9233 ));
9234 }
9235 let password_hash =
9236 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
9237 let scram_sha_256 =
9238 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
9239 let mysql_caching_sha2 =
9240 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
9241 let _ddl = self.ddl_lock.lock();
9242 let _security_write = self.security_write()?;
9243 let _commit = self.commit_lock.lock();
9244 let epoch = self.epoch.bump_assigned();
9245 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9246 let mut next_catalog = self.catalog.read().clone();
9247 if next_catalog.require_auth {
9248 return Err(MongrelError::InvalidArgument(
9249 "database already has require_auth enabled".into(),
9250 ));
9251 }
9252 if next_catalog
9253 .users
9254 .iter()
9255 .any(|u| u.username == admin_username)
9256 {
9257 return Err(MongrelError::InvalidArgument(format!(
9258 "user {admin_username:?} already exists"
9259 )));
9260 }
9261 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
9262 let id = next_catalog.next_user_id;
9263 next_catalog.next_user_id = id
9264 .checked_add(1)
9265 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
9266 next_catalog.users.push(crate::auth::UserEntry {
9267 id,
9268 username: admin_username.to_string(),
9269 password_hash,
9270 scram_sha_256: Some(scram_sha_256),
9271 mysql_caching_sha2: Some(mysql_caching_sha2),
9272 roles: Vec::new(),
9273 is_admin: true,
9274 created_epoch: epoch.0,
9275 });
9276 next_catalog.require_auth = true;
9277 advance_security_version(&mut next_catalog)?;
9278 next_catalog.db_epoch = epoch.0;
9279 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9280 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9284 let principal = crate::auth::Principal {
9285 user_id: id,
9286 created_epoch: epoch.0,
9287 username: admin_username.to_string(),
9288 is_admin: true,
9289 roles: Vec::new(),
9290 permissions: Vec::new(),
9291 };
9292 *self.principal.write() = Some(principal.clone());
9293 self.auth_state.set_principal(Some(principal));
9294 }
9295 publish
9296 }
9297
9298 pub fn disable_auth(&self) -> Result<()> {
9315 let _ddl = self.ddl_lock.lock();
9316 let _security_write = self.security_write()?;
9317 let _commit = self.commit_lock.lock();
9318 let epoch = self.epoch.bump_assigned();
9319 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9320 let mut next_catalog = self.catalog.read().clone();
9321 if !next_catalog.require_auth {
9322 return Err(MongrelError::InvalidArgument(
9323 "database does not have require_auth enabled".into(),
9324 ));
9325 }
9326 next_catalog.require_auth = false;
9327 advance_security_version(&mut next_catalog)?;
9328 next_catalog.db_epoch = epoch.0;
9329 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9330 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9332 *self.principal.write() = None;
9333 }
9334 publish
9335 }
9336
9337 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
9344 self.ensure_owner_process()?;
9345 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
9346 return Err(MongrelError::ReadOnlyReplica);
9347 }
9348 if self.principal.read().is_some() {
9349 self.refresh_principal().map_err(|error| match error {
9350 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
9351 error => error,
9352 })?;
9353 }
9354 if !self.catalog.read().require_auth {
9355 return Ok(());
9356 }
9357 let guard = self.principal.read();
9358 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
9359 if p.has_permission(perm) {
9360 Ok(())
9361 } else {
9362 Err(MongrelError::PermissionDenied {
9363 required: perm.clone(),
9364 principal: p.username.clone(),
9365 })
9366 }
9367 }
9368
9369 pub fn require_table(
9374 &self,
9375 table: &str,
9376 perm: crate::auth_state::RequiredPermission,
9377 ) -> Result<()> {
9378 self.require(&perm.into_permission(table))
9379 }
9380
9381 pub fn triggers(&self) -> Vec<StoredTrigger> {
9382 self.catalog
9383 .read()
9384 .triggers
9385 .iter()
9386 .map(|t| t.trigger.clone())
9387 .collect()
9388 }
9389
9390 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
9391 self.catalog
9392 .read()
9393 .triggers
9394 .iter()
9395 .find(|t| t.trigger.name == name)
9396 .map(|t| t.trigger.clone())
9397 }
9398
9399 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9400 self.create_trigger_inner(trigger, None, None)
9401 }
9402
9403 pub fn create_trigger_controlled<F>(
9404 &self,
9405 trigger: StoredTrigger,
9406 mut before_publish: F,
9407 ) -> Result<StoredTrigger>
9408 where
9409 F: FnMut() -> Result<()>,
9410 {
9411 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
9412 }
9413
9414 pub fn create_trigger_as_controlled<F>(
9415 &self,
9416 trigger: StoredTrigger,
9417 principal: Option<&crate::auth::Principal>,
9418 mut before_publish: F,
9419 ) -> Result<StoredTrigger>
9420 where
9421 F: FnMut() -> Result<()>,
9422 {
9423 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
9424 }
9425
9426 fn create_trigger_inner(
9427 &self,
9428 mut trigger: StoredTrigger,
9429 principal: Option<&crate::auth::Principal>,
9430 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9431 ) -> Result<StoredTrigger> {
9432 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9433 trigger: trigger.clone(),
9434 };
9435 self.require_for(
9436 principal,
9437 &crate::catalog_cmds::required_permission(&command),
9438 )?;
9439 let _g = self.ddl_lock.lock();
9440 let _security_write = self.security_write()?;
9441 self.require_for(
9442 principal,
9443 &crate::catalog_cmds::required_permission(&command),
9444 )?;
9445 trigger.validate()?;
9446 self.validate_trigger_references(&trigger)
9447 .map_err(trigger_validation_error)?;
9448 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9452 let commit_lock = Arc::clone(&self.commit_lock);
9453 let _c = commit_lock.lock();
9454 let epoch = self.epoch.bump_assigned();
9455 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9456 trigger.created_epoch = epoch.0;
9457 trigger.updated_epoch = epoch.0;
9458 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9459 trigger: trigger.clone(),
9460 };
9461 let mut next_catalog = self.catalog.read().clone();
9462 self.apply_catalog_command_to(&mut next_catalog, command)?;
9463 next_catalog.db_epoch = epoch.0;
9464 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9465 Ok(trigger)
9466 }
9467
9468 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9469 self.create_or_replace_trigger_inner(trigger, None, None)
9470 }
9471
9472 pub fn create_or_replace_trigger_controlled<F>(
9473 &self,
9474 trigger: StoredTrigger,
9475 mut before_publish: F,
9476 ) -> Result<StoredTrigger>
9477 where
9478 F: FnMut() -> Result<()>,
9479 {
9480 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
9481 }
9482
9483 pub fn create_or_replace_trigger_as_controlled<F>(
9484 &self,
9485 trigger: StoredTrigger,
9486 principal: Option<&crate::auth::Principal>,
9487 mut before_publish: F,
9488 ) -> Result<StoredTrigger>
9489 where
9490 F: FnMut() -> Result<()>,
9491 {
9492 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
9493 }
9494
9495 fn create_or_replace_trigger_inner(
9496 &self,
9497 trigger: StoredTrigger,
9498 principal: Option<&crate::auth::Principal>,
9499 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9500 ) -> Result<StoredTrigger> {
9501 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9502 trigger: trigger.clone(),
9503 };
9504 self.require_for(
9505 principal,
9506 &crate::catalog_cmds::required_permission(&command),
9507 )?;
9508 let _g = self.ddl_lock.lock();
9509 let _security_write = self.security_write()?;
9510 self.require_for(
9511 principal,
9512 &crate::catalog_cmds::required_permission(&command),
9513 )?;
9514 trigger.validate()?;
9515 self.validate_trigger_references(&trigger)
9516 .map_err(trigger_validation_error)?;
9517 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9521 let commit_lock = Arc::clone(&self.commit_lock);
9522 let _c = commit_lock.lock();
9523 let epoch = self.epoch.bump_assigned();
9524 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9525 let mut next_catalog = self.catalog.read().clone();
9526 let replaced = match next_catalog
9531 .triggers
9532 .iter()
9533 .position(|t| t.trigger.name == trigger.name)
9534 {
9535 Some(idx) => next_catalog.triggers[idx]
9536 .trigger
9537 .replaced(trigger.clone(), epoch.0)?,
9538 None => {
9539 let mut next = trigger;
9540 next.created_epoch = epoch.0;
9541 next.updated_epoch = epoch.0;
9542 next
9543 }
9544 };
9545 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9546 trigger: replaced.clone(),
9547 };
9548 self.apply_catalog_command_to(&mut next_catalog, command)?;
9549 next_catalog.db_epoch = epoch.0;
9550 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9551 Ok(replaced)
9552 }
9553
9554 pub fn drop_trigger(&self, name: &str) -> Result<()> {
9555 self.drop_trigger_with_epoch(name).map(|_| ())
9556 }
9557
9558 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
9560 self.drop_triggers_with_epoch(&[name.to_string()])
9561 }
9562
9563 pub fn drop_trigger_with_epoch_controlled<F>(
9564 &self,
9565 name: &str,
9566 before_publish: F,
9567 ) -> Result<Epoch>
9568 where
9569 F: FnMut() -> Result<()>,
9570 {
9571 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
9572 }
9573
9574 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
9576 self.drop_triggers_with_epoch_inner(names, None, None)
9577 }
9578
9579 pub fn drop_triggers_with_epoch_controlled<F>(
9580 &self,
9581 names: &[String],
9582 mut before_publish: F,
9583 ) -> Result<Epoch>
9584 where
9585 F: FnMut() -> Result<()>,
9586 {
9587 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
9588 }
9589
9590 pub fn drop_triggers_with_epoch_as_controlled<F>(
9591 &self,
9592 names: &[String],
9593 principal: Option<&crate::auth::Principal>,
9594 mut before_publish: F,
9595 ) -> Result<Epoch>
9596 where
9597 F: FnMut() -> Result<()>,
9598 {
9599 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
9600 }
9601
9602 fn drop_triggers_with_epoch_inner(
9603 &self,
9604 names: &[String],
9605 principal: Option<&crate::auth::Principal>,
9606 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9607 ) -> Result<Epoch> {
9608 if names.is_empty() {
9609 return Err(MongrelError::InvalidArgument(
9610 "at least one trigger name is required".into(),
9611 ));
9612 }
9613 let commands: Vec<crate::catalog_cmds::CatalogCommand> = names
9614 .iter()
9615 .map(|name| crate::catalog_cmds::CatalogCommand::DropTrigger { name: name.clone() })
9616 .collect();
9617 for command in &commands {
9618 self.require_for(
9619 principal,
9620 &crate::catalog_cmds::required_permission(command),
9621 )?;
9622 }
9623 let _g = self.ddl_lock.lock();
9624 let _security_write = self.security_write()?;
9625 for command in &commands {
9626 self.require_for(
9627 principal,
9628 &crate::catalog_cmds::required_permission(command),
9629 )?;
9630 }
9631 {
9636 let cat = self.catalog.read();
9637 for command in &commands {
9638 crate::catalog_cmds::apply(&cat, command)?;
9639 }
9640 }
9641 let commit_lock = Arc::clone(&self.commit_lock);
9642 let _c = commit_lock.lock();
9643 let epoch = self.epoch.bump_assigned();
9644 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9645 let mut next_catalog = self.catalog.read().clone();
9646 for command in commands {
9647 self.apply_catalog_command_to(&mut next_catalog, command)?;
9648 }
9649 next_catalog.db_epoch = epoch.0;
9650 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9651 Ok(epoch)
9652 }
9653
9654 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
9655 self.catalog.read().external_tables.clone()
9656 }
9657
9658 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
9659 self.catalog
9660 .read()
9661 .external_tables
9662 .iter()
9663 .find(|t| t.name == name)
9664 .cloned()
9665 }
9666
9667 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
9668 self.create_external_table_inner(entry, None)
9669 }
9670
9671 pub fn create_external_table_controlled<F>(
9672 &self,
9673 entry: ExternalTableEntry,
9674 mut before_publish: F,
9675 ) -> Result<ExternalTableEntry>
9676 where
9677 F: FnMut() -> Result<()>,
9678 {
9679 self.create_external_table_inner(entry, Some(&mut before_publish))
9680 }
9681
9682 fn create_external_table_inner(
9683 &self,
9684 mut entry: ExternalTableEntry,
9685 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9686 ) -> Result<ExternalTableEntry> {
9687 self.require(&crate::auth::Permission::Ddl)?;
9688 let _g = self.ddl_lock.lock();
9689 let _security_write = self.security_write()?;
9690 self.require(&crate::auth::Permission::Ddl)?;
9691 entry.validate()?;
9692 {
9693 let cat = self.catalog.read();
9694 if cat.live(&entry.name).is_some()
9695 || cat.external_tables.iter().any(|t| t.name == entry.name)
9696 {
9697 return Err(MongrelError::InvalidArgument(format!(
9698 "table {:?} already exists",
9699 entry.name
9700 )));
9701 }
9702 }
9703 let commit_lock = Arc::clone(&self.commit_lock);
9704 let _c = commit_lock.lock();
9705 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
9709 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
9710 let epoch = self.epoch.bump_assigned();
9711 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9712 entry.created_epoch = epoch.0;
9713 let mut next_catalog = self.catalog.read().clone();
9714 next_catalog.external_tables.push(entry.clone());
9715 next_catalog.db_epoch = epoch.0;
9716 self.publish_catalog_candidate_with_prelude(
9717 next_catalog,
9718 epoch,
9719 &mut _epoch_guard,
9720 before_publish,
9721 vec![(
9722 EXTERNAL_TABLE_ID,
9723 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9724 name: entry.name.clone(),
9725 generation_epoch: epoch.0,
9726 }),
9727 )],
9728 )?;
9729 Ok(entry)
9730 }
9731
9732 pub fn drop_external_table(&self, name: &str) -> Result<()> {
9733 self.drop_external_table_with_epoch(name).map(|_| ())
9734 }
9735
9736 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
9738 self.drop_external_table_with_epoch_inner(name, None)
9739 }
9740
9741 pub fn drop_external_table_with_epoch_controlled<F>(
9742 &self,
9743 name: &str,
9744 mut before_publish: F,
9745 ) -> Result<Epoch>
9746 where
9747 F: FnMut() -> Result<()>,
9748 {
9749 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
9750 }
9751
9752 fn drop_external_table_with_epoch_inner(
9753 &self,
9754 name: &str,
9755 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9756 ) -> Result<Epoch> {
9757 self.require(&crate::auth::Permission::Ddl)?;
9758 let _g = self.ddl_lock.lock();
9759 let _security_write = self.security_write()?;
9760 self.require(&crate::auth::Permission::Ddl)?;
9761 let commit_lock = Arc::clone(&self.commit_lock);
9762 let _c = commit_lock.lock();
9763 let epoch = self.epoch.bump_assigned();
9764 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9765 let mut next_catalog = self.catalog.read().clone();
9766 let before = next_catalog.external_tables.len();
9767 next_catalog.external_tables.retain(|t| t.name != name);
9768 if next_catalog.external_tables.len() == before {
9769 return Err(MongrelError::NotFound(format!(
9770 "external table {name:?} not found"
9771 )));
9772 }
9773 next_catalog.db_epoch = epoch.0;
9774 self.publish_catalog_candidate_with_prelude(
9775 next_catalog,
9776 epoch,
9777 &mut _epoch_guard,
9778 before_publish,
9779 vec![(
9780 EXTERNAL_TABLE_ID,
9781 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9782 name: name.to_string(),
9783 generation_epoch: epoch.0,
9784 }),
9785 )],
9786 )?;
9787 let state_dir = self.root.join(VTAB_DIR).join(name);
9788 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
9789 return Err(MongrelError::DurableCommit {
9790 epoch: epoch.0,
9791 message: format!(
9792 "external table was dropped but connector-state cleanup failed: {error}"
9793 ),
9794 });
9795 }
9796 Ok(epoch)
9797 }
9798
9799 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
9800 let txn_id = self.alloc_txn_id()?;
9801 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9802 self.commit_transaction_with_external_states(
9803 txn_id,
9804 self.epoch.visible(),
9805 Vec::new(),
9806 vec![(name.to_string(), state.to_vec())],
9807 Vec::new(),
9808 principal,
9809 catalog_bound,
9810 None,
9811 crate::txn::TxnCommitContext::internal(),
9812 )
9813 .map(|(epoch, _)| epoch)
9814 }
9815
9816 pub fn trigger_config(&self) -> TriggerConfig {
9817 use std::sync::atomic::Ordering;
9818 TriggerConfig {
9819 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
9820 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
9821 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
9822 }
9823 }
9824
9825 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
9826 use std::sync::atomic::Ordering;
9827 if config.max_depth == 0 {
9828 return Err(MongrelError::InvalidArgument(
9829 "trigger max_depth must be greater than 0".into(),
9830 ));
9831 }
9832 self.trigger_recursive
9833 .store(config.recursive_triggers, Ordering::Relaxed);
9834 self.trigger_max_depth
9835 .store(config.max_depth, Ordering::Relaxed);
9836 self.trigger_max_loop_iterations
9837 .store(config.max_loop_iterations, Ordering::Relaxed);
9838 Ok(())
9839 }
9840
9841 pub fn set_recursive_triggers(&self, recursive: bool) {
9842 use std::sync::atomic::Ordering;
9843 self.trigger_recursive.store(recursive, Ordering::Relaxed);
9844 }
9845
9846 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
9850 self.notify.subscribe()
9851 }
9852
9853 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
9854 self.change_wake.subscribe()
9855 }
9856
9857 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
9862 let control = crate::ExecutionControl::new(None);
9863 self.change_events_since_controlled(last_event_id, &control)
9864 }
9865
9866 pub fn change_events_since_controlled(
9868 &self,
9869 last_event_id: Option<&str>,
9870 control: &crate::ExecutionControl,
9871 ) -> Result<CdcBatch> {
9872 use crate::wal::Op;
9873
9874 control.checkpoint()?;
9875 let resume = match last_event_id {
9876 Some(id) => {
9877 let (epoch, index) = id.split_once(':').ok_or_else(|| {
9878 MongrelError::InvalidArgument(format!(
9879 "invalid CDC event id {id:?}; expected <epoch>:<index>"
9880 ))
9881 })?;
9882 Some((
9883 epoch.parse::<u64>().map_err(|error| {
9884 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
9885 })?,
9886 index.parse::<u32>().map_err(|error| {
9887 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
9888 })?,
9889 ))
9890 }
9891 None => None,
9892 };
9893
9894 let mut wal = self.shared_wal.lock();
9895 wal.group_sync()?;
9896 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
9897 let records = crate::wal::SharedWal::replay_with_dek_controlled(
9898 &self.root,
9899 wal_dek.as_ref(),
9900 control,
9901 CDC_MAX_WAL_RECORDS,
9902 CDC_MAX_WAL_REPLAY_BYTES,
9903 )?;
9904 drop(wal);
9905 control.checkpoint()?;
9906
9907 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
9908 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
9909 for (index, record) in records.iter().enumerate() {
9910 if index % 256 == 0 {
9911 control.checkpoint()?;
9912 }
9913 if let Op::TxnCommit { epoch, added_runs } = &record.op {
9914 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
9915 }
9916 if let Op::SpilledRows { table_id, rows } = &record.op {
9917 spilled_payloads
9918 .entry((record.txn_id, *table_id))
9919 .or_default()
9920 .push(rows);
9921 }
9922 }
9923 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
9924 let current_epoch = self.epoch.committed().0;
9925 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
9926 let gap = resume.is_some_and(|(epoch, _)| {
9927 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
9928 });
9929 if gap {
9930 return Ok(CdcBatch {
9931 events: Vec::new(),
9932 current_epoch,
9933 earliest_epoch,
9934 gap: true,
9935 });
9936 }
9937
9938 let table_names: HashMap<u64, String> = self
9939 .catalog
9940 .read()
9941 .tables
9942 .iter()
9943 .map(|entry| (entry.table_id, entry.name.clone()))
9944 .collect();
9945 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
9946 let mut retained_bytes = 0_usize;
9947 for (index, record) in records.iter().enumerate() {
9948 if index % 256 == 0 {
9949 control.checkpoint()?;
9950 }
9951 if !commits.contains_key(&record.txn_id) {
9952 continue;
9953 }
9954 let Op::BeforeImage {
9955 table_id,
9956 row_id,
9957 row,
9958 } = &record.op
9959 else {
9960 continue;
9961 };
9962 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9963 return Err(MongrelError::ResourceLimitExceeded {
9964 resource: "CDC before-image bytes",
9965 requested: row.len(),
9966 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9967 });
9968 }
9969 let before: crate::memtable::Row = bincode::deserialize(row)?;
9970 if before_images.len() >= CDC_MAX_ROWS {
9971 return Err(MongrelError::ResourceLimitExceeded {
9972 resource: "CDC before-image rows",
9973 requested: before_images.len().saturating_add(1),
9974 limit: CDC_MAX_ROWS,
9975 });
9976 }
9977 charge_cdc_bytes(
9978 &mut retained_bytes,
9979 cdc_row_storage_bytes(&before),
9980 "CDC retained bytes",
9981 )?;
9982 before_images.insert((record.txn_id, *table_id, row_id.0), before);
9983 }
9984 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
9985 let mut events = Vec::new();
9986 let mut decoded_rows = before_images.len();
9987 for (record_index, record) in records.iter().enumerate() {
9988 if record_index % 256 == 0 {
9989 control.checkpoint()?;
9990 }
9991 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
9992 continue;
9993 };
9994 let event = match &record.op {
9995 Op::Put { table_id, rows } => {
9996 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9997 return Err(MongrelError::ResourceLimitExceeded {
9998 resource: "CDC inline row bytes",
9999 requested: rows.len(),
10000 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
10001 });
10002 }
10003 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
10004 decoded_rows = decoded_rows.saturating_add(rows.len());
10005 if decoded_rows > CDC_MAX_ROWS {
10006 return Err(MongrelError::ResourceLimitExceeded {
10007 resource: "CDC decoded rows",
10008 requested: decoded_rows,
10009 limit: CDC_MAX_ROWS,
10010 });
10011 }
10012 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
10013 let mut peak_bytes = retained_bytes;
10014 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
10015 let data = serde_json::to_value(rows)
10016 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
10017 Some((*table_id, "put", data, event_bytes))
10018 }
10019 Op::Delete { table_id, row_ids } => {
10020 let before = row_ids
10021 .iter()
10022 .filter_map(|row_id| {
10023 before_images
10024 .get(&(record.txn_id, *table_id, row_id.0))
10025 .cloned()
10026 })
10027 .collect::<Vec<_>>();
10028 let event_bytes = cdc_rows_json_bytes(&before)
10029 .saturating_add(
10030 row_ids
10031 .len()
10032 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
10033 )
10034 .saturating_add(512);
10035 let mut peak_bytes = retained_bytes;
10036 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
10037 Some((
10038 *table_id,
10039 "delete",
10040 serde_json::json!({
10041 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
10042 "before": before,
10043 }),
10044 event_bytes,
10045 ))
10046 }
10047 Op::TruncateTable { table_id } => {
10048 Some((*table_id, "truncate", serde_json::Value::Null, 512))
10049 }
10050 _ => None,
10051 };
10052 if let Some((table_id, op, data, event_bytes)) = event {
10053 let index = operation_indices.entry(record.txn_id).or_insert(0);
10054 let event_position = (*commit_epoch, *index);
10055 *index = index.saturating_add(1);
10056 if resume.is_some_and(|position| event_position <= position) {
10057 continue;
10058 }
10059 if events.len() >= CDC_MAX_EVENTS {
10060 return Err(MongrelError::ResourceLimitExceeded {
10061 resource: "CDC events",
10062 requested: events.len().saturating_add(1),
10063 limit: CDC_MAX_EVENTS,
10064 });
10065 }
10066 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
10067 events.push(ChangeEvent {
10068 id: Some(format!("{}:{}", event_position.0, event_position.1)),
10069 channel: "changes".into(),
10070 table_id: Some(table_id),
10071 table: table_names.get(&table_id).cloned().unwrap_or_default(),
10072 op: op.into(),
10073 epoch: *commit_epoch,
10074 txn_id: Some(record.txn_id),
10075 message: None,
10076 data: Some(data),
10077 });
10078 }
10079 if let Op::TxnCommit { added_runs, .. } = &record.op {
10080 for run in added_runs {
10081 control.checkpoint()?;
10082 let index = operation_indices.entry(record.txn_id).or_insert(0);
10083 let event_position = (*commit_epoch, *index);
10084 *index = index.saturating_add(1);
10085 if resume.is_some_and(|position| event_position <= position) {
10086 continue;
10087 }
10088 let mut rows = if let Some(payloads) =
10089 spilled_payloads.get(&(record.txn_id, run.table_id))
10090 {
10091 let mut rows = Vec::new();
10092 for payload in payloads {
10093 control.checkpoint()?;
10094 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
10095 return Err(MongrelError::ResourceLimitExceeded {
10096 resource: "CDC spilled row bytes",
10097 requested: payload.len(),
10098 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
10099 });
10100 }
10101 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
10102 if decoded_rows
10103 .saturating_add(rows.len())
10104 .saturating_add(chunk.len())
10105 > CDC_MAX_ROWS
10106 {
10107 return Err(MongrelError::ResourceLimitExceeded {
10108 resource: "CDC decoded rows",
10109 requested: decoded_rows
10110 .saturating_add(rows.len())
10111 .saturating_add(chunk.len()),
10112 limit: CDC_MAX_ROWS,
10113 });
10114 }
10115 rows.extend(chunk);
10116 }
10117 rows
10118 } else {
10119 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
10120 return Ok(CdcBatch {
10121 events: Vec::new(),
10122 current_epoch,
10123 earliest_epoch,
10124 gap: true,
10125 });
10126 };
10127 let table = handle.lock();
10128 let mut reader = match table.open_reader(run.run_id) {
10129 Ok(reader) => reader,
10130 Err(_) => {
10131 return Ok(CdcBatch {
10132 events: Vec::new(),
10133 current_epoch,
10134 earliest_epoch,
10135 gap: true,
10136 })
10137 }
10138 };
10139 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
10140 let rows = reader.all_rows_controlled(control, remaining)?;
10141 drop(reader);
10142 drop(table);
10143 rows
10144 };
10145 for row in &mut rows {
10146 row.committed_epoch = Epoch(*commit_epoch);
10147 }
10148 decoded_rows = decoded_rows.saturating_add(rows.len());
10149 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
10150 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
10151 if events.len() >= CDC_MAX_EVENTS {
10152 return Err(MongrelError::ResourceLimitExceeded {
10153 resource: "CDC events",
10154 requested: events.len().saturating_add(1),
10155 limit: CDC_MAX_EVENTS,
10156 });
10157 }
10158 events.push(ChangeEvent {
10159 id: Some(format!("{}:{}", event_position.0, event_position.1)),
10160 channel: "changes".into(),
10161 table_id: Some(run.table_id),
10162 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
10163 op: "put_run".into(),
10164 epoch: *commit_epoch,
10165 txn_id: Some(record.txn_id),
10166 message: None,
10167 data: Some(serde_json::json!({
10168 "run_id": run.run_id.to_string(),
10169 "row_count": run.row_count,
10170 "min_row_id": run.min_row_id,
10171 "max_row_id": run.max_row_id,
10172 "rows": rows,
10173 })),
10174 });
10175 }
10176 }
10177 }
10178 control.checkpoint()?;
10179 Ok(CdcBatch {
10180 events,
10181 current_epoch,
10182 earliest_epoch,
10183 gap: false,
10184 })
10185 }
10186
10187 pub fn notify(&self, channel: &str, message: Option<String>) {
10190 let _ = self.notify.send(ChangeEvent {
10191 id: None,
10192 channel: channel.to_string(),
10193 table_id: None,
10194 table: String::new(),
10195 op: "notify".into(),
10196 epoch: self.epoch.visible().0,
10197 txn_id: None,
10198 message,
10199 data: None,
10200 });
10201 }
10202
10203 pub fn call_procedure(
10204 &self,
10205 name: &str,
10206 args: HashMap<String, crate::Value>,
10207 ) -> Result<ProcedureCallResult> {
10208 self.call_procedure_as(name, args, None)
10209 }
10210
10211 pub fn call_procedure_as(
10212 &self,
10213 name: &str,
10214 args: HashMap<String, crate::Value>,
10215 principal: Option<&crate::auth::Principal>,
10216 ) -> Result<ProcedureCallResult> {
10217 let control = crate::ExecutionControl::new(None);
10218 self.call_procedure_as_controlled(name, args, principal, &control, || true)
10219 }
10220
10221 #[doc(hidden)]
10224 pub fn call_procedure_as_bound(
10225 &self,
10226 expected: &StoredProcedure,
10227 args: HashMap<String, crate::Value>,
10228 principal: Option<&crate::auth::Principal>,
10229 ) -> Result<ProcedureCallResult> {
10230 self.require_for(principal, &crate::auth::Permission::All)?;
10231 let procedure = self.procedure(&expected.name).ok_or_else(|| {
10232 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
10233 })?;
10234 if &procedure != expected {
10235 return Err(MongrelError::Conflict(format!(
10236 "procedure {:?} changed after request authorization",
10237 expected.name
10238 )));
10239 }
10240 let control = crate::ExecutionControl::new(None);
10241 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
10242 }
10243
10244 #[doc(hidden)]
10249 pub fn call_procedure_as_controlled<F>(
10250 &self,
10251 name: &str,
10252 args: HashMap<String, crate::Value>,
10253 principal: Option<&crate::auth::Principal>,
10254 control: &crate::ExecutionControl,
10255 before_commit: F,
10256 ) -> Result<ProcedureCallResult>
10257 where
10258 F: FnOnce() -> bool,
10259 {
10260 self.require_for(principal, &crate::auth::Permission::All)?;
10264 let procedure = self
10265 .procedure(name)
10266 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
10267 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
10268 }
10269
10270 fn execute_procedure_as_controlled<F>(
10271 &self,
10272 procedure: StoredProcedure,
10273 args: HashMap<String, crate::Value>,
10274 principal: Option<&crate::auth::Principal>,
10275 control: &crate::ExecutionControl,
10276 before_commit: F,
10277 ) -> Result<ProcedureCallResult>
10278 where
10279 F: FnOnce() -> bool,
10280 {
10281 let args = bind_procedure_args(&procedure, args)?;
10282 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
10283 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
10284 if has_writes {
10285 let mut tx = self.begin_as(principal.cloned());
10286 let run = (|| {
10287 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10288 if step_index % 256 == 0 {
10289 control.checkpoint()?;
10290 }
10291 let output = self.execute_procedure_step(
10292 step,
10293 &args,
10294 &outputs,
10295 Some(&mut tx),
10296 principal,
10297 Some(control),
10298 )?;
10299 outputs.insert(step.id().to_string(), output);
10300 }
10301 control.checkpoint()?;
10302 eval_return_output(&procedure.body.return_value, &args, &outputs)
10303 })();
10304 match run {
10305 Ok(output) => {
10306 control.checkpoint()?;
10307 if !before_commit() {
10308 tx.rollback();
10309 return Err(MongrelError::Cancelled);
10310 }
10311 let epoch = tx.commit()?.0;
10312 Ok(ProcedureCallResult {
10313 epoch: Some(epoch),
10314 output,
10315 })
10316 }
10317 Err(e) => {
10318 tx.rollback();
10319 Err(e)
10320 }
10321 }
10322 } else {
10323 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10324 if step_index % 256 == 0 {
10325 control.checkpoint()?;
10326 }
10327 let output = self.execute_procedure_step(
10328 step,
10329 &args,
10330 &outputs,
10331 None,
10332 principal,
10333 Some(control),
10334 )?;
10335 outputs.insert(step.id().to_string(), output);
10336 }
10337 control.checkpoint()?;
10338 Ok(ProcedureCallResult {
10339 epoch: None,
10340 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
10341 })
10342 }
10343 }
10344
10345 fn execute_procedure_step(
10346 &self,
10347 step: &ProcedureStep,
10348 args: &HashMap<String, crate::Value>,
10349 outputs: &HashMap<String, ProcedureCallOutput>,
10350 tx: Option<&mut crate::txn::Transaction<'_>>,
10351 principal: Option<&crate::auth::Principal>,
10352 control: Option<&crate::ExecutionControl>,
10353 ) -> Result<ProcedureCallOutput> {
10354 if let Some(control) = control {
10355 control.checkpoint()?;
10356 }
10357 match step {
10358 ProcedureStep::NativeQuery {
10359 table,
10360 conditions,
10361 projection,
10362 limit,
10363 ..
10364 } => {
10365 let mut q = crate::Query::new();
10366 for condition in conditions {
10367 q = q.and(eval_condition(condition, args, outputs)?);
10368 }
10369 let fallback_control = crate::ExecutionControl::new(None);
10370 let query_control = control.unwrap_or(&fallback_control);
10371 let mut rows = self.query_for_principal_controlled(
10372 table,
10373 &q,
10374 projection.as_deref(),
10375 principal,
10376 false,
10377 query_control,
10378 )?;
10379 if let Some(limit) = limit {
10380 rows.truncate(*limit);
10381 }
10382 let mut output = Vec::with_capacity(rows.len());
10383 for (row_index, row) in rows.into_iter().enumerate() {
10384 if row_index % 256 == 0 {
10385 if let Some(control) = control {
10386 control.checkpoint()?;
10387 }
10388 }
10389 output.push(ProcedureCallRow {
10390 row_id: Some(row.row_id),
10391 columns: row.columns,
10392 });
10393 }
10394 Ok(ProcedureCallOutput::Rows(output))
10395 }
10396 ProcedureStep::Put {
10397 table,
10398 cells,
10399 returning,
10400 ..
10401 } => {
10402 let tx = tx.ok_or_else(|| {
10403 MongrelError::InvalidArgument(
10404 "write procedure step requires a transaction".into(),
10405 )
10406 })?;
10407 let cells = eval_cells(cells, args, outputs)?;
10408 if *returning {
10409 let out = tx.put_returning(table, cells)?;
10410 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10411 row_id: None,
10412 columns: out.row.columns.into_iter().collect(),
10413 }))
10414 } else {
10415 tx.put(table, cells)?;
10416 Ok(ProcedureCallOutput::Null)
10417 }
10418 }
10419 ProcedureStep::Upsert {
10420 table,
10421 cells,
10422 update_cells,
10423 returning,
10424 ..
10425 } => {
10426 let tx = tx.ok_or_else(|| {
10427 MongrelError::InvalidArgument(
10428 "write procedure step requires a transaction".into(),
10429 )
10430 })?;
10431 let cells = eval_cells(cells, args, outputs)?;
10432 let action = match update_cells {
10433 Some(update_cells) => {
10434 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
10435 }
10436 None => crate::UpsertAction::DoNothing,
10437 };
10438 let out = tx.upsert(table, cells, action)?;
10439 if *returning {
10440 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10441 row_id: None,
10442 columns: out.row.columns.into_iter().collect(),
10443 }))
10444 } else {
10445 Ok(ProcedureCallOutput::Null)
10446 }
10447 }
10448 ProcedureStep::DeleteByPk { table, pk, .. } => {
10449 let tx = tx.ok_or_else(|| {
10450 MongrelError::InvalidArgument(
10451 "write procedure step requires a transaction".into(),
10452 )
10453 })?;
10454 let pk = eval_value(pk, args, outputs)?;
10455 let handle = self.table(table)?;
10456 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
10457 MongrelError::NotFound("procedure delete_by_pk target not found".into())
10458 })?;
10459 tx.delete(table, row_id)?;
10460 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
10461 }
10462 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
10463 "DeleteRows procedure step is not supported by the core executor yet".into(),
10464 )),
10465 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
10466 "SqlQuery procedure step must be executed by mongreldb-query".into(),
10467 )),
10468 }
10469 }
10470
10471 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
10472 let cat = self.catalog.read();
10473 for step in &procedure.body.steps {
10474 let Some(table_name) = step.table() else {
10475 continue;
10476 };
10477 let schema = &cat
10478 .live(table_name)
10479 .ok_or_else(|| {
10480 MongrelError::InvalidArgument(format!(
10481 "procedure {:?} references unknown table {table_name:?}",
10482 procedure.name
10483 ))
10484 })?
10485 .schema;
10486 match step {
10487 ProcedureStep::NativeQuery {
10488 conditions,
10489 projection,
10490 ..
10491 } => {
10492 for condition in conditions {
10493 validate_condition_columns(condition, schema)?;
10494 }
10495 if let Some(projection) = projection {
10496 for id in projection {
10497 validate_column_id(*id, schema)?;
10498 }
10499 }
10500 }
10501 ProcedureStep::Put { cells, .. } => {
10502 for cell in cells {
10503 validate_column_id(cell.column_id, schema)?;
10504 }
10505 }
10506 ProcedureStep::Upsert {
10507 cells,
10508 update_cells,
10509 ..
10510 } => {
10511 for cell in cells {
10512 validate_column_id(cell.column_id, schema)?;
10513 }
10514 if let Some(update_cells) = update_cells {
10515 for cell in update_cells {
10516 validate_column_id(cell.column_id, schema)?;
10517 }
10518 }
10519 }
10520 ProcedureStep::DeleteByPk { .. } => {
10521 if schema.primary_key().is_none() {
10522 return Err(MongrelError::InvalidArgument(format!(
10523 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
10524 procedure.name
10525 )));
10526 }
10527 }
10528 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
10529 }
10530 }
10531 Ok(())
10532 }
10533
10534 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
10535 let cat = self.catalog.read();
10536 let target_schema = match &trigger.target {
10537 TriggerTarget::Table(target_name) => cat
10538 .live(target_name)
10539 .ok_or_else(|| {
10540 MongrelError::InvalidArgument(format!(
10541 "trigger {:?} references unknown target table {target_name:?}",
10542 trigger.name
10543 ))
10544 })?
10545 .schema
10546 .clone(),
10547 TriggerTarget::View(_) => Schema {
10548 columns: trigger.target_columns.clone(),
10549 ..Schema::default()
10550 },
10551 };
10552 for col in &trigger.update_of {
10553 if target_schema.column(col).is_none() {
10554 return Err(MongrelError::InvalidArgument(format!(
10555 "trigger {:?} UPDATE OF references unknown column {col:?}",
10556 trigger.name
10557 )));
10558 }
10559 }
10560 if let Some(expr) = &trigger.when {
10561 validate_trigger_expr(expr, &target_schema, trigger.event)?;
10562 }
10563 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
10564 for step in &trigger.program.steps {
10565 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
10566 {
10567 return Err(MongrelError::InvalidArgument(
10568 "SetNew trigger steps are only valid in BEFORE triggers".into(),
10569 ));
10570 }
10571 validate_trigger_step(
10572 step,
10573 &cat,
10574 &target_schema,
10575 trigger.event,
10576 &mut select_schemas,
10577 )?;
10578 }
10579 Ok(())
10580 }
10581
10582 pub fn begin(&self) -> crate::txn::Transaction<'_> {
10584 self.begin_with_isolation(crate::txn::IsolationLevel::default())
10585 }
10586
10587 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
10588 let principal = self.principal.read().clone();
10589 let catalog_bound = principal
10590 .as_ref()
10591 .is_some_and(|principal| principal.user_id != 0);
10592 (principal, catalog_bound)
10593 }
10594
10595 pub fn begin_as(
10596 &self,
10597 principal: Option<crate::auth::Principal>,
10598 ) -> crate::txn::Transaction<'_> {
10599 let catalog_bound = principal
10600 .as_ref()
10601 .is_some_and(|principal| principal.user_id != 0);
10602 let txn_id = self.alloc_txn_id();
10603 let read = self.visible_snapshot();
10604 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10605 .with_principal(principal, catalog_bound)
10606 }
10607
10608 pub fn begin_with_isolation(
10610 &self,
10611 level: crate::txn::IsolationLevel,
10612 ) -> crate::txn::Transaction<'_> {
10613 let txn_id = self.alloc_txn_id();
10614 let read = self.visible_snapshot();
10617 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10618 crate::txn::Transaction::new(self, txn_id, read, level)
10619 .with_principal(principal, catalog_bound)
10620 }
10621
10622 pub fn begin_with_external_trigger_bridge<'a>(
10625 &'a self,
10626 bridge: &'a dyn ExternalTriggerBridge,
10627 ) -> crate::txn::Transaction<'a> {
10628 let txn_id = self.alloc_txn_id();
10629 let read = self.visible_snapshot();
10630 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10631 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10632 .with_external_trigger_bridge(bridge)
10633 .with_principal(principal, catalog_bound)
10634 }
10635
10636 pub fn begin_with_external_trigger_bridge_as<'a>(
10637 &'a self,
10638 bridge: &'a dyn ExternalTriggerBridge,
10639 principal: Option<crate::auth::Principal>,
10640 ) -> crate::txn::Transaction<'a> {
10641 let catalog_bound = principal
10642 .as_ref()
10643 .is_some_and(|principal| principal.user_id != 0);
10644 let txn_id = self.alloc_txn_id();
10645 let read = self.visible_snapshot();
10646 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10647 .with_external_trigger_bridge(bridge)
10648 .with_principal(principal, catalog_bound)
10649 }
10650
10651 pub fn transaction<T>(
10653 &self,
10654 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10655 ) -> Result<T> {
10656 let mut tx = self.begin();
10657 match f(&mut tx) {
10658 Ok(out) => {
10659 tx.commit()?;
10660 Ok(out)
10661 }
10662 Err(e) => {
10663 tx.rollback();
10664 Err(e)
10665 }
10666 }
10667 }
10668
10669 pub fn transaction_with_row_ids<T>(
10670 &self,
10671 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10672 ) -> Result<(T, Vec<RowId>)> {
10673 let mut tx = self.begin();
10674 match f(&mut tx) {
10675 Ok(output) => {
10676 let (_, row_ids) = tx.commit_with_row_ids()?;
10677 Ok((output, row_ids))
10678 }
10679 Err(error) => {
10680 tx.rollback();
10681 Err(error)
10682 }
10683 }
10684 }
10685
10686 pub fn transaction_for_current_principal<T>(
10687 &self,
10688 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10689 ) -> Result<T> {
10690 if self.principal.read().is_some() {
10691 self.refresh_principal()?;
10692 }
10693 let mut transaction = self.begin_as(self.principal.read().clone());
10694 match f(&mut transaction) {
10695 Ok(output) => {
10696 transaction.commit()?;
10697 Ok(output)
10698 }
10699 Err(error) => {
10700 transaction.rollback();
10701 Err(error)
10702 }
10703 }
10704 }
10705
10706 pub fn transaction_for_current_principal_with_epoch<T>(
10707 &self,
10708 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10709 ) -> Result<(Epoch, T)> {
10710 if self.principal.read().is_some() {
10711 self.refresh_principal()?;
10712 }
10713 let mut transaction = self.begin_as(self.principal.read().clone());
10714 match f(&mut transaction) {
10715 Ok(output) => {
10716 let epoch = transaction.commit()?;
10717 Ok((epoch, output))
10718 }
10719 Err(error) => {
10720 transaction.rollback();
10721 Err(error)
10722 }
10723 }
10724 }
10725
10726 pub fn transaction_with_row_ids_for_current_principal<T>(
10727 &self,
10728 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10729 ) -> Result<(T, Vec<RowId>)> {
10730 if self.principal.read().is_some() {
10731 self.refresh_principal()?;
10732 }
10733 let mut transaction = self.begin_as(self.principal.read().clone());
10734 match f(&mut transaction) {
10735 Ok(output) => {
10736 let (_, row_ids) = transaction.commit_with_row_ids()?;
10737 Ok((output, row_ids))
10738 }
10739 Err(error) => {
10740 transaction.rollback();
10741 Err(error)
10742 }
10743 }
10744 }
10745
10746 pub fn transaction_with_external_trigger_bridge<'a, T>(
10749 &'a self,
10750 bridge: &'a dyn ExternalTriggerBridge,
10751 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10752 ) -> Result<T> {
10753 let mut tx = self.begin_with_external_trigger_bridge(bridge);
10754 match f(&mut tx) {
10755 Ok(out) => {
10756 tx.commit()?;
10757 Ok(out)
10758 }
10759 Err(e) => {
10760 tx.rollback();
10761 Err(e)
10762 }
10763 }
10764 }
10765
10766 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
10767 &'a self,
10768 bridge: &'a dyn ExternalTriggerBridge,
10769 principal: Option<crate::auth::Principal>,
10770 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10771 ) -> Result<T> {
10772 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
10773 match f(&mut tx) {
10774 Ok(output) => {
10775 tx.commit()?;
10776 Ok(output)
10777 }
10778 Err(error) => {
10779 tx.rollback();
10780 Err(error)
10781 }
10782 }
10783 }
10784
10785 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
10788 self.active_txns.register(epoch)
10789 }
10790
10791 fn fill_auto_increment_for_staging(
10792 &self,
10793 txn_id: u64,
10794 staging: &mut [(u64, crate::txn::Staged)],
10795 control: Option<&crate::ExecutionControl>,
10796 ) -> Result<()> {
10797 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
10798 for (index, (table_id, staged)) in staging.iter().enumerate() {
10799 commit_prepare_checkpoint(control, index)?;
10800 if matches!(staged, crate::txn::Staged::Put(_)) {
10801 puts_by_table.entry(*table_id).or_default().push(index);
10802 }
10803 }
10804
10805 {
10812 let mut barrier_tables: Vec<u64> = puts_by_table
10813 .iter()
10814 .filter(|(table_id, indexes)| {
10815 indexes.iter().any(|index| {
10816 matches!(
10817 &staging[*index].1,
10818 crate::txn::Staged::Put(cells)
10819 if self.table_auto_inc_would_allocate(**table_id, cells)
10820 )
10821 })
10822 })
10823 .map(|(table_id, _)| *table_id)
10824 .collect();
10825 barrier_tables.sort_unstable();
10826 for table_id in barrier_tables {
10827 self.acquire_txn_lock(
10828 txn_id,
10829 crate::locks::LockKey::sequence_barrier(&format!("auto_inc:{table_id}")),
10830 crate::locks::LockMode::Exclusive,
10831 control,
10832 )?;
10833 }
10834 }
10835
10836 let tables = self.tables.read();
10837 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
10838 commit_prepare_checkpoint(control, table_index)?;
10839 if let Some(handle) = tables.get(&table_id) {
10840 #[cfg(test)]
10841 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10842 let mut t = handle.lock();
10843 for (fill_index, index) in indexes.into_iter().enumerate() {
10844 commit_prepare_checkpoint(control, fill_index)?;
10845 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
10846 t.fill_auto_inc(cells)?;
10847 }
10848 }
10849 }
10850 }
10851 Ok(())
10852 }
10853
10854 fn expand_table_triggers(
10855 &self,
10856 txn_id: u64,
10857 staging: &mut Vec<(u64, crate::txn::Staged)>,
10858 read_epoch: Epoch,
10859 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
10860 external_states: &mut Vec<(String, Vec<u8>)>,
10861 control: Option<&crate::ExecutionControl>,
10862 ) -> Result<()> {
10863 commit_prepare_checkpoint(control, 0)?;
10864 let mut external_writes = Vec::new();
10865 let config = self.trigger_config();
10866 if config.recursive_triggers {
10867 let chunk = std::mem::take(staging);
10868 let stacks = vec![Vec::new(); chunk.len()];
10869 *staging = self.expand_trigger_chunk(
10870 txn_id,
10871 chunk,
10872 stacks,
10873 read_epoch,
10874 0,
10875 config.max_depth,
10876 &mut external_writes,
10877 &config,
10878 control,
10879 )?;
10880 self.apply_external_trigger_writes(
10881 external_writes,
10882 external_trigger_bridge,
10883 external_states,
10884 staging,
10885 control,
10886 )?;
10887 return Ok(());
10888 }
10889
10890 let mut expansion =
10891 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
10892 if !expansion.before.is_empty() {
10893 let mut final_staging = expansion.before;
10894 final_staging.extend(filter_ignored_staging(
10895 std::mem::take(staging),
10896 &expansion.ignored_indices,
10897 ));
10898 *staging = final_staging;
10899 } else if !expansion.ignored_indices.is_empty() {
10900 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
10901 }
10902 staging.append(&mut expansion.after);
10903 external_writes.append(&mut expansion.before_external);
10904 external_writes.append(&mut expansion.after_external);
10905 self.apply_external_trigger_writes(
10906 external_writes,
10907 external_trigger_bridge,
10908 external_states,
10909 staging,
10910 control,
10911 )?;
10912 Ok(())
10913 }
10914
10915 #[allow(clippy::too_many_arguments)]
10916 fn expand_trigger_chunk(
10917 &self,
10918 txn_id: u64,
10919 mut chunk: Vec<(u64, crate::txn::Staged)>,
10920 stacks: Vec<Vec<String>>,
10921 read_epoch: Epoch,
10922 depth: u32,
10923 max_depth: u32,
10924 external_writes: &mut Vec<ExternalTriggerWrite>,
10925 config: &TriggerConfig,
10926 control: Option<&crate::ExecutionControl>,
10927 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
10928 if chunk.is_empty() {
10929 return Ok(Vec::new());
10930 }
10931 commit_prepare_checkpoint(control, 0)?;
10932 self.fill_auto_increment_for_staging(txn_id, &mut chunk, control)?;
10933 let expansion = self.expand_table_triggers_once(
10934 &mut chunk,
10935 read_epoch,
10936 Some(&stacks),
10937 config,
10938 control,
10939 )?;
10940 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
10941 let stack = expansion
10942 .before_stacks
10943 .first()
10944 .or_else(|| expansion.after_stacks.first())
10945 .cloned()
10946 .unwrap_or_default();
10947 return Err(MongrelError::TriggerValidation(format!(
10948 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
10949 Self::format_trigger_stack(&stack)
10950 )));
10951 }
10952
10953 let mut out = Vec::new();
10954 external_writes.extend(expansion.before_external);
10955 out.extend(self.expand_trigger_chunk(
10956 txn_id,
10957 expansion.before,
10958 expansion.before_stacks,
10959 read_epoch,
10960 depth + 1,
10961 max_depth,
10962 external_writes,
10963 config,
10964 control,
10965 )?);
10966 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
10967 external_writes.extend(expansion.after_external);
10968 out.extend(self.expand_trigger_chunk(
10969 txn_id,
10970 expansion.after,
10971 expansion.after_stacks,
10972 read_epoch,
10973 depth + 1,
10974 max_depth,
10975 external_writes,
10976 config,
10977 control,
10978 )?);
10979 Ok(out)
10980 }
10981
10982 fn apply_external_trigger_writes(
10983 &self,
10984 writes: Vec<ExternalTriggerWrite>,
10985 bridge: Option<&dyn ExternalTriggerBridge>,
10986 external_states: &mut Vec<(String, Vec<u8>)>,
10987 staging: &mut Vec<(u64, crate::txn::Staged)>,
10988 control: Option<&crate::ExecutionControl>,
10989 ) -> Result<()> {
10990 if writes.is_empty() {
10991 return Ok(());
10992 }
10993 let bridge = bridge.ok_or_else(|| {
10994 MongrelError::TriggerValidation(
10995 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
10996 )
10997 })?;
10998 for (write_index, write) in writes.into_iter().enumerate() {
10999 commit_prepare_checkpoint(control, write_index)?;
11000 let table = write.table().to_string();
11001 let entry = self.external_table(&table).ok_or_else(|| {
11002 MongrelError::NotFound(format!("external table {table:?} not found"))
11003 })?;
11004 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
11005 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
11006 external_states.push((table, result.state));
11007 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
11008 commit_prepare_checkpoint(control, base_index)?;
11009 match base_write {
11010 ExternalTriggerBaseWrite::Put { table, cells } => {
11011 let table_id = self.table_id(&table)?;
11012 staging.push((table_id, crate::txn::Staged::Put(cells)));
11013 }
11014 ExternalTriggerBaseWrite::Delete { table, row_id } => {
11015 let table_id = self.table_id(&table)?;
11016 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
11017 }
11018 }
11019 }
11020 }
11021 dedup_external_states_in_place(external_states);
11022 Ok(())
11023 }
11024
11025 fn expand_table_triggers_once(
11026 &self,
11027 staging: &mut Vec<(u64, crate::txn::Staged)>,
11028 read_epoch: Epoch,
11029 trigger_stacks: Option<&[Vec<String>]>,
11030 config: &TriggerConfig,
11031 control: Option<&crate::ExecutionControl>,
11032 ) -> Result<TriggerExpansion> {
11033 commit_prepare_checkpoint(control, 0)?;
11034 let triggers: Vec<StoredTrigger> = self
11035 .catalog
11036 .read()
11037 .triggers
11038 .iter()
11039 .filter(|entry| {
11040 entry.trigger.enabled
11041 && matches!(
11042 entry.trigger.timing,
11043 TriggerTiming::Before | TriggerTiming::After
11044 )
11045 && matches!(entry.trigger.target, TriggerTarget::Table(_))
11046 })
11047 .map(|entry| entry.trigger.clone())
11048 .collect();
11049 if triggers.is_empty() || staging.is_empty() {
11050 return Ok(TriggerExpansion::default());
11051 }
11052
11053 let before_triggers = triggers
11054 .iter()
11055 .filter(|trigger| trigger.timing == TriggerTiming::Before)
11056 .cloned()
11057 .collect::<Vec<_>>();
11058 let after_triggers = triggers
11059 .iter()
11060 .filter(|trigger| trigger.timing == TriggerTiming::After)
11061 .cloned()
11062 .collect::<Vec<_>>();
11063
11064 let mut before_added = Vec::new();
11065 let mut before_stacks = Vec::new();
11066 let mut before_external = Vec::new();
11067 let mut ignored_indices = std::collections::BTreeSet::new();
11068 if !before_triggers.is_empty() {
11069 let before_events =
11070 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
11071 let mut out = TriggerProgramOutput {
11072 added: &mut before_added,
11073 added_stacks: &mut before_stacks,
11074 added_external: &mut before_external,
11075 ignored_indices: &mut ignored_indices,
11076 };
11077 self.execute_triggers_for_events(
11078 &before_triggers,
11079 &before_events,
11080 Some(staging),
11081 &mut out,
11082 config,
11083 read_epoch,
11084 control,
11085 )?;
11086 }
11087
11088 let after_events = if after_triggers.is_empty() {
11089 Vec::new()
11090 } else {
11091 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
11092 .into_iter()
11093 .filter(|event| {
11094 !event
11095 .op_indices
11096 .iter()
11097 .any(|idx| ignored_indices.contains(idx))
11098 })
11099 .collect()
11100 };
11101
11102 let mut after_added = Vec::new();
11103 let mut after_stacks = Vec::new();
11104 let mut after_external = Vec::new();
11105 let mut out = TriggerProgramOutput {
11106 added: &mut after_added,
11107 added_stacks: &mut after_stacks,
11108 added_external: &mut after_external,
11109 ignored_indices: &mut ignored_indices,
11110 };
11111 self.execute_triggers_for_events(
11112 &after_triggers,
11113 &after_events,
11114 None,
11115 &mut out,
11116 config,
11117 read_epoch,
11118 control,
11119 )?;
11120 Ok(TriggerExpansion {
11121 before: before_added,
11122 before_stacks,
11123 before_external,
11124 after: after_added,
11125 after_stacks,
11126 after_external,
11127 ignored_indices,
11128 })
11129 }
11130
11131 #[allow(clippy::too_many_arguments)]
11132 fn execute_triggers_for_events(
11133 &self,
11134 triggers: &[StoredTrigger],
11135 events: &[WriteEvent],
11136 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11137 out: &mut TriggerProgramOutput<'_>,
11138 config: &TriggerConfig,
11139 read_epoch: Epoch,
11140 control: Option<&crate::ExecutionControl>,
11141 ) -> Result<()> {
11142 let mut checkpoint_index = 0_usize;
11143 for event in events {
11144 for trigger in triggers {
11145 commit_prepare_checkpoint(control, checkpoint_index)?;
11146 checkpoint_index += 1;
11147 if event
11148 .op_indices
11149 .iter()
11150 .any(|idx| out.ignored_indices.contains(idx))
11151 {
11152 break;
11153 }
11154 let matches = {
11155 let cat = self.catalog.read();
11156 trigger_matches_event(trigger, event, &cat)?
11157 };
11158 if !matches {
11159 continue;
11160 }
11161 if let Some(when) = &trigger.when {
11162 if !eval_trigger_expr(when, event)? {
11163 continue;
11164 }
11165 }
11166 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
11167 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
11168 return Err(MongrelError::TriggerValidation(format!(
11169 "trigger recursion cycle detected; trigger stack: {}",
11170 Self::format_trigger_stack(&trigger_stack)
11171 )));
11172 }
11173 let outcome = match staging.as_mut() {
11174 Some(staging) => self.execute_trigger_program(
11175 trigger,
11176 event,
11177 Some(&mut **staging),
11178 out,
11179 &trigger_stack,
11180 config,
11181 read_epoch,
11182 control,
11183 )?,
11184 None => self.execute_trigger_program(
11185 trigger,
11186 event,
11187 None,
11188 out,
11189 &trigger_stack,
11190 config,
11191 read_epoch,
11192 control,
11193 )?,
11194 };
11195 if outcome == TriggerProgramOutcome::Ignore {
11196 out.ignored_indices.extend(event.op_indices.iter().copied());
11197 break;
11198 }
11199 }
11200 }
11201 Ok(())
11202 }
11203
11204 fn trigger_events_for_staging(
11205 &self,
11206 staging: &[(u64, crate::txn::Staged)],
11207 read_epoch: Epoch,
11208 trigger_stacks: Option<&[Vec<String>]>,
11209 control: Option<&crate::ExecutionControl>,
11210 ) -> Result<Vec<WriteEvent>> {
11211 use crate::txn::Staged;
11212 use std::collections::{HashMap, VecDeque};
11213
11214 let snapshot = self.snapshot_for_epoch(read_epoch);
11215 let cat = self.catalog.read();
11216 let mut table_names = HashMap::new();
11217 let mut table_schemas = HashMap::new();
11218 for entry in cat
11219 .tables
11220 .iter()
11221 .filter(|entry| matches!(entry.state, TableState::Live))
11222 {
11223 table_names.insert(entry.table_id, entry.name.clone());
11224 table_schemas.insert(entry.table_id, entry.schema.clone());
11225 }
11226 drop(cat);
11227
11228 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
11229 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11230 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11231
11232 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11233 commit_prepare_checkpoint(control, idx)?;
11234 let Some(schema) = table_schemas.get(table_id) else {
11235 continue;
11236 };
11237 let Some(pk) = schema.primary_key() else {
11238 continue;
11239 };
11240 match staged {
11241 Staged::Delete(row_id) => {
11242 let handle = self.table_by_id(*table_id)?;
11243 let Some(row) = handle.lock().get(*row_id, snapshot) else {
11244 continue;
11245 };
11246 let Some(pk_value) = row.columns.get(&pk.id) else {
11247 continue;
11248 };
11249 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
11250 delete_by_key
11251 .entry((*table_id, pk_value.encode_key()))
11252 .or_default()
11253 .push_back(idx);
11254 }
11255 Staged::Put(cells) => {
11256 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
11257 put_by_key
11258 .entry((*table_id, value.encode_key()))
11259 .or_default()
11260 .push_back(idx);
11261 }
11262 }
11263 Staged::Update { row_id, .. } => {
11264 let handle = self.table_by_id(*table_id)?;
11265 let row = handle.lock().get(*row_id, snapshot);
11266 if let Some(row) = row {
11267 old_rows.insert(idx, TriggerRowImage::from_row(row));
11268 }
11269 }
11270 Staged::Truncate => {}
11271 }
11272 }
11273
11274 let mut paired_delete = std::collections::HashSet::new();
11275 let mut paired_put = std::collections::HashSet::new();
11276 let mut events = Vec::new();
11277
11278 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
11279 commit_prepare_checkpoint(control, pair_index)?;
11280 let Some(puts) = put_by_key.get_mut(key) else {
11281 continue;
11282 };
11283 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
11284 paired_delete.insert(delete_idx);
11285 paired_put.insert(put_idx);
11286 let (table_id, _) = &staging[put_idx];
11287 let Some(table_name) = table_names.get(table_id).cloned() else {
11288 continue;
11289 };
11290 let old = old_rows.get(&delete_idx).cloned();
11291 let new = match &staging[put_idx].1 {
11292 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
11293 _ => None,
11294 };
11295 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11296 events.push(WriteEvent {
11297 table: table_name,
11298 kind: TriggerEvent::Update,
11299 old,
11300 new,
11301 changed_columns,
11302 op_indices: vec![delete_idx, put_idx],
11303 put_idx: Some(put_idx),
11304 trigger_stack: Self::trigger_stack_for_indices(
11305 trigger_stacks,
11306 &[delete_idx, put_idx],
11307 ),
11308 });
11309 }
11310 }
11311
11312 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11313 commit_prepare_checkpoint(control, idx)?;
11314 let Some(table_name) = table_names.get(table_id).cloned() else {
11315 continue;
11316 };
11317 match staged {
11318 Staged::Put(cells) if !paired_put.contains(&idx) => {
11319 let new = Some(TriggerRowImage::from_cells(cells));
11320 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
11321 events.push(WriteEvent {
11322 table: table_name,
11323 kind: TriggerEvent::Insert,
11324 old: None,
11325 new,
11326 changed_columns,
11327 op_indices: vec![idx],
11328 put_idx: Some(idx),
11329 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11330 });
11331 }
11332 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
11333 let old = match old_rows.get(&idx).cloned() {
11334 Some(old) => Some(old),
11335 None => {
11336 let handle = self.table_by_id(*table_id)?;
11337 let row = handle.lock().get(*row_id, snapshot);
11338 row.map(TriggerRowImage::from_row)
11339 }
11340 };
11341 let Some(old) = old else {
11342 continue;
11343 };
11344 let changed_columns = old.columns.keys().copied().collect();
11345 events.push(WriteEvent {
11346 table: table_name,
11347 kind: TriggerEvent::Delete,
11348 old: Some(old),
11349 new: None,
11350 changed_columns,
11351 op_indices: vec![idx],
11352 put_idx: None,
11353 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11354 });
11355 }
11356 Staged::Update { new_row: cells, .. } => {
11357 let old = old_rows.get(&idx).cloned();
11358 let new = Some(TriggerRowImage::from_cells(cells));
11359 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11360 events.push(WriteEvent {
11361 table: table_name,
11362 kind: TriggerEvent::Update,
11363 old,
11364 new,
11365 changed_columns,
11366 op_indices: vec![idx],
11367 put_idx: Some(idx),
11368 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11369 });
11370 }
11371 Staged::Truncate => {}
11372 _ => {}
11373 }
11374 }
11375
11376 Ok(events)
11377 }
11378
11379 #[allow(clippy::too_many_arguments)]
11380 fn execute_trigger_program(
11381 &self,
11382 trigger: &StoredTrigger,
11383 event: &WriteEvent,
11384 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11385 out: &mut TriggerProgramOutput<'_>,
11386 trigger_stack: &[String],
11387 config: &TriggerConfig,
11388 read_epoch: Epoch,
11389 control: Option<&crate::ExecutionControl>,
11390 ) -> Result<TriggerProgramOutcome> {
11391 let mut event = event.clone();
11392 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
11393 self.execute_trigger_steps(
11394 trigger,
11395 &trigger.program.steps,
11396 &mut event,
11397 staging,
11398 out,
11399 trigger_stack,
11400 config,
11401 &mut select_results,
11402 0,
11403 None,
11404 read_epoch,
11405 control,
11406 )
11407 }
11408
11409 #[allow(clippy::too_many_arguments)]
11410 fn execute_trigger_steps(
11411 &self,
11412 trigger: &StoredTrigger,
11413 steps: &[TriggerStep],
11414 event: &mut WriteEvent,
11415 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11416 out: &mut TriggerProgramOutput<'_>,
11417 trigger_stack: &[String],
11418 config: &TriggerConfig,
11419 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
11420 depth: u32,
11421 selected: Option<&TriggerRowImage>,
11422 read_epoch: Epoch,
11423 control: Option<&crate::ExecutionControl>,
11424 ) -> Result<TriggerProgramOutcome> {
11425 let _ = depth;
11426 for (step_index, step) in steps.iter().enumerate() {
11427 commit_prepare_checkpoint(control, step_index)?;
11428 match step {
11429 TriggerStep::SetNew { cells } => {
11430 if trigger.timing != TriggerTiming::Before {
11431 return Err(MongrelError::InvalidArgument(
11432 "SetNew trigger step is only valid in BEFORE triggers".into(),
11433 ));
11434 }
11435 let put_idx = event.put_idx.ok_or_else(|| {
11436 MongrelError::InvalidArgument(
11437 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
11438 )
11439 })?;
11440 let staging = staging.as_deref_mut().ok_or_else(|| {
11441 MongrelError::InvalidArgument(
11442 "SetNew trigger step requires mutable trigger staging".into(),
11443 )
11444 })?;
11445 let mut update_changed_columns = None;
11446 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
11447 Some(crate::txn::Staged::Put(cells)) => cells,
11448 Some(crate::txn::Staged::Update {
11449 new_row,
11450 changed_columns,
11451 ..
11452 }) => {
11453 update_changed_columns = Some(changed_columns);
11454 new_row
11455 }
11456 _ => {
11457 return Err(MongrelError::InvalidArgument(
11458 "SetNew trigger step target row is not mutable".into(),
11459 ))
11460 }
11461 };
11462 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
11463 row_cells.retain(|(id, _)| *id != column_id);
11464 row_cells.push((column_id, value.clone()));
11465 if let Some(changed_columns) = &mut update_changed_columns {
11466 changed_columns.push(column_id);
11467 }
11468 if let Some(new) = &mut event.new {
11469 new.columns.insert(column_id, value);
11470 }
11471 }
11472 row_cells.sort_by_key(|(id, _)| *id);
11473 if let Some(changed_columns) = update_changed_columns {
11474 changed_columns.sort_unstable();
11475 changed_columns.dedup();
11476 }
11477 }
11478 TriggerStep::Insert { table, cells } => {
11479 let cells = eval_trigger_cells(cells, event, selected)?;
11480 if let Ok(table_id) = self.table_id(table) {
11481 out.added.push((table_id, crate::txn::Staged::Put(cells)));
11482 out.added_stacks.push(trigger_stack.to_vec());
11483 } else if self.external_table(table).is_some() {
11484 out.added_external.push(ExternalTriggerWrite::Insert {
11485 table: table.clone(),
11486 cells,
11487 });
11488 } else {
11489 return Err(MongrelError::NotFound(format!(
11490 "trigger {:?} insert target {table:?} not found",
11491 trigger.name
11492 )));
11493 }
11494 }
11495 TriggerStep::UpdateByPk { table, pk, cells } => {
11496 let pk = eval_trigger_value(pk, event, selected)?;
11497 let cells = eval_trigger_cells(cells, event, selected)?;
11498 if self.external_table(table).is_some() {
11499 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
11500 table: table.clone(),
11501 pk,
11502 cells,
11503 });
11504 } else {
11505 let row_id = self
11506 .table(table)?
11507 .lock()
11508 .lookup_pk(&pk.encode_key())
11509 .ok_or_else(|| {
11510 MongrelError::NotFound(format!(
11511 "trigger {:?} update target not found",
11512 trigger.name
11513 ))
11514 })?;
11515 let handle = self.table(table)?;
11516 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
11517 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
11518 MongrelError::NotFound(format!(
11519 "trigger {:?} update target not visible",
11520 trigger.name
11521 ))
11522 })?;
11523 let mut changed_columns = cells
11524 .iter()
11525 .map(|(column_id, _)| *column_id)
11526 .collect::<Vec<_>>();
11527 changed_columns.sort_unstable();
11528 changed_columns.dedup();
11529 let mut merged = old.columns;
11530 for (column_id, value) in cells {
11531 merged.insert(column_id, value);
11532 }
11533 out.added.push((
11534 self.table_id(table)?,
11535 crate::txn::Staged::Update {
11536 row_id,
11537 new_row: merged.into_iter().collect(),
11538 changed_columns,
11539 },
11540 ));
11541 out.added_stacks.push(trigger_stack.to_vec());
11542 }
11543 }
11544 TriggerStep::DeleteByPk { table, pk } => {
11545 let pk = eval_trigger_value(pk, event, selected)?;
11546 if self.external_table(table).is_some() {
11547 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
11548 table: table.clone(),
11549 pk,
11550 });
11551 } else {
11552 let row_id = self
11553 .table(table)?
11554 .lock()
11555 .lookup_pk(&pk.encode_key())
11556 .ok_or_else(|| {
11557 MongrelError::NotFound(format!(
11558 "trigger {:?} delete target not found",
11559 trigger.name
11560 ))
11561 })?;
11562 out.added
11563 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
11564 out.added_stacks.push(trigger_stack.to_vec());
11565 }
11566 }
11567 TriggerStep::Select {
11568 id,
11569 table,
11570 conditions,
11571 } => {
11572 let schema = self.table(table)?.lock().schema().clone();
11573 let snapshot = self.snapshot_for_epoch(read_epoch);
11574 let handle = self.table(table)?;
11575 let rows = match control {
11576 Some(control) => {
11577 handle.lock().visible_rows_controlled(snapshot, control)?
11578 }
11579 None => handle.lock().visible_rows(snapshot)?,
11580 };
11581 let mut matched = Vec::new();
11582 for (row_index, row) in rows.into_iter().enumerate() {
11583 commit_prepare_checkpoint(control, row_index)?;
11584 let image = TriggerRowImage::from_row(row);
11585 let passes = conditions
11586 .iter()
11587 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11588 .collect::<Result<Vec<_>>>()?
11589 .into_iter()
11590 .all(|b| b);
11591 if passes {
11592 matched.push(image);
11593 }
11594 }
11595 if let Some(pk) = schema.primary_key() {
11596 matched.sort_by(|a, b| {
11597 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
11598 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
11599 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
11600 });
11601 }
11602 select_results.insert(id.clone(), matched);
11603 }
11604 TriggerStep::Foreach { id, steps } => {
11605 let rows = select_results.get(id).ok_or_else(|| {
11606 MongrelError::InvalidArgument(format!(
11607 "trigger {:?} foreach references unknown select id {id:?}",
11608 trigger.name
11609 ))
11610 })?;
11611 if rows.len() > config.max_loop_iterations as usize {
11612 return Err(MongrelError::InvalidArgument(format!(
11613 "trigger {:?} foreach exceeded max_loop_iterations ({})",
11614 trigger.name, config.max_loop_iterations
11615 )));
11616 }
11617 for (row_index, row) in rows.clone().into_iter().enumerate() {
11618 commit_prepare_checkpoint(control, row_index)?;
11619 let result = self.execute_trigger_steps(
11620 trigger,
11621 steps,
11622 event,
11623 staging.as_deref_mut(),
11624 out,
11625 trigger_stack,
11626 config,
11627 select_results,
11628 depth + 1,
11629 Some(&row),
11630 read_epoch,
11631 control,
11632 )?;
11633 if result == TriggerProgramOutcome::Ignore {
11634 return Ok(TriggerProgramOutcome::Ignore);
11635 }
11636 }
11637 }
11638 TriggerStep::DeleteWhere { table, conditions } => {
11639 let schema = self.table(table)?.lock().schema().clone();
11640 let snapshot = self.snapshot_for_epoch(read_epoch);
11641 let handle = self.table(table)?;
11642 let rows = match control {
11643 Some(control) => {
11644 handle.lock().visible_rows_controlled(snapshot, control)?
11645 }
11646 None => handle.lock().visible_rows(snapshot)?,
11647 };
11648 let table_id = self.table_id(table)?;
11649 let mut to_delete = Vec::new();
11650 for (row_index, row) in rows.into_iter().enumerate() {
11651 commit_prepare_checkpoint(control, row_index)?;
11652 let image = TriggerRowImage::from_row(row.clone());
11653 let passes = conditions
11654 .iter()
11655 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11656 .collect::<Result<Vec<_>>>()?
11657 .into_iter()
11658 .all(|b| b);
11659 if passes {
11660 to_delete.push((table_id, row.row_id));
11661 }
11662 }
11663 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
11664 commit_prepare_checkpoint(control, row_index)?;
11665 out.added
11666 .push((table_id, crate::txn::Staged::Delete(row_id)));
11667 out.added_stacks.push(trigger_stack.to_vec());
11668 }
11669 }
11670 TriggerStep::UpdateWhere {
11671 table,
11672 conditions,
11673 cells,
11674 } => {
11675 let schema = self.table(table)?.lock().schema().clone();
11676 let snapshot = self.snapshot_for_epoch(read_epoch);
11677 let handle = self.table(table)?;
11678 let rows = match control {
11679 Some(control) => {
11680 handle.lock().visible_rows_controlled(snapshot, control)?
11681 }
11682 None => handle.lock().visible_rows(snapshot)?,
11683 };
11684 let table_id = self.table_id(table)?;
11685 let mut changed_columns =
11686 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
11687 changed_columns.sort_unstable();
11688 changed_columns.dedup();
11689 let mut to_update = Vec::new();
11690 for (row_index, row) in rows.into_iter().enumerate() {
11691 commit_prepare_checkpoint(control, row_index)?;
11692 let image = TriggerRowImage::from_row(row.clone());
11693 let passes = conditions
11694 .iter()
11695 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11696 .collect::<Result<Vec<_>>>()?
11697 .into_iter()
11698 .all(|b| b);
11699 if passes {
11700 let new_cells = cells
11701 .iter()
11702 .map(|cell| {
11703 Ok((
11704 cell.column_id,
11705 eval_trigger_value(&cell.value, event, Some(&image))?,
11706 ))
11707 })
11708 .collect::<Result<Vec<_>>>()?;
11709 let mut merged = row.columns.clone();
11710 for (column_id, value) in new_cells {
11711 merged.insert(column_id, value);
11712 }
11713 to_update.push((table_id, row.row_id, merged));
11714 }
11715 }
11716 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
11717 {
11718 commit_prepare_checkpoint(control, row_index)?;
11719 out.added.push((
11720 table_id,
11721 crate::txn::Staged::Update {
11722 row_id,
11723 new_row: merged.into_iter().collect(),
11724 changed_columns: changed_columns.clone(),
11725 },
11726 ));
11727 out.added_stacks.push(trigger_stack.to_vec());
11728 }
11729 }
11730 TriggerStep::Raise { action, message } => match action {
11731 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
11732 TriggerRaiseAction::Abort
11733 | TriggerRaiseAction::Fail
11734 | TriggerRaiseAction::Rollback => {
11735 let message = eval_trigger_value(message, event, selected)?;
11736 return Err(MongrelError::TriggerValidation(format!(
11737 "trigger {:?} raised: {}; trigger stack: {}",
11738 trigger.name,
11739 trigger_message(message),
11740 Self::format_trigger_stack(trigger_stack)
11741 )));
11742 }
11743 },
11744 }
11745 }
11746 Ok(TriggerProgramOutcome::Continue)
11747 }
11748
11749 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
11750 let Some(stacks) = stacks else {
11751 return Vec::new();
11752 };
11753 let mut out = Vec::new();
11754 for idx in indices {
11755 let Some(stack) = stacks.get(*idx) else {
11756 continue;
11757 };
11758 for name in stack {
11759 if !out.iter().any(|existing| existing == name) {
11760 out.push(name.clone());
11761 }
11762 }
11763 }
11764 out
11765 }
11766
11767 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
11768 let mut out = stack.to_vec();
11769 out.push(trigger_name.to_string());
11770 out
11771 }
11772
11773 fn format_trigger_stack(stack: &[String]) -> String {
11774 if stack.is_empty() {
11775 "<root>".into()
11776 } else {
11777 stack.join(" -> ")
11778 }
11779 }
11780
11781 fn acquire_unique_key_claims(
11802 &self,
11803 txn_id: u64,
11804 staging: &[(u64, crate::txn::Staged)],
11805 control: Option<&crate::ExecutionControl>,
11806 ) -> Result<()> {
11807 let catalog = self.catalog.read();
11808 let has_uniques = staging.iter().any(|(table_id, staged)| {
11809 matches!(
11810 staged,
11811 crate::txn::Staged::Put(_) | crate::txn::Staged::Update { .. }
11812 ) && catalog.tables.iter().any(|entry| {
11813 entry.table_id == *table_id
11814 && (entry.schema.primary_key().is_some()
11815 || !entry.schema.constraints.uniques.is_empty())
11816 })
11817 });
11818 if !has_uniques {
11819 return Ok(());
11820 }
11821 let mut claims: Vec<(u64, Vec<u8>)> = Vec::new();
11822 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11823 commit_prepare_checkpoint(control, staged_index)?;
11824 let cells = match staged {
11825 crate::txn::Staged::Put(cells) => cells,
11826 crate::txn::Staged::Update { new_row, .. } => new_row,
11827 _ => continue,
11828 };
11829 let Some(entry) = catalog
11830 .tables
11831 .iter()
11832 .find(|entry| entry.table_id == *table_id)
11833 else {
11834 continue;
11835 };
11836 for column in &entry.schema.columns {
11837 if !column
11838 .flags
11839 .contains(crate::schema::ColumnFlags::PRIMARY_KEY)
11840 {
11841 continue;
11842 }
11843 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == column.id) {
11844 let mut key = b"pk:".to_vec();
11845 key.extend_from_slice(&value.encode_key());
11846 claims.push((*table_id, key));
11847 }
11848 }
11849 let cells_map: HashMap<u16, Value> = cells.iter().cloned().collect();
11854 for uc in &entry.schema.constraints.uniques {
11855 if let Some(composite) =
11856 crate::constraint::encode_composite_key(&uc.columns, &cells_map)
11857 {
11858 let mut key = format!("uq{}:", uc.id).into_bytes();
11859 key.extend_from_slice(&composite);
11860 claims.push((*table_id, key));
11861 }
11862 }
11863 }
11864 claims.sort();
11865 claims.dedup();
11866 for (table_id, key) in claims {
11867 self.acquire_txn_lock(
11868 txn_id,
11869 crate::locks::LockKey::key(table_id, key),
11870 crate::locks::LockMode::Exclusive,
11871 control,
11872 )?;
11873 }
11874 Ok(())
11875 }
11876
11877 fn acquire_fk_lock(
11881 &self,
11882 txn_id: u64,
11883 table_id: u64,
11884 key: &[u8],
11885 mode: crate::locks::LockMode,
11886 control: Option<&crate::ExecutionControl>,
11887 ) -> Result<()> {
11888 let mut namespaced = b"fk:".to_vec();
11889 namespaced.extend_from_slice(key);
11890 self.acquire_txn_lock(
11891 txn_id,
11892 crate::locks::LockKey::key(table_id, namespaced),
11893 mode,
11894 control,
11895 )?;
11896 let hook = self.fk_lock_hook.lock().clone();
11899 if let Some(hook) = hook {
11900 hook();
11901 }
11902 Ok(())
11903 }
11904
11905 fn validate_constraints(
11906 &self,
11907 txn_id: u64,
11908 staging: &mut Vec<(u64, crate::txn::Staged)>,
11909 read_epoch: Epoch,
11910 principal: Option<&crate::auth::Principal>,
11911 control: Option<&crate::ExecutionControl>,
11912 ) -> Result<()> {
11913 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
11914 use crate::memtable::Row;
11915 use crate::txn::Staged;
11916 use std::collections::HashSet;
11917
11918 commit_prepare_checkpoint(control, 0)?;
11919 let snapshot = self.snapshot_for_epoch(read_epoch);
11922 let (live, table_ids_by_name) = {
11926 let catalog = self.catalog.read();
11927 let any_constraints = catalog.tables.iter().any(|entry| {
11928 matches!(entry.state, TableState::Live | TableState::Building { .. })
11929 && !entry.schema.constraints.is_empty()
11930 });
11931 if !any_constraints {
11932 drop(catalog);
11933 self.materialize_generated_embeddings(staging, control)?;
11934 return Ok(());
11935 }
11936 let live: Vec<(u64, String, crate::schema::Schema)> = catalog
11937 .tables
11938 .iter()
11939 .filter(|entry| {
11940 matches!(entry.state, TableState::Live | TableState::Building { .. })
11941 })
11942 .map(|entry| (entry.table_id, entry.name.clone(), entry.schema.clone()))
11943 .collect();
11944 let table_ids_by_name: HashMap<String, u64> = catalog
11945 .tables
11946 .iter()
11947 .map(|entry| (entry.name.clone(), entry.table_id))
11948 .collect();
11949 (live, table_ids_by_name)
11950 };
11951
11952 let mut rows_cache: HashMap<u64, Arc<Vec<Row>>> = HashMap::new();
11955 let mut load_rows = |table_id: u64| -> Result<Arc<Vec<Row>>> {
11956 if let Some(rows) = rows_cache.get(&table_id) {
11957 return Ok(Arc::clone(rows));
11958 }
11959 let handle = self.table_by_id(table_id)?;
11960 let rows = Arc::new(match control {
11961 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
11962 None => handle.lock().visible_rows(snapshot)?,
11963 });
11964 rows_cache.insert(table_id, Arc::clone(&rows));
11965 Ok(rows)
11966 };
11967
11968 let mut processed_updates = HashSet::new();
11973 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
11974 let mut update_pass = 0_usize;
11975 loop {
11976 commit_prepare_checkpoint(control, update_pass)?;
11977 update_pass += 1;
11978 let updates: Vec<PendingUpdate> = staging
11979 .iter()
11980 .enumerate()
11981 .filter_map(|(index, (table_id, op))| match op {
11982 Staged::Update {
11983 row_id,
11984 new_row: cells,
11985 ..
11986 } if !processed_updates.contains(&index) => {
11987 Some((index, *table_id, *row_id, cells.clone()))
11988 }
11989 _ => None,
11990 })
11991 .collect();
11992 if updates.is_empty() {
11993 break;
11994 }
11995 let mut new_ops = Vec::new();
11996 for (update_index, (index, table_id, row_id, new_cells)) in
11997 updates.into_iter().enumerate()
11998 {
11999 commit_prepare_checkpoint(control, update_index)?;
12000 processed_updates.insert(index);
12001 let Some(tname) = live
12002 .iter()
12003 .find(|(id, _, _)| *id == table_id)
12004 .map(|(_, name, _)| name.as_str())
12005 else {
12006 continue;
12007 };
12008 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
12009 continue;
12010 };
12011 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
12012 for (child_id, _child_name, child_schema) in &live {
12013 for fk in &child_schema.constraints.foreign_keys {
12014 if fk.ref_table != tname {
12015 continue;
12016 }
12017 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
12018 else {
12019 continue;
12020 };
12021 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
12022 == Some(old_key.as_slice())
12023 {
12024 continue;
12025 }
12026 if fk.on_update == FkAction::Restrict {
12027 continue;
12028 }
12029 self.acquire_fk_lock(
12034 txn_id,
12035 table_id,
12036 &old_key,
12037 crate::locks::LockMode::Exclusive,
12038 control,
12039 )?;
12040 let child_rows = load_rows(*child_id)?;
12041 for (child_index, child) in child_rows.iter().enumerate() {
12042 commit_prepare_checkpoint(control, child_index)?;
12043 if encode_composite_key(&fk.columns, &child.columns).as_deref()
12044 != Some(old_key.as_slice())
12045 {
12046 continue;
12047 }
12048 if staging.iter().any(|(id, op)| {
12049 *id == *child_id
12050 && matches!(op, Staged::Delete(id) if *id == child.row_id)
12051 }) {
12052 continue;
12053 }
12054 let mut cells: Vec<(u16, Value)> = child
12055 .columns
12056 .iter()
12057 .map(|(column_id, value)| (*column_id, value.clone()))
12058 .collect();
12059 for (child_column, parent_column) in
12060 fk.columns.iter().zip(&fk.ref_columns)
12061 {
12062 cells.retain(|(column_id, _)| column_id != child_column);
12063 let value = match fk.on_update {
12064 FkAction::Cascade => {
12065 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
12066 }
12067 FkAction::SetNull => Value::Null,
12068 FkAction::Restrict => {
12069 return Err(MongrelError::Other(
12070 "restricted foreign-key update reached cascade preparation"
12071 .into(),
12072 ));
12073 }
12074 };
12075 cells.push((*child_column, value));
12076 }
12077 cells.sort_by_key(|(column_id, _)| *column_id);
12078 if let Some(existing_index) = staging.iter().position(|(id, op)| {
12079 *id == *child_id
12080 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
12081 }) {
12082 if let Staged::Update {
12083 new_row: existing,
12084 changed_columns,
12085 ..
12086 } = &mut staging[existing_index].1 {
12087 changed_columns.extend(fk.columns.iter().copied());
12088 changed_columns.sort_unstable();
12089 changed_columns.dedup();
12090 if *existing != cells {
12091 *existing = cells;
12092 processed_updates.remove(&existing_index);
12093 }
12094 }
12095 } else {
12096 new_ops.push((
12097 *child_id,
12098 Staged::Update {
12099 row_id: child.row_id,
12100 new_row: cells,
12101 changed_columns: fk.columns.clone(),
12102 },
12103 ));
12104 }
12105 }
12106 }
12107 }
12108 }
12109 staging.extend(new_ops);
12110 }
12111
12112 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
12117 let mut cascade_pass = 0_usize;
12118 loop {
12119 commit_prepare_checkpoint(control, cascade_pass)?;
12120 cascade_pass += 1;
12121 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
12122 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
12123 .iter()
12124 .filter_map(|(t, op)| match op {
12125 Staged::Delete(rid) => Some((*t, *rid)),
12126 _ => None,
12127 })
12128 .collect();
12129 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
12130 commit_prepare_checkpoint(control, delete_index)?;
12131 if !cascaded.insert((table_id, rid.0)) {
12132 continue;
12133 }
12134 let Some(tname) = live
12135 .iter()
12136 .find(|(t, _, _)| *t == table_id)
12137 .map(|(_, n, _)| n.as_str())
12138 else {
12139 continue;
12140 };
12141 let parent_handle = self.table_by_id(table_id)?;
12142 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
12143 continue;
12144 };
12145 for (child_id, _child_name, child_schema) in &live {
12146 for fk in &child_schema.constraints.foreign_keys {
12147 if fk.ref_table != tname {
12148 continue;
12149 }
12150 let Some(parent_key) =
12151 encode_composite_key(&fk.ref_columns, &parent_row.columns)
12152 else {
12153 continue;
12154 };
12155 let key_preserved = staging.iter().any(|(t, op)| {
12164 if *t != table_id {
12165 return false;
12166 }
12167 let Staged::Put(cells) = op else {
12168 return false;
12169 };
12170 let map: HashMap<u16, crate::memtable::Value> =
12171 cells.iter().cloned().collect();
12172 encode_composite_key(&fk.ref_columns, &map).as_deref()
12173 == Some(parent_key.as_slice())
12174 });
12175 if key_preserved {
12176 continue;
12177 }
12178 self.acquire_fk_lock(
12185 txn_id,
12186 table_id,
12187 &parent_key,
12188 crate::locks::LockMode::Exclusive,
12189 control,
12190 )?;
12191 match fk.on_delete {
12192 FkAction::Restrict => continue,
12193 FkAction::Cascade => {
12194 let child_rows = load_rows(*child_id)?;
12195 for (child_index, cr) in child_rows.iter().enumerate() {
12196 commit_prepare_checkpoint(control, child_index)?;
12197 if !cascaded.contains(&(*child_id, cr.row_id.0))
12198 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12199 == Some(parent_key.as_slice())
12200 {
12201 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
12202 }
12203 }
12204 }
12205 FkAction::SetNull => {
12206 let child_rows = load_rows(*child_id)?;
12207 for (child_index, cr) in child_rows.iter().enumerate() {
12208 commit_prepare_checkpoint(control, child_index)?;
12209 if !cascaded.contains(&(*child_id, cr.row_id.0))
12210 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12211 == Some(parent_key.as_slice())
12212 {
12213 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
12216 .columns
12217 .iter()
12218 .map(|(k, v)| (*k, v.clone()))
12219 .collect();
12220 for cid in &fk.columns {
12221 cells.retain(|(k, _)| k != cid);
12222 cells.push((*cid, crate::memtable::Value::Null));
12223 }
12224 new_ops.push((
12225 *child_id,
12226 Staged::Update {
12227 row_id: cr.row_id,
12228 new_row: cells,
12229 changed_columns: fk.columns.clone(),
12230 },
12231 ));
12232 }
12233 }
12234 }
12235 }
12236 }
12237 }
12238 }
12239 if new_ops.is_empty() {
12240 break;
12241 }
12242 staging.extend(new_ops);
12243 }
12244
12245 self.validate_write_permissions(staging, principal, control)?;
12249 self.validate_security_writes(staging, read_epoch, principal, control)?;
12250 self.materialize_generated_embeddings(staging, control)?;
12251
12252 let staged_deletes: HashSet<(u64, u64)> = staging
12256 .iter()
12257 .filter_map(|(t, op)| match op {
12258 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
12259 _ => None,
12260 })
12261 .collect();
12262
12263 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
12265
12266 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
12268 commit_prepare_checkpoint(control, operation_index)?;
12269 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id) else {
12270 continue;
12271 };
12272 let cells_map: HashMap<u16, crate::memtable::Value>;
12273 match op {
12274 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
12275 cells_map = cells.iter().cloned().collect();
12276
12277 if !schema.constraints.checks.is_empty() {
12279 validate_checks(&schema.constraints.checks, &cells_map)?;
12280 }
12281
12282 for uc in &schema.constraints.uniques {
12284 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
12285 continue; };
12287 let marker = (*table_id, uc.id, key.clone());
12288 if !seen_unique.insert(marker) {
12289 return Err(MongrelError::Conflict(format!(
12290 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
12291 uc.name
12292 )));
12293 }
12294 let rows = load_rows(*table_id)?;
12295 for (row_index, r) in rows.iter().enumerate() {
12296 commit_prepare_checkpoint(control, row_index)?;
12297 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
12300 continue;
12301 }
12302 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
12303 if theirs == key {
12304 return Err(MongrelError::Conflict(format!(
12305 "UNIQUE constraint '{}' on table '{tname}' violated",
12306 uc.name
12307 )));
12308 }
12309 }
12310 }
12311 }
12312
12313 for fk in &schema.constraints.foreign_keys {
12315 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
12316 continue; };
12318 let Some(parent_id) = table_ids_by_name.get(&fk.ref_table).copied() else {
12319 return Err(MongrelError::InvalidArgument(format!(
12320 "FOREIGN KEY '{}' references unknown table '{}'",
12321 fk.name, fk.ref_table
12322 )));
12323 };
12324 self.acquire_fk_lock(
12329 txn_id,
12330 parent_id,
12331 &child_key,
12332 crate::locks::LockMode::Shared,
12333 control,
12334 )?;
12335 let parent_rows = load_rows(parent_id)?;
12336 let mut found = false;
12337 for (row_index, r) in parent_rows.iter().enumerate() {
12338 commit_prepare_checkpoint(control, row_index)?;
12339 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
12340 continue;
12341 }
12342 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
12343 if pkey == child_key {
12344 found = true;
12345 break;
12346 }
12347 }
12348 }
12349 if !found {
12356 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
12357 commit_prepare_checkpoint(control, staged_index)?;
12358 if *st_table != parent_id {
12359 continue;
12360 }
12361 if let Staged::Put(pcells)
12362 | Staged::Update {
12363 new_row: pcells, ..
12364 } = st_op
12365 {
12366 let pmap: HashMap<u16, crate::memtable::Value> =
12367 pcells.iter().cloned().collect();
12368 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
12369 {
12370 if pkey == child_key {
12371 found = true;
12372 break;
12373 }
12374 }
12375 }
12376 }
12377 }
12378 if !found {
12379 return Err(MongrelError::Conflict(format!(
12380 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
12381 fk.name, fk.ref_table
12382 )));
12383 }
12384 }
12385
12386 if let Staged::Update { row_id, .. } = op {
12391 let parent_handle = self.table_by_id(*table_id)?;
12392 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
12393 continue;
12394 };
12395 for (child_id, child_name, child_schema) in &live {
12396 for fk in &child_schema.constraints.foreign_keys {
12397 if fk.ref_table != *tname || fk.on_update != FkAction::Restrict {
12398 continue;
12399 }
12400 let Some(old_key) =
12401 encode_composite_key(&fk.ref_columns, &old_parent.columns)
12402 else {
12403 continue;
12404 };
12405 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
12406 == Some(old_key.as_slice())
12407 {
12408 continue;
12409 }
12410 self.acquire_fk_lock(
12415 txn_id,
12416 *table_id,
12417 &old_key,
12418 crate::locks::LockMode::Exclusive,
12419 control,
12420 )?;
12421 for (child_index, child) in load_rows(*child_id)?.iter().enumerate()
12422 {
12423 commit_prepare_checkpoint(control, child_index)?;
12424 if encode_composite_key(&fk.columns, &child.columns).as_deref()
12425 != Some(old_key.as_slice())
12426 {
12427 continue;
12428 }
12429 let replacement = staging.iter().find_map(|(id, op)| {
12430 if *id != *child_id {
12431 return None;
12432 }
12433 match op {
12434 Staged::Delete(id) if *id == child.row_id => Some(None),
12435 Staged::Update {
12436 row_id,
12437 new_row: cells,
12438 ..
12439 } if *row_id == child.row_id => {
12440 let map: HashMap<u16, Value> =
12441 cells.iter().cloned().collect();
12442 Some(encode_composite_key(&fk.columns, &map))
12443 }
12444 _ => None,
12445 }
12446 });
12447 if replacement.is_some_and(|key| {
12448 key.as_deref() != Some(old_key.as_slice())
12449 }) {
12450 continue;
12451 }
12452 return Err(MongrelError::Conflict(format!(
12453 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
12454 fk.name
12455 )));
12456 }
12457 }
12458 }
12459 }
12460 }
12461 Staged::Delete(rid) => {
12462 let parent_handle = self.table_by_id(*table_id)?;
12466 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
12467 continue;
12468 };
12469 for (child_id, child_name, child_schema) in &live {
12470 for fk in &child_schema.constraints.foreign_keys {
12471 if fk.ref_table != *tname || fk.on_delete != FkAction::Restrict {
12472 continue;
12473 }
12474 let Some(parent_key) =
12475 encode_composite_key(&fk.ref_columns, &parent_row.columns)
12476 else {
12477 continue;
12478 };
12479 let child_rows = load_rows(*child_id)?;
12480 for (row_index, r) in child_rows.iter().enumerate() {
12481 commit_prepare_checkpoint(control, row_index)?;
12482 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
12485 continue;
12486 }
12487 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
12488 if ck == parent_key {
12489 return Err(MongrelError::Conflict(format!(
12490 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
12491 fk.name
12492 )));
12493 }
12494 }
12495 }
12496 }
12497 }
12498 }
12499 Staged::Truncate => {
12500 for (child_id, child_name, child_schema) in &live {
12504 for fk in &child_schema.constraints.foreign_keys {
12505 if fk.ref_table != *tname {
12506 continue;
12507 }
12508 let child_rows = load_rows(*child_id)?;
12509 if child_rows
12510 .iter()
12511 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
12512 {
12513 return Err(MongrelError::Conflict(format!(
12514 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
12515 fk.name
12516 )));
12517 }
12518 }
12519 }
12520 }
12521 }
12522 }
12523 Ok(())
12524 }
12525
12526 fn validate_write_permissions(
12527 &self,
12528 staging: &[(u64, crate::txn::Staged)],
12529 principal: Option<&crate::auth::Principal>,
12530 control: Option<&crate::ExecutionControl>,
12531 ) -> Result<()> {
12532 commit_prepare_checkpoint(control, 0)?;
12533 if principal.is_none() && !self.auth_state.require_auth() {
12534 return Ok(());
12535 }
12536 let principal = principal.ok_or(MongrelError::AuthRequired)?;
12537 let needs = summarize_write_permissions(staging);
12538 let catalog = self.catalog.read();
12539
12540 if needs.values().any(|need| need.truncate) {
12541 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
12542 }
12543 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
12544 commit_prepare_checkpoint(control, need_index)?;
12545 let entry = catalog
12546 .tables
12547 .iter()
12548 .find(|entry| {
12549 entry.table_id == table_id
12550 && matches!(entry.state, TableState::Live | TableState::Building { .. })
12551 })
12552 .ok_or_else(|| {
12553 MongrelError::NotFound(format!(
12554 "live table {table_id} not found during write validation"
12555 ))
12556 })?;
12557 if matches!(entry.state, TableState::Building { .. }) {
12558 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
12559 continue;
12560 }
12561 if need.insert {
12562 Self::require_columns_for_principal(
12563 &entry.name,
12564 &entry.schema,
12565 crate::auth::ColumnOperation::Insert,
12566 &need.insert_columns,
12567 principal,
12568 )?;
12569 }
12570 if need.update {
12571 Self::require_columns_for_principal(
12572 &entry.name,
12573 &entry.schema,
12574 crate::auth::ColumnOperation::Update,
12575 &need.update_columns,
12576 principal,
12577 )?;
12578 }
12579 if need.delete {
12580 self.require_for(
12581 Some(principal),
12582 &crate::auth::Permission::Delete {
12583 table: entry.name.clone(),
12584 },
12585 )?;
12586 }
12587 }
12588 Ok(())
12589 }
12590
12591 fn validate_security_writes(
12592 &self,
12593 staging: &[(u64, crate::txn::Staged)],
12594 read_epoch: Epoch,
12595 explicit_principal: Option<&crate::auth::Principal>,
12596 control: Option<&crate::ExecutionControl>,
12597 ) -> Result<()> {
12598 commit_prepare_checkpoint(control, 0)?;
12599 use crate::security::PolicyCommand;
12600 use crate::txn::Staged;
12601
12602 let catalog = self.catalog.read();
12603 if catalog.security.rls_tables.is_empty() {
12604 return Ok(());
12605 }
12606 let security = catalog.security.clone();
12607 let table_names = catalog
12608 .tables
12609 .iter()
12610 .filter(|entry| matches!(entry.state, TableState::Live))
12611 .map(|entry| (entry.table_id, entry.name.clone()))
12612 .collect::<HashMap<_, _>>();
12613 drop(catalog);
12614 if !staging.iter().any(|(table_id, _)| {
12615 table_names
12616 .get(table_id)
12617 .is_some_and(|table| security.rls_enabled(table))
12618 }) {
12619 return Ok(());
12620 }
12621 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
12622
12623 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
12624 commit_prepare_checkpoint(control, operation_index)?;
12625 let Some(table) = table_names.get(table_id) else {
12626 continue;
12627 };
12628 if !security.rls_enabled(table) || principal.is_admin {
12629 continue;
12630 }
12631 let denied = |command| MongrelError::PermissionDenied {
12632 required: match command {
12633 PolicyCommand::Insert => crate::auth::Permission::Insert {
12634 table: table.clone(),
12635 },
12636 PolicyCommand::Update => crate::auth::Permission::Update {
12637 table: table.clone(),
12638 },
12639 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
12640 crate::auth::Permission::Delete {
12641 table: table.clone(),
12642 }
12643 }
12644 },
12645 principal: principal.username.clone(),
12646 };
12647 match operation {
12648 Staged::Put(cells) => {
12649 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
12650 row.columns.extend(cells.iter().cloned());
12651 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
12652 return Err(denied(PolicyCommand::Insert));
12653 }
12654 }
12655 Staged::Update {
12656 row_id,
12657 new_row: cells,
12658 ..
12659 } => {
12660 let old = self
12661 .table_by_id(*table_id)?
12662 .lock()
12663 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12664 .ok_or_else(|| {
12665 MongrelError::NotFound(format!("row {} not found", row_id.0))
12666 })?;
12667 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
12668 return Err(denied(PolicyCommand::Update));
12669 }
12670 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
12671 new.columns.extend(cells.iter().cloned());
12672 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
12673 return Err(denied(PolicyCommand::Update));
12674 }
12675 }
12676 Staged::Delete(row_id) => {
12677 let old = self
12678 .table_by_id(*table_id)?
12679 .lock()
12680 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12681 .ok_or_else(|| {
12682 MongrelError::NotFound(format!("row {} not found", row_id.0))
12683 })?;
12684 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
12685 return Err(denied(PolicyCommand::Delete));
12686 }
12687 }
12688 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
12689 }
12690 }
12691 Ok(())
12692 }
12693
12694 #[allow(clippy::too_many_arguments)]
12701 pub(crate) fn commit_transaction_with_external_states(
12702 &self,
12703 txn_id: u64,
12704 read_epoch: Epoch,
12705 staging: Vec<(u64, crate::txn::Staged)>,
12706 external_states: Vec<(String, Vec<u8>)>,
12707 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12708 security_principal: Option<crate::auth::Principal>,
12709 principal_catalog_bound: bool,
12710 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12711 context: crate::txn::TxnCommitContext,
12712 ) -> Result<(Epoch, Vec<RowId>)> {
12713 self.commit_transaction_with_external_states_inner(
12714 txn_id,
12715 read_epoch,
12716 staging,
12717 external_states,
12718 materialized_view_updates,
12719 security_principal,
12720 principal_catalog_bound,
12721 external_trigger_bridge,
12722 context,
12723 None,
12724 None,
12725 )
12726 }
12727
12728 #[allow(clippy::too_many_arguments)]
12729 pub(crate) fn commit_transaction_with_external_states_controlled(
12730 &self,
12731 txn_id: u64,
12732 read_epoch: Epoch,
12733 staging: Vec<(u64, crate::txn::Staged)>,
12734 external_states: Vec<(String, Vec<u8>)>,
12735 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12736 security_principal: Option<crate::auth::Principal>,
12737 principal_catalog_bound: bool,
12738 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12739 context: crate::txn::TxnCommitContext,
12740 control: &crate::ExecutionControl,
12741 before_commit: &mut dyn FnMut() -> Result<()>,
12742 ) -> Result<(Epoch, Vec<RowId>)> {
12743 self.commit_transaction_with_external_states_inner(
12744 txn_id,
12745 read_epoch,
12746 staging,
12747 external_states,
12748 materialized_view_updates,
12749 security_principal,
12750 principal_catalog_bound,
12751 external_trigger_bridge,
12752 context,
12753 Some(control),
12754 Some(before_commit),
12755 )
12756 }
12757
12758 #[allow(clippy::too_many_arguments)]
12759 fn commit_transaction_with_external_states_inner(
12760 &self,
12761 txn_id: u64,
12762 read_epoch: Epoch,
12763 mut staging: Vec<(u64, crate::txn::Staged)>,
12764 external_states: Vec<(String, Vec<u8>)>,
12765 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12766 mut security_principal: Option<crate::auth::Principal>,
12767 principal_catalog_bound: bool,
12768 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12769 context: crate::txn::TxnCommitContext,
12770 control: Option<&crate::ExecutionControl>,
12771 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
12772 ) -> Result<(Epoch, Vec<RowId>)> {
12773 use crate::memtable::Row;
12774 use crate::txn::{Staged, StagedOp, WriteKey};
12775 use crate::wal::Op;
12776 use std::collections::hash_map::DefaultHasher;
12777 use std::hash::{Hash, Hasher};
12778 use std::sync::atomic::Ordering;
12779
12780 if txn_id == crate::wal::SYSTEM_TXN_ID {
12781 return Err(MongrelError::Full(
12782 "per-open transaction id namespace exhausted; reopen the database".into(),
12783 ));
12784 }
12785 if self.read_only {
12786 return Err(MongrelError::ReadOnlyReplica);
12787 }
12788 commit_prepare_checkpoint(control, 0)?;
12789 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
12790 self.refresh_security_catalog_if_stale(observed_security_version)?;
12791 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
12792 if self.auth_state.require_auth() && security_principal.is_none() {
12793 return Err(MongrelError::AuthRequired);
12794 }
12795 {
12796 let catalog = self.catalog.read();
12797 if principal_catalog_bound
12798 || security_principal
12799 .as_ref()
12800 .is_some_and(|principal| principal.user_id != 0)
12801 {
12802 let principal = security_principal
12803 .as_ref()
12804 .ok_or(MongrelError::AuthRequired)?;
12805 security_principal =
12806 Self::resolve_bound_principal_from_catalog(&catalog, principal);
12807 if security_principal.is_none() {
12808 return Err(MongrelError::AuthRequired);
12809 }
12810 }
12811 }
12812 let _replication_guard = self.replication_barrier.read();
12813 if self.poisoned.load(Ordering::Relaxed) {
12814 return Err(MongrelError::Other(
12815 "database poisoned by fsync error".into(),
12816 ));
12817 }
12818 let _operation = self.admit_operation()?;
12822
12823 if context.state.is_some() {
12832 self.acquire_txn_lock(
12833 txn_id,
12834 crate::locks::LockKey::schema_barrier(),
12835 crate::locks::LockMode::Shared,
12836 control,
12837 )?;
12838 }
12839 let _txn_lock_guard = TxnLockGuard {
12840 locks: &self.lock_manager,
12841 txn_id,
12842 };
12843
12844 let idempotency_request = match &context.idempotency {
12850 Some(request) => match self.idempotency.check_and_reserve(request)? {
12851 crate::txn::IdempotencyCheck::Replay(receipt) => {
12852 let epoch = Epoch(receipt.log_position.index);
12853 if let Some(state) = &context.state {
12854 state.committed(receipt);
12855 }
12856 return Ok((epoch, Vec::new()));
12857 }
12858 crate::txn::IdempotencyCheck::Reserved => Some(request.clone()),
12859 },
12860 None => None,
12861 };
12862 let mut idempotency_guard = idempotency_request.as_ref().map(|request| {
12864 crate::txn::IdempotencyReservationGuard::new(&self.idempotency, request.clone())
12865 });
12866 let mut external_states = dedup_external_states(external_states);
12867 if !external_states.is_empty() {
12868 let cat = self.catalog.read();
12869 for (name, _) in &external_states {
12870 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
12871 return Err(MongrelError::NotFound(format!(
12872 "external table {name:?} not found"
12873 )));
12874 }
12875 }
12876 }
12877 let prepared_materialized_views = {
12878 let mut deduplicated = HashMap::new();
12879 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
12880 {
12881 commit_prepare_checkpoint(control, definition_index)?;
12882 if definition.name.is_empty() || definition.query.trim().is_empty() {
12883 return Err(MongrelError::InvalidArgument(
12884 "materialized view name and query must not be empty".into(),
12885 ));
12886 }
12887 deduplicated.insert(definition.name.clone(), definition);
12888 }
12889 let catalog = self.catalog.read();
12890 let mut prepared = Vec::with_capacity(deduplicated.len());
12891 for (definition_index, definition) in deduplicated.into_values().enumerate() {
12892 commit_prepare_checkpoint(control, definition_index)?;
12893 let table_id = catalog
12894 .live(&definition.name)
12895 .ok_or_else(|| {
12896 MongrelError::NotFound(format!(
12897 "materialized view table {:?} not found",
12898 definition.name
12899 ))
12900 })?
12901 .table_id;
12902 prepared.push((table_id, definition));
12903 }
12904 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
12905 prepared
12906 };
12907
12908 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12911 self.expand_table_triggers(
12912 txn_id,
12913 &mut staging,
12914 read_epoch,
12915 external_trigger_bridge,
12916 &mut external_states,
12917 control,
12918 )?;
12919 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12920 external_states = dedup_external_states(external_states);
12921 let expected_external_generations = {
12922 let catalog = self.catalog.read();
12923 let mut generations = HashMap::with_capacity(external_states.len());
12924 for (name, _) in &external_states {
12925 let entry = catalog
12926 .external_tables
12927 .iter()
12928 .find(|entry| entry.name == *name)
12929 .ok_or_else(|| {
12930 MongrelError::NotFound(format!("external table {name:?} not found"))
12931 })?;
12932 generations.insert(name.clone(), entry.created_epoch);
12933 }
12934 generations
12935 };
12936
12937 self.acquire_unique_key_claims(txn_id, &staging, control)?;
12943 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
12947 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
12948 self.validate_constraints(
12954 txn_id,
12955 &mut staging,
12956 read_epoch,
12957 security_principal.as_ref(),
12958 control,
12959 )?;
12960 let mut normalized = Vec::with_capacity(staging.len() * 2);
12961 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
12962 commit_prepare_checkpoint(control, staged_index)?;
12963 match op {
12964 crate::txn::Staged::Update {
12965 row_id,
12966 new_row: cells,
12967 ..
12968 } => {
12969 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
12970 normalized.push((table_id, crate::txn::Staged::Put(cells)));
12971 }
12972 op => normalized.push((table_id, op)),
12973 }
12974 }
12975 staging = normalized;
12976 let has_changes = !staging.is_empty()
12977 || !external_states.is_empty()
12978 || !prepared_materialized_views.is_empty();
12979 let truncated_tables: HashSet<u64> = staging
12980 .iter()
12981 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
12982 .collect();
12983
12984 let write_keys = {
12985 let cat = self.catalog.read();
12986 let mut keys: Vec<WriteKey> = Vec::new();
12987 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12988 commit_prepare_checkpoint(control, staged_index)?;
12989 match staged {
12990 Staged::Put(cells) => {
12991 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
12992 for col in &entry.schema.columns {
12993 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
12994 if let Some((_, val)) =
12995 cells.iter().find(|(id, _)| *id == col.id)
12996 {
12997 let mut h = DefaultHasher::new();
12998 val.encode_key().hash(&mut h);
12999 keys.push(WriteKey::Unique {
13000 table_id: *table_id,
13001 index_id: 0,
13002 key_hash: h.finish(),
13003 });
13004 }
13005 }
13006 }
13007 if !entry.schema.constraints.uniques.is_empty() {
13014 let cells_map: HashMap<u16, Value> =
13015 cells.iter().cloned().collect();
13016 for uc in &entry.schema.constraints.uniques {
13017 if let Some(key_bytes) = crate::constraint::encode_composite_key(
13018 &uc.columns,
13019 &cells_map,
13020 ) {
13021 let mut h = DefaultHasher::new();
13022 key_bytes.hash(&mut h);
13023 keys.push(WriteKey::Unique {
13024 table_id: *table_id,
13025 index_id: uc.id | 0x8000,
13026 key_hash: h.finish(),
13027 });
13028 }
13029 }
13030 }
13031 }
13032 }
13033 Staged::Delete(rid) => keys.push(WriteKey::Row {
13034 table_id: *table_id,
13035 row_id: rid.0,
13036 }),
13037 Staged::Truncate => keys.push(WriteKey::Table {
13038 table_id: *table_id,
13039 }),
13040 Staged::Update { .. } => {
13041 return Err(MongrelError::Other(
13042 "transaction contains an unnormalized update during preparation".into(),
13043 ));
13044 }
13045 }
13046 }
13047 for (external_index, (name, _)) in external_states.iter().enumerate() {
13048 commit_prepare_checkpoint(control, external_index)?;
13049 let mut h = DefaultHasher::new();
13050 name.hash(&mut h);
13051 keys.push(WriteKey::Unique {
13052 table_id: EXTERNAL_TABLE_ID,
13053 index_id: 0,
13054 key_hash: h.finish(),
13055 });
13056 }
13057 keys
13058 };
13059
13060 let min_active = self.active_txns.min_read_epoch();
13062 if min_active < u64::MAX {
13063 self.conflicts.prune_below(Epoch(min_active));
13064 }
13065
13066 let ssi_keys = match context.isolation.canonical() {
13073 crate::txn::IsolationLevel::Serializable => {
13074 crate::txn::ssi_validation_keys(&context.read_set, &context.predicate_set)
13075 }
13076 _ => Vec::new(),
13077 };
13078
13079 if self.conflicts.conflicts(&write_keys, read_epoch) {
13083 return Err(MongrelError::Conflict(
13084 "write-write conflict (pre-validate, first-committer-wins)".into(),
13085 ));
13086 }
13087 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
13088 return Err(MongrelError::SerializationFailure {
13089 message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
13090 .into(),
13091 });
13092 }
13093 let pre_validate_version = self.conflicts.version();
13094
13095 let mut spilled: Vec<SpilledRun> = Vec::new();
13099 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
13100 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
13104 {
13105 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
13106 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
13107 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
13108 commit_prepare_checkpoint(control, staged_index)?;
13109 if let Staged::Put(cells) = staged {
13110 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
13111 bytes.saturating_add(value.estimated_bytes())
13112 });
13113 let table_bytes = table_bytes.entry(*table_id).or_default();
13114 *table_bytes = table_bytes.saturating_add(bytes);
13115 put_indexes.entry(*table_id).or_default().push(staged_index);
13116 }
13117 }
13118 let tables = self.tables.read();
13119 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
13120 commit_prepare_checkpoint(control, table_index)?;
13121 if bytes
13122 <= self
13123 .spill_threshold
13124 .load(std::sync::atomic::Ordering::Relaxed)
13125 {
13126 continue;
13127 }
13128 let Some(handle) = tables.get(&table_id) else {
13129 continue;
13130 };
13131 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
13132 let mut t = handle.lock();
13133 let tdir = t.table_dir().to_path_buf();
13134 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
13135 std::fs::create_dir_all(&txn_dir)?;
13136 let run_id = t.alloc_run_id()? as u128;
13137 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
13138 let final_path = t.run_path(run_id as u64);
13139
13140 let mut rows: Vec<Row> = Vec::new();
13141 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
13142 commit_prepare_checkpoint(control, put_index)?;
13143 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
13144 return Err(MongrelError::Other(
13145 "transaction put index no longer references a put".into(),
13146 ));
13147 };
13148 t.validate_cells_not_null(cells)?;
13149 let row_id = t.alloc_row_id()?;
13150 let mut row = Row::new(row_id, Epoch(0));
13151 row.columns.extend(std::mem::take(cells));
13152 rows.push(row);
13153 }
13154 let schema = t.schema_ref().clone();
13155 let kek = t.kek_ref().cloned();
13156 let specs = t.indexable_column_specs();
13157 drop(t);
13158
13159 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
13160 .uniform_epoch(true);
13161 if let Some(ref kek) = kek {
13162 writer = writer.with_encryption(kek.as_ref(), specs);
13163 }
13164 commit_prepare_checkpoint(control, 0)?;
13165 let header = writer.write(&pending_path, &rows)?;
13166 commit_prepare_checkpoint(control, 0)?;
13167 let row_count = header.row_count;
13168 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
13169 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
13170
13171 spilled.push(SpilledRun {
13172 table_id,
13173 run_id,
13174 pending_path,
13175 final_path,
13176 rows,
13177 row_count,
13178 min_rid,
13179 max_rid,
13180 content_hash: header.content_hash,
13181 });
13182 spilled_tables.insert(table_id);
13183 }
13184 }
13185
13186 if spill_guard.is_some() {
13188 if let Some(hook) = self.spill_hook.lock().as_ref() {
13189 hook();
13190 }
13191 }
13192
13193 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13204 .take(staging.len())
13205 .collect();
13206 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13207 .take(staging.len())
13208 .collect();
13209 {
13210 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
13211 for (index, (table_id, staged)) in staging.iter().enumerate() {
13212 commit_prepare_checkpoint(control, index)?;
13213 if matches!(staged, Staged::Delete(_))
13214 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
13215 {
13216 indexes_by_table.entry(*table_id).or_default().push(index);
13217 }
13218 }
13219 let tables = self.tables.read();
13220 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
13221 commit_prepare_checkpoint(control, table_index)?;
13222 let handle = tables.get(&table_id).ok_or_else(|| {
13223 MongrelError::NotFound(format!("table {table_id} not mounted"))
13224 })?;
13225 #[cfg(test)]
13226 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13227 let mut t = handle.lock();
13228 for (prepare_index, index) in indexes.into_iter().enumerate() {
13229 commit_prepare_checkpoint(control, prepare_index)?;
13230 match &staging[index].1 {
13231 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
13232 t.validate_cells_not_null(cells)?;
13233 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
13234 for (column, value) in cells {
13235 row.columns.insert(*column, value.clone());
13236 }
13237 prebuilt[index] = Some(row);
13238 }
13239 Staged::Delete(row_id) => {
13240 delete_images[index] =
13241 t.get(*row_id, self.snapshot_for_epoch(read_epoch));
13242 }
13243 Staged::Put(_) | Staged::Truncate => {}
13244 Staged::Update { .. } => {
13245 return Err(MongrelError::Other(
13246 "transaction contains an unnormalized update during row preparation"
13247 .into(),
13248 ));
13249 }
13250 }
13251 }
13252 }
13253 }
13254
13255 let prepared_table_handles = {
13259 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
13260 let put_table_ids: HashSet<u64> = staging
13261 .iter()
13262 .filter_map(|(table_id, staged)| {
13263 matches!(staged, Staged::Put(_)).then_some(*table_id)
13264 })
13265 .collect();
13266 let tables = self.tables.read();
13267 let mut handles = HashMap::with_capacity(table_ids.len());
13268 for (table_index, table_id) in table_ids.into_iter().enumerate() {
13269 commit_prepare_checkpoint(control, table_index)?;
13270 let handle = tables.get(&table_id).ok_or_else(|| {
13271 MongrelError::NotFound(format!("table {table_id} not mounted"))
13272 })?;
13273 if put_table_ids.contains(&table_id) {
13274 match control {
13275 Some(control) => {
13276 handle.lock().prepare_durable_publish_controlled(control)?
13277 }
13278 None => handle.lock().prepare_durable_publish()?,
13279 }
13280 }
13281 handles.insert(table_id, handle.clone());
13282 }
13283 handles
13284 };
13285
13286 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
13290
13291 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
13292 .iter()
13293 .map(|run| {
13294 (
13295 run.table_id,
13296 run.rows.iter().map(|row| row.row_id).collect(),
13297 )
13298 })
13299 .collect();
13300 let committed_row_ids = staging
13301 .iter()
13302 .enumerate()
13303 .filter_map(|(index, (table_id, staged))| {
13304 if !matches!(staged, Staged::Put(_)) {
13305 return None;
13306 }
13307 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
13308 spilled_row_ids
13309 .get_mut(table_id)
13310 .and_then(VecDeque::pop_front)
13311 })
13312 })
13313 .collect();
13314
13315 let mut prepared_external = Vec::with_capacity(external_states.len());
13316 for (external_index, (name, state)) in external_states.iter().enumerate() {
13317 commit_prepare_checkpoint(control, external_index)?;
13318 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
13319 prepared_external.push((name.clone(), state.clone(), pending));
13320 }
13321
13322 if context.state.is_some() {
13332 self.lock_manager
13333 .release(txn_id, &crate::locks::LockKey::schema_barrier());
13334 }
13335 let added_runs: Vec<crate::wal::AddedRun> = spilled
13336 .iter()
13337 .map(|s| crate::wal::AddedRun {
13338 table_id: s.table_id,
13339 run_id: s.run_id,
13340 row_count: s.row_count,
13341 level: 0,
13342 min_row_id: s.min_rid,
13343 max_row_id: s.max_rid,
13344 content_hash: s.content_hash,
13345 })
13346 .collect();
13347 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
13348 hook();
13349 }
13350 let security_guard = self.security_coordinator.gate.read();
13354 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
13355 return Err(MongrelError::Conflict(
13356 "security policy changed during write".into(),
13357 ));
13358 }
13359 if spill_guard.is_some() {
13360 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
13361 hook();
13362 }
13363 }
13364 let commit_guard = self.commit_lock.lock();
13365 let catalog_generation_result = (|| {
13366 {
13367 let catalog = self.catalog.read();
13368 for table_id in prepared_table_handles.keys() {
13369 let is_current = catalog.tables.iter().any(|entry| {
13370 entry.table_id == *table_id
13371 && matches!(entry.state, TableState::Live | TableState::Building { .. })
13372 });
13373 if !is_current {
13374 return Err(MongrelError::Conflict(format!(
13375 "table {table_id} changed during transaction preparation"
13376 )));
13377 }
13378 }
13379 for (name, created_epoch) in &expected_external_generations {
13380 let current = catalog
13381 .external_tables
13382 .iter()
13383 .find(|entry| entry.name == *name)
13384 .map(|entry| entry.created_epoch);
13385 if current != Some(*created_epoch) {
13386 return Err(MongrelError::Conflict(format!(
13387 "external table {name:?} changed during transaction preparation"
13388 )));
13389 }
13390 }
13391 for (table_id, definition) in &prepared_materialized_views {
13392 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
13393 if current != Some(*table_id) {
13394 return Err(MongrelError::Conflict(format!(
13395 "materialized view {:?} changed during transaction preparation",
13396 definition.name
13397 )));
13398 }
13399 }
13400 if trigger_catalog_binding(&catalog) != trigger_binding {
13401 return Err(MongrelError::Conflict(
13402 "trigger or referenced table generation changed during transaction preparation"
13403 .into(),
13404 ));
13405 }
13406 }
13407 let tables = self.tables.read();
13408 for (table_id, prepared) in &prepared_table_handles {
13409 if !tables
13410 .get(table_id)
13411 .is_some_and(|current| current.ptr_eq(prepared))
13412 {
13413 return Err(MongrelError::Conflict(format!(
13414 "table {table_id} mount changed during transaction preparation"
13415 )));
13416 }
13417 }
13418 Ok(())
13419 })();
13420 if let Err(error) = catalog_generation_result {
13421 drop(commit_guard);
13422 for (_, _, pending) in &prepared_external {
13423 let _ = std::fs::remove_file(pending);
13424 }
13425 return Err(error);
13426 }
13427 let new_epoch = self.epoch.assigned().next();
13431 let commit_ts = match context.read_ts {
13437 Some(read_ts) => self.hlc.commit_timestamp([read_ts]),
13438 None => self.hlc.now().map_err(|skew| {
13439 MongrelError::Other(format!(
13440 "clock skew rejected commit timestamp allocation: {skew}"
13441 ))
13442 })?,
13443 };
13444 let mut spilled_wal_bytes = 0;
13445 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
13446 let spill_prepare = (|| {
13447 for run in &mut spilled {
13448 for row in &mut run.rows {
13449 row.committed_epoch = new_epoch;
13450 row.commit_ts = Some(commit_ts);
13451 }
13452 for rows in encode_spilled_row_chunks(
13453 &run.rows,
13454 &mut spilled_wal_bytes,
13455 SPILLED_WAL_TOTAL_MAX_BYTES,
13456 control,
13457 )? {
13458 spilled_wal_records.push((
13459 run.table_id,
13460 Op::SpilledRows {
13461 table_id: run.table_id,
13462 rows,
13463 },
13464 ));
13465 }
13466 }
13467 Result::<()>::Ok(())
13468 })();
13469 if let Err(error) = spill_prepare {
13470 for (_, _, pending) in &prepared_external {
13471 let _ = std::fs::remove_file(pending);
13472 }
13473 return Err(error);
13474 }
13475 let (
13476 new_epoch,
13477 mut _epoch_guard,
13478 applies,
13479 committed_materialized_views,
13480 commit_seq,
13481 commit_ts,
13482 ) = {
13483 let mut wal = self.shared_wal.lock();
13484
13485 if self.conflicts.version() != pre_validate_version {
13490 if self.conflicts.conflicts(&write_keys, read_epoch) {
13491 drop(wal);
13494 for (_, _, pending) in &prepared_external {
13495 let _ = std::fs::remove_file(pending);
13496 }
13497 return Err(MongrelError::Conflict(
13498 "write-write conflict (sequencer delta re-check)".into(),
13499 ));
13500 }
13501 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
13502 drop(wal);
13506 for (_, _, pending) in &prepared_external {
13507 let _ = std::fs::remove_file(pending);
13508 }
13509 return Err(MongrelError::SerializationFailure {
13510 message:
13511 "a concurrent commit invalidated this transaction's reads (sequencer re-check)"
13512 .into(),
13513 });
13514 }
13515 }
13516
13517 if let Some(control) = control {
13518 if let Err(error) = control.checkpoint() {
13519 drop(wal);
13520 for (_, _, pending) in &prepared_external {
13521 let _ = std::fs::remove_file(pending);
13522 }
13523 return Err(error);
13524 }
13525 }
13526 let mut applies = Vec::<TableApplyBatch>::new();
13527 let mut apply_indexes = HashMap::<u64, usize>::new();
13528 let mut committed_materialized_views = Vec::new();
13529 let mut wal_records = spilled_wal_records;
13530
13531 let mut index = 0;
13532 while index < staging.len() {
13533 let table_id = staging[index].0;
13534 let handle = prepared_table_handles
13535 .get(&table_id)
13536 .cloned()
13537 .ok_or_else(|| {
13538 MongrelError::NotFound(format!("table {table_id} not prepared"))
13539 })?;
13540 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
13541 let index = applies.len();
13542 applies.push(TableApplyBatch {
13543 table_id,
13544 handle,
13545 ops: Vec::new(),
13546 });
13547 index
13548 });
13549
13550 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
13553 {
13554 index += 1;
13555 continue;
13556 }
13557
13558 match &staging[index].1 {
13559 Staged::Put(_) => {
13560 let mut rows = Vec::new();
13561 while index < staging.len()
13562 && staging[index].0 == table_id
13563 && matches!(&staging[index].1, Staged::Put(_))
13564 {
13565 let mut row = prebuilt[index].take().ok_or_else(|| {
13566 MongrelError::Other(
13567 "transaction prepare lost a prebuilt put row".into(),
13568 )
13569 })?;
13570 row.committed_epoch = new_epoch;
13571 row.commit_ts = Some(commit_ts);
13572 rows.push(row);
13573 index += 1;
13574 }
13575 let payload = bincode::serialize(&rows)
13576 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
13577 wal_records.push((
13578 table_id,
13579 Op::Put {
13580 table_id,
13581 rows: payload,
13582 },
13583 ));
13584 applies[batch_index].ops.push(StagedOp::Put(rows));
13585 }
13586 Staged::Delete(_) => {
13587 let mut row_ids = Vec::new();
13588 while index < staging.len()
13589 && staging[index].0 == table_id
13590 && matches!(&staging[index].1, Staged::Delete(_))
13591 {
13592 let Staged::Delete(row_id) = &staging[index].1 else {
13593 return Err(MongrelError::Other(
13594 "transaction delete batch changed during WAL preparation"
13595 .into(),
13596 ));
13597 };
13598 if let Some(before) = &delete_images[index] {
13599 wal_records.push((
13600 table_id,
13601 Op::BeforeImage {
13602 table_id,
13603 row_id: *row_id,
13604 row: bincode::serialize(before).map_err(|error| {
13605 MongrelError::Other(format!(
13606 "before-image serialize: {error}"
13607 ))
13608 })?,
13609 },
13610 ));
13611 }
13612 row_ids.push(*row_id);
13613 index += 1;
13614 }
13615 wal_records.push((
13616 table_id,
13617 Op::Delete {
13618 table_id,
13619 row_ids: row_ids.clone(),
13620 },
13621 ));
13622 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
13623 }
13624 Staged::Truncate => {
13625 wal_records.push((table_id, Op::TruncateTable { table_id }));
13626 applies[batch_index].ops.push(StagedOp::Truncate);
13627 index += 1;
13628 }
13629 Staged::Update { .. } => {
13630 return Err(MongrelError::Other(
13631 "transaction contains an unnormalized update at the sequencer".into(),
13632 ));
13633 }
13634 }
13635 }
13636
13637 for (name, state, _) in &prepared_external {
13638 wal_records.push((
13639 EXTERNAL_TABLE_ID,
13640 Op::ExternalTableState {
13641 name: name.clone(),
13642 state: state.clone(),
13643 },
13644 ));
13645 }
13646
13647 for (table_id, definition) in &prepared_materialized_views {
13648 let mut definition = definition.clone();
13649 definition.last_refresh_epoch = new_epoch.0;
13650 wal_records.push((
13651 *table_id,
13652 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
13653 name: definition.name.clone(),
13654 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
13655 }),
13656 ));
13657 committed_materialized_views.push(definition);
13658 }
13659 if !committed_materialized_views.is_empty() {
13660 let mut next_catalog = self.catalog.read().clone();
13661 for definition in &committed_materialized_views {
13662 if let Some(existing) = next_catalog
13663 .materialized_views
13664 .iter_mut()
13665 .find(|existing| existing.name == definition.name)
13666 {
13667 *existing = definition.clone();
13668 } else {
13669 next_catalog.materialized_views.push(definition.clone());
13670 }
13671 }
13672 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13673 wal_records.push((
13674 WAL_TABLE_ID,
13675 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
13676 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
13677 }),
13678 ));
13679 }
13680
13681 if let Some(control) = control {
13682 if let Err(error) = control.checkpoint() {
13683 drop(wal);
13684 for (_, _, pending) in &prepared_external {
13685 let _ = std::fs::remove_file(pending);
13686 }
13687 return Err(error);
13688 }
13689 }
13690 if let Some(before_commit) = before_commit.as_mut() {
13691 if let Err(error) = before_commit() {
13692 drop(wal);
13693 for (_, _, pending) in &prepared_external {
13694 let _ = std::fs::remove_file(pending);
13695 }
13696 return Err(error);
13697 }
13698 }
13699
13700 let assigned_epoch = self.epoch.bump_assigned();
13701 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
13702 if assigned_epoch != new_epoch {
13703 for (_, _, pending) in &prepared_external {
13704 let _ = std::fs::remove_file(pending);
13705 }
13706 return Err(MongrelError::Conflict(
13707 "commit epoch changed while sequencer lock was held".into(),
13708 ));
13709 }
13710
13711 if let Err(fault) = mongreldb_fault::inject("txn.decision.before") {
13718 for (_, _, pending) in &prepared_external {
13719 let _ = std::fs::remove_file(pending);
13720 }
13721 return Err(crate::commit_log::fault_as_io(fault));
13722 }
13723 if let Some(state) = &context.state {
13724 state.enter_commit_critical();
13725 }
13726
13727 prepared_run_links.disarm();
13731
13732 let commit_seq = self
13736 .standalone_commit_log
13737 .append_transaction(
13738 &mut wal,
13739 txn_id,
13740 new_epoch,
13741 commit_ts,
13742 wal_records,
13743 &added_runs,
13744 )
13745 .map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
13746
13747 self.conflicts.record(&write_keys, new_epoch);
13752 (
13753 new_epoch,
13754 _epoch_guard,
13755 applies,
13756 committed_materialized_views,
13757 commit_seq,
13758 commit_ts,
13759 )
13760 };
13761 drop(commit_guard);
13762
13763 let receipt =
13765 self.await_durable_commit_with_ts(txn_id, commit_seq, new_epoch, commit_ts)?;
13766 drop(security_guard);
13767
13768 if let Some(request) = &idempotency_request {
13772 if let Err(error) =
13773 self.idempotency
13774 .complete(request, txn_id, new_epoch, receipt.commit_ts)
13775 {
13776 return Err(MongrelError::DurableCommit {
13777 epoch: new_epoch.0,
13778 message: format!("idempotency record was not durable: {error}"),
13779 });
13780 }
13781 if let Some(guard) = idempotency_guard.as_mut() {
13782 guard.disarm();
13783 }
13784 }
13785
13786 let publish_result: Result<()> = {
13788 let mut first_error = None;
13789 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
13790 for run in &spilled {
13791 spilled_by_table.entry(run.table_id).or_default().push(run);
13792 }
13793 let mut modified_tables = Vec::with_capacity(applies.len());
13794 for batch in applies {
13797 #[cfg(test)]
13798 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13799 let mut t = batch.handle.lock();
13800 for op in batch.ops {
13801 match op {
13802 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
13803 StagedOp::Delete(row_ids) => {
13804 for row_id in row_ids {
13805 t.apply_delete_at(row_id, new_epoch, Some(commit_ts));
13806 }
13807 }
13808 StagedOp::Truncate => t.apply_truncate(new_epoch),
13809 }
13810 }
13811 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
13812 for run in runs {
13813 t.link_run(crate::manifest::RunRef {
13814 run_id: run.run_id,
13815 level: 0,
13816 epoch_created: new_epoch.0,
13817 row_count: run.row_count,
13818 });
13819 t.apply_run_metadata_prepared(&run.rows)?;
13820 if truncated_tables.contains(&batch.table_id) {
13821 t.set_flushed_epoch(new_epoch);
13826 }
13827 }
13828 }
13829 t.invalidate_pending_cache();
13830 drop(t);
13831 modified_tables.push(batch.handle);
13832 }
13833
13834 for handle in modified_tables {
13838 #[cfg(test)]
13839 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
13840 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
13841 first_error.get_or_insert(error);
13842 }
13843 }
13844 for (name, _, pending) in &prepared_external {
13845 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
13846 first_error.get_or_insert(error);
13847 }
13848 }
13849 if !committed_materialized_views.is_empty() {
13850 let mut next_catalog = self.catalog.read().clone();
13851 for definition in committed_materialized_views {
13852 if let Some(existing) = next_catalog
13853 .materialized_views
13854 .iter_mut()
13855 .find(|existing| existing.name == definition.name)
13856 {
13857 *existing = definition;
13858 } else {
13859 next_catalog.materialized_views.push(definition);
13860 }
13861 }
13862 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13863 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
13864 first_error.get_or_insert(error);
13865 }
13866 }
13867 match first_error {
13868 Some(error) => Err(error),
13869 None => Ok(()),
13870 }
13871 };
13872
13873 if has_changes {
13874 let _ = self.change_wake.send(());
13875 }
13876 self.finish_durable_publish(new_epoch, &mut _epoch_guard, &receipt, publish_result)?;
13877 if let Some(state) = &context.state {
13881 state.committed(receipt.clone());
13882 }
13883 mongreldb_fault::inject("txn.decision.after").map_err(|fault| {
13887 MongrelError::DurableCommit {
13888 epoch: new_epoch.0,
13889 message: fault.to_string(),
13890 }
13891 })?;
13892 Ok((new_epoch, committed_row_ids))
13893 }
13894
13895 pub fn visible_snapshot(&self) -> Snapshot {
13899 self.snapshot_for_epoch(self.epoch.visible())
13900 }
13901
13902 pub fn snapshot_for_epoch(&self, epoch: Epoch) -> Snapshot {
13907 let commit_ts = self
13908 .commit_ts_for_epoch(epoch)
13909 .filter(|ts| *ts != mongreldb_types::hlc::HlcTimestamp::ZERO)
13910 .or_else(|| self.hlc.now().ok())
13911 .unwrap_or(mongreldb_types::hlc::HlcTimestamp::MAX);
13912 Snapshot::at_hlc(epoch, commit_ts)
13913 }
13914
13915 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
13918 let snap = self.visible_snapshot();
13919 let g = self.snapshots.register(snap.epoch);
13920 (snap, g)
13921 }
13922
13923 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
13926 let snap = self.visible_snapshot();
13927 let g = self.snapshots.register_owned(snap.epoch);
13928 (snap, g)
13929 }
13930
13931 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
13936 let _guard = self.ddl_lock.lock();
13937 let current = self.epoch.visible();
13938 let (old_epochs, old_start) = self.snapshots.history_config();
13939 let earliest_already_guaranteed = if old_epochs == 0 {
13940 current
13941 } else {
13942 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
13943 };
13944 let start = if epochs == 0 {
13945 current
13946 } else {
13947 earliest_already_guaranteed
13948 };
13949 let published = std::cell::Cell::new(false);
13950 let result = write_history_retention(&self.root, epochs, start, || {
13951 self.snapshots.configure_history(epochs, start);
13952 published.set(true);
13953 });
13954 match result {
13955 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
13956 epoch: current.0,
13957 message: format!("history-retention publication was not durable: {error}"),
13958 }),
13959 result => result,
13960 }
13961 }
13962
13963 pub fn history_retention_epochs(&self) -> u64 {
13964 self.snapshots.history_config().0
13965 }
13966
13967 pub fn earliest_retained_epoch(&self) -> Epoch {
13968 let current = self.epoch.visible();
13969 self.snapshots.history_floor(current).unwrap_or(current)
13970 }
13971
13972 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
13977 let current = self.epoch.visible();
13978 if epoch > current {
13979 return Err(MongrelError::InvalidArgument(format!(
13980 "epoch {} is in the future; current epoch is {}",
13981 epoch.0, current.0
13982 )));
13983 }
13984 let earliest = self.earliest_retained_epoch();
13985 if epoch < earliest {
13986 return Err(MongrelError::InvalidArgument(format!(
13987 "epoch {} is no longer retained; earliest available epoch is {}",
13988 epoch.0, earliest.0
13989 )));
13990 }
13991 let guard = self.snapshots.register_owned(epoch);
13992 let snap = match self.commit_ts_for_epoch(epoch) {
13993 Some(commit_ts) => Snapshot::at_hlc(epoch, commit_ts),
13994 None => Snapshot::at(epoch),
13995 };
13996 Ok((snap, guard))
13997 }
13998
13999 pub fn table_names(&self) -> Vec<String> {
14001 self.catalog
14002 .read()
14003 .tables
14004 .iter()
14005 .filter(|t| matches!(t.state, TableState::Live))
14006 .map(|t| t.name.clone())
14007 .collect()
14008 }
14009
14010 pub fn close(&self) -> Result<()> {
14016 for name in self.table_names() {
14017 if let Ok(handle) = self.table(&name) {
14018 if let Err(e) = handle.lock().close() {
14019 eprintln!("[close] flush failed for {name}: {e}");
14020 }
14021 }
14022 }
14023 Ok(())
14024 }
14025
14026 pub fn compact(&self) -> Result<(usize, usize)> {
14033 self.require(&crate::auth::Permission::Ddl)?;
14034 let _operation = self.admit_operation()?;
14036 let mut compacted = 0;
14037 let mut skipped = 0;
14038 for name in self.table_names() {
14039 let Ok(handle) = self.table(&name) else {
14040 continue;
14041 };
14042 {
14043 let mut t = handle.lock();
14044 let before = t.run_count();
14045 if before < 2 && !t.should_compact() {
14046 skipped += 1;
14047 continue;
14048 }
14049 match t.compact() {
14050 Ok(()) => {
14051 let after = t.run_count();
14052 compacted += 1;
14053 eprintln!("[compact] {name}: {before} -> {after} runs");
14054 }
14055 Err(e) => {
14056 eprintln!("[compact] {name}: compaction failed: {e}");
14057 skipped += 1;
14058 }
14059 }
14060 }
14061 }
14062 Ok((compacted, skipped))
14063 }
14064
14065 pub fn compact_table(&self, name: &str) -> Result<bool> {
14068 self.require(&crate::auth::Permission::Ddl)?;
14069 let handle = self.table(name)?;
14070 let mut t = handle.lock();
14071 let before = t.run_count();
14072 if before < 2 {
14073 return Ok(false);
14074 }
14075 t.compact()?;
14076 Ok(t.run_count() < before)
14077 }
14078
14079 pub fn table(&self, name: &str) -> Result<TableHandle> {
14081 self.ensure_owner_process()?;
14082 let cat = self.catalog.read();
14083 let entry = cat
14084 .live(name)
14085 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14086 let id = entry.table_id;
14087 drop(cat);
14088 self.tables
14089 .read()
14090 .get(&id)
14091 .cloned()
14092 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
14093 }
14094
14095 pub fn rebuild_indexes(&self, table: &str) -> Result<()> {
14099 let handle = self.table(table)?;
14100 let mut guard = handle.lock();
14101 guard.rebuild_indexes()
14102 }
14103
14104 pub fn rebuild_all_indexes(&self) -> Result<usize> {
14107 let handles: Vec<_> = self.tables.read().values().cloned().collect();
14108 let mut n = 0usize;
14109 for handle in handles {
14110 handle.lock().rebuild_indexes()?;
14111 n += 1;
14112 }
14113 Ok(n)
14114 }
14115
14116 pub fn has_ttl_tables(&self) -> bool {
14119 self.tables
14120 .read()
14121 .values()
14122 .any(|table| table.lock().ttl().is_some())
14123 }
14124
14125 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
14128 self.tables
14129 .read()
14130 .get(&id)
14131 .cloned()
14132 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
14133 }
14134
14135 pub(crate) fn table_auto_inc_would_allocate(
14141 &self,
14142 table_id: u64,
14143 cells: &[(u16, Value)],
14144 ) -> bool {
14145 let catalog = self.catalog.read();
14146 let Some(entry) = catalog
14147 .tables
14148 .iter()
14149 .find(|entry| entry.table_id == table_id)
14150 else {
14151 return false;
14152 };
14153 let Some(column) = entry.schema.auto_increment_column() else {
14154 return false;
14155 };
14156 matches!(
14157 cells.iter().find(|(id, _)| *id == column.id),
14158 None | Some((_, Value::Null))
14159 )
14160 }
14161
14162 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
14168 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
14169 return Err(MongrelError::InvalidArgument(format!(
14170 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
14171 )));
14172 }
14173 self.create_table_with_state(name, schema, TableState::Live)
14174 }
14175
14176 #[doc(hidden)]
14178 pub fn create_building_table(
14179 &self,
14180 build_name: &str,
14181 intended_name: &str,
14182 query_id: &str,
14183 schema: Schema,
14184 ) -> Result<u64> {
14185 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14186 || intended_name.is_empty()
14187 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14188 || query_id.is_empty()
14189 {
14190 return Err(MongrelError::InvalidArgument(
14191 "invalid CTAS building-table identity".into(),
14192 ));
14193 }
14194 self.create_table_with_state(
14195 build_name,
14196 schema,
14197 TableState::Building {
14198 intended_name: intended_name.to_string(),
14199 query_id: query_id.to_string(),
14200 created_at_unix_nanos: current_unix_nanos(),
14201 replaces_table_id: None,
14202 },
14203 )
14204 }
14205
14206 #[doc(hidden)]
14209 pub fn create_rebuilding_table(
14210 &self,
14211 build_name: &str,
14212 intended_name: &str,
14213 query_id: &str,
14214 schema: Schema,
14215 ) -> Result<u64> {
14216 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14217 || intended_name.is_empty()
14218 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14219 || query_id.is_empty()
14220 {
14221 return Err(MongrelError::InvalidArgument(
14222 "invalid rebuilding-table identity".into(),
14223 ));
14224 }
14225 let replaces_table_id = self
14226 .catalog
14227 .read()
14228 .live(intended_name)
14229 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
14230 .table_id;
14231 self.create_table_with_state(
14232 build_name,
14233 schema,
14234 TableState::Building {
14235 intended_name: intended_name.to_string(),
14236 query_id: query_id.to_string(),
14237 created_at_unix_nanos: current_unix_nanos(),
14238 replaces_table_id: Some(replaces_table_id),
14239 },
14240 )
14241 }
14242
14243 fn create_table_with_state(
14244 &self,
14245 name: &str,
14246 schema: Schema,
14247 state: TableState,
14248 ) -> Result<u64> {
14249 use crate::wal::DdlOp;
14250 use std::sync::atomic::Ordering;
14251
14252 self.require(&crate::auth::Permission::Ddl)?;
14253 if self.poisoned.load(Ordering::Relaxed) {
14254 return Err(MongrelError::Other(
14255 "database poisoned by fsync error".into(),
14256 ));
14257 }
14258 let _operation = self.admit_operation()?;
14260 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14261 let _g = self.ddl_lock.lock();
14262 let _security_write = self.security_write()?;
14263 self.require(&crate::auth::Permission::Ddl)?;
14264 {
14265 let cat = self.catalog.read();
14266 match &state {
14267 TableState::Live => {
14268 if cat.live(name).is_some() || cat.building_for(name).is_some() {
14269 return Err(MongrelError::InvalidArgument(format!(
14270 "table {name:?} already exists or is being built"
14271 )));
14272 }
14273 }
14274 TableState::Building {
14275 intended_name,
14276 replaces_table_id,
14277 ..
14278 } => {
14279 let target_matches = match replaces_table_id {
14280 Some(table_id) => cat
14281 .live(intended_name)
14282 .is_some_and(|entry| entry.table_id == *table_id),
14283 None => cat.live(intended_name).is_none(),
14284 };
14285 if !target_matches || cat.building_for(intended_name).is_some() {
14286 return Err(MongrelError::InvalidArgument(format!(
14287 "table {intended_name:?} changed or is already being built"
14288 )));
14289 }
14290 if cat.building(name).is_some() {
14291 return Err(MongrelError::InvalidArgument(format!(
14292 "building table {name:?} already exists"
14293 )));
14294 }
14295 }
14296 TableState::Dropped { .. } => {
14297 return Err(MongrelError::InvalidArgument(
14298 "cannot create a dropped table".into(),
14299 ));
14300 }
14301 }
14302 }
14303
14304 let commit_lock = Arc::clone(&self.commit_lock);
14309 let _c = commit_lock.lock();
14310 let live_create = matches!(state, TableState::Live);
14311 let legacy_table_id = if live_create {
14312 None
14313 } else {
14314 let mut cat = self.catalog.write();
14315 let id = cat.next_table_id;
14316 cat.next_table_id = id
14317 .checked_add(1)
14318 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
14319 Some(id)
14320 };
14321 let epoch = self.epoch.bump_assigned();
14322 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14323 let txn_id = self.alloc_txn_id()?;
14324
14325 let mut schema = schema;
14326 if let Some(table_id) = legacy_table_id {
14327 schema.schema_id = table_id;
14331 schema.validate_auto_increment()?;
14339 schema.validate_defaults()?;
14340 schema.validate_ai()?;
14341 for index in &schema.indexes {
14342 index.validate_options()?;
14343 }
14344 for constraint in &schema.constraints.checks {
14345 constraint.expr.validate()?;
14346 }
14347 }
14348
14349 let mut next_catalog = self.catalog.read().clone();
14350 let (table_id, schema) = match legacy_table_id {
14351 Some(table_id) => {
14352 next_catalog.tables.push(CatalogEntry {
14353 table_id,
14354 name: name.to_string(),
14355 schema: schema.clone(),
14356 state: state.clone(),
14357 created_epoch: epoch.0,
14358 });
14359 (table_id, schema)
14360 }
14361 None => {
14362 let command = crate::catalog_cmds::CatalogCommand::CreateTable {
14366 name: name.to_string(),
14367 schema,
14368 created_epoch: epoch.0,
14369 };
14370 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
14371 let crate::catalog_cmds::CatalogDelta::TableCreated { entry } = delta else {
14372 return Err(MongrelError::Other(
14373 "CreateTable resolved to an unexpected catalog delta".into(),
14374 ));
14375 };
14376 (entry.table_id, entry.schema)
14377 }
14378 };
14379 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14380
14381 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
14384 let canonical_tdir = self.root.join(&table_relative);
14385 let table_root = Arc::new(
14386 self.durable_root
14387 .create_directory_all_pinned(&table_relative)?,
14388 );
14389 let tdir = table_root.io_path()?;
14390 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
14391 let ctx = SharedCtx {
14392 root_guard: Some(table_root),
14393 epoch: Arc::clone(&self.epoch),
14394 page_cache: Arc::clone(&self.page_cache),
14395 decoded_cache: Arc::clone(&self.decoded_cache),
14396 snapshots: Arc::clone(&self.snapshots),
14397 kek: self.kek.clone(),
14398 commit_lock: Arc::clone(&self.commit_lock),
14399 shared: Some(crate::engine::SharedWalCtx {
14400 wal: Arc::clone(&self.shared_wal),
14401 group: Arc::clone(&self.group),
14402 poisoned: Arc::clone(&self.poisoned),
14403 txn_ids: Arc::clone(&self.next_txn_id),
14404 change_wake: self.change_wake.clone(),
14405 lifecycle: Arc::clone(&self.lifecycle),
14406 hlc: Arc::clone(&self.hlc),
14407 }),
14408 table_name: Some(name.to_string()),
14409 auth: self.table_auth_checker(),
14410 read_only: self.read_only,
14411 };
14412 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
14413
14414 let schema_json = DdlOp::encode_schema(&schema)?;
14417 let ddl = match &state {
14418 TableState::Live => DdlOp::CreateTable {
14419 table_id,
14420 name: name.to_string(),
14421 schema_json,
14422 },
14423 TableState::Building {
14424 intended_name,
14425 query_id,
14426 created_at_unix_nanos,
14427 replaces_table_id,
14428 } => match replaces_table_id {
14429 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
14430 table_id,
14431 build_name: name.to_string(),
14432 intended_name: intended_name.clone(),
14433 query_id: query_id.clone(),
14434 created_at_unix_nanos: *created_at_unix_nanos,
14435 replaces_table_id: *replaces_table_id,
14436 schema_json,
14437 },
14438 None => DdlOp::CreateBuildingTable {
14439 table_id,
14440 build_name: name.to_string(),
14441 intended_name: intended_name.clone(),
14442 query_id: query_id.clone(),
14443 created_at_unix_nanos: *created_at_unix_nanos,
14444 schema_json,
14445 },
14446 },
14447 TableState::Dropped { .. } => {
14448 return Err(MongrelError::InvalidArgument(
14449 "cannot create a table in dropped state".into(),
14450 ));
14451 }
14452 };
14453 let commit_seq = {
14454 let mut wal = self.shared_wal.lock();
14455 let append: Result<u64> = (|| {
14456 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
14457 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14458 wal.append_commit(txn_id, epoch, &[])
14459 })();
14460 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14461 };
14462 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14463 pending_table_dir.disarm();
14464
14465 self.tables
14468 .write()
14469 .insert(table_id, TableHandle::new(table));
14470 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14471 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14472 Ok(table_id)
14473 }
14474
14475 pub fn drop_table(&self, name: &str) -> Result<()> {
14477 self.drop_table_with_epoch(name).map(|_| ())
14478 }
14479
14480 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
14482 self.drop_table_with_state(name, false, None)
14483 }
14484
14485 pub fn drop_table_with_epoch_controlled<F>(
14486 &self,
14487 name: &str,
14488 mut before_commit: F,
14489 ) -> Result<Epoch>
14490 where
14491 F: FnMut() -> Result<()>,
14492 {
14493 self.drop_table_with_state(name, false, Some(&mut before_commit))
14494 }
14495
14496 #[doc(hidden)]
14498 pub fn discard_building_table(&self, name: &str) -> Result<()> {
14499 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
14500 return Err(MongrelError::InvalidArgument(
14501 "not a CTAS building table".into(),
14502 ));
14503 }
14504 self.drop_table_with_state(name, true, None).map(|_| ())
14505 }
14506
14507 fn drop_table_with_state(
14508 &self,
14509 name: &str,
14510 building: bool,
14511 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14512 ) -> Result<Epoch> {
14513 use crate::wal::DdlOp;
14514 use std::sync::atomic::Ordering;
14515
14516 self.require(&crate::auth::Permission::Ddl)?;
14517 if self.poisoned.load(Ordering::Relaxed) {
14518 return Err(MongrelError::Other(
14519 "database poisoned by fsync error".into(),
14520 ));
14521 }
14522 let _operation = self.admit_operation()?;
14524 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14525 let _g = self.ddl_lock.lock();
14526 let _security_write = self.security_write()?;
14527 self.require(&crate::auth::Permission::Ddl)?;
14528 let table_id = {
14529 let cat = self.catalog.read();
14530 if building {
14531 cat.building(name)
14532 } else {
14533 cat.live(name)
14534 }
14535 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
14536 .table_id
14537 };
14538
14539 let commit_lock = Arc::clone(&self.commit_lock);
14540 let _c = commit_lock.lock();
14541 let epoch = self.epoch.bump_assigned();
14542 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14543 let txn_id = self.alloc_txn_id()?;
14544 let mut next_catalog = self.catalog.read().clone();
14545 if building {
14546 let entry = next_catalog
14549 .tables
14550 .iter_mut()
14551 .find(|t| t.table_id == table_id)
14552 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14553 entry.state = TableState::Dropped { at_epoch: epoch.0 };
14554 next_catalog.triggers.retain(|trigger| {
14555 !matches!(
14556 &trigger.trigger.target,
14557 TriggerTarget::Table(target) if target == name
14558 )
14559 });
14560 next_catalog
14561 .materialized_views
14562 .retain(|definition| definition.name != name);
14563 next_catalog
14564 .security
14565 .rls_tables
14566 .retain(|table| table != name);
14567 next_catalog
14568 .security
14569 .policies
14570 .retain(|policy| policy.table != name);
14571 next_catalog
14572 .security
14573 .masks
14574 .retain(|mask| mask.table != name);
14575 for role in &mut next_catalog.roles {
14576 role.permissions
14577 .retain(|permission| permission_table(permission) != Some(name));
14578 }
14579 advance_security_version(&mut next_catalog)?;
14580 } else {
14581 let command = crate::catalog_cmds::CatalogCommand::DropTable {
14585 name: name.to_string(),
14586 at_epoch: epoch.0,
14587 };
14588 self.apply_catalog_command_to(&mut next_catalog, command)?;
14589 }
14590 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14591 let commit_seq = {
14592 let mut wal = self.shared_wal.lock();
14593 if let Some(before_commit) = before_commit {
14594 before_commit()?;
14595 }
14596 let append: Result<u64> = (|| {
14597 wal.append(
14598 txn_id,
14599 table_id,
14600 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
14601 )?;
14602 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14603 wal.append_commit(txn_id, epoch, &[])
14604 })();
14605 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14606 };
14607 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14608
14609 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14610 self.tables.write().remove(&table_id);
14611 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14612 Ok(epoch)
14613 }
14614
14615 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
14624 self.rename_table_with_epoch(name, new_name).map(|_| ())
14625 }
14626
14627 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
14629 self.rename_table_with_epoch_inner(name, new_name, None)
14630 }
14631
14632 pub fn rename_table_with_epoch_controlled<F>(
14633 &self,
14634 name: &str,
14635 new_name: &str,
14636 mut before_commit: F,
14637 ) -> Result<Epoch>
14638 where
14639 F: FnMut() -> Result<()>,
14640 {
14641 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
14642 }
14643
14644 fn rename_table_with_epoch_inner(
14645 &self,
14646 name: &str,
14647 new_name: &str,
14648 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14649 ) -> Result<Epoch> {
14650 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14651 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14652 {
14653 return Err(MongrelError::InvalidArgument(
14654 "the CTAS building-table namespace is reserved".into(),
14655 ));
14656 }
14657 self.rename_table_with_state(name, new_name, false, None, before_commit)
14658 }
14659
14660 #[doc(hidden)]
14662 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14663 self.publish_building_table_inner(build_name, new_name, None)
14664 }
14665
14666 #[doc(hidden)]
14667 pub fn publish_building_table_controlled<F>(
14668 &self,
14669 build_name: &str,
14670 new_name: &str,
14671 mut before_commit: F,
14672 ) -> Result<Epoch>
14673 where
14674 F: FnMut() -> Result<()>,
14675 {
14676 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
14677 }
14678
14679 fn publish_building_table_inner(
14680 &self,
14681 build_name: &str,
14682 new_name: &str,
14683 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14684 ) -> Result<Epoch> {
14685 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14686 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14687 {
14688 return Err(MongrelError::InvalidArgument(
14689 "invalid CTAS publish identity".into(),
14690 ));
14691 }
14692 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
14693 }
14694
14695 #[doc(hidden)]
14697 pub fn publish_materialized_building_table(
14698 &self,
14699 build_name: &str,
14700 new_name: &str,
14701 definition: crate::catalog::MaterializedViewEntry,
14702 ) -> Result<Epoch> {
14703 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
14704 }
14705
14706 #[doc(hidden)]
14707 pub fn publish_materialized_building_table_controlled<F>(
14708 &self,
14709 build_name: &str,
14710 new_name: &str,
14711 definition: crate::catalog::MaterializedViewEntry,
14712 mut before_commit: F,
14713 ) -> Result<Epoch>
14714 where
14715 F: FnMut() -> Result<()>,
14716 {
14717 self.publish_materialized_building_table_inner(
14718 build_name,
14719 new_name,
14720 definition,
14721 Some(&mut before_commit),
14722 )
14723 }
14724
14725 fn publish_materialized_building_table_inner(
14726 &self,
14727 build_name: &str,
14728 new_name: &str,
14729 definition: crate::catalog::MaterializedViewEntry,
14730 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14731 ) -> Result<Epoch> {
14732 if definition.name != new_name || definition.query.trim().is_empty() {
14733 return Err(MongrelError::InvalidArgument(
14734 "invalid materialized-view publication".into(),
14735 ));
14736 }
14737 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
14738 }
14739
14740 #[doc(hidden)]
14742 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14743 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
14744 }
14745
14746 #[doc(hidden)]
14747 pub fn publish_rebuilding_table_controlled<F>(
14748 &self,
14749 build_name: &str,
14750 new_name: &str,
14751 mut before_commit: F,
14752 ) -> Result<Epoch>
14753 where
14754 F: FnMut() -> Result<()>,
14755 {
14756 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
14757 }
14758
14759 #[doc(hidden)]
14761 pub fn publish_materialized_rebuilding_table_controlled<F>(
14762 &self,
14763 build_name: &str,
14764 new_name: &str,
14765 definition: crate::catalog::MaterializedViewEntry,
14766 mut before_commit: F,
14767 ) -> Result<Epoch>
14768 where
14769 F: FnMut() -> Result<()>,
14770 {
14771 self.publish_rebuilding_table_inner(
14772 build_name,
14773 new_name,
14774 Some(definition),
14775 Some(&mut before_commit),
14776 )
14777 }
14778
14779 fn publish_rebuilding_table_inner(
14780 &self,
14781 build_name: &str,
14782 new_name: &str,
14783 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14784 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14785 ) -> Result<Epoch> {
14786 use crate::wal::DdlOp;
14787
14788 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14789 || new_name.is_empty()
14790 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14791 {
14792 return Err(MongrelError::InvalidArgument(
14793 "invalid rebuilding-table publish identity".into(),
14794 ));
14795 }
14796 if materialized_view.as_ref().is_some_and(|definition| {
14797 definition.name != new_name || definition.query.trim().is_empty()
14798 }) {
14799 return Err(MongrelError::InvalidArgument(
14800 "invalid materialized-view replacement".into(),
14801 ));
14802 }
14803 self.require(&crate::auth::Permission::Ddl)?;
14804 if self.poisoned.load(Ordering::Relaxed) {
14805 return Err(MongrelError::Other(
14806 "database poisoned by fsync error".into(),
14807 ));
14808 }
14809 let _operation = self.admit_operation()?;
14811 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14812 let _ddl = self.ddl_lock.lock();
14813 let _security_write = self.security_write()?;
14814 let (table_id, replaced_table_id) = {
14815 let catalog = self.catalog.read();
14816 let build = catalog.building(build_name).ok_or_else(|| {
14817 MongrelError::NotFound(format!("building table {build_name:?} not found"))
14818 })?;
14819 let replaced_table_id = match &build.state {
14820 TableState::Building {
14821 intended_name,
14822 replaces_table_id: Some(replaced_table_id),
14823 ..
14824 } if intended_name == new_name => *replaced_table_id,
14825 _ => {
14826 return Err(MongrelError::InvalidArgument(format!(
14827 "building table {build_name:?} is not a replacement for {new_name:?}"
14828 )))
14829 }
14830 };
14831 if catalog
14832 .live(new_name)
14833 .is_none_or(|entry| entry.table_id != replaced_table_id)
14834 {
14835 return Err(MongrelError::Conflict(format!(
14836 "table {new_name:?} changed while its replacement was built"
14837 )));
14838 }
14839 (build.table_id, replaced_table_id)
14840 };
14841
14842 let _commit = self.commit_lock.lock();
14843 let epoch = self.epoch.assigned().next();
14844 let txn_id = self.alloc_txn_id()?;
14845 let mut next_catalog = self.catalog.read().clone();
14846 apply_rebuilding_publish(
14847 &mut next_catalog,
14848 table_id,
14849 replaced_table_id,
14850 new_name,
14851 epoch.0,
14852 )?;
14853 if let Some(definition) = materialized_view.as_mut() {
14854 definition.last_refresh_epoch = epoch.0;
14855 }
14856 let materialized_view_json = materialized_view
14857 .as_ref()
14858 .map(DdlOp::encode_materialized_view)
14859 .transpose()?;
14860 if let Some(definition) = materialized_view {
14861 if let Some(existing) = next_catalog
14862 .materialized_views
14863 .iter_mut()
14864 .find(|existing| existing.name == definition.name)
14865 {
14866 *existing = definition;
14867 } else {
14868 next_catalog.materialized_views.push(definition);
14869 }
14870 }
14871 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14872 if let Some(before_commit) = before_commit {
14873 before_commit()?;
14874 }
14875 let assigned_epoch = self.epoch.bump_assigned();
14876 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
14877 if assigned_epoch != epoch {
14878 return Err(MongrelError::Conflict(
14879 "commit epoch changed while sequencer lock was held".into(),
14880 ));
14881 }
14882 let commit_seq = {
14883 let mut wal = self.shared_wal.lock();
14884 let append: Result<u64> = (|| {
14885 wal.append(
14886 txn_id,
14887 table_id,
14888 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
14889 table_id,
14890 replaced_table_id,
14891 new_name: new_name.to_string(),
14892 }),
14893 )?;
14894 if let Some(definition_json) = materialized_view_json {
14895 wal.append(
14896 txn_id,
14897 table_id,
14898 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14899 name: new_name.to_string(),
14900 definition_json,
14901 }),
14902 )?;
14903 }
14904 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14905 wal.append_commit(txn_id, epoch, &[])
14906 })();
14907 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14908 };
14909 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14910
14911 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14912 self.tables.write().remove(&replaced_table_id);
14913 if let Some(table) = self.tables.read().get(&table_id) {
14914 table.lock().set_catalog_name(new_name.to_string());
14915 }
14916 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
14917 Ok(epoch)
14918 }
14919
14920 fn rename_table_with_state(
14921 &self,
14922 name: &str,
14923 new_name: &str,
14924 building: bool,
14925 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14926 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14927 ) -> Result<Epoch> {
14928 use crate::wal::DdlOp;
14929 use std::sync::atomic::Ordering;
14930
14931 self.require(&crate::auth::Permission::Ddl)?;
14932 if self.poisoned.load(Ordering::Relaxed) {
14933 return Err(MongrelError::Other(
14934 "database poisoned by fsync error".into(),
14935 ));
14936 }
14937
14938 if name == new_name {
14941 return Ok(self.visible_epoch());
14942 }
14943 if new_name.is_empty() {
14944 return Err(MongrelError::InvalidArgument(
14945 "rename_table: new name must not be empty".into(),
14946 ));
14947 }
14948 let _operation = self.admit_operation()?;
14950 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14951 let _g = self.ddl_lock.lock();
14952 let _security_write = self.security_write()?;
14953 self.require(&crate::auth::Permission::Ddl)?;
14954 let table_id = {
14955 let cat = self.catalog.read();
14956 let src = if building {
14957 cat.building(name)
14958 } else {
14959 cat.live(name)
14960 }
14961 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14962 if building
14963 && !matches!(
14964 &src.state,
14965 TableState::Building { intended_name, .. } if intended_name == new_name
14966 )
14967 {
14968 return Err(MongrelError::InvalidArgument(format!(
14969 "building table {name:?} is not reserved for {new_name:?}"
14970 )));
14971 }
14972 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
14976 return Err(MongrelError::InvalidArgument(format!(
14977 "rename_table: a table named {new_name:?} already exists"
14978 )));
14979 }
14980 src.table_id
14981 };
14982
14983 let commit_lock = Arc::clone(&self.commit_lock);
14984 let _c = commit_lock.lock();
14985 let epoch = self.epoch.bump_assigned();
14986 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14987 let txn_id = self.alloc_txn_id()?;
14988 if let Some(definition) = materialized_view.as_mut() {
14989 definition.last_refresh_epoch = epoch.0;
14990 }
14991 let materialized_view_json = materialized_view
14992 .as_ref()
14993 .map(DdlOp::encode_materialized_view)
14994 .transpose()?;
14995 let mut next_catalog = self.catalog.read().clone();
14996 if building {
14997 let entry = next_catalog
15000 .tables
15001 .iter_mut()
15002 .find(|t| t.table_id == table_id)
15003 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
15004 entry.name = new_name.to_string();
15005 entry.state = TableState::Live;
15006 for trigger in &mut next_catalog.triggers {
15007 if matches!(
15008 &trigger.trigger.target,
15009 TriggerTarget::Table(target) if target == name
15010 ) {
15011 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
15012 }
15013 }
15014 if let Some(definition) = next_catalog
15015 .materialized_views
15016 .iter_mut()
15017 .find(|definition| definition.name == name)
15018 {
15019 definition.name = new_name.to_string();
15020 }
15021 if let Some(definition) = materialized_view.take() {
15022 next_catalog.materialized_views.push(definition);
15023 }
15024 for table in &mut next_catalog.security.rls_tables {
15025 if table == name {
15026 *table = new_name.to_string();
15027 }
15028 }
15029 for policy in &mut next_catalog.security.policies {
15030 if policy.table == name {
15031 policy.table = new_name.to_string();
15032 }
15033 }
15034 for mask in &mut next_catalog.security.masks {
15035 if mask.table == name {
15036 mask.table = new_name.to_string();
15037 }
15038 }
15039 for role in &mut next_catalog.roles {
15040 for permission in &mut role.permissions {
15041 rename_permission_table(permission, name, new_name);
15042 }
15043 }
15044 advance_security_version(&mut next_catalog)?;
15045 } else {
15046 let command = crate::catalog_cmds::CatalogCommand::RenameTable {
15050 name: name.to_string(),
15051 new_name: new_name.to_string(),
15052 at_epoch: epoch.0,
15053 };
15054 self.apply_catalog_command_to(&mut next_catalog, command)?;
15055 }
15056 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15057 let ddl = if building {
15058 DdlOp::PublishBuildingTable {
15059 table_id,
15060 new_name: new_name.to_string(),
15061 }
15062 } else {
15063 DdlOp::RenameTable {
15064 table_id,
15065 new_name: new_name.to_string(),
15066 }
15067 };
15068 let commit_seq = {
15069 let mut wal = self.shared_wal.lock();
15070 if let Some(before_commit) = before_commit {
15071 before_commit()?;
15072 }
15073 let append: Result<u64> = (|| {
15074 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
15075 if let Some(definition_json) = materialized_view_json {
15076 wal.append(
15077 txn_id,
15078 table_id,
15079 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
15080 name: new_name.to_string(),
15081 definition_json,
15082 }),
15083 )?;
15084 }
15085 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15086 wal.append_commit(txn_id, epoch, &[])
15087 })();
15088 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15089 };
15090 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15091
15092 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
15093 if let Some(table) = self.tables.read().get(&table_id) {
15096 table.lock().set_catalog_name(new_name.to_string());
15097 }
15098 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
15099 Ok(epoch)
15100 }
15101
15102 pub fn add_column(
15108 &self,
15109 table_name: &str,
15110 name: &str,
15111 ty: TypeId,
15112 flags: ColumnFlags,
15113 default_value: Option<crate::schema::DefaultExpr>,
15114 ) -> Result<ColumnDef> {
15115 self.add_column_with_id(table_name, name, ty, flags, default_value, None)
15116 }
15117
15118 pub fn add_column_with_id(
15120 &self,
15121 table_name: &str,
15122 name: &str,
15123 ty: TypeId,
15124 flags: ColumnFlags,
15125 default_value: Option<crate::schema::DefaultExpr>,
15126 requested_id: Option<u16>,
15127 ) -> Result<ColumnDef> {
15128 use crate::wal::DdlOp;
15129 use std::sync::atomic::Ordering;
15130
15131 self.require(&crate::auth::Permission::Ddl)?;
15132 if self.poisoned.load(Ordering::Relaxed) {
15133 return Err(MongrelError::Other(
15134 "database poisoned by fsync error".into(),
15135 ));
15136 }
15137 let _operation = self.admit_operation()?;
15138 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
15139 let _ddl = self.ddl_lock.lock();
15140 let _security_write = self.security_write()?;
15141 self.require(&crate::auth::Permission::Ddl)?;
15142 let table_id = {
15143 let catalog = self.catalog.read();
15144 catalog
15145 .live(table_name)
15146 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15147 .table_id
15148 };
15149 let handle =
15150 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15151 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15152 })?;
15153 let durable_epoch = std::cell::Cell::new(None);
15154 let result: Result<ColumnDef> = (|| {
15155 let mut table = handle.lock();
15156 let (column, prepared_schema) =
15157 table.prepare_add_column(name, ty, flags, default_value, requested_id)?;
15158 let command = crate::catalog_cmds::CatalogCommand::AddColumn {
15159 table: table_name.to_string(),
15160 column: column.clone(),
15161 };
15162 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
15163
15164 let commit_lock = Arc::clone(&self.commit_lock);
15165 let _commit = commit_lock.lock();
15166 let epoch = self.epoch.bump_assigned();
15167 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15168 let txn_id = self.alloc_txn_id()?;
15169 let column_json = DdlOp::encode_column(&column)?;
15170 let mut next_catalog = self.catalog.read().clone();
15171 let catalog_entry_index = next_catalog
15172 .tables
15173 .iter()
15174 .position(|entry| entry.table_id == table_id)
15175 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
15176 self.apply_catalog_command_to(&mut next_catalog, command)?;
15177 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
15178 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15179 let commit_seq = {
15180 let mut wal = self.shared_wal.lock();
15181 let append: Result<u64> = (|| {
15182 wal.append(
15183 txn_id,
15184 table_id,
15185 crate::wal::Op::Ddl(DdlOp::AlterTable {
15186 table_id,
15187 column_json,
15188 }),
15189 )?;
15190 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15191 wal.append_commit(txn_id, epoch, &[])
15192 })();
15193 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15194 };
15195 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15196 durable_epoch.set(Some(epoch));
15197
15198 table.apply_altered_schema_prepared(prepared_schema);
15199 let schema = table.schema().clone();
15200 let table_checkpoint = table.checkpoint_altered_schema();
15201 drop(table);
15202 next_catalog.tables[catalog_entry_index].schema = schema;
15203 let catalog_result =
15204 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15205 *self.catalog.write() = next_catalog;
15206 self.publish_committed(&receipt, epoch)?;
15207 epoch_guard.disarm();
15208 if let Err(error) = table_checkpoint.and(catalog_result) {
15209 self.poisoned.store(true, Ordering::Relaxed);
15210 self.lifecycle.poison();
15211 return Err(MongrelError::DurableCommit {
15212 epoch: epoch.0,
15213 message: error.to_string(),
15214 });
15215 }
15216 Ok(column)
15217 })();
15218 result.map_err(|error| match (durable_epoch.get(), error) {
15219 (_, error @ MongrelError::DurableCommit { .. }) => error,
15220 (Some(epoch), error) => MongrelError::DurableCommit {
15221 epoch: epoch.0,
15222 message: error.to_string(),
15223 },
15224 (None, error) => error,
15225 })
15226 }
15227
15228 pub fn alter_column(
15229 &self,
15230 table_name: &str,
15231 column_name: &str,
15232 change: AlterColumn,
15233 ) -> Result<ColumnDef> {
15234 self.alter_column_with_epoch(table_name, column_name, change)
15235 .map(|(column, _)| column)
15236 }
15237
15238 pub fn alter_column_with_epoch(
15239 &self,
15240 table_name: &str,
15241 column_name: &str,
15242 change: AlterColumn,
15243 ) -> Result<(ColumnDef, Option<Epoch>)> {
15244 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
15245 }
15246
15247 pub fn alter_column_with_epoch_controlled<B, A>(
15252 &self,
15253 table_name: &str,
15254 column_name: &str,
15255 change: AlterColumn,
15256 control: &crate::ExecutionControl,
15257 mut before_commit: B,
15258 mut after_commit: A,
15259 ) -> Result<(ColumnDef, Option<Epoch>)>
15260 where
15261 B: FnMut() -> Result<()>,
15262 A: FnMut(Option<Epoch>) -> Result<()>,
15263 {
15264 self.alter_column_with_epoch_inner(
15265 table_name,
15266 column_name,
15267 change,
15268 Some(control),
15269 Some(&mut before_commit),
15270 Some(&mut after_commit),
15271 )
15272 }
15273
15274 #[allow(clippy::too_many_arguments)]
15275 fn alter_column_with_epoch_inner(
15276 &self,
15277 table_name: &str,
15278 column_name: &str,
15279 change: AlterColumn,
15280 control: Option<&crate::ExecutionControl>,
15281 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
15282 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
15283 ) -> Result<(ColumnDef, Option<Epoch>)> {
15284 use crate::wal::DdlOp;
15285 use std::sync::atomic::Ordering;
15286
15287 self.require(&crate::auth::Permission::Ddl)?;
15288 commit_prepare_checkpoint(control, 0)?;
15289 if self.poisoned.load(Ordering::Relaxed) {
15290 return Err(MongrelError::Other(
15291 "database poisoned by fsync error".into(),
15292 ));
15293 }
15294 let _operation = self.admit_operation()?;
15296 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
15297 let _g = self.ddl_lock.lock();
15298 let table_id = {
15299 let cat = self.catalog.read();
15300 cat.live(table_name)
15301 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15302 .table_id
15303 };
15304 let handle =
15305 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15306 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15307 })?;
15308
15309 let backfill = {
15315 let table = handle.lock();
15316 let old = table
15317 .schema()
15318 .column(column_name)
15319 .cloned()
15320 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
15321 let next_flags = change.flags.unwrap_or(old.flags);
15322 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
15323 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
15324 && old.default_value.is_some()
15325 {
15326 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
15327 let mut updates = Vec::new();
15328 let rows = match control {
15329 Some(control) => table.visible_rows_controlled(snapshot, control)?,
15330 None => table.visible_rows(snapshot)?,
15331 };
15332 for (row_index, row) in rows.into_iter().enumerate() {
15333 commit_prepare_checkpoint(control, row_index)?;
15334 if row
15335 .columns
15336 .get(&old.id)
15337 .is_some_and(|value| !matches!(value, Value::Null))
15338 {
15339 continue;
15340 }
15341 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
15342 table.apply_defaults(&mut cells)?;
15343 updates.push((
15344 table_id,
15345 crate::txn::Staged::Update {
15346 row_id: row.row_id,
15347 new_row: cells,
15348 changed_columns: vec![old.id],
15349 },
15350 ));
15351 }
15352 updates
15353 } else {
15354 Vec::new()
15355 }
15356 };
15357 let durable_epoch = std::cell::Cell::new(None);
15358 let backfill_epoch = if backfill.is_empty() {
15359 None
15360 } else {
15361 let (principal, catalog_bound) = self.transaction_principal_snapshot();
15362 let txn_id = self.alloc_txn_id()?;
15363 let mut entered_fence = false;
15364 let commit_result = match (control, before_commit.as_deref_mut()) {
15365 (Some(control), Some(before_commit)) => self
15366 .commit_transaction_with_external_states_controlled(
15367 txn_id,
15368 self.epoch.visible(),
15369 backfill,
15370 Vec::new(),
15371 Vec::new(),
15372 principal.clone(),
15373 catalog_bound,
15374 None,
15375 crate::txn::TxnCommitContext::internal(),
15376 control,
15377 &mut || {
15378 before_commit()?;
15379 entered_fence = true;
15380 Ok(())
15381 },
15382 )
15383 .map(|(epoch, _)| epoch),
15384 _ => self
15385 .commit_transaction_with_external_states(
15386 txn_id,
15387 self.epoch.visible(),
15388 backfill,
15389 Vec::new(),
15390 Vec::new(),
15391 principal,
15392 catalog_bound,
15393 None,
15394 crate::txn::TxnCommitContext::internal(),
15395 )
15396 .map(|(epoch, _)| epoch),
15397 };
15398 let commit_result = if entered_fence {
15399 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15400 } else {
15401 commit_result
15402 };
15403 match &commit_result {
15404 Ok(epoch) => durable_epoch.set(Some(*epoch)),
15405 Err(MongrelError::DurableCommit { epoch, .. }) => {
15406 durable_epoch.set(Some(Epoch(*epoch)));
15407 }
15408 Err(_) => {}
15409 }
15410 Some(commit_result?)
15411 };
15412 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
15413 let _security_write = self.security_write()?;
15414 self.require(&crate::auth::Permission::Ddl)?;
15415 if self
15416 .catalog
15417 .read()
15418 .live(table_name)
15419 .is_none_or(|entry| entry.table_id != table_id)
15420 {
15421 return Err(MongrelError::Conflict(format!(
15422 "table {table_name:?} changed during ALTER"
15423 )));
15424 }
15425 let mut table = handle.lock();
15426 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
15427 let renamed_column = (column.name != column_name).then(|| column.name.clone());
15428 let Some(prepared_schema) = prepared_schema else {
15429 return Ok((column, backfill_epoch));
15430 };
15431 let command = crate::catalog_cmds::CatalogCommand::AlterColumn {
15436 table: table_name.to_string(),
15437 column: column.clone(),
15438 };
15439 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
15440
15441 let commit_lock = Arc::clone(&self.commit_lock);
15442 let _c = commit_lock.lock();
15443 let epoch = self.epoch.bump_assigned();
15444 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15445 let txn_id = self.alloc_txn_id()?;
15446 let column_json = DdlOp::encode_column(&column)?;
15447 let mut next_catalog = self.catalog.read().clone();
15448 let catalog_entry_index = next_catalog
15449 .tables
15450 .iter()
15451 .position(|entry| entry.table_id == table_id)
15452 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
15453 if let Some(new_column_name) = &renamed_column {
15454 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
15455 commit_prepare_checkpoint(control, trigger_index)?;
15456 if matches!(
15457 &trigger.trigger.target,
15458 TriggerTarget::Table(target) if target == table_name
15459 ) {
15460 trigger.trigger = trigger.trigger.renamed_update_column(
15461 column_name,
15462 new_column_name.clone(),
15463 epoch.0,
15464 )?;
15465 }
15466 }
15467 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
15468 commit_prepare_checkpoint(control, role_index)?;
15469 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
15470 commit_prepare_checkpoint(control, permission_index)?;
15471 rename_permission_column(
15472 permission,
15473 table_name,
15474 column_name,
15475 new_column_name,
15476 );
15477 }
15478 }
15479 advance_security_version(&mut next_catalog)?;
15480 }
15481 self.apply_catalog_command_to(&mut next_catalog, command)?;
15487 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
15488 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15489 commit_prepare_checkpoint(control, 0)?;
15490 let mut entered_fence = false;
15491 if let Some(before_commit) = before_commit.as_deref_mut() {
15492 before_commit()?;
15493 entered_fence = true;
15494 }
15495 let commit_result: Result<Epoch> = (|| {
15496 let commit_seq = {
15497 let mut wal = self.shared_wal.lock();
15498 let append: Result<u64> = (|| {
15499 wal.append(
15500 txn_id,
15501 table_id,
15502 crate::wal::Op::Ddl(DdlOp::AlterTable {
15503 table_id,
15504 column_json,
15505 }),
15506 )?;
15507 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15508 wal.append_commit(txn_id, epoch, &[])
15509 })();
15510 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15511 };
15512 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15513 durable_epoch.set(Some(epoch));
15514
15515 table.apply_altered_schema_prepared(prepared_schema);
15516 let schema = table.schema().clone();
15517 let table_checkpoint = table.checkpoint_altered_schema();
15518 drop(table);
15519 next_catalog.tables[catalog_entry_index].schema = schema;
15520 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15521 let catalog_result =
15522 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15523 let security_version = next_catalog.security_version;
15524 *self.catalog.write() = next_catalog;
15525 if renamed_column.is_some() {
15526 self.security_coordinator
15527 .version
15528 .store(security_version, Ordering::Release);
15529 }
15530 self.publish_committed(&receipt, epoch)?;
15531 _epoch_guard.disarm();
15532 if let Err(error) = table_checkpoint.and(catalog_result) {
15533 self.poisoned.store(true, Ordering::Relaxed);
15534 self.lifecycle.poison();
15535 return Err(MongrelError::DurableCommit {
15536 epoch: epoch.0,
15537 message: error.to_string(),
15538 });
15539 }
15540 Ok(epoch)
15541 })();
15542 let commit_result = if entered_fence {
15543 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15544 } else {
15545 commit_result
15546 };
15547 let epoch = commit_result?;
15548 Ok((column, Some(epoch)))
15549 })();
15550 result.map_err(|error| match (durable_epoch.get(), error) {
15551 (_, error @ MongrelError::DurableCommit { .. }) => error,
15552 (Some(epoch), error) => MongrelError::DurableCommit {
15553 epoch: epoch.0,
15554 message: error.to_string(),
15555 },
15556 (None, error) => error,
15557 })
15558 }
15559
15560 pub fn set_table_ttl(
15563 &self,
15564 table_name: &str,
15565 column_name: &str,
15566 duration_nanos: u64,
15567 ) -> Result<crate::manifest::TtlPolicy> {
15568 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
15569 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
15570 }
15571
15572 #[doc(hidden)]
15574 pub fn set_building_table_ttl(
15575 &self,
15576 table_name: &str,
15577 column_name: &str,
15578 duration_nanos: u64,
15579 ) -> Result<crate::manifest::TtlPolicy> {
15580 let policy = self.replace_table_ttl_with_state(
15581 table_name,
15582 Some((column_name, duration_nanos)),
15583 true,
15584 )?;
15585 policy
15586 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
15587 }
15588
15589 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
15590 self.replace_table_ttl(table_name, None)?;
15591 Ok(())
15592 }
15593
15594 fn replace_table_ttl(
15595 &self,
15596 table_name: &str,
15597 requested: Option<(&str, u64)>,
15598 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15599 self.replace_table_ttl_with_state(table_name, requested, false)
15600 }
15601
15602 fn replace_table_ttl_with_state(
15603 &self,
15604 table_name: &str,
15605 requested: Option<(&str, u64)>,
15606 building: bool,
15607 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15608 use crate::wal::DdlOp;
15609 use std::sync::atomic::Ordering;
15610
15611 self.require(&crate::auth::Permission::Ddl)?;
15612 if self.poisoned.load(Ordering::Relaxed) {
15613 return Err(MongrelError::Other(
15614 "database poisoned by fsync error".into(),
15615 ));
15616 }
15617
15618 let _g = self.ddl_lock.lock();
15619 let _security_write = self.security_write()?;
15620 self.require(&crate::auth::Permission::Ddl)?;
15621 let table_id = {
15622 let cat = self.catalog.read();
15623 if building {
15624 cat.building(table_name)
15625 } else {
15626 cat.live(table_name)
15627 }
15628 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15629 .table_id
15630 };
15631 let handle =
15632 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15633 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15634 })?;
15635 let mut table = handle.lock();
15636 let policy = match requested {
15637 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
15638 None => None,
15639 };
15640 if table.ttl() == policy {
15641 return Ok(policy);
15642 }
15643
15644 let commit_lock = Arc::clone(&self.commit_lock);
15645 let _c = commit_lock.lock();
15646 let epoch = self.epoch.bump_assigned();
15647 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15648 let txn_id = self.alloc_txn_id()?;
15649 let policy_json = DdlOp::encode_ttl(policy)?;
15650 let mut next_catalog = self.catalog.read().clone();
15651 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15652 let commit_seq = {
15653 let mut wal = self.shared_wal.lock();
15654 let append: Result<u64> = (|| {
15655 wal.append(
15656 txn_id,
15657 table_id,
15658 crate::wal::Op::Ddl(DdlOp::SetTtl {
15659 table_id,
15660 policy_json,
15661 }),
15662 )?;
15663 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15664 wal.append_commit(txn_id, epoch, &[])
15665 })();
15666 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15667 };
15668 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15669
15670 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
15671 drop(table);
15672 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
15673 publish_error.get_or_insert(error);
15674 }
15675 self.finish_durable_publish(
15676 epoch,
15677 &mut _epoch_guard,
15678 &receipt,
15679 publish_error.map_or(Ok(()), Err),
15680 )?;
15681 Ok(policy)
15682 }
15683
15684 pub fn gc(&self) -> Result<usize> {
15690 let control = crate::ExecutionControl::new(None);
15691 self.gc_controlled(&control, || true)
15692 }
15693
15694 #[doc(hidden)]
15697 pub fn gc_controlled<F>(
15698 &self,
15699 control: &crate::ExecutionControl,
15700 before_publish: F,
15701 ) -> Result<usize>
15702 where
15703 F: FnOnce() -> bool,
15704 {
15705 self.gc_controlled_with_receipt(control, before_publish)
15706 .map(|(reclaimed, _)| reclaimed)
15707 }
15708
15709 #[doc(hidden)]
15712 pub fn gc_controlled_with_receipt<F>(
15713 &self,
15714 control: &crate::ExecutionControl,
15715 before_publish: F,
15716 ) -> Result<(usize, Option<MaintenanceReceipt>)>
15717 where
15718 F: FnOnce() -> bool,
15719 {
15720 enum Candidate {
15721 Directory(PathBuf),
15722 File(PathBuf),
15723 }
15724
15725 self.require(&crate::auth::Permission::Ddl)?;
15726 let _operation = self.admit_operation()?;
15728 let _ddl = self.ddl_lock.lock();
15729 self.require(&crate::auth::Permission::Ddl)?;
15730 control.checkpoint()?;
15731 let maintenance_epoch = self.epoch.visible();
15732 let min_active = self.snapshots.min_active(maintenance_epoch).0;
15733 let mut candidates = Vec::new();
15734
15735 let cat = self.catalog.read();
15737 for (entry_index, entry) in cat.tables.iter().enumerate() {
15738 if entry_index % 256 == 0 {
15739 control.checkpoint()?;
15740 }
15741 if let TableState::Dropped { at_epoch } = entry.state {
15742 if at_epoch <= min_active {
15743 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
15744 if tdir.exists() {
15745 candidates.push(Candidate::Directory(tdir));
15746 }
15747 }
15748 }
15749 }
15750 drop(cat);
15751
15752 let cat = self.catalog.read();
15757 for (entry_index, entry) in cat.tables.iter().enumerate() {
15758 if entry_index % 256 == 0 {
15759 control.checkpoint()?;
15760 }
15761 if !matches!(entry.state, TableState::Live) {
15762 continue;
15763 }
15764 let txn_dir = self
15765 .root
15766 .join(TABLES_DIR)
15767 .join(entry.table_id.to_string())
15768 .join("_txn");
15769 if !txn_dir.exists() {
15770 continue;
15771 }
15772 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
15773 if sub_index % 256 == 0 {
15774 control.checkpoint()?;
15775 }
15776 let sub = sub?;
15777 let name = sub.file_name();
15778 let Some(name) = name.to_str() else { continue };
15779 let is_active = name
15781 .parse::<u64>()
15782 .map(|id| self.active_spills.is_active(id))
15783 .unwrap_or(false);
15784 if is_active {
15785 continue;
15786 }
15787 candidates.push(Candidate::Directory(sub.path()));
15788 }
15789 }
15790 drop(cat);
15791
15792 let external_names = {
15793 let cat = self.catalog.read();
15794 cat.external_tables
15795 .iter()
15796 .map(|entry| entry.name.clone())
15797 .collect::<std::collections::HashSet<_>>()
15798 };
15799 let vtab_dir = self.root.join(VTAB_DIR);
15800 if vtab_dir.exists() {
15801 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
15802 if entry_index % 256 == 0 {
15803 control.checkpoint()?;
15804 }
15805 let entry = entry?;
15806 let name = entry.file_name();
15807 let Some(name) = name.to_str() else { continue };
15808 if external_names.contains(name) {
15809 continue;
15810 }
15811 let path = entry.path();
15812 if path.is_dir() {
15813 candidates.push(Candidate::Directory(path));
15814 } else {
15815 candidates.push(Candidate::File(path));
15816 }
15817 }
15818 }
15819
15820 let tables = self
15824 .tables
15825 .read()
15826 .iter()
15827 .map(|(table_id, handle)| (*table_id, handle.clone()))
15828 .collect::<Vec<_>>();
15829 let mut retiring = Vec::new();
15830 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
15831 if table_index % 256 == 0 {
15832 control.checkpoint()?;
15833 }
15834 let backup_pinned: HashSet<u128> = self
15835 .backup_pins
15836 .lock()
15837 .keys()
15838 .filter_map(|(pinned_table, run_id)| {
15839 (*pinned_table == *table_id).then_some(*run_id)
15840 })
15841 .collect();
15842 if handle
15843 .lock()
15844 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
15845 {
15846 retiring.push((handle.clone(), backup_pinned));
15847 }
15848 }
15849
15850 let all_durable = self.active_spills.is_idle()
15858 && tables.iter().all(|(_, handle)| {
15859 let g = handle.lock();
15860 g.memtable_len() == 0 && g.mutable_run_len() == 0
15861 });
15862 let retain = self
15863 .replication_wal_retention_segments
15864 .load(std::sync::atomic::Ordering::Relaxed);
15865 let reap_wal = all_durable
15866 && self
15867 .shared_wal
15868 .lock()
15869 .has_gc_segments_retain_recent(retain)?;
15870
15871 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
15872 return Ok((0, None));
15873 }
15874 control.checkpoint()?;
15875 if !before_publish() {
15876 return Err(MongrelError::Cancelled);
15877 }
15878
15879 let mut reclaimed = 0;
15880 for candidate in candidates {
15881 match candidate {
15882 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
15883 Candidate::File(path) => std::fs::remove_file(path)?,
15884 }
15885 reclaimed += 1;
15886 }
15887 for (handle, backup_pinned) in retiring {
15888 reclaimed += handle
15889 .lock()
15890 .reap_retiring(Epoch(min_active), &backup_pinned)?;
15891 }
15892 if reap_wal {
15893 reclaimed += self
15894 .shared_wal
15895 .lock()
15896 .gc_segments_retain_recent(u64::MAX, retain)?;
15897 }
15898
15899 Ok((
15900 reclaimed,
15901 Some(MaintenanceReceipt {
15902 epoch: maintenance_epoch,
15903 }),
15904 ))
15905 }
15906
15907 pub fn checkpoint(&self) -> Result<()> {
15925 self.checkpoint_controlled(|| Ok(()))
15926 }
15927
15928 #[doc(hidden)]
15931 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
15932 where
15933 F: FnOnce() -> Result<()>,
15934 {
15935 self.require(&crate::auth::Permission::Ddl)?;
15936 let _operation = self.admit_operation()?;
15938 let _replication = self.replication_barrier.write();
15942 let _ddl = self.ddl_lock.lock();
15943 let _security = self.security_coordinator.gate.read();
15944 self.require(&crate::auth::Permission::Ddl)?;
15945
15946 let mut handles = self
15947 .tables
15948 .read()
15949 .iter()
15950 .map(|(table_id, handle)| (*table_id, handle.clone()))
15951 .collect::<Vec<_>>();
15952 handles.sort_by_key(|(table_id, _)| *table_id);
15953 let mut tables = handles
15954 .iter()
15955 .map(|(table_id, handle)| (*table_id, handle.lock()))
15956 .collect::<Vec<_>>();
15957
15958 for (_, table) in &mut tables {
15960 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
15961 {
15962 table.force_flush()?;
15963 }
15964 }
15965
15966 for (_, table) in &mut tables {
15969 if table.run_count() >= 2 || table.should_compact() {
15970 table.compact()?;
15971 }
15972 }
15973
15974 before_wal_reset()?;
15975
15976 let maintenance_epoch = self.epoch.visible();
15978 let min_active = self.snapshots.min_active(maintenance_epoch);
15979 for (table_id, table) in &mut tables {
15980 let backup_pinned: HashSet<u128> = self
15981 .backup_pins
15982 .lock()
15983 .keys()
15984 .filter_map(|(pinned_table, run_id)| {
15985 (*pinned_table == *table_id).then_some(*run_id)
15986 })
15987 .collect();
15988 table.reap_retiring(min_active, &backup_pinned)?;
15989 }
15990
15991 self.shared_wal.lock().reset_after_checkpoint()?;
15994
15995 let catalog_snapshot = self.catalog.read().clone();
15997 for entry in &catalog_snapshot.tables {
15998 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
15999 crate::durable_file::remove_directory_all(
16000 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
16001 )?;
16002 }
16003 if !matches!(entry.state, TableState::Live) {
16004 continue;
16005 }
16006 let transaction_dir = self
16007 .root
16008 .join(TABLES_DIR)
16009 .join(entry.table_id.to_string())
16010 .join("_txn");
16011 if transaction_dir.is_dir() {
16012 for child in std::fs::read_dir(&transaction_dir)? {
16013 let child = child?;
16014 let active = child
16015 .file_name()
16016 .to_str()
16017 .and_then(|name| name.parse::<u64>().ok())
16018 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
16019 if !active {
16020 crate::durable_file::remove_directory_all(&child.path())?;
16021 }
16022 }
16023 }
16024 }
16025 let external_names = catalog_snapshot
16026 .external_tables
16027 .iter()
16028 .map(|entry| entry.name.as_str())
16029 .collect::<HashSet<_>>();
16030 let external_root = self.root.join(VTAB_DIR);
16031 if external_root.is_dir() {
16032 for entry in std::fs::read_dir(&external_root)? {
16033 let entry = entry?;
16034 let name = entry.file_name();
16035 if name
16036 .to_str()
16037 .is_some_and(|name| external_names.contains(name))
16038 {
16039 continue;
16040 }
16041 if entry.file_type()?.is_dir() {
16042 crate::durable_file::remove_directory_all(&entry.path())?;
16043 } else {
16044 std::fs::remove_file(entry.path())?;
16045 crate::durable_file::sync_directory(&external_root)?;
16046 }
16047 }
16048 }
16049
16050 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
16053 let visible = self.epoch.visible();
16054 for (_, table) in &tables {
16055 table.persist_manifest(visible)?;
16056 }
16057
16058 Ok(())
16059 }
16060 fn alloc_txn_id(&self) -> Result<u64> {
16061 self.ensure_owner_process()?;
16062 crate::txn::allocate_txn_id(&self.next_txn_id)
16063 }
16064
16065 pub fn allocate_lock_txn_id(&self) -> Result<u64> {
16069 self.alloc_txn_id()
16070 }
16071
16072 pub fn set_spill_threshold(&self, bytes: u64) {
16076 self.spill_threshold
16077 .store(bytes, std::sync::atomic::Ordering::Relaxed);
16078 }
16079
16080 #[doc(hidden)]
16084 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16085 *self.spill_hook.lock() = Some(Box::new(f));
16086 }
16087
16088 #[doc(hidden)]
16091 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16092 *self.security_commit_hook.lock() = Some(Box::new(f));
16093 }
16094
16095 #[doc(hidden)]
16098 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16099 *self.catalog_commit_hook.lock() = Some(Box::new(f));
16100 }
16101
16102 #[doc(hidden)]
16105 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16106 *self.backup_hook.lock() = Some(Box::new(f));
16107 }
16108
16109 #[doc(hidden)]
16113 pub fn __set_fk_lock_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16114 *self.fk_lock_hook.lock() = Some(Arc::new(f));
16115 }
16116
16117 #[doc(hidden)]
16119 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16120 *self.replication_hook.lock() = Some(Box::new(f));
16121 }
16122
16123 #[doc(hidden)]
16127 pub fn __wal_group_sync_count(&self) -> u64 {
16128 self.shared_wal.lock().group_sync_count()
16129 }
16130
16131 #[doc(hidden)]
16134 pub fn __poison(&self) {
16135 self.poisoned
16136 .store(true, std::sync::atomic::Ordering::Relaxed);
16137 }
16138
16139 pub fn check(&self) -> Vec<CheckIssue> {
16152 match self.check_inner(None) {
16153 Ok(issues) => issues,
16154 Err(error) => vec![CheckIssue {
16155 table_id: WAL_TABLE_ID,
16156 table_name: "shared WAL".into(),
16157 severity: "error".into(),
16158 description: error.to_string(),
16159 }],
16160 }
16161 }
16162
16163 #[doc(hidden)]
16165 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
16166 self.check_inner(Some(control))
16167 }
16168
16169 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
16170 let mut issues = Vec::new();
16171 let io_root = self.durable_root.io_path()?;
16172 let cat = self.catalog.read();
16173 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
16174 for (table_index, entry) in cat.tables.iter().enumerate() {
16175 if table_index % 256 == 0 {
16176 if let Some(control) = control {
16177 control.checkpoint()?;
16178 }
16179 }
16180 if !matches!(entry.state, TableState::Live) {
16181 continue;
16182 }
16183 let tdir = io_root.join(TABLES_DIR).join(entry.table_id.to_string());
16184 let mut err = |sev: &str, desc: String| {
16185 issues.push(CheckIssue {
16186 table_id: entry.table_id,
16187 table_name: entry.name.clone(),
16188 severity: sev.into(),
16189 description: desc,
16190 });
16191 };
16192 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
16193 Ok(m) => m,
16194 Err(e) => {
16195 err("error", format!("manifest read failed: {e}"));
16196 continue;
16197 }
16198 };
16199 if m.flushed_epoch > m.current_epoch {
16200 err(
16201 "error",
16202 format!(
16203 "flushed_epoch {} exceeds current_epoch {} (impossible)",
16204 m.flushed_epoch, m.current_epoch
16205 ),
16206 );
16207 }
16208
16209 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
16210 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
16211 for (run_index, rr) in m.runs.iter().enumerate() {
16212 if run_index % 256 == 0 {
16213 if let Some(control) = control {
16214 control.checkpoint()?;
16215 }
16216 }
16217 referenced.insert(rr.run_id);
16218 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
16219 if !run_path.exists() {
16220 err("error", format!("missing run file: r-{}.sr", rr.run_id));
16221 continue;
16222 }
16223 match crate::sorted_run::RunReader::open(
16224 &run_path,
16225 entry.schema.clone(),
16226 self.kek.clone(),
16227 ) {
16228 Ok(reader) => {
16229 if reader.row_count() as u64 != rr.row_count {
16230 err(
16231 "error",
16232 format!(
16233 "run r-{} row count mismatch: manifest {} vs run {}",
16234 rr.run_id,
16235 rr.row_count,
16236 reader.row_count()
16237 ),
16238 );
16239 }
16240 }
16241 Err(e) => {
16242 err(
16243 "error",
16244 format!("run r-{} integrity check failed: {e}", rr.run_id),
16245 );
16246 }
16247 }
16248 }
16249
16250 for r in &m.retiring {
16254 referenced.insert(r.run_id);
16255 }
16256
16257 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
16259 for (entry_index, ent) in rd.flatten().enumerate() {
16260 if entry_index % 256 == 0 {
16261 if let Some(control) = control {
16262 control.checkpoint()?;
16263 }
16264 }
16265 let p = ent.path();
16266 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
16267 continue;
16268 }
16269 let run_id = p
16270 .file_stem()
16271 .and_then(|s| s.to_str())
16272 .and_then(|s| s.strip_prefix("r-"))
16273 .and_then(|s| s.parse::<u128>().ok());
16274 if let Some(id) = run_id {
16275 if !referenced.contains(&id) {
16276 err(
16277 "warning",
16278 format!("orphan run file r-{id}.sr not referenced by the manifest"),
16279 );
16280 }
16281 }
16282 }
16283 }
16284 }
16285
16286 let external_names = cat
16287 .external_tables
16288 .iter()
16289 .map(|entry| entry.name.clone())
16290 .collect::<std::collections::HashSet<_>>();
16291 let vtab_dir = io_root.join(VTAB_DIR);
16292 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
16293 for (entry_index, entry) in entries.flatten().enumerate() {
16294 if entry_index % 256 == 0 {
16295 if let Some(control) = control {
16296 control.checkpoint()?;
16297 }
16298 }
16299 let name = entry.file_name();
16300 let Some(name) = name.to_str() else { continue };
16301 if !external_names.contains(name) {
16302 issues.push(CheckIssue {
16303 table_id: EXTERNAL_TABLE_ID,
16304 table_name: name.to_string(),
16305 severity: "warning".into(),
16306 description: format!(
16307 "orphan external table state entry {:?} not referenced by the catalog",
16308 entry.path()
16309 ),
16310 });
16311 }
16312 }
16313 }
16314
16315 if let Some(control) = control {
16322 control.checkpoint()?;
16323 }
16324 for (seg, msg) in self.shared_wal.lock().verify_segments() {
16325 issues.push(CheckIssue {
16326 table_id: WAL_TABLE_ID,
16327 table_name: "<wal>".into(),
16328 severity: "error".into(),
16329 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
16330 });
16331 }
16332 Ok(issues)
16333 }
16334
16335 pub fn doctor(&self) -> Result<Vec<u64>> {
16339 let control = crate::ExecutionControl::new(None);
16340 self.doctor_controlled(&control, || true)
16341 }
16342
16343 #[doc(hidden)]
16347 pub fn doctor_controlled<F>(
16348 &self,
16349 control: &crate::ExecutionControl,
16350 before_publish: F,
16351 ) -> Result<Vec<u64>>
16352 where
16353 F: FnOnce() -> bool,
16354 {
16355 self.doctor_controlled_with_receipt(control, before_publish)
16356 .map(|(quarantined, _)| quarantined)
16357 }
16358
16359 #[doc(hidden)]
16362 pub fn doctor_controlled_with_receipt<F>(
16363 &self,
16364 control: &crate::ExecutionControl,
16365 before_publish: F,
16366 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
16367 where
16368 F: FnOnce() -> bool,
16369 {
16370 let _ddl = self.ddl_lock.lock();
16373 let _security_write = self.security_write()?;
16374 let issues = self.check_inner(Some(control))?;
16375 let bad_tables: std::collections::HashSet<u64> = issues
16380 .iter()
16381 .filter(|i| {
16382 i.severity == "error"
16383 && i.table_id != WAL_TABLE_ID
16384 && i.table_id != EXTERNAL_TABLE_ID
16385 })
16386 .map(|i| i.table_id)
16387 .collect();
16388 if bad_tables.is_empty() {
16389 return Ok((Vec::new(), None));
16390 }
16391 let _commit = self.commit_lock.lock();
16392 control.checkpoint()?;
16393 if !before_publish() {
16394 return Err(MongrelError::Cancelled);
16395 }
16396 let maintenance_epoch = self.epoch.bump_assigned();
16397 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
16398
16399 let qdir = self.root.join("_quarantine");
16400 crate::durable_file::create_directory(&qdir)?;
16401 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
16402 bad_tables.sort_unstable();
16403
16404 let mut handles = self
16408 .tables
16409 .read()
16410 .iter()
16411 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
16412 .map(|(table_id, handle)| (*table_id, handle.clone()))
16413 .collect::<Vec<_>>();
16414 handles.sort_by_key(|(table_id, _)| *table_id);
16415 let mut table_guards = handles
16416 .iter()
16417 .map(|(table_id, handle)| (*table_id, handle.lock()))
16418 .collect::<Vec<_>>();
16419
16420 let mut next_catalog = self.catalog.read().clone();
16421 for table_id in &bad_tables {
16422 if let Some(entry) = next_catalog
16423 .tables
16424 .iter_mut()
16425 .find(|entry| entry.table_id == *table_id)
16426 {
16427 entry.state = TableState::Dropped {
16428 at_epoch: maintenance_epoch.0,
16429 };
16430 }
16431 }
16432 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
16433
16434 let txn_id = self.alloc_txn_id()?;
16435 let commit_seq = {
16436 let mut wal = self.shared_wal.lock();
16437 let append: Result<u64> = (|| {
16438 for table_id in &bad_tables {
16439 wal.append(
16440 txn_id,
16441 *table_id,
16442 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
16443 table_id: *table_id,
16444 }),
16445 )?;
16446 }
16447 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
16448 wal.append_commit(txn_id, maintenance_epoch, &[])
16449 })();
16450 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
16451 };
16452 let receipt = self.await_durable_commit(txn_id, commit_seq, maintenance_epoch)?;
16453 for (_, table) in &mut table_guards {
16454 table.mark_unavailable_after_quarantine();
16455 }
16456 {
16457 let mut live_tables = self.tables.write();
16458 for table_id in &bad_tables {
16459 live_tables.remove(table_id);
16460 }
16461 }
16462 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
16463 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, &receipt, checkpoint)?;
16464
16465 drop(table_guards);
16469 drop(handles);
16470
16471 for table_id in &bad_tables {
16475 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
16476 if source.exists() {
16477 let destination = qdir.join(table_id.to_string());
16478 if let Err(error) = crate::durable_file::rename(&source, &destination) {
16479 return Err(MongrelError::DurableCommit {
16480 epoch: maintenance_epoch.0,
16481 message: format!(
16482 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
16483 ),
16484 });
16485 }
16486 }
16487 }
16488 Ok((
16489 bad_tables,
16490 Some(MaintenanceReceipt {
16491 epoch: maintenance_epoch,
16492 }),
16493 ))
16494 }
16495
16496 #[allow(dead_code)]
16498 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
16499 self.kek.as_ref()
16500 }
16501
16502 #[allow(dead_code)]
16504 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
16505 &self.epoch
16506 }
16507
16508 #[allow(dead_code)]
16510 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
16511 &self.snapshots
16512 }
16513}
16514
16515fn external_state_dir(root: &Path, name: &str) -> PathBuf {
16516 root.join(VTAB_DIR).join(name)
16517}
16518
16519fn append_catalog_snapshot(
16520 wal: &mut crate::wal::SharedWal,
16521 txn_id: u64,
16522 catalog: &Catalog,
16523) -> Result<()> {
16524 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
16525 wal.append(
16526 txn_id,
16527 WAL_TABLE_ID,
16528 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
16529 )?;
16530 Ok(())
16531}
16532
16533fn filter_ignored_staging(
16534 staging: Vec<(u64, crate::txn::Staged)>,
16535 ignored_indices: &std::collections::BTreeSet<usize>,
16536) -> Vec<(u64, crate::txn::Staged)> {
16537 if ignored_indices.is_empty() {
16538 return staging;
16539 }
16540 staging
16541 .into_iter()
16542 .enumerate()
16543 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
16544 .collect()
16545}
16546
16547fn external_state_file(root: &Path, name: &str) -> PathBuf {
16548 external_state_dir(root, name).join("state.json")
16549}
16550
16551fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
16552 let path = external_state_file(root, name);
16553 match std::fs::read(path) {
16554 Ok(bytes) => Ok(bytes),
16555 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
16556 Err(e) => Err(e.into()),
16557 }
16558}
16559
16560fn current_external_state_bytes(
16561 root: &Path,
16562 external_states: &[(String, Vec<u8>)],
16563 name: &str,
16564) -> Result<Vec<u8>> {
16565 for (table, state) in external_states.iter().rev() {
16566 if table == name {
16567 return Ok(state.clone());
16568 }
16569 }
16570 read_external_state_file(root, name)
16571}
16572
16573fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
16574 let mut out = external_states;
16575 dedup_external_states_in_place(&mut out);
16576 out
16577}
16578
16579fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
16580 let mut seen = std::collections::HashSet::new();
16581 let mut out = Vec::with_capacity(external_states.len());
16582 for (name, state) in std::mem::take(external_states).into_iter().rev() {
16583 if seen.insert(name.clone()) {
16584 out.push((name, state));
16585 }
16586 }
16587 out.reverse();
16588 *external_states = out;
16589}
16590
16591fn prepare_external_state_file(
16592 root: &Path,
16593 name: &str,
16594 state: &[u8],
16595 txn_id: u64,
16596) -> Result<PathBuf> {
16597 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
16598 let dir = external_state_dir(root, name);
16599 crate::durable_file::create_directory(&dir)?;
16600 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
16601 {
16602 let mut file = std::fs::OpenOptions::new()
16603 .create_new(true)
16604 .write(true)
16605 .open(&pending)?;
16606 file.write_all(state)?;
16607 file.sync_all()?;
16608 }
16609 Ok(pending)
16610}
16611
16612fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
16613 let path = external_state_file(root, name);
16614 crate::durable_file::replace(pending, &path)?;
16615 Ok(())
16616}
16617
16618fn write_external_state_file(
16619 durable: &crate::durable_file::DurableRoot,
16620 name: &str,
16621 state: &[u8],
16622) -> Result<()> {
16623 let directory = Path::new(VTAB_DIR).join(name);
16624 durable.create_directory_all(&directory)?;
16625 durable.write_atomic(directory.join("state.json"), state)?;
16626 Ok(())
16627}
16628
16629fn validate_recovered_data_table(
16630 catalog: &Catalog,
16631 tables: &HashMap<u64, TableHandle>,
16632 table_id: u64,
16633 commit_epoch: u64,
16634 offset: u64,
16635) -> Result<bool> {
16636 let entry = catalog
16637 .tables
16638 .iter()
16639 .find(|entry| entry.table_id == table_id)
16640 .ok_or_else(|| MongrelError::CorruptWal {
16641 offset,
16642 reason: format!("committed record references unknown table {table_id}"),
16643 })?;
16644 if commit_epoch < entry.created_epoch {
16645 return Err(MongrelError::CorruptWal {
16646 offset,
16647 reason: format!(
16648 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16649 entry.created_epoch
16650 ),
16651 });
16652 }
16653 match entry.state {
16654 TableState::Dropped { at_epoch } => {
16655 let abandoned_build_boundary =
16660 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16661 if commit_epoch >= at_epoch && !abandoned_build_boundary {
16662 Err(MongrelError::CorruptWal {
16663 offset,
16664 reason: format!(
16665 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16666 ),
16667 })
16668 } else {
16669 Ok(false)
16670 }
16671 }
16672 TableState::Live | TableState::Building { .. } => {
16673 if tables.contains_key(&table_id) {
16674 Ok(true)
16675 } else {
16676 Err(MongrelError::CorruptWal {
16677 offset,
16678 reason: format!("live table {table_id} has no mounted recovery handle"),
16679 })
16680 }
16681 }
16682 }
16683}
16684
16685type RecoveryTableStage = (
16686 Vec<crate::memtable::Row>,
16687 Vec<(crate::rowid::RowId, Epoch)>,
16688 Option<Epoch>,
16689 Epoch,
16690);
16691
16692#[derive(Clone)]
16693struct RecoveryValidationTable {
16694 schema: Schema,
16695 flushed_epoch: u64,
16696}
16697
16698fn validate_shared_wal_recovery_plan(
16699 durable_root: &crate::durable_file::DurableRoot,
16700 catalog: &Catalog,
16701 recovered_table_ids: &HashSet<u64>,
16702 reconciled_table_ids: &HashSet<u64>,
16703 meta_dek: Option<&[u8; META_DEK_LEN]>,
16704 kek: Option<Arc<crate::encryption::Kek>>,
16705 records: &[crate::wal::Record],
16706) -> Result<()> {
16707 use crate::wal::{DdlOp, Op};
16708
16709 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
16710 for entry in &catalog.tables {
16711 if !matches!(entry.state, TableState::Live) {
16712 continue;
16713 }
16714 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
16715 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
16716 Ok(manifest) => Some(manifest),
16717 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
16718 Err(error) => return Err(error),
16719 };
16720 let flushed_epoch = if let Some(manifest) = manifest {
16721 if manifest.table_id != entry.table_id {
16722 return Err(MongrelError::Conflict(format!(
16723 "catalog table {} storage identity mismatch",
16724 entry.table_id
16725 )));
16726 }
16727 if (manifest.schema_id != entry.schema.schema_id
16728 && !reconciled_table_ids.contains(&entry.table_id))
16729 || manifest.flushed_epoch > manifest.current_epoch
16730 || manifest.global_idx_epoch > manifest.current_epoch
16731 || manifest.next_row_id == u64::MAX
16732 || manifest.auto_inc_next < 0
16733 || manifest.auto_inc_next == i64::MAX
16734 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
16735 {
16736 return Err(MongrelError::InvalidArgument(format!(
16737 "table {} manifest counters or schema identity are invalid",
16738 entry.table_id
16739 )));
16740 }
16741 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
16742 crate::global_idx::read_durable_for(
16743 durable_root,
16744 &relative_dir,
16745 entry.table_id,
16746 &entry.schema,
16747 idx_dek.as_deref(),
16748 )?;
16749 let mut run_ids = HashSet::new();
16750 let mut maximum_row_id = None::<u64>;
16751 for run in &manifest.runs {
16752 if run.run_id >= u64::MAX as u128
16753 || run.epoch_created > manifest.current_epoch
16754 || !run_ids.insert(run.run_id)
16755 {
16756 return Err(MongrelError::InvalidArgument(format!(
16757 "table {} manifest contains an invalid or duplicate run id",
16758 entry.table_id
16759 )));
16760 }
16761 let relative = relative_dir
16762 .join(crate::engine::RUNS_DIR)
16763 .join(format!("r-{}.sr", run.run_id as u64));
16764 let file = durable_root.open_regular(&relative)?;
16765 let mut reader = crate::sorted_run::RunReader::open_file(
16766 file,
16767 entry.schema.clone(),
16768 kek.clone(),
16769 )?;
16770 let header = reader.header();
16771 if header.run_id != run.run_id
16772 || header.level != run.level
16773 || header.row_count != run.row_count
16774 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
16775 || header.is_uniform_epoch() && header.epoch_created != 0
16776 || header.schema_id > entry.schema.schema_id
16777 {
16778 return Err(MongrelError::InvalidArgument(format!(
16779 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
16780 entry.table_id,
16781 run.run_id,
16782 header.run_id,
16783 header.level,
16784 header.row_count,
16785 header.epoch_created,
16786 header.schema_id,
16787 run.run_id,
16788 run.level,
16789 run.row_count,
16790 run.epoch_created,
16791 entry.schema.schema_id,
16792 )));
16793 }
16794 if header.row_count != 0 {
16795 maximum_row_id = Some(
16796 maximum_row_id
16797 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
16798 );
16799 }
16800 reader.validate_all_pages()?;
16801 }
16802 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
16803 return Err(MongrelError::InvalidArgument(format!(
16804 "table {} next_row_id does not advance beyond persisted rows",
16805 entry.table_id
16806 )));
16807 }
16808 for run in &manifest.retiring {
16809 if run.run_id >= u64::MAX as u128
16810 || run.retire_epoch > manifest.current_epoch
16811 || !run_ids.insert(run.run_id)
16812 {
16813 return Err(MongrelError::InvalidArgument(format!(
16814 "table {} manifest contains an invalid or aliased retired run",
16815 entry.table_id
16816 )));
16817 }
16818 }
16819 manifest.flushed_epoch
16820 } else {
16821 if !recovered_table_ids.contains(&entry.table_id) {
16822 return Err(MongrelError::NotFound(format!(
16823 "live table {} manifest is missing",
16824 entry.table_id
16825 )));
16826 }
16827 0
16828 };
16829 tables.insert(
16830 entry.table_id,
16831 RecoveryValidationTable {
16832 schema: entry.schema.clone(),
16833 flushed_epoch,
16834 },
16835 );
16836 }
16837
16838 let committed = records
16839 .iter()
16840 .filter_map(|record| match record.op {
16841 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
16842 _ => None,
16843 })
16844 .collect::<HashMap<_, _>>();
16845 let mut run_ids = HashSet::new();
16846 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
16847 for record in records {
16848 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
16849 continue;
16850 };
16851 match &record.op {
16852 Op::Put { table_id, rows } => {
16853 let table = validate_recovery_data_table_plan(
16854 catalog,
16855 &tables,
16856 *table_id,
16857 commit_epoch,
16858 record.seq.0,
16859 )?;
16860 let decoded: Vec<crate::memtable::Row> =
16861 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
16862 offset: record.seq.0,
16863 reason: format!(
16864 "committed Put payload for transaction {} could not be decoded: {error}",
16865 record.txn_id
16866 ),
16867 })?;
16868 if let Some(table) = table {
16869 for row in &decoded {
16870 if !recovered_row_ids
16871 .entry(*table_id)
16872 .or_default()
16873 .insert(row.row_id.0)
16874 {
16875 return Err(MongrelError::CorruptWal {
16876 offset: record.seq.0,
16877 reason: format!(
16878 "committed WAL repeats recovered row id {} for table {table_id}",
16879 row.row_id.0
16880 ),
16881 });
16882 }
16883 validate_recovered_row(&table.schema, row)?;
16884 }
16885 }
16886 }
16887 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
16888 validate_recovery_data_table_plan(
16889 catalog,
16890 &tables,
16891 *table_id,
16892 commit_epoch,
16893 record.seq.0,
16894 )?;
16895 }
16896 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
16897 Op::Ddl(DdlOp::ResetExternalTableState {
16898 name,
16899 generation_epoch,
16900 }) => {
16901 if *generation_epoch != commit_epoch {
16902 return Err(MongrelError::CorruptWal {
16903 offset: record.seq.0,
16904 reason: format!(
16905 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
16906 ),
16907 });
16908 }
16909 validate_recovered_external_name(name)?;
16910 }
16911 Op::TxnCommit { added_runs, .. } => {
16912 for added in added_runs {
16913 let Some(table) = validate_recovery_data_table_plan(
16914 catalog,
16915 &tables,
16916 added.table_id,
16917 commit_epoch,
16918 record.seq.0,
16919 )?
16920 else {
16921 continue;
16922 };
16923 if added.run_id >= u64::MAX as u128
16924 || !run_ids.insert((added.table_id, added.run_id))
16925 {
16926 return Err(MongrelError::CorruptWal {
16927 offset: record.seq.0,
16928 reason: format!(
16929 "duplicate or invalid recovered run {} for table {}",
16930 added.run_id, added.table_id
16931 ),
16932 });
16933 }
16934 if commit_epoch <= table.flushed_epoch {
16935 continue;
16936 }
16937 validate_planned_spilled_run(
16938 durable_root,
16939 record.txn_id,
16940 commit_epoch,
16941 added,
16942 &table.schema,
16943 kek.clone(),
16944 )?;
16945 }
16946 }
16947 _ => {}
16948 }
16949 }
16950 Ok(())
16951}
16952
16953fn validate_recovery_data_table_plan<'a>(
16954 catalog: &Catalog,
16955 tables: &'a HashMap<u64, RecoveryValidationTable>,
16956 table_id: u64,
16957 commit_epoch: u64,
16958 offset: u64,
16959) -> Result<Option<&'a RecoveryValidationTable>> {
16960 let entry = catalog
16961 .tables
16962 .iter()
16963 .find(|entry| entry.table_id == table_id)
16964 .ok_or_else(|| MongrelError::CorruptWal {
16965 offset,
16966 reason: format!("committed record references unknown table {table_id}"),
16967 })?;
16968 if commit_epoch < entry.created_epoch {
16969 return Err(MongrelError::CorruptWal {
16970 offset,
16971 reason: format!(
16972 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16973 entry.created_epoch
16974 ),
16975 });
16976 }
16977 match entry.state {
16978 TableState::Dropped { at_epoch } => {
16979 let abandoned =
16980 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16981 if commit_epoch >= at_epoch && !abandoned {
16982 return Err(MongrelError::CorruptWal {
16983 offset,
16984 reason: format!(
16985 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16986 ),
16987 });
16988 }
16989 Ok(None)
16990 }
16991 TableState::Live => {
16992 tables
16993 .get(&table_id)
16994 .map(Some)
16995 .ok_or_else(|| MongrelError::CorruptWal {
16996 offset,
16997 reason: format!("live table {table_id} has no recovery plan"),
16998 })
16999 }
17000 TableState::Building { .. } => Err(MongrelError::CorruptWal {
17001 offset,
17002 reason: format!("building table {table_id} was not normalized before recovery"),
17003 }),
17004 }
17005}
17006
17007fn validate_planned_spilled_run(
17008 root: &crate::durable_file::DurableRoot,
17009 txn_id: u64,
17010 commit_epoch: u64,
17011 added: &crate::wal::AddedRun,
17012 schema: &Schema,
17013 kek: Option<Arc<crate::encryption::Kek>>,
17014) -> Result<()> {
17015 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
17016 let destination = table
17017 .join(crate::engine::RUNS_DIR)
17018 .join(format!("r-{}.sr", added.run_id as u64));
17019 let pending = table
17020 .join("_txn")
17021 .join(txn_id.to_string())
17022 .join(format!("r-{}.sr", added.run_id as u64));
17023 let file = match root.open_regular(&destination) {
17024 Ok(file) => file,
17025 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
17026 root.open_regular(&pending).map_err(|pending_error| {
17027 if pending_error.kind() == std::io::ErrorKind::NotFound {
17028 MongrelError::CorruptWal {
17029 offset: commit_epoch,
17030 reason: format!(
17031 "committed spilled run {} for transaction {txn_id} is missing",
17032 added.run_id
17033 ),
17034 }
17035 } else {
17036 pending_error.into()
17037 }
17038 })?
17039 }
17040 Err(error) => return Err(error.into()),
17041 };
17042 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
17043 let header = reader.header();
17044 if header.run_id != added.run_id
17045 || header.content_hash != added.content_hash
17046 || header.row_count != added.row_count
17047 || header.level != added.level
17048 || header.min_row_id != added.min_row_id
17049 || header.max_row_id != added.max_row_id
17050 || header.schema_id != schema.schema_id
17051 || !header.is_uniform_epoch()
17052 || header.epoch_created != 0
17053 {
17054 return Err(MongrelError::CorruptWal {
17055 offset: commit_epoch,
17056 reason: format!(
17057 "committed spilled run {} metadata differs from WAL",
17058 added.run_id
17059 ),
17060 });
17061 }
17062 reader.validate_all_pages()?;
17063 Ok(())
17064}
17065
17066#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17086pub enum StagedTxnWrite {
17087 Put {
17092 table_id: u64,
17094 rows: Vec<u8>,
17096 },
17097 Delete {
17099 table_id: u64,
17101 row_ids: Vec<u64>,
17103 },
17104}
17105
17106impl StagedTxnWrite {
17107 pub fn encode(&self) -> Result<Vec<u8>> {
17109 Ok(bincode::serialize(self)?)
17110 }
17111
17112 pub fn decode(bytes: &[u8]) -> Result<Self> {
17114 Ok(bincode::deserialize(bytes)?)
17115 }
17116}
17117
17118pub fn translate_records_for_replication(
17155 records: &[crate::wal::Record],
17156) -> Result<Vec<crate::wal::Record>> {
17157 use crate::wal::Op;
17158
17159 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
17162 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
17163 })?;
17164 if records.iter().any(|record| record.txn_id != txn_id) {
17165 return Err(MongrelError::InvalidArgument(
17166 "replicated transaction payload mixes transaction ids".into(),
17167 ));
17168 }
17169 let commits = records
17170 .iter()
17171 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
17172 .count();
17173 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
17174 return Err(MongrelError::InvalidArgument(
17175 "replicated transaction payload must end in exactly one commit marker".into(),
17176 ));
17177 }
17178
17179 let mut spilled_rows: HashMap<u64, Vec<crate::memtable::Row>> = HashMap::new();
17183 for record in records {
17184 if let Op::SpilledRows { table_id, rows } = &record.op {
17185 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(rows).map_err(|error| {
17186 MongrelError::InvalidArgument(format!(
17187 "spilled row payload for table {table_id} cannot decode for replication: \
17188 {error}"
17189 ))
17190 })?;
17191 spilled_rows.entry(*table_id).or_default().extend(chunk);
17192 }
17193 }
17194
17195 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
17199 unreachable!("one trailing commit marker validated above");
17200 };
17201 for run in added_runs {
17202 let rows = spilled_rows.get(&run.table_id).ok_or_else(|| {
17203 MongrelError::InvalidArgument(format!(
17204 "commit links spilled run {} for table {} but carries no logical row records \
17205 for it",
17206 run.run_id, run.table_id
17207 ))
17208 })?;
17209 let covered = rows
17210 .iter()
17211 .filter(|row| row.row_id.0 >= run.min_row_id && row.row_id.0 <= run.max_row_id)
17212 .count() as u64;
17213 if covered != run.row_count {
17214 return Err(MongrelError::InvalidArgument(format!(
17215 "commit links spilled run {} for table {} ({} rows in [{}, {}]) but the logical \
17216 row records cover {} rows",
17217 run.run_id, run.table_id, run.row_count, run.min_row_id, run.max_row_id, covered
17218 )));
17219 }
17220 }
17221
17222 let translated = records
17225 .iter()
17226 .map(|record| {
17227 let op = match &record.op {
17228 Op::SpilledRows { table_id, rows } => Op::Put {
17229 table_id: *table_id,
17230 rows: rows.clone(),
17231 },
17232 Op::TxnCommit { epoch, .. } => Op::TxnCommit {
17233 epoch: *epoch,
17234 added_runs: Vec::new(),
17235 },
17236 op => op.clone(),
17237 };
17238 crate::wal::Record::new(record.seq, record.txn_id, op)
17239 })
17240 .collect();
17241 Ok(translated)
17242}
17243
17244fn recover_shared_wal(
17245 durable_root: &crate::durable_file::DurableRoot,
17246 tables: &HashMap<u64, TableHandle>,
17247 catalog: &Catalog,
17248 epoch: &EpochAuthority,
17249 records: &[crate::wal::Record],
17250) -> Result<()> {
17251 use crate::memtable::Row;
17252 use crate::wal::{DdlOp, Op};
17253
17254 let mut committed: HashMap<u64, u64> = HashMap::new();
17256 let mut commit_ts_by_txn: HashMap<u64, mongreldb_types::hlc::HlcTimestamp> = HashMap::new();
17258 let mut spilled_to_link: Vec<(
17259 u64, u64, Vec<crate::wal::AddedRun>,
17262 )> = Vec::new();
17263 for r in records {
17264 match &r.op {
17265 Op::CommitTimestamp { unix_nanos } => {
17266 commit_ts_by_txn.insert(
17267 r.txn_id,
17268 mongreldb_types::hlc::HlcTimestamp {
17269 physical_micros: unix_nanos / 1_000,
17270 logical: 0,
17271 node_tiebreaker: 0,
17272 },
17273 );
17274 }
17275 Op::TxnCommit {
17276 epoch: ce,
17277 ref added_runs,
17278 } => {
17279 committed.insert(r.txn_id, *ce);
17280 if !added_runs.is_empty() {
17281 spilled_to_link.push((r.txn_id, *ce, added_runs.clone()));
17282 }
17283 }
17284 _ => {}
17285 }
17286 }
17287 for record in records {
17288 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
17289 continue;
17290 };
17291 match &record.op {
17292 Op::Put { table_id, .. }
17293 | Op::Delete { table_id, .. }
17294 | Op::TruncateTable { table_id } => {
17295 validate_recovered_data_table(
17296 catalog,
17297 tables,
17298 *table_id,
17299 commit_epoch,
17300 record.seq.0,
17301 )?;
17302 }
17303 Op::TxnCommit { added_runs, .. } => {
17304 for run in added_runs {
17305 validate_recovered_data_table(
17306 catalog,
17307 tables,
17308 run.table_id,
17309 commit_epoch,
17310 record.seq.0,
17311 )?;
17312 }
17313 }
17314 _ => {}
17315 }
17316 }
17317 let truncated_transactions: HashSet<(u64, u64)> = records
17318 .iter()
17319 .filter_map(|record| {
17320 committed.get(&record.txn_id)?;
17321 match record.op {
17322 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
17323 _ => None,
17324 }
17325 })
17326 .collect();
17327
17328 enum ExternalRecoveryAction {
17330 Write { name: String, state: Vec<u8> },
17331 Reset { name: String },
17332 }
17333 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
17334 let mut external_actions = Vec::new();
17335 let mut max_epoch = epoch.visible().0;
17336 for r in records.iter().cloned() {
17337 let Some(&ce) = committed.get(&r.txn_id) else {
17338 continue; };
17340 let commit_epoch = Epoch(ce);
17341 max_epoch = max_epoch.max(ce);
17342 match r.op {
17343 Op::Put { table_id, rows } => {
17344 let skip = tables
17346 .get(&table_id)
17347 .map(|h| h.lock().flushed_epoch() >= ce)
17348 .unwrap_or(true);
17349 if skip {
17350 continue;
17351 }
17352 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
17353 MongrelError::CorruptWal {
17354 offset: r.seq.0,
17355 reason: format!(
17356 "committed Put payload for transaction {} could not be decoded: {error}",
17357 r.txn_id
17358 ),
17359 }
17360 })?;
17361 let txn_commit_ts = commit_ts_by_txn.get(&r.txn_id).copied();
17366 let rows: Vec<Row> = rows
17367 .into_iter()
17368 .map(|mut row| {
17369 row.committed_epoch = commit_epoch;
17370 if row.commit_ts.is_none() {
17371 row.commit_ts = txn_commit_ts;
17372 }
17373 row
17374 })
17375 .collect();
17376 let entry = stage
17377 .entry(table_id)
17378 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17379 entry.0.extend(rows);
17380 entry.3 = commit_epoch;
17381 }
17382 Op::Delete { table_id, row_ids } => {
17383 let skip = tables
17384 .get(&table_id)
17385 .map(|h| h.lock().flushed_epoch() >= ce)
17386 .unwrap_or(true);
17387 if skip {
17388 continue;
17389 }
17390 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
17391 let entry = stage
17392 .entry(table_id)
17393 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17394 entry.1.extend(dels);
17395 entry.3 = commit_epoch;
17396 }
17397 Op::TruncateTable { table_id } => {
17398 let skip = tables
17399 .get(&table_id)
17400 .map(|h| h.lock().flushed_epoch() >= ce)
17401 .unwrap_or(true);
17402 if skip {
17403 continue;
17404 }
17405 stage.insert(
17406 table_id,
17407 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
17408 );
17409 }
17410 Op::ExternalTableState { name, state } => {
17411 let current_generation = catalog
17412 .external_tables
17413 .iter()
17414 .find(|entry| entry.name == name)
17415 .map(|entry| entry.created_epoch);
17416 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
17417 validate_recovered_external_name(&name)?;
17418 external_actions.push(ExternalRecoveryAction::Write { name, state });
17419 }
17420 }
17421 Op::Ddl(DdlOp::ResetExternalTableState {
17422 name,
17423 generation_epoch,
17424 }) => {
17425 if generation_epoch != ce {
17426 return Err(MongrelError::CorruptWal {
17427 offset: r.seq.0,
17428 reason: format!(
17429 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
17430 ),
17431 });
17432 }
17433 validate_recovered_external_name(&name)?;
17434 external_actions.push(ExternalRecoveryAction::Reset { name });
17435 }
17436 Op::Flush { .. }
17437 | Op::TxnCommit { .. }
17438 | Op::TxnAbort
17439 | Op::Ddl(_)
17440 | Op::BeforeImage { .. }
17441 | Op::CommitTimestamp { .. }
17442 | Op::SpilledRows { .. } => {}
17443 }
17444 }
17445 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
17446 added_runs.retain(|added| {
17447 tables
17448 .get(&added.table_id)
17449 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
17450 });
17451 }
17452 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
17453 validate_recovery_table_stages(tables, &stage)?;
17454 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
17455
17456 for action in external_actions {
17460 match action {
17461 ExternalRecoveryAction::Write { name, state } => {
17462 write_external_state_file(durable_root, &name, &state)?;
17463 }
17464 ExternalRecoveryAction::Reset { name } => {
17465 durable_root.create_directory_all(VTAB_DIR)?;
17466 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
17467 }
17468 }
17469 }
17470 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
17471 let Some(handle) = tables.get(&table_id) else {
17472 continue;
17473 };
17474 let mut t = handle.lock();
17475 if let Some(epoch) = truncate_epoch {
17476 t.apply_truncate(epoch);
17477 }
17478 t.recover_apply(rows, deletes)?;
17479 let rows = t.visible_rows(Snapshot::unbounded())?;
17483 t.live_count = rows.len() as u64;
17484 t.persist_manifest(table_epoch.max(epoch.visible()))?;
17488 }
17489
17490 for (txn_id, ce, added_runs) in &spilled_to_link {
17494 for ar in added_runs {
17495 let Some(handle) = tables.get(&ar.table_id) else {
17496 continue;
17497 };
17498 let mut t = handle.lock();
17499 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
17500 let destination = table_dir
17501 .join(crate::engine::RUNS_DIR)
17502 .join(format!("r-{}.sr", ar.run_id));
17503 match durable_root.open_regular(&destination) {
17504 Ok(_) => {}
17505 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
17506 let pending = table_dir
17507 .join("_txn")
17508 .join(txn_id.to_string())
17509 .join(format!("r-{}.sr", ar.run_id));
17510 durable_root.rename_file_new(&pending, &destination)?;
17511 }
17512 Err(error) => return Err(error.into()),
17513 }
17514 let linked = t.recover_spilled_run(crate::manifest::RunRef {
17520 run_id: ar.run_id,
17521 level: ar.level,
17522 epoch_created: *ce,
17523 row_count: ar.row_count,
17524 });
17525 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
17526 if replaced {
17527 t.set_flushed_epoch(Epoch(*ce));
17528 }
17529 if linked || replaced {
17530 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
17531 }
17532 }
17533 }
17534
17535 epoch.advance_recovered(Epoch(max_epoch));
17536 Ok(())
17537}
17538
17539fn reconcile_recovered_table_metadata(
17540 tables: &HashMap<u64, TableHandle>,
17541 epoch: Epoch,
17542) -> Result<()> {
17543 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
17544 table_ids.sort_unstable();
17545 let mut plans = Vec::with_capacity(table_ids.len());
17546 for table_id in &table_ids {
17547 let handle = tables.get(table_id).ok_or_else(|| {
17548 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17549 })?;
17550 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
17551 }
17552 for (table_id, plan) in plans {
17555 let handle = tables.get(&table_id).ok_or_else(|| {
17556 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17557 })?;
17558 handle.lock().apply_recovered_metadata(plan, epoch)?;
17559 }
17560 Ok(())
17561}
17562
17563fn validate_recovered_external_name(name: &str) -> Result<()> {
17564 if name.is_empty()
17565 || !name.chars().all(|character| {
17566 character.is_ascii_alphanumeric() || character == '_' || character == '-'
17567 })
17568 {
17569 return Err(MongrelError::CorruptWal {
17570 offset: 0,
17571 reason: format!("unsafe recovered external-table name {name:?}"),
17572 });
17573 }
17574 Ok(())
17575}
17576
17577fn validate_recovery_table_stages(
17578 tables: &HashMap<u64, TableHandle>,
17579 stages: &HashMap<u64, RecoveryTableStage>,
17580) -> Result<()> {
17581 for (table_id, (rows, _, _, _)) in stages {
17582 let handle = tables
17583 .get(table_id)
17584 .ok_or_else(|| MongrelError::CorruptWal {
17585 offset: *table_id,
17586 reason: format!("recovery stage references unmounted table {table_id}"),
17587 })?;
17588 let table = handle.lock();
17589 table.visible_rows(Snapshot::unbounded())?;
17592 for row in rows {
17593 validate_recovered_row(table.schema(), row)?;
17594 }
17595 }
17596 Ok(())
17597}
17598
17599fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
17600 if row.deleted || row.row_id.0 == u64::MAX {
17601 return Err(MongrelError::CorruptWal {
17602 offset: row.row_id.0,
17603 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
17604 });
17605 }
17606 let cells = row
17607 .columns
17608 .iter()
17609 .map(|(column, value)| (*column, value.clone()))
17610 .collect::<Vec<_>>();
17611 schema
17612 .validate_persisted_values(&cells)
17613 .map_err(|error| MongrelError::CorruptWal {
17614 offset: row.row_id.0,
17615 reason: format!("recovered row violates table schema: {error}"),
17616 })?;
17617 if schema.auto_increment_column().is_some_and(|column| {
17618 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
17619 }) {
17620 return Err(MongrelError::CorruptWal {
17621 offset: row.row_id.0,
17622 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
17623 });
17624 }
17625 Ok(())
17626}
17627
17628fn validate_recovery_spilled_runs(
17629 root: &crate::durable_file::DurableRoot,
17630 tables: &HashMap<u64, TableHandle>,
17631 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
17632) -> Result<()> {
17633 let mut identities = HashSet::new();
17634 for (txn_id, commit_epoch, added_runs) in spilled {
17635 for added in added_runs {
17636 if added.run_id >= u64::MAX as u128 {
17637 return Err(MongrelError::CorruptWal {
17638 offset: *commit_epoch,
17639 reason: format!(
17640 "recovered run id {} exceeds the on-disk namespace",
17641 added.run_id
17642 ),
17643 });
17644 }
17645 let Some(handle) = tables.get(&added.table_id) else {
17646 continue;
17647 };
17648 if !identities.insert((added.table_id, added.run_id)) {
17649 return Err(MongrelError::CorruptWal {
17650 offset: *commit_epoch,
17651 reason: format!(
17652 "duplicate recovered run {} for table {}",
17653 added.run_id, added.table_id
17654 ),
17655 });
17656 }
17657 let table = handle.lock();
17658 validate_planned_spilled_run(
17659 root,
17660 *txn_id,
17661 *commit_epoch,
17662 added,
17663 table.schema(),
17664 table.kek(),
17665 )?;
17666 }
17667 }
17668 Ok(())
17669}
17670
17671fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
17672 match condition {
17673 ProcedureCondition::Pk { .. } => {
17674 if schema.primary_key().is_none() {
17675 return Err(MongrelError::InvalidArgument(
17676 "procedure condition Pk references a table without a primary key".into(),
17677 ));
17678 }
17679 }
17680 ProcedureCondition::BitmapEq { column_id, .. }
17681 | ProcedureCondition::BitmapIn { column_id, .. }
17682 | ProcedureCondition::Range { column_id, .. }
17683 | ProcedureCondition::RangeF64 { column_id, .. }
17684 | ProcedureCondition::IsNull { column_id }
17685 | ProcedureCondition::IsNotNull { column_id }
17686 | ProcedureCondition::FmContains { column_id, .. } => {
17687 validate_column_id(*column_id, schema)?;
17688 }
17689 }
17690 Ok(())
17691}
17692
17693fn bind_procedure_args(
17694 procedure: &StoredProcedure,
17695 mut args: HashMap<String, crate::Value>,
17696) -> Result<HashMap<String, crate::Value>> {
17697 let mut out = HashMap::new();
17698 for param in &procedure.params {
17699 let value = match args.remove(¶m.name) {
17700 Some(value) => value,
17701 None => param.default.clone().ok_or_else(|| {
17702 MongrelError::InvalidArgument(format!(
17703 "missing required procedure parameter {:?}",
17704 param.name
17705 ))
17706 })?,
17707 };
17708 if !param.nullable && matches!(value, crate::Value::Null) {
17709 return Err(MongrelError::InvalidArgument(format!(
17710 "procedure parameter {:?} must not be NULL",
17711 param.name
17712 )));
17713 }
17714 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
17715 return Err(MongrelError::InvalidArgument(format!(
17716 "procedure parameter {:?} has wrong type",
17717 param.name
17718 )));
17719 }
17720 out.insert(param.name.clone(), value);
17721 }
17722 if let Some(extra) = args.keys().next() {
17723 return Err(MongrelError::InvalidArgument(format!(
17724 "unknown procedure parameter {extra:?}"
17725 )));
17726 }
17727 Ok(out)
17728}
17729
17730fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
17731 matches!(
17732 (value, ty),
17733 (crate::Value::Bool(_), crate::TypeId::Bool)
17734 | (crate::Value::Int64(_), crate::TypeId::Int8)
17735 | (crate::Value::Int64(_), crate::TypeId::Int16)
17736 | (crate::Value::Int64(_), crate::TypeId::Int32)
17737 | (crate::Value::Int64(_), crate::TypeId::Int64)
17738 | (crate::Value::Int64(_), crate::TypeId::UInt8)
17739 | (crate::Value::Int64(_), crate::TypeId::UInt16)
17740 | (crate::Value::Int64(_), crate::TypeId::UInt32)
17741 | (crate::Value::Int64(_), crate::TypeId::UInt64)
17742 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
17743 | (crate::Value::Int64(_), crate::TypeId::Date32)
17744 | (crate::Value::Float64(_), crate::TypeId::Float32)
17745 | (crate::Value::Float64(_), crate::TypeId::Float64)
17746 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
17747 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
17748 | (
17749 crate::Value::GeneratedEmbedding(_),
17750 crate::TypeId::Embedding { .. }
17751 )
17752 )
17753}
17754
17755fn eval_cells(
17756 cells: &[crate::procedure::ProcedureCell],
17757 args: &HashMap<String, crate::Value>,
17758 outputs: &HashMap<String, ProcedureCallOutput>,
17759) -> Result<Vec<(u16, crate::Value)>> {
17760 cells
17761 .iter()
17762 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
17763 .collect()
17764}
17765
17766fn eval_condition(
17767 condition: &ProcedureCondition,
17768 args: &HashMap<String, crate::Value>,
17769 outputs: &HashMap<String, ProcedureCallOutput>,
17770) -> Result<crate::Condition> {
17771 Ok(match condition {
17772 ProcedureCondition::Pk { value } => {
17773 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
17774 }
17775 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
17776 column_id: *column_id,
17777 value: eval_value(value, args, outputs)?.encode_key(),
17778 },
17779 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
17780 column_id: *column_id,
17781 values: values
17782 .iter()
17783 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
17784 .collect::<Result<Vec<_>>>()?,
17785 },
17786 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
17787 column_id: *column_id,
17788 lo: expect_i64(eval_value(lo, args, outputs)?)?,
17789 hi: expect_i64(eval_value(hi, args, outputs)?)?,
17790 },
17791 ProcedureCondition::RangeF64 {
17792 column_id,
17793 lo,
17794 lo_inclusive,
17795 hi,
17796 hi_inclusive,
17797 } => crate::Condition::RangeF64 {
17798 column_id: *column_id,
17799 lo: expect_f64(eval_value(lo, args, outputs)?)?,
17800 lo_inclusive: *lo_inclusive,
17801 hi: expect_f64(eval_value(hi, args, outputs)?)?,
17802 hi_inclusive: *hi_inclusive,
17803 },
17804 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
17805 column_id: *column_id,
17806 },
17807 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
17808 column_id: *column_id,
17809 },
17810 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
17811 column_id: *column_id,
17812 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
17813 },
17814 })
17815}
17816
17817fn eval_value(
17818 value: &ProcedureValue,
17819 args: &HashMap<String, crate::Value>,
17820 outputs: &HashMap<String, ProcedureCallOutput>,
17821) -> Result<crate::Value> {
17822 match value {
17823 ProcedureValue::Literal(value) => Ok(value.clone()),
17824 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
17825 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17826 }),
17827 ProcedureValue::StepScalar(id) => match outputs.get(id) {
17828 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
17829 _ => Err(MongrelError::InvalidArgument(format!(
17830 "procedure step {id:?} did not return a scalar"
17831 ))),
17832 },
17833 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
17834 Err(MongrelError::InvalidArgument(
17835 "row-valued procedure reference cannot be used as a scalar".into(),
17836 ))
17837 }
17838 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
17839 "structured procedure value cannot be used as a scalar cell".into(),
17840 )),
17841 }
17842}
17843
17844fn eval_return_output(
17845 value: &ProcedureValue,
17846 args: &HashMap<String, crate::Value>,
17847 outputs: &HashMap<String, ProcedureCallOutput>,
17848) -> Result<ProcedureCallOutput> {
17849 match value {
17850 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
17851 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
17852 args.get(name).cloned().ok_or_else(|| {
17853 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17854 })?,
17855 )),
17856 ProcedureValue::StepRows(id)
17857 | ProcedureValue::StepRow(id)
17858 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
17859 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
17860 }),
17861 ProcedureValue::Object(fields) => {
17862 let mut out = Vec::with_capacity(fields.len());
17863 for (name, value) in fields {
17864 out.push((name.clone(), eval_return_output(value, args, outputs)?));
17865 }
17866 Ok(ProcedureCallOutput::Object(out))
17867 }
17868 ProcedureValue::Array(values) => {
17869 let mut out = Vec::with_capacity(values.len());
17870 for value in values {
17871 out.push(eval_return_output(value, args, outputs)?);
17872 }
17873 Ok(ProcedureCallOutput::Array(out))
17874 }
17875 }
17876}
17877
17878fn expect_i64(value: crate::Value) -> Result<i64> {
17879 match value {
17880 crate::Value::Int64(value) => Ok(value),
17881 _ => Err(MongrelError::InvalidArgument(
17882 "procedure value must be Int64".into(),
17883 )),
17884 }
17885}
17886
17887fn expect_f64(value: crate::Value) -> Result<f64> {
17888 match value {
17889 crate::Value::Float64(value) => Ok(value),
17890 _ => Err(MongrelError::InvalidArgument(
17891 "procedure value must be Float64".into(),
17892 )),
17893 }
17894}
17895
17896fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
17897 match value {
17898 crate::Value::Bytes(value) => Ok(value),
17899 _ => Err(MongrelError::InvalidArgument(
17900 "procedure value must be Bytes".into(),
17901 )),
17902 }
17903}
17904
17905fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
17906 if schema.columns.iter().any(|c| c.id == column_id) {
17907 Ok(())
17908 } else {
17909 Err(MongrelError::InvalidArgument(format!(
17910 "unknown column id {column_id}"
17911 )))
17912 }
17913}
17914
17915fn trigger_matches_event(
17916 trigger: &StoredTrigger,
17917 event: &WriteEvent,
17918 cat: &Catalog,
17919) -> Result<bool> {
17920 if trigger.event != event.kind {
17921 return Ok(false);
17922 }
17923 let TriggerTarget::Table(target) = &trigger.target else {
17924 return Ok(false);
17925 };
17926 if target != &event.table {
17927 return Ok(false);
17928 }
17929 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
17930 let schema = &cat
17931 .live(target)
17932 .ok_or_else(|| {
17933 MongrelError::InvalidArgument(format!(
17934 "trigger {:?} references unknown table {target:?}",
17935 trigger.name
17936 ))
17937 })?
17938 .schema;
17939 let mut watched = Vec::with_capacity(trigger.update_of.len());
17940 for name in &trigger.update_of {
17941 let col = schema.column(name).ok_or_else(|| {
17942 MongrelError::InvalidArgument(format!(
17943 "trigger {:?} references unknown UPDATE OF column {name:?}",
17944 trigger.name
17945 ))
17946 })?;
17947 watched.push(col.id);
17948 }
17949 if !event
17950 .changed_columns
17951 .iter()
17952 .any(|column_id| watched.contains(column_id))
17953 {
17954 return Ok(false);
17955 }
17956 }
17957 Ok(true)
17958}
17959
17960fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
17961 let mut ids = std::collections::BTreeSet::new();
17962 if let Some(old) = old {
17963 ids.extend(old.columns.keys().copied());
17964 }
17965 if let Some(new) = new {
17966 ids.extend(new.columns.keys().copied());
17967 }
17968 ids.into_iter()
17969 .filter(|id| {
17970 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
17971 })
17972 .collect()
17973}
17974
17975fn eval_trigger_cells(
17976 cells: &[crate::trigger::TriggerCell],
17977 event: &WriteEvent,
17978 selected: Option<&TriggerRowImage>,
17979) -> Result<Vec<(u16, Value)>> {
17980 cells
17981 .iter()
17982 .map(|cell| {
17983 Ok((
17984 cell.column_id,
17985 eval_trigger_value(&cell.value, event, selected)?,
17986 ))
17987 })
17988 .collect()
17989}
17990
17991fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
17992 match expr {
17993 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
17994 Value::Bool(value) => Ok(value),
17995 Value::Null => Ok(false),
17996 other => Err(MongrelError::InvalidArgument(format!(
17997 "trigger WHEN value must be boolean, got {other:?}"
17998 ))),
17999 },
18000 TriggerExpr::Eq { left, right } => Ok(values_equal(
18001 &eval_trigger_value(left, event, None)?,
18002 &eval_trigger_value(right, event, None)?,
18003 )),
18004 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
18005 &eval_trigger_value(left, event, None)?,
18006 &eval_trigger_value(right, event, None)?,
18007 )),
18008 TriggerExpr::Lt { left, right } => match value_order(
18009 &eval_trigger_value(left, event, None)?,
18010 &eval_trigger_value(right, event, None)?,
18011 ) {
18012 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
18013 None => Ok(false),
18014 },
18015 TriggerExpr::Lte { left, right } => match value_order(
18016 &eval_trigger_value(left, event, None)?,
18017 &eval_trigger_value(right, event, None)?,
18018 ) {
18019 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
18020 None => Ok(false),
18021 },
18022 TriggerExpr::Gt { left, right } => match value_order(
18023 &eval_trigger_value(left, event, None)?,
18024 &eval_trigger_value(right, event, None)?,
18025 ) {
18026 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
18027 None => Ok(false),
18028 },
18029 TriggerExpr::Gte { left, right } => match value_order(
18030 &eval_trigger_value(left, event, None)?,
18031 &eval_trigger_value(right, event, None)?,
18032 ) {
18033 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
18034 None => Ok(false),
18035 },
18036 TriggerExpr::IsNull(value) => Ok(matches!(
18037 eval_trigger_value(value, event, None)?,
18038 Value::Null
18039 )),
18040 TriggerExpr::IsNotNull(value) => Ok(!matches!(
18041 eval_trigger_value(value, event, None)?,
18042 Value::Null
18043 )),
18044 TriggerExpr::And { left, right } => {
18045 if !eval_trigger_expr(left, event)? {
18046 Ok(false)
18047 } else {
18048 Ok(eval_trigger_expr(right, event)?)
18049 }
18050 }
18051 TriggerExpr::Or { left, right } => {
18052 if eval_trigger_expr(left, event)? {
18053 Ok(true)
18054 } else {
18055 Ok(eval_trigger_expr(right, event)?)
18056 }
18057 }
18058 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
18059 }
18060}
18061
18062fn eval_trigger_condition(
18063 condition: &TriggerCondition,
18064 event: &WriteEvent,
18065 selected: &TriggerRowImage,
18066 schema: &Schema,
18067) -> Result<bool> {
18068 match condition {
18069 TriggerCondition::Pk { value } => {
18070 let pk = schema.primary_key().ok_or_else(|| {
18071 MongrelError::InvalidArgument(
18072 "trigger condition Pk references a table without a primary key".into(),
18073 )
18074 })?;
18075 let lhs = eval_trigger_value(value, event, Some(selected))?;
18076 Ok(values_equal(
18077 &lhs,
18078 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
18079 ))
18080 }
18081 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
18082 selected.columns.get(column_id).unwrap_or(&Value::Null),
18083 &eval_trigger_value(value, event, Some(selected))?,
18084 )),
18085 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
18086 selected.columns.get(column_id).unwrap_or(&Value::Null),
18087 &eval_trigger_value(value, event, Some(selected))?,
18088 )),
18089 TriggerCondition::Lt { column_id, value } => match value_order(
18090 selected.columns.get(column_id).unwrap_or(&Value::Null),
18091 &eval_trigger_value(value, event, Some(selected))?,
18092 ) {
18093 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
18094 None => Ok(false),
18095 },
18096 TriggerCondition::Lte { column_id, value } => match value_order(
18097 selected.columns.get(column_id).unwrap_or(&Value::Null),
18098 &eval_trigger_value(value, event, Some(selected))?,
18099 ) {
18100 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
18101 None => Ok(false),
18102 },
18103 TriggerCondition::Gt { column_id, value } => match value_order(
18104 selected.columns.get(column_id).unwrap_or(&Value::Null),
18105 &eval_trigger_value(value, event, Some(selected))?,
18106 ) {
18107 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
18108 None => Ok(false),
18109 },
18110 TriggerCondition::Gte { column_id, value } => match value_order(
18111 selected.columns.get(column_id).unwrap_or(&Value::Null),
18112 &eval_trigger_value(value, event, Some(selected))?,
18113 ) {
18114 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
18115 None => Ok(false),
18116 },
18117 TriggerCondition::IsNull { column_id } => Ok(matches!(
18118 selected.columns.get(column_id),
18119 None | Some(Value::Null)
18120 )),
18121 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
18122 selected.columns.get(column_id),
18123 None | Some(Value::Null)
18124 )),
18125 TriggerCondition::And { left, right } => {
18126 if !eval_trigger_condition(left, event, selected, schema)? {
18127 Ok(false)
18128 } else {
18129 Ok(eval_trigger_condition(right, event, selected, schema)?)
18130 }
18131 }
18132 TriggerCondition::Or { left, right } => {
18133 if eval_trigger_condition(left, event, selected, schema)? {
18134 Ok(true)
18135 } else {
18136 Ok(eval_trigger_condition(right, event, selected, schema)?)
18137 }
18138 }
18139 TriggerCondition::Not(condition) => {
18140 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
18141 }
18142 }
18143}
18144
18145fn eval_trigger_value(
18146 value: &TriggerValue,
18147 event: &WriteEvent,
18148 selected: Option<&TriggerRowImage>,
18149) -> Result<Value> {
18150 match value {
18151 TriggerValue::Literal(value) => Ok(value.clone()),
18152 TriggerValue::NewColumn(column_id) => event
18153 .new
18154 .as_ref()
18155 .and_then(|row| row.columns.get(column_id))
18156 .cloned()
18157 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
18158 TriggerValue::OldColumn(column_id) => event
18159 .old
18160 .as_ref()
18161 .and_then(|row| row.columns.get(column_id))
18162 .cloned()
18163 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
18164 TriggerValue::SelectedColumn(column_id) => selected
18165 .and_then(|row| row.columns.get(column_id))
18166 .cloned()
18167 .ok_or_else(|| {
18168 MongrelError::InvalidArgument("SELECTED column is not available".into())
18169 }),
18170 }
18171}
18172
18173fn values_equal(left: &Value, right: &Value) -> bool {
18174 match (left, right) {
18175 (Value::Null, Value::Null) => true,
18176 (Value::Bool(a), Value::Bool(b)) => a == b,
18177 (Value::Int64(a), Value::Int64(b)) => a == b,
18178 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
18179 (Value::Bytes(a), Value::Bytes(b)) => a == b,
18180 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => {
18181 let a = a.as_embedding().unwrap();
18182 let b = b.as_embedding().unwrap();
18183 a.len() == b.len()
18184 && a.iter()
18185 .zip(b.iter())
18186 .all(|(a, b)| a.to_bits() == b.to_bits())
18187 }
18188 _ => false,
18189 }
18190}
18191
18192fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
18193 match (left, right) {
18194 (Value::Null, _) | (_, Value::Null) => None,
18195 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
18196 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
18197 (Value::Int64(a), Value::Float64(b)) => {
18200 let af = *a as f64;
18201 Some(af.total_cmp(b))
18202 }
18203 (Value::Float64(a), Value::Int64(b)) => {
18206 let bf = *b as f64;
18207 Some(a.total_cmp(&bf))
18208 }
18209 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
18210 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
18211 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => None,
18212 _ => None,
18213 }
18214}
18215
18216fn trigger_message(value: Value) -> String {
18217 match value {
18218 Value::Null => "NULL".into(),
18219 Value::Bool(value) => value.to_string(),
18220 Value::Int64(value) => value.to_string(),
18221 Value::Float64(value) => value.to_string(),
18222 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
18223 Value::Embedding(value) => format!("{value:?}"),
18224 Value::GeneratedEmbedding(value) => format!("{:?}", value.vector),
18225 Value::Decimal(value) => value.to_string(),
18226 Value::Interval {
18227 months,
18228 days,
18229 nanos,
18230 } => format!("{months}m {days}d {nanos}ns"),
18231 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
18232 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
18233 }
18234}
18235
18236fn validate_trigger_step<'a>(
18237 step: &TriggerStep,
18238 cat: &'a Catalog,
18239 target_schema: &Schema,
18240 event: TriggerEvent,
18241 select_schemas: &mut HashMap<String, &'a Schema>,
18242) -> Result<()> {
18243 match step {
18244 TriggerStep::SetNew { cells } => {
18245 if event == TriggerEvent::Delete {
18246 return Err(MongrelError::InvalidArgument(
18247 "SetNew trigger step is not valid for DELETE triggers".into(),
18248 ));
18249 }
18250 for cell in cells {
18251 validate_column_id(cell.column_id, target_schema)?;
18252 validate_trigger_value(&cell.value, target_schema, event)?;
18253 }
18254 }
18255 TriggerStep::Insert { table, cells } => {
18256 let schema = trigger_write_schema(cat, table, "insert")?;
18257 for cell in cells {
18258 validate_column_id(cell.column_id, schema)?;
18259 validate_trigger_value(&cell.value, target_schema, event)?;
18260 }
18261 }
18262 TriggerStep::UpdateByPk { table, pk, cells } => {
18263 let schema = trigger_write_schema(cat, table, "update")?;
18264 if schema.primary_key().is_none() {
18265 return Err(MongrelError::InvalidArgument(format!(
18266 "trigger update_by_pk references table {table:?} without a primary key"
18267 )));
18268 }
18269 validate_trigger_value(pk, target_schema, event)?;
18270 for cell in cells {
18271 validate_column_id(cell.column_id, schema)?;
18272 validate_trigger_value(&cell.value, target_schema, event)?;
18273 }
18274 }
18275 TriggerStep::DeleteByPk { table, pk } => {
18276 let schema = trigger_write_schema(cat, table, "delete")?;
18277 if schema.primary_key().is_none() {
18278 return Err(MongrelError::InvalidArgument(format!(
18279 "trigger delete_by_pk references table {table:?} without a primary key"
18280 )));
18281 }
18282 validate_trigger_value(pk, target_schema, event)?;
18283 }
18284 TriggerStep::Select {
18285 id,
18286 table,
18287 conditions,
18288 } => {
18289 let schema = trigger_read_schema(cat, table)?;
18290 for condition in conditions {
18291 validate_trigger_condition(condition, schema, target_schema, event)?;
18292 }
18293 if select_schemas.contains_key(id) {
18294 return Err(MongrelError::InvalidArgument(format!(
18295 "duplicate select id {id:?} in trigger program"
18296 )));
18297 }
18298 select_schemas.insert(id.clone(), schema);
18299 }
18300 TriggerStep::Foreach { id, steps } => {
18301 if !select_schemas.contains_key(id) {
18302 return Err(MongrelError::InvalidArgument(format!(
18303 "foreach references unknown select id {id:?}"
18304 )));
18305 }
18306 let mut inner_select_schemas = select_schemas.clone();
18307 for step in steps {
18308 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
18309 }
18310 }
18311 TriggerStep::DeleteWhere { table, conditions } => {
18312 let schema = trigger_write_schema(cat, table, "delete")?;
18313 for condition in conditions {
18314 validate_trigger_condition(condition, schema, target_schema, event)?;
18315 }
18316 }
18317 TriggerStep::UpdateWhere {
18318 table,
18319 conditions,
18320 cells,
18321 } => {
18322 let schema = trigger_write_schema(cat, table, "update")?;
18323 for condition in conditions {
18324 validate_trigger_condition(condition, schema, target_schema, event)?;
18325 }
18326 for cell in cells {
18327 validate_column_id(cell.column_id, schema)?;
18328 validate_trigger_value(&cell.value, target_schema, event)?;
18329 }
18330 }
18331 TriggerStep::Raise { message, .. } => {
18332 validate_trigger_value(message, target_schema, event)?
18333 }
18334 }
18335 Ok(())
18336}
18337
18338fn trigger_validation_error(error: MongrelError) -> MongrelError {
18339 match error {
18340 MongrelError::TriggerValidation(_) => error,
18341 MongrelError::InvalidArgument(message)
18342 | MongrelError::Conflict(message)
18343 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
18344 error => error,
18345 }
18346}
18347
18348fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
18349 if let Some(entry) = cat.live(table) {
18350 return Ok(&entry.schema);
18351 }
18352 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18353 let allowed = match op {
18354 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
18355 "update" | "delete" => entry.capabilities.writable,
18356 _ => false,
18357 };
18358 if !allowed {
18359 return Err(MongrelError::InvalidArgument(format!(
18360 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
18361 entry.module
18362 )));
18363 }
18364 if !entry.capabilities.transaction_safe {
18365 return Err(MongrelError::InvalidArgument(format!(
18366 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
18367 entry.module
18368 )));
18369 }
18370 return Ok(&entry.declared_schema);
18371 }
18372 Err(MongrelError::InvalidArgument(format!(
18373 "trigger references unknown table {table:?}"
18374 )))
18375}
18376
18377fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
18378 if let Some(entry) = cat.live(table) {
18379 return Ok(&entry.schema);
18380 }
18381 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18382 if entry.capabilities.trigger_safe {
18383 return Ok(&entry.declared_schema);
18384 }
18385 return Err(MongrelError::InvalidArgument(format!(
18386 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
18387 entry.module
18388 )));
18389 }
18390 Err(MongrelError::InvalidArgument(format!(
18391 "trigger references unknown table {table:?}"
18392 )))
18393}
18394
18395fn validate_trigger_condition(
18396 condition: &TriggerCondition,
18397 schema: &Schema,
18398 target_schema: &Schema,
18399 event: TriggerEvent,
18400) -> Result<()> {
18401 match condition {
18402 TriggerCondition::Pk { value } => {
18403 if schema.primary_key().is_none() {
18404 return Err(MongrelError::InvalidArgument(
18405 "trigger condition Pk references a table without a primary key".into(),
18406 ));
18407 }
18408 validate_trigger_value(value, target_schema, event)
18409 }
18410 TriggerCondition::Eq { column_id, value }
18411 | TriggerCondition::NotEq { column_id, value }
18412 | TriggerCondition::Lt { column_id, value }
18413 | TriggerCondition::Lte { column_id, value }
18414 | TriggerCondition::Gt { column_id, value }
18415 | TriggerCondition::Gte { column_id, value } => {
18416 validate_column_id(*column_id, schema)?;
18417 validate_trigger_value(value, target_schema, event)
18418 }
18419 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
18420 validate_column_id(*column_id, schema)
18421 }
18422 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
18423 validate_trigger_condition(left, schema, target_schema, event)?;
18424 validate_trigger_condition(right, schema, target_schema, event)
18425 }
18426 TriggerCondition::Not(condition) => {
18427 validate_trigger_condition(condition, schema, target_schema, event)
18428 }
18429 }
18430}
18431
18432fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
18433 match expr {
18434 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
18435 validate_trigger_value(value, schema, event)
18436 }
18437 TriggerExpr::Eq { left, right }
18438 | TriggerExpr::NotEq { left, right }
18439 | TriggerExpr::Lt { left, right }
18440 | TriggerExpr::Lte { left, right }
18441 | TriggerExpr::Gt { left, right }
18442 | TriggerExpr::Gte { left, right } => {
18443 validate_trigger_value(left, schema, event)?;
18444 validate_trigger_value(right, schema, event)
18445 }
18446 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
18447 validate_trigger_expr(left, schema, event)?;
18448 validate_trigger_expr(right, schema, event)
18449 }
18450 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
18451 }
18452}
18453
18454fn validate_trigger_value(
18455 value: &TriggerValue,
18456 schema: &Schema,
18457 event: TriggerEvent,
18458) -> Result<()> {
18459 match value {
18460 TriggerValue::Literal(_) => Ok(()),
18461 TriggerValue::NewColumn(id) => {
18462 if event == TriggerEvent::Delete {
18463 return Err(MongrelError::InvalidArgument(
18464 "DELETE triggers cannot reference NEW".into(),
18465 ));
18466 }
18467 validate_column_id(*id, schema)
18468 }
18469 TriggerValue::OldColumn(id) => {
18470 if event == TriggerEvent::Insert {
18471 return Err(MongrelError::InvalidArgument(
18472 "INSERT triggers cannot reference OLD".into(),
18473 ));
18474 }
18475 validate_column_id(*id, schema)
18476 }
18477 TriggerValue::SelectedColumn(_) => Ok(()),
18481 }
18482}
18483
18484const COMMIT_TS_LEDGER_CAP: usize = 10_000;
18489
18490fn commit_ts_ledger_from_recovery(
18498 records: &[crate::wal::Record],
18499) -> std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp> {
18500 use crate::wal::Op;
18501 let mut commits = HashMap::new();
18502 let mut timestamps = HashMap::new();
18503 for record in records {
18504 match &record.op {
18505 Op::TxnCommit { epoch, .. } => {
18506 commits.insert(record.txn_id, *epoch);
18507 }
18508 Op::CommitTimestamp { unix_nanos } => {
18509 timestamps.insert(record.txn_id, *unix_nanos);
18510 }
18511 _ => {}
18512 }
18513 }
18514 let mut ledger = std::collections::BTreeMap::new();
18515 for (txn_id, epoch) in commits {
18516 let Some(unix_nanos) = timestamps.get(&txn_id) else {
18517 continue;
18518 };
18519 ledger.insert(
18520 epoch,
18521 mongreldb_types::hlc::HlcTimestamp {
18522 physical_micros: unix_nanos / 1_000,
18523 logical: 0,
18524 node_tiebreaker: 0,
18525 },
18526 );
18527 }
18528 while ledger.len() > COMMIT_TS_LEDGER_CAP {
18529 ledger.pop_first();
18530 }
18531 ledger
18532}
18533
18534fn recover_ddl_from_wal(
18540 root: &Path,
18541 durable_root: Option<&crate::durable_file::DurableRoot>,
18542 target_catalog: &mut Catalog,
18543 meta_dek: Option<&[u8; META_DEK_LEN]>,
18544 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
18545 apply: bool,
18546 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18547) -> Result<()> {
18548 use crate::wal::SharedWal;
18549 let records = match durable_root {
18550 Some(root) => SharedWal::replay_durable_with_dek_limited(
18551 root,
18552 wal_dek,
18553 crate::wal::WalReplayLimits::from_env()?,
18554 )?,
18555 None => SharedWal::replay_with_dek(root, wal_dek)?,
18556 };
18557 recover_ddl_from_records(
18558 root,
18559 durable_root,
18560 target_catalog,
18561 meta_dek,
18562 apply,
18563 table_roots,
18564 &records,
18565 )
18566}
18567
18568fn recover_ddl_from_records(
18569 root: &Path,
18570 durable_root: Option<&crate::durable_file::DurableRoot>,
18571 target_catalog: &mut Catalog,
18572 meta_dek: Option<&[u8; META_DEK_LEN]>,
18573 apply: bool,
18574 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18575 records: &[crate::wal::Record],
18576) -> Result<()> {
18577 use crate::wal::{DdlOp, Op};
18578
18579 let original_catalog = target_catalog.clone();
18580 let mut recovered_catalog = original_catalog.clone();
18581 let cat = &mut recovered_catalog;
18582 let mut created_table_ids = HashSet::<u64>::new();
18583 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
18584
18585 let mut committed: HashMap<u64, u64> = HashMap::new();
18586 for r in records {
18587 if let Op::TxnCommit { epoch: ce, .. } = r.op {
18588 committed.insert(r.txn_id, ce);
18589 }
18590 }
18591 let catalog_snapshot_txns = records
18592 .iter()
18593 .filter_map(|record| {
18594 (committed.contains_key(&record.txn_id)
18595 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
18596 .then_some(record.txn_id)
18597 })
18598 .collect::<HashSet<_>>();
18599
18600 let mut changed = false;
18601 let mut applied_catalog_epoch = cat.db_epoch;
18602 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
18603 for r in records.iter().cloned() {
18604 let Some(&ce) = committed.get(&r.txn_id) else {
18605 continue;
18606 };
18607 let txn_id = r.txn_id;
18608 match r.op {
18609 Op::Ddl(DdlOp::CreateTable {
18610 table_id,
18611 ref name,
18612 ref schema_json,
18613 }) => {
18614 if cat.tables.iter().any(|t| t.table_id == table_id) {
18615 continue;
18616 }
18617 let schema = DdlOp::decode_schema(schema_json)?;
18618 validate_recovered_schema(&schema)?;
18619 created_table_ids.insert(table_id);
18620 cat.tables.push(CatalogEntry {
18621 table_id,
18622 name: name.clone(),
18623 schema,
18624 state: TableState::Live,
18625 created_epoch: ce,
18626 });
18627 cat.next_table_id =
18628 cat.next_table_id
18629 .max(table_id.checked_add(1).ok_or_else(|| {
18630 MongrelError::Full("table id namespace exhausted".into())
18631 })?);
18632 changed = true;
18633 }
18634 Op::Ddl(DdlOp::CreateBuildingTable {
18635 table_id,
18636 ref build_name,
18637 ref intended_name,
18638 ref query_id,
18639 created_at_unix_nanos,
18640 ref schema_json,
18641 }) => {
18642 if cat.tables.iter().any(|table| table.table_id == table_id) {
18643 continue;
18644 }
18645 let schema = DdlOp::decode_schema(schema_json)?;
18646 validate_recovered_schema(&schema)?;
18647 created_table_ids.insert(table_id);
18648 cat.tables.push(CatalogEntry {
18649 table_id,
18650 name: build_name.clone(),
18651 schema,
18652 state: TableState::Building {
18653 intended_name: intended_name.clone(),
18654 query_id: query_id.clone(),
18655 created_at_unix_nanos,
18656 replaces_table_id: None,
18657 },
18658 created_epoch: ce,
18659 });
18660 cat.next_table_id =
18661 cat.next_table_id
18662 .max(table_id.checked_add(1).ok_or_else(|| {
18663 MongrelError::Full("table id namespace exhausted".into())
18664 })?);
18665 changed = true;
18666 }
18667 Op::Ddl(DdlOp::CreateRebuildingTable {
18668 table_id,
18669 ref build_name,
18670 ref intended_name,
18671 ref query_id,
18672 created_at_unix_nanos,
18673 replaces_table_id,
18674 ref schema_json,
18675 }) => {
18676 if cat.tables.iter().any(|table| table.table_id == table_id) {
18677 continue;
18678 }
18679 let schema = DdlOp::decode_schema(schema_json)?;
18680 validate_recovered_schema(&schema)?;
18681 created_table_ids.insert(table_id);
18682 cat.tables.push(CatalogEntry {
18683 table_id,
18684 name: build_name.clone(),
18685 schema,
18686 state: TableState::Building {
18687 intended_name: intended_name.clone(),
18688 query_id: query_id.clone(),
18689 created_at_unix_nanos,
18690 replaces_table_id: Some(replaces_table_id),
18691 },
18692 created_epoch: ce,
18693 });
18694 cat.next_table_id =
18695 cat.next_table_id
18696 .max(table_id.checked_add(1).ok_or_else(|| {
18697 MongrelError::Full("table id namespace exhausted".into())
18698 })?);
18699 changed = true;
18700 }
18701 Op::Ddl(DdlOp::DropTable { table_id }) => {
18702 let mut dropped_name = None;
18703 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18704 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18705 dropped_name = Some(entry.name.clone());
18706 entry.state = TableState::Dropped { at_epoch: ce };
18707 changed = true;
18708 }
18709 }
18710 if let Some(name) = dropped_name {
18711 let before = cat.materialized_views.len();
18712 cat.materialized_views
18713 .retain(|definition| definition.name != name);
18714 changed |= before != cat.materialized_views.len();
18715 cat.security.rls_tables.retain(|table| table != &name);
18716 cat.security.policies.retain(|policy| policy.table != name);
18717 cat.security.masks.retain(|mask| mask.table != name);
18718 for role in &mut cat.roles {
18719 role.permissions
18720 .retain(|permission| permission_table(permission) != Some(&name));
18721 }
18722 if !catalog_snapshot_txns.contains(&txn_id) {
18723 advance_security_version(cat)?;
18724 }
18725 }
18726 }
18727 Op::Ddl(DdlOp::PublishBuildingTable {
18728 table_id,
18729 ref new_name,
18730 }) => {
18731 if let Some(entry) = cat
18732 .tables
18733 .iter_mut()
18734 .find(|table| table.table_id == table_id)
18735 {
18736 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
18737 entry.name = new_name.clone();
18738 entry.state = TableState::Live;
18739 changed = true;
18740 }
18741 }
18742 }
18743 Op::Ddl(DdlOp::ReplaceBuildingTable {
18744 table_id,
18745 replaced_table_id,
18746 ref new_name,
18747 }) => {
18748 changed |=
18749 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
18750 }
18751 Op::Ddl(DdlOp::RenameTable {
18752 table_id,
18753 ref new_name,
18754 }) => {
18755 let mut old_name = None;
18756 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18757 if entry.name != *new_name {
18758 old_name = Some(entry.name.clone());
18759 entry.name = new_name.clone();
18760 changed = true;
18761 }
18762 }
18763 if let Some(old_name) = old_name {
18764 if let Some(definition) = cat
18765 .materialized_views
18766 .iter_mut()
18767 .find(|definition| definition.name == old_name)
18768 {
18769 definition.name = new_name.clone();
18770 }
18771 for table in &mut cat.security.rls_tables {
18772 if *table == old_name {
18773 *table = new_name.clone();
18774 }
18775 }
18776 for policy in &mut cat.security.policies {
18777 if policy.table == old_name {
18778 policy.table = new_name.clone();
18779 }
18780 }
18781 for mask in &mut cat.security.masks {
18782 if mask.table == old_name {
18783 mask.table = new_name.clone();
18784 }
18785 }
18786 for role in &mut cat.roles {
18787 for permission in &mut role.permissions {
18788 rename_permission_table(permission, &old_name, new_name);
18789 }
18790 }
18791 if !catalog_snapshot_txns.contains(&txn_id) {
18792 advance_security_version(cat)?;
18793 }
18794 }
18795 }
18799 Op::Ddl(DdlOp::AlterTable {
18800 table_id,
18801 ref column_json,
18802 }) => {
18803 let column = DdlOp::decode_column(column_json)?;
18804 let mut renamed = None;
18805 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18806 renamed = entry
18807 .schema
18808 .columns
18809 .iter()
18810 .find(|existing| existing.id == column.id && existing.name != column.name)
18811 .map(|existing| {
18812 (
18813 entry.name.clone(),
18814 existing.name.clone(),
18815 column.name.clone(),
18816 )
18817 });
18818 if apply_recovered_column_def(&mut entry.schema, column)? {
18819 validate_recovered_schema(&entry.schema)?;
18820 changed = true;
18821 }
18822 }
18823 if let Some((table, old_name, new_name)) = renamed {
18824 for role in &mut cat.roles {
18825 for permission in &mut role.permissions {
18826 rename_permission_column(permission, &table, &old_name, &new_name);
18827 }
18828 }
18829 if !catalog_snapshot_txns.contains(&txn_id) {
18830 advance_security_version(cat)?;
18831 }
18832 }
18833 }
18834 Op::Ddl(DdlOp::SetTtl {
18835 table_id,
18836 ref policy_json,
18837 }) => {
18838 let policy = DdlOp::decode_ttl(policy_json)?;
18839 let entry = cat
18840 .tables
18841 .iter()
18842 .find(|entry| entry.table_id == table_id)
18843 .ok_or_else(|| {
18844 MongrelError::Schema(format!(
18845 "recovered TTL references unknown table id {table_id}"
18846 ))
18847 })?;
18848 if let Some(policy) = policy {
18849 let valid = entry
18850 .schema
18851 .columns
18852 .iter()
18853 .find(|column| column.id == policy.column_id)
18854 .is_some_and(|column| {
18855 column.ty == TypeId::TimestampNanos
18856 && policy.duration_nanos > 0
18857 && policy.duration_nanos <= i64::MAX as u64
18858 });
18859 if !valid {
18860 return Err(MongrelError::Schema(format!(
18861 "invalid recovered TTL policy for table id {table_id}"
18862 )));
18863 }
18864 }
18865 ttl_updates.insert(table_id, (policy, ce));
18866 }
18867 Op::Ddl(DdlOp::SetMaterializedView {
18868 ref name,
18869 ref definition_json,
18870 }) => {
18871 let definition = DdlOp::decode_materialized_view(definition_json)?;
18872 if definition.name != *name {
18873 return Err(MongrelError::Schema(format!(
18874 "materialized view WAL name mismatch: {name:?}"
18875 )));
18876 }
18877 if cat.live(name).is_some() {
18878 if let Some(existing) = cat
18879 .materialized_views
18880 .iter_mut()
18881 .find(|existing| existing.name == *name)
18882 {
18883 if *existing != definition {
18884 *existing = definition;
18885 changed = true;
18886 }
18887 } else {
18888 cat.materialized_views.push(definition);
18889 changed = true;
18890 }
18891 }
18892 }
18893 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
18894 let security = DdlOp::decode_security(security_json)?;
18895 validate_security_catalog(cat, &security)?;
18896 if cat.security != security {
18897 cat.security = security;
18898 if !catalog_snapshot_txns.contains(&txn_id) {
18899 advance_security_version(cat)?;
18900 }
18901 changed = true;
18902 }
18903 }
18904 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
18905 let target = match key.as_str() {
18906 "user_version" => &mut cat.user_version,
18907 "application_id" => &mut cat.application_id,
18908 _ => {
18909 return Err(MongrelError::InvalidArgument(format!(
18910 "unsupported recovered SQL pragma {key:?}"
18911 )))
18912 }
18913 };
18914 if *target != Some(value) {
18915 *target = Some(value);
18916 cat.db_epoch = cat.db_epoch.max(ce);
18917 changed = true;
18918 }
18919 }
18920 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
18921 if ce <= applied_catalog_epoch {
18922 continue;
18923 }
18924 let snapshot = DdlOp::decode_catalog(catalog_json)?;
18925 if snapshot.db_epoch != ce {
18926 return Err(MongrelError::Schema(format!(
18927 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
18928 snapshot.db_epoch
18929 )));
18930 }
18931 validate_recovered_catalog(&snapshot)?;
18932 validate_catalog_transition(cat, &snapshot)?;
18933 *cat = snapshot;
18934 applied_catalog_epoch = ce;
18935 changed = true;
18936 }
18937 _ => {}
18938 }
18939 }
18940
18941 if cat.db_epoch < max_committed_epoch {
18942 cat.db_epoch = max_committed_epoch;
18943 changed = true;
18944 }
18945 changed |= repair_catalog_allocator_counters(cat)?;
18946
18947 validate_recovered_catalog(cat)?;
18948 let storage_reconciliation = validate_recovered_storage_plan(
18949 root,
18950 durable_root,
18951 cat,
18952 &created_table_ids,
18953 &ttl_updates,
18954 meta_dek,
18955 )?;
18956
18957 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
18958 if apply && (changed || needs_storage_apply) {
18959 for table_id in storage_reconciliation {
18960 let entry = cat
18961 .tables
18962 .iter()
18963 .find(|entry| entry.table_id == table_id)
18964 .ok_or_else(|| MongrelError::CorruptWal {
18965 offset: table_id,
18966 reason: "recovery storage plan lost its catalog table".into(),
18967 })?;
18968 ensure_recovered_table_storage(
18969 table_roots
18970 .and_then(|roots| roots.get(&table_id))
18971 .map(Arc::as_ref),
18972 durable_root,
18973 &root.join(TABLES_DIR).join(table_id.to_string()),
18974 table_id,
18975 &entry.schema,
18976 meta_dek,
18977 )?;
18978 }
18979 for (table_id, (policy, ttl_epoch)) in ttl_updates {
18980 let Some(entry) = cat.tables.iter().find(|entry| {
18981 entry.table_id == table_id
18982 && matches!(entry.state, TableState::Live | TableState::Building { .. })
18983 }) else {
18984 continue;
18985 };
18986 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
18987 {
18988 root.try_clone()?
18989 } else if let Some(root) = durable_root {
18990 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
18991 } else {
18992 crate::durable_file::DurableRoot::open(
18993 root.join(TABLES_DIR).join(table_id.to_string()),
18994 )?
18995 };
18996 let table_dir = table_root.io_path()?;
18997 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
18998 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
18999 manifest.ttl = policy;
19000 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
19001 manifest.schema_id = entry.schema.schema_id;
19002 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
19003 }
19004 }
19005 if changed {
19006 match durable_root {
19007 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
19008 None => catalog::write_atomic(root, cat, meta_dek)?,
19009 }
19010 }
19011 }
19012 *target_catalog = recovered_catalog;
19013 Ok(())
19014}
19015
19016fn ensure_recovered_table_storage(
19017 pinned_table: Option<&crate::durable_file::DurableRoot>,
19018 durable_root: Option<&crate::durable_file::DurableRoot>,
19019 fallback_table_dir: &Path,
19020 table_id: u64,
19021 schema: &Schema,
19022 meta_dek: Option<&[u8; META_DEK_LEN]>,
19023) -> Result<()> {
19024 let table_root = if let Some(root) = pinned_table {
19025 root.try_clone()?
19026 } else if let Some(root) = durable_root {
19027 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
19028 match root.open_directory(&relative) {
19029 Ok(table) => table,
19030 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
19031 root.create_directory_all_pinned(relative)?
19032 }
19033 Err(error) => return Err(error.into()),
19034 }
19035 } else {
19036 crate::durable_file::create_directory_all(fallback_table_dir)?;
19037 crate::durable_file::DurableRoot::open(fallback_table_dir)?
19038 };
19039 let table_dir = table_root.io_path()?;
19040 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
19041 Ok(manifest) => {
19042 if manifest.table_id != table_id {
19043 return Err(MongrelError::Conflict(format!(
19044 "recovered table directory id mismatch: expected {table_id}, found {}",
19045 manifest.table_id
19046 )));
19047 }
19048 Some(manifest)
19049 }
19050 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
19051 Err(error) => return Err(error),
19052 };
19053
19054 table_root.create_directory_all(crate::engine::WAL_DIR)?;
19055 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
19056 crate::engine::write_schema(&table_dir, schema)?;
19057
19058 if let Some(mut manifest) = existing_manifest.take() {
19059 if manifest.schema_id != schema.schema_id {
19060 manifest.schema_id = schema.schema_id;
19061 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
19062 }
19063 } else {
19064 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
19066 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
19067 }
19068 Ok(())
19069}
19070
19071fn validate_recovered_schema(schema: &Schema) -> Result<()> {
19072 schema.validate_auto_increment()?;
19073 schema.validate_defaults()?;
19074 schema.validate_ai()?;
19075 let mut column_ids = HashSet::new();
19076 let mut column_names = HashSet::new();
19077 for column in &schema.columns {
19078 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
19079 return Err(MongrelError::Schema(
19080 "recovered schema contains duplicate columns".into(),
19081 ));
19082 }
19083 match &column.ty {
19084 TypeId::Decimal128 { precision, scale }
19085 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
19086 {
19087 return Err(MongrelError::Schema(format!(
19088 "column {:?} has invalid decimal precision or scale",
19089 column.name
19090 )));
19091 }
19092 TypeId::Enum { variants }
19093 if variants.is_empty()
19094 || variants.iter().any(String::is_empty)
19095 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
19096 {
19097 return Err(MongrelError::Schema(format!(
19098 "column {:?} has invalid enum variants",
19099 column.name
19100 )));
19101 }
19102 _ => {}
19103 }
19104 }
19105 let mut index_names = HashSet::new();
19106 for index in &schema.indexes {
19107 index.validate_options()?;
19108 if index.name.is_empty()
19109 || !index_names.insert(index.name.as_str())
19110 || schema
19111 .columns
19112 .iter()
19113 .all(|column| column.id != index.column_id)
19114 {
19115 return Err(MongrelError::Schema(format!(
19116 "recovered index {:?} references missing column {}",
19117 index.name, index.column_id
19118 )));
19119 }
19120 }
19121 let mut colocated = HashSet::new();
19122 for group in &schema.colocation {
19123 if group.is_empty()
19124 || group.iter().any(|id| !column_ids.contains(id))
19125 || group.iter().any(|id| !colocated.insert(*id))
19126 {
19127 return Err(MongrelError::Schema(
19128 "recovered schema contains invalid column co-location groups".into(),
19129 ));
19130 }
19131 }
19132
19133 let mut constraint_ids = HashSet::new();
19134 let mut constraint_names = HashSet::<String>::new();
19135 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
19136 if name.is_empty()
19137 || !constraint_ids.insert(id)
19138 || !constraint_names.insert(name.to_owned())
19139 {
19140 return Err(MongrelError::Schema(
19141 "recovered schema contains duplicate or empty constraint identities".into(),
19142 ));
19143 }
19144 Ok(())
19145 };
19146 for unique in &schema.constraints.uniques {
19147 validate_constraint_identity(unique.id, &unique.name)?;
19148 if unique.columns.is_empty()
19149 || unique.columns.iter().any(|id| !column_ids.contains(id))
19150 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
19151 {
19152 return Err(MongrelError::Schema(format!(
19153 "unique constraint {:?} has invalid columns",
19154 unique.name
19155 )));
19156 }
19157 }
19158 for foreign_key in &schema.constraints.foreign_keys {
19159 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
19160 if foreign_key.ref_table.is_empty()
19161 || foreign_key.columns.is_empty()
19162 || foreign_key.columns.len() != foreign_key.ref_columns.len()
19163 || foreign_key
19164 .columns
19165 .iter()
19166 .any(|id| !column_ids.contains(id))
19167 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
19168 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
19169 != foreign_key.ref_columns.len()
19170 {
19171 return Err(MongrelError::Schema(format!(
19172 "foreign key {:?} has invalid columns",
19173 foreign_key.name
19174 )));
19175 }
19176 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
19177 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
19178 && foreign_key.columns.iter().any(|id| {
19179 schema
19180 .columns
19181 .iter()
19182 .find(|column| column.id == *id)
19183 .is_none_or(|column| {
19184 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
19185 })
19186 })
19187 {
19188 return Err(MongrelError::Schema(format!(
19189 "foreign key {:?} uses SET NULL on a non-nullable column",
19190 foreign_key.name
19191 )));
19192 }
19193 }
19194 for check in &schema.constraints.checks {
19195 validate_constraint_identity(check.id, &check.name)?;
19196 check.expr.validate()?;
19197 validate_check_columns(&check.expr, &column_ids)?;
19198 }
19199 Ok(())
19200}
19201
19202fn validate_check_columns(
19203 expression: &crate::constraint::CheckExpr,
19204 column_ids: &HashSet<u16>,
19205) -> Result<()> {
19206 use crate::constraint::CheckExpr;
19207 match expression {
19208 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
19209 if column_ids.contains(id) {
19210 Ok(())
19211 } else {
19212 Err(MongrelError::Schema(format!(
19213 "check constraint references unknown column {id}"
19214 )))
19215 }
19216 }
19217 CheckExpr::Regex { col, .. } => {
19218 if column_ids.contains(col) {
19219 Ok(())
19220 } else {
19221 Err(MongrelError::Schema(format!(
19222 "check constraint references unknown column {col}"
19223 )))
19224 }
19225 }
19226 CheckExpr::Add(left, right)
19227 | CheckExpr::Sub(left, right)
19228 | CheckExpr::Mul(left, right)
19229 | CheckExpr::Div(left, right)
19230 | CheckExpr::Mod(left, right)
19231 | CheckExpr::Eq(left, right)
19232 | CheckExpr::Ne(left, right)
19233 | CheckExpr::Lt(left, right)
19234 | CheckExpr::Le(left, right)
19235 | CheckExpr::Gt(left, right)
19236 | CheckExpr::Ge(left, right)
19237 | CheckExpr::And(left, right)
19238 | CheckExpr::Or(left, right) => {
19239 validate_check_columns(left, column_ids)?;
19240 validate_check_columns(right, column_ids)
19241 }
19242 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
19243 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
19244 }
19245}
19246
19247fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
19248 for (name, prior, candidate) in [
19249 ("db_epoch", current.db_epoch, next.db_epoch),
19250 ("next_table_id", current.next_table_id, next.next_table_id),
19251 (
19252 "next_segment_no",
19253 current.next_segment_no,
19254 next.next_segment_no,
19255 ),
19256 ("next_user_id", current.next_user_id, next.next_user_id),
19257 (
19258 "security_version",
19259 current.security_version,
19260 next.security_version,
19261 ),
19262 ] {
19263 if candidate < prior {
19264 return Err(MongrelError::Schema(format!(
19265 "catalog snapshot rolls back {name} from {prior} to {candidate}"
19266 )));
19267 }
19268 }
19269 for prior in ¤t.tables {
19270 let Some(candidate) = next
19271 .tables
19272 .iter()
19273 .find(|entry| entry.table_id == prior.table_id)
19274 else {
19275 return Err(MongrelError::Schema(format!(
19276 "catalog snapshot removes table identity {}",
19277 prior.table_id
19278 )));
19279 };
19280 if candidate.created_epoch != prior.created_epoch
19281 || candidate.schema.schema_id < prior.schema.schema_id
19282 || matches!(prior.state, TableState::Dropped { .. })
19283 && !matches!(candidate.state, TableState::Dropped { .. })
19284 {
19285 return Err(MongrelError::Schema(format!(
19286 "catalog snapshot rolls back table identity {}",
19287 prior.table_id
19288 )));
19289 }
19290 }
19291 for prior in ¤t.users {
19292 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
19293 if candidate.username != prior.username
19294 || candidate.created_epoch != prior.created_epoch
19295 {
19296 return Err(MongrelError::Schema(format!(
19297 "catalog snapshot reuses user identity {}",
19298 prior.id
19299 )));
19300 }
19301 }
19302 }
19303 Ok(())
19304}
19305
19306fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
19307 let mut table_ids = HashSet::new();
19308 let mut active_names = HashSet::new();
19309 let mut max_table_id = None::<u64>;
19310 for entry in &catalog.tables {
19311 if !table_ids.insert(entry.table_id) {
19312 return Err(MongrelError::Schema(format!(
19313 "catalog contains duplicate table id {}",
19314 entry.table_id
19315 )));
19316 }
19317 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
19318 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
19319 return Err(MongrelError::Schema(format!(
19320 "catalog table {} has invalid name or creation epoch",
19321 entry.table_id
19322 )));
19323 }
19324 validate_recovered_schema(&entry.schema)?;
19325 match &entry.state {
19326 TableState::Live => {
19327 if !active_names.insert(entry.name.as_str()) {
19328 return Err(MongrelError::Schema(format!(
19329 "catalog contains duplicate active table name {:?}",
19330 entry.name
19331 )));
19332 }
19333 }
19334 TableState::Dropped { at_epoch } => {
19335 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
19336 return Err(MongrelError::Schema(format!(
19337 "catalog table {} has invalid drop epoch {at_epoch}",
19338 entry.table_id
19339 )));
19340 }
19341 }
19342 TableState::Building {
19343 intended_name,
19344 query_id,
19345 replaces_table_id,
19346 ..
19347 } => {
19348 if intended_name.is_empty() || query_id.is_empty() {
19349 return Err(MongrelError::Schema(format!(
19350 "building table {} has empty identity fields",
19351 entry.table_id
19352 )));
19353 }
19354 if !active_names.insert(entry.name.as_str()) {
19355 return Err(MongrelError::Schema(format!(
19356 "catalog contains duplicate active/building table name {:?}",
19357 entry.name
19358 )));
19359 }
19360 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
19361 return Err(MongrelError::Schema(
19362 "building table cannot replace itself".into(),
19363 ));
19364 }
19365 }
19366 }
19367 }
19368 if let Some(maximum) = max_table_id {
19369 let required = maximum
19370 .checked_add(1)
19371 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19372 if catalog.next_table_id < required {
19373 return Err(MongrelError::Schema(format!(
19374 "catalog next_table_id {} precedes required {required}",
19375 catalog.next_table_id
19376 )));
19377 }
19378 }
19379 for entry in &catalog.tables {
19380 if let TableState::Building {
19381 replaces_table_id: Some(replaced),
19382 ..
19383 } = entry.state
19384 {
19385 if !table_ids.contains(&replaced) {
19386 return Err(MongrelError::Schema(format!(
19387 "building table {} replaces unknown table {replaced}",
19388 entry.table_id
19389 )));
19390 }
19391 }
19392 }
19393 for entry in &catalog.tables {
19394 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19395 validate_foreign_key_targets(catalog, &entry.schema)?;
19396 }
19397 }
19398
19399 let mut external_names = HashSet::new();
19400 for entry in &catalog.external_tables {
19401 entry.validate()?;
19402 validate_recovered_schema(&entry.declared_schema)?;
19403 if !entry.declared_schema.constraints.is_empty() {
19404 return Err(MongrelError::Schema(format!(
19405 "external table {:?} cannot carry engine-enforced constraints",
19406 entry.name
19407 )));
19408 }
19409 if entry.created_epoch > catalog.db_epoch
19410 || !external_names.insert(entry.name.as_str())
19411 || active_names.contains(entry.name.as_str())
19412 {
19413 return Err(MongrelError::Schema(format!(
19414 "invalid or duplicate external table {:?}",
19415 entry.name
19416 )));
19417 }
19418 }
19419
19420 let mut procedure_names = HashSet::new();
19421 for entry in &catalog.procedures {
19422 entry.procedure.validate()?;
19423 if entry.procedure.created_epoch > entry.procedure.updated_epoch
19424 || entry.procedure.updated_epoch > catalog.db_epoch
19425 || !procedure_names.insert(entry.procedure.name.as_str())
19426 {
19427 return Err(MongrelError::Schema(format!(
19428 "invalid or duplicate procedure {:?}",
19429 entry.procedure.name
19430 )));
19431 }
19432 validate_recovered_procedure_references(catalog, &entry.procedure)?;
19433 }
19434
19435 let mut trigger_names = HashSet::new();
19436 for entry in &catalog.triggers {
19437 entry.trigger.validate()?;
19438 if entry.trigger.created_epoch > entry.trigger.updated_epoch
19439 || entry.trigger.updated_epoch > catalog.db_epoch
19440 || !trigger_names.insert(entry.trigger.name.as_str())
19441 {
19442 return Err(MongrelError::Schema(format!(
19443 "invalid or duplicate trigger {:?}",
19444 entry.trigger.name
19445 )));
19446 }
19447 validate_recovered_trigger_references(catalog, &entry.trigger)?;
19448 }
19449
19450 let mut views = HashSet::new();
19451 for view in &catalog.materialized_views {
19452 let target = catalog.live(&view.name).ok_or_else(|| {
19453 MongrelError::Schema(format!(
19454 "materialized view {:?} has no live table",
19455 view.name
19456 ))
19457 })?;
19458 if view.name.is_empty()
19459 || view.query.trim().is_empty()
19460 || view.last_refresh_epoch > catalog.db_epoch
19461 || !views.insert(view.name.as_str())
19462 {
19463 return Err(MongrelError::Schema(format!(
19464 "materialized view {:?} has no unique live table",
19465 view.name
19466 )));
19467 }
19468 if let Some(incremental) = &view.incremental {
19469 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
19470 MongrelError::Schema(format!(
19471 "materialized view {:?} references missing source {:?}",
19472 view.name, incremental.source_table
19473 ))
19474 })?;
19475 if source.table_id != incremental.source_table_id
19476 || source
19477 .schema
19478 .columns
19479 .iter()
19480 .all(|column| column.id != incremental.group_column)
19481 {
19482 return Err(MongrelError::Schema(format!(
19483 "materialized view {:?} has invalid incremental source",
19484 view.name
19485 )));
19486 }
19487 let target_ids = target
19488 .schema
19489 .columns
19490 .iter()
19491 .map(|column| column.id)
19492 .collect::<HashSet<_>>();
19493 let mut output_ids = HashSet::new();
19494 let count_outputs = incremental
19495 .outputs
19496 .iter()
19497 .filter(|output| {
19498 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19499 })
19500 .count();
19501 if incremental.checkpoint_event_id.is_empty()
19502 || !target_ids.contains(&incremental.group_output_column)
19503 || !target_ids.contains(&incremental.count_output_column)
19504 || incremental.outputs.is_empty()
19505 || count_outputs != 1
19506 || incremental.outputs.iter().any(|output| {
19507 !target_ids.contains(&output.output_column)
19508 || output.output_column == incremental.group_output_column
19509 || !output_ids.insert(output.output_column)
19510 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19511 && output.output_column != incremental.count_output_column
19512 || match output.kind {
19513 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
19514 source
19515 .schema
19516 .columns
19517 .iter()
19518 .all(|column| column.id != source_column)
19519 }
19520 crate::catalog::IncrementalAggregateKind::Count => false,
19521 }
19522 })
19523 {
19524 return Err(MongrelError::Schema(format!(
19525 "materialized view {:?} has invalid incremental outputs",
19526 view.name
19527 )));
19528 }
19529 }
19530 }
19531
19532 validate_security_catalog(catalog, &catalog.security)?;
19533 validate_recovered_auth_catalog(catalog)?;
19534 Ok(())
19535}
19536
19537fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
19538 let mut changed = false;
19539 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
19540 let required = maximum
19541 .checked_add(1)
19542 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19543 if catalog.next_table_id < required {
19544 catalog.next_table_id = required;
19545 changed = true;
19546 }
19547 }
19548 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
19549 let required = maximum
19550 .checked_add(1)
19551 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
19552 if catalog.next_user_id < required {
19553 catalog.next_user_id = required;
19554 changed = true;
19555 }
19556 }
19557 Ok(changed)
19558}
19559
19560fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
19561 for foreign_key in &schema.constraints.foreign_keys {
19562 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
19563 MongrelError::Schema(format!(
19564 "foreign key {:?} references unknown live table {:?}",
19565 foreign_key.name, foreign_key.ref_table
19566 ))
19567 })?;
19568 let referenced_unique = parent
19569 .schema
19570 .constraints
19571 .uniques
19572 .iter()
19573 .any(|unique| unique.columns == foreign_key.ref_columns)
19574 || foreign_key.ref_columns.len() == 1
19575 && parent
19576 .schema
19577 .primary_key()
19578 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
19579 if !referenced_unique {
19580 return Err(MongrelError::Schema(format!(
19581 "foreign key {:?} does not reference a unique key",
19582 foreign_key.name
19583 )));
19584 }
19585 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
19586 let local = schema.columns.iter().find(|column| column.id == *local_id);
19587 let referenced = parent
19588 .schema
19589 .columns
19590 .iter()
19591 .find(|column| column.id == *parent_id);
19592 if local
19593 .zip(referenced)
19594 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
19595 {
19596 return Err(MongrelError::Schema(format!(
19597 "foreign key {:?} has missing or incompatible columns",
19598 foreign_key.name
19599 )));
19600 }
19601 }
19602 }
19603 Ok(())
19604}
19605
19606fn validate_recovered_procedure_references(
19607 catalog: &Catalog,
19608 procedure: &StoredProcedure,
19609) -> Result<()> {
19610 for step in &procedure.body.steps {
19611 let Some(table_name) = step.table() else {
19612 continue;
19613 };
19614 let schema = &catalog
19615 .live(table_name)
19616 .ok_or_else(|| {
19617 MongrelError::Schema(format!(
19618 "procedure {:?} references unknown table {table_name:?}",
19619 procedure.name
19620 ))
19621 })?
19622 .schema;
19623 match step {
19624 ProcedureStep::NativeQuery {
19625 conditions,
19626 projection,
19627 ..
19628 } => {
19629 for condition in conditions {
19630 validate_condition_columns(condition, schema)?;
19631 }
19632 for id in projection.iter().flatten() {
19633 validate_column_id(*id, schema)?;
19634 }
19635 }
19636 ProcedureStep::Put { cells, .. } => {
19637 for cell in cells {
19638 validate_column_id(cell.column_id, schema)?;
19639 }
19640 }
19641 ProcedureStep::Upsert {
19642 cells,
19643 update_cells,
19644 ..
19645 } => {
19646 for cell in cells.iter().chain(update_cells.iter().flatten()) {
19647 validate_column_id(cell.column_id, schema)?;
19648 }
19649 }
19650 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
19651 return Err(MongrelError::Schema(format!(
19652 "procedure {:?} deletes by primary key on table without one",
19653 procedure.name
19654 )));
19655 }
19656 ProcedureStep::DeleteByPk { .. }
19657 | ProcedureStep::DeleteRows { .. }
19658 | ProcedureStep::SqlQuery { .. } => {}
19659 }
19660 }
19661 Ok(())
19662}
19663
19664fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
19665 let target_schema = match &trigger.target {
19666 TriggerTarget::Table(name) => catalog
19667 .live(name)
19668 .ok_or_else(|| {
19669 MongrelError::Schema(format!(
19670 "trigger {:?} references unknown table {name:?}",
19671 trigger.name
19672 ))
19673 })?
19674 .schema
19675 .clone(),
19676 TriggerTarget::View(_) => Schema {
19677 columns: trigger.target_columns.clone(),
19678 ..Schema::default()
19679 },
19680 };
19681 for column in &trigger.update_of {
19682 if target_schema.column(column).is_none() {
19683 return Err(MongrelError::Schema(format!(
19684 "trigger {:?} references unknown UPDATE OF column {column:?}",
19685 trigger.name
19686 )));
19687 }
19688 }
19689 if let Some(expr) = &trigger.when {
19690 validate_trigger_expr(expr, &target_schema, trigger.event)?;
19691 }
19692 let mut selects = HashMap::new();
19693 for step in &trigger.program.steps {
19694 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
19695 return Err(MongrelError::Schema(
19696 "SetNew is only valid in BEFORE triggers".into(),
19697 ));
19698 }
19699 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
19700 }
19701 Ok(())
19702}
19703
19704fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
19705 let mut role_names = HashSet::new();
19706 for role in &catalog.roles {
19707 if role.name.is_empty()
19708 || role.created_epoch > catalog.db_epoch
19709 || !role_names.insert(role.name.as_str())
19710 {
19711 return Err(MongrelError::Schema(format!(
19712 "invalid or duplicate role {:?}",
19713 role.name
19714 )));
19715 }
19716 for permission in &role.permissions {
19717 if let Some(table) = permission_table(permission) {
19718 let schema = catalog
19719 .live(table)
19720 .map(|entry| &entry.schema)
19721 .or_else(|| {
19722 catalog
19723 .external_tables
19724 .iter()
19725 .find(|entry| entry.name == table)
19726 .map(|entry| &entry.declared_schema)
19727 })
19728 .ok_or_else(|| {
19729 MongrelError::Schema(format!(
19730 "role {:?} references unknown table {table:?}",
19731 role.name
19732 ))
19733 })?;
19734 let columns = match permission {
19735 crate::auth::Permission::SelectColumns { columns, .. }
19736 | crate::auth::Permission::InsertColumns { columns, .. }
19737 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
19738 _ => None,
19739 };
19740 if columns.is_some_and(|columns| {
19741 columns.is_empty()
19742 || columns.iter().any(|column| schema.column(column).is_none())
19743 }) {
19744 return Err(MongrelError::Schema(format!(
19745 "role {:?} contains invalid column permissions",
19746 role.name
19747 )));
19748 }
19749 }
19750 }
19751 }
19752 let mut user_ids = HashSet::new();
19753 let mut usernames = HashSet::new();
19754 let mut maximum_user_id = 0;
19755 for user in &catalog.users {
19756 maximum_user_id = maximum_user_id.max(user.id);
19757 if user.id == 0
19758 || user.username.is_empty()
19759 || user.password_hash.is_empty()
19760 || user.created_epoch > catalog.db_epoch
19761 || !user_ids.insert(user.id)
19762 || !usernames.insert(user.username.as_str())
19763 || user
19764 .roles
19765 .iter()
19766 .any(|role| !role_names.contains(role.as_str()))
19767 {
19768 return Err(MongrelError::Schema(format!(
19769 "invalid or duplicate user {:?}",
19770 user.username
19771 )));
19772 }
19773 }
19774 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
19775 return Err(MongrelError::Schema(
19776 "catalog next_user_id does not advance beyond existing user ids".into(),
19777 ));
19778 }
19779 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
19780 return Err(MongrelError::Schema(
19781 "authenticated catalog has no administrator".into(),
19782 ));
19783 }
19784 Ok(())
19785}
19786
19787fn validate_recovered_storage_plan(
19788 root: &Path,
19789 durable_root: Option<&crate::durable_file::DurableRoot>,
19790 catalog: &Catalog,
19791 created_table_ids: &HashSet<u64>,
19792 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
19793 meta_dek: Option<&[u8; META_DEK_LEN]>,
19794) -> Result<Vec<u64>> {
19795 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
19796 let mut reconcile = Vec::new();
19797 for entry in &catalog.tables {
19798 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19799 continue;
19800 }
19801 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19802 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
19803 let table_exists = match durable_root {
19804 Some(root) => match root.open_directory(&relative_dir) {
19805 Ok(_) => true,
19806 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19807 Err(error) => return Err(error.into()),
19808 },
19809 None => table_dir.is_dir(),
19810 };
19811 if !table_exists {
19812 if created_table_ids.contains(&entry.table_id) {
19813 reconcile.push(entry.table_id);
19814 continue;
19815 }
19816 return Err(MongrelError::NotFound(format!(
19817 "catalog table {} storage is missing",
19818 entry.table_id
19819 )));
19820 }
19821 let manifest_result = match durable_root {
19822 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
19823 None => crate::manifest::read(&table_dir, meta_dek),
19824 };
19825 let manifest = match manifest_result {
19826 Ok(manifest) => manifest,
19827 Err(MongrelError::Io(error))
19828 if created_table_ids.contains(&entry.table_id)
19829 && error.kind() == std::io::ErrorKind::NotFound =>
19830 {
19831 reconcile.push(entry.table_id);
19832 continue;
19833 }
19834 Err(error) => return Err(error),
19835 };
19836 if manifest.table_id != entry.table_id {
19837 return Err(MongrelError::Conflict(format!(
19838 "catalog table {} storage identity mismatch",
19839 entry.table_id
19840 )));
19841 }
19842 let schema_result = match durable_root {
19843 Some(root) => root
19844 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
19845 .map_err(MongrelError::from),
19846 None => crate::durable_file::open_regular_nofollow(
19847 &table_dir.join(crate::engine::SCHEMA_FILENAME),
19848 ),
19849 };
19850 let file = match schema_result {
19851 Ok(file) => file,
19852 Err(MongrelError::Io(error))
19853 if created_table_ids.contains(&entry.table_id)
19854 && error.kind() == std::io::ErrorKind::NotFound =>
19855 {
19856 reconcile.push(entry.table_id);
19857 continue;
19858 }
19859 Err(error) => return Err(error),
19860 };
19861 let length = file.metadata()?.len();
19862 if length > MAX_SCHEMA_BYTES {
19863 return Err(MongrelError::ResourceLimitExceeded {
19864 resource: "recovered schema bytes",
19865 requested: usize::try_from(length).unwrap_or(usize::MAX),
19866 limit: MAX_SCHEMA_BYTES as usize,
19867 });
19868 }
19869 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
19870 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
19871 if manifest.schema_id != entry.schema.schema_id
19872 || crate::wal::DdlOp::encode_schema(&disk_schema)?
19873 != crate::wal::DdlOp::encode_schema(&entry.schema)?
19874 {
19875 reconcile.push(entry.table_id);
19876 }
19877 }
19878 for table_id in ttl_updates.keys() {
19879 if !catalog.tables.iter().any(|entry| {
19880 entry.table_id == *table_id
19881 && matches!(entry.state, TableState::Live | TableState::Building { .. })
19882 }) {
19883 continue;
19884 }
19885 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
19886 let table_exists = match durable_root {
19887 Some(root) => match root.open_directory(&relative_dir) {
19888 Ok(_) => true,
19889 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19890 Err(error) => return Err(error.into()),
19891 },
19892 None => root.join(&relative_dir).is_dir(),
19893 };
19894 if !table_exists && !created_table_ids.contains(table_id) {
19895 return Err(MongrelError::NotFound(format!(
19896 "TTL recovery table {table_id} storage is missing"
19897 )));
19898 }
19899 }
19900 reconcile.sort_unstable();
19901 reconcile.dedup();
19902 Ok(reconcile)
19903}
19904
19905fn validate_catalog_table_storage(
19906 root: &crate::durable_file::DurableRoot,
19907 catalog: &Catalog,
19908 meta_dek: Option<&[u8; META_DEK_LEN]>,
19909) -> Result<()> {
19910 for entry in &catalog.tables {
19911 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19912 continue;
19913 }
19914 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19915 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
19916 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
19917 return Err(MongrelError::Conflict(format!(
19918 "catalog table {} storage identity mismatch",
19919 entry.table_id
19920 )));
19921 }
19922 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
19923 }
19924 Ok(())
19925}
19926
19927fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
19928 match schema.columns.iter_mut().find(|c| c.id == column.id) {
19929 Some(existing) if *existing == column => Ok(false),
19930 Some(existing) => {
19931 *existing = column;
19932 schema.schema_id = schema
19933 .schema_id
19934 .checked_add(1)
19935 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19936 Ok(true)
19937 }
19938 None => {
19939 schema.columns.push(column);
19940 schema.schema_id = schema
19941 .schema_id
19942 .checked_add(1)
19943 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19944 Ok(true)
19945 }
19946 }
19947}
19948
19949fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
19950 use crate::auth::Permission;
19951 match permission {
19952 Permission::Select { table }
19953 | Permission::Insert { table }
19954 | Permission::Update { table }
19955 | Permission::Delete { table }
19956 | Permission::SelectColumns { table, .. }
19957 | Permission::InsertColumns { table, .. }
19958 | Permission::UpdateColumns { table, .. } => Some(table),
19959 Permission::All | Permission::Ddl | Permission::Admin => None,
19960 }
19961}
19962
19963fn apply_rebuilding_publish(
19964 catalog: &mut Catalog,
19965 table_id: u64,
19966 replaced_table_id: u64,
19967 new_name: &str,
19968 epoch: u64,
19969) -> Result<bool> {
19970 let already_published = catalog.tables.iter().any(|entry| {
19971 entry.table_id == table_id
19972 && entry.name == new_name
19973 && matches!(entry.state, TableState::Live)
19974 }) && catalog.tables.iter().any(|entry| {
19975 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
19976 });
19977 if already_published {
19978 return Ok(false);
19979 }
19980 let schema = catalog
19981 .tables
19982 .iter()
19983 .find(|entry| entry.table_id == table_id)
19984 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
19985 .schema
19986 .clone();
19987 let replaced = catalog
19988 .tables
19989 .iter_mut()
19990 .find(|entry| entry.table_id == replaced_table_id)
19991 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
19992 replaced.state = TableState::Dropped { at_epoch: epoch };
19993 let replacement = catalog
19994 .tables
19995 .iter_mut()
19996 .find(|entry| entry.table_id == table_id)
19997 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
19998 replacement.name = new_name.to_string();
19999 replacement.state = TableState::Live;
20000
20001 for role in &mut catalog.roles {
20002 role.permissions.retain_mut(|permission| {
20003 retain_rebuilt_permission_columns(permission, new_name, &schema)
20004 });
20005 }
20006 for definition in &mut catalog.materialized_views {
20007 if let Some(incremental) = definition.incremental.as_mut() {
20008 if incremental.source_table == new_name
20009 && incremental.source_table_id == replaced_table_id
20010 {
20011 incremental.source_table_id = table_id;
20012 }
20013 }
20014 }
20015 advance_security_version(catalog)?;
20016 Ok(true)
20017}
20018
20019fn retain_rebuilt_permission_columns(
20020 permission: &mut crate::auth::Permission,
20021 target_table: &str,
20022 schema: &Schema,
20023) -> bool {
20024 use crate::auth::Permission;
20025 let columns = match permission {
20026 Permission::SelectColumns { table, columns }
20027 | Permission::InsertColumns { table, columns }
20028 | Permission::UpdateColumns { table, columns }
20029 if table == target_table =>
20030 {
20031 Some(columns)
20032 }
20033 _ => None,
20034 };
20035 if let Some(columns) = columns {
20036 columns.retain(|column| schema.column(column).is_some());
20037 !columns.is_empty()
20038 } else {
20039 true
20040 }
20041}
20042
20043fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
20044 use crate::auth::Permission;
20045 let table = match permission {
20046 Permission::Select { table }
20047 | Permission::Insert { table }
20048 | Permission::Update { table }
20049 | Permission::Delete { table }
20050 | Permission::SelectColumns { table, .. }
20051 | Permission::InsertColumns { table, .. }
20052 | Permission::UpdateColumns { table, .. } => Some(table),
20053 Permission::All | Permission::Ddl | Permission::Admin => None,
20054 };
20055 if let Some(table) = table.filter(|table| table.as_str() == old) {
20056 *table = new.to_string();
20057 }
20058}
20059
20060fn rename_permission_column(
20061 permission: &mut crate::auth::Permission,
20062 target_table: &str,
20063 old: &str,
20064 new: &str,
20065) {
20066 use crate::auth::Permission;
20067 let columns = match permission {
20068 Permission::SelectColumns { table, columns }
20069 | Permission::InsertColumns { table, columns }
20070 | Permission::UpdateColumns { table, columns }
20071 if table == target_table =>
20072 {
20073 Some(columns)
20074 }
20075 _ => None,
20076 };
20077 if let Some(column) = columns
20078 .into_iter()
20079 .flatten()
20080 .find(|column| column.as_str() == old)
20081 {
20082 *column = new.to_string();
20083 }
20084}
20085
20086pub(crate) fn validate_security_catalog(
20087 catalog: &Catalog,
20088 security: &crate::security::SecurityCatalog,
20089) -> Result<()> {
20090 let mut policy_names = HashSet::new();
20091 for table in &security.rls_tables {
20092 if catalog.live(table).is_none() {
20093 return Err(MongrelError::NotFound(format!(
20094 "RLS table {table:?} not found"
20095 )));
20096 }
20097 }
20098 for policy in &security.policies {
20099 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
20100 return Err(MongrelError::InvalidArgument(format!(
20101 "duplicate policy {:?} on {:?}",
20102 policy.name, policy.table
20103 )));
20104 }
20105 let schema = &catalog
20106 .live(&policy.table)
20107 .ok_or_else(|| {
20108 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
20109 })?
20110 .schema;
20111 if let Some(expression) = &policy.using {
20112 validate_security_expression(expression, schema)?;
20113 }
20114 if let Some(expression) = &policy.with_check {
20115 validate_security_expression(expression, schema)?;
20116 }
20117 }
20118 let mut mask_names = HashSet::new();
20119 for mask in &security.masks {
20120 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
20121 return Err(MongrelError::InvalidArgument(format!(
20122 "duplicate mask {:?} on {:?}",
20123 mask.name, mask.table
20124 )));
20125 }
20126 let column = catalog
20127 .live(&mask.table)
20128 .and_then(|entry| {
20129 entry
20130 .schema
20131 .columns
20132 .iter()
20133 .find(|column| column.id == mask.column)
20134 })
20135 .ok_or_else(|| {
20136 MongrelError::NotFound(format!(
20137 "mask column {} on {:?} not found",
20138 mask.column, mask.table
20139 ))
20140 })?;
20141 if matches!(
20142 mask.strategy,
20143 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
20144 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
20145 {
20146 return Err(MongrelError::InvalidArgument(format!(
20147 "mask {:?} requires a string/bytes column",
20148 mask.name
20149 )));
20150 }
20151 }
20152 Ok(())
20153}
20154
20155fn validate_security_expression(
20156 expression: &crate::security::SecurityExpr,
20157 schema: &Schema,
20158) -> Result<()> {
20159 use crate::security::SecurityExpr;
20160 match expression {
20161 SecurityExpr::True => Ok(()),
20162 SecurityExpr::ColumnEqCurrentUser { column }
20163 | SecurityExpr::ColumnEqValue { column, .. } => {
20164 if schema
20165 .columns
20166 .iter()
20167 .any(|candidate| candidate.id == *column)
20168 {
20169 Ok(())
20170 } else {
20171 Err(MongrelError::InvalidArgument(format!(
20172 "security expression references unknown column id {column}"
20173 )))
20174 }
20175 }
20176 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
20177 validate_security_expression(left, schema)?;
20178 validate_security_expression(right, schema)
20179 }
20180 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
20181 }
20182}
20183
20184fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
20186 let referenced = cat
20187 .tables
20188 .iter()
20189 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
20190 .map(|entry| entry.table_id)
20191 .collect::<HashSet<_>>();
20192 let tables_dir = root.join(TABLES_DIR);
20193 let entries = match std::fs::read_dir(&tables_dir) {
20194 Ok(entries) => entries,
20195 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
20196 Err(error) => return Err(error.into()),
20197 };
20198 for entry in entries {
20199 let entry = entry?;
20200 if !entry.file_type()?.is_dir() {
20201 continue;
20202 }
20203 let file_name = entry.file_name();
20204 let Some(name) = file_name.to_str() else {
20205 continue;
20206 };
20207 let Ok(table_id) = name.parse::<u64>() else {
20208 continue;
20209 };
20210 if name != table_id.to_string() {
20211 continue;
20212 }
20213 if !referenced.contains(&table_id) {
20214 crate::durable_file::remove_directory_all(&entry.path())?;
20215 }
20216 }
20217 Ok(())
20218}
20219
20220fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
20225 for entry in &cat.tables {
20226 let txn_dir = root
20227 .join(TABLES_DIR)
20228 .join(entry.table_id.to_string())
20229 .join("_txn");
20230 if txn_dir.exists() {
20231 let _ = std::fs::remove_dir_all(&txn_dir);
20232 }
20233 }
20234}
20235
20236#[cfg(test)]
20237mod write_permission_tests {
20238 use super::*;
20239 use crate::txn::Staged;
20240
20241 struct NoopExternalBridge;
20242
20243 impl ExternalTriggerBridge for NoopExternalBridge {
20244 fn apply_trigger_external_write(
20245 &self,
20246 _entry: &ExternalTableEntry,
20247 base_state: Vec<u8>,
20248 _op: ExternalTriggerWrite,
20249 ) -> Result<ExternalTriggerWriteResult> {
20250 Ok(ExternalTriggerWriteResult::new(base_state))
20251 }
20252 }
20253
20254 fn assert_txn_namespace_full<T>(result: Result<T>) {
20255 assert!(matches!(result, Err(MongrelError::Full(_))));
20256 }
20257
20258 #[test]
20259 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
20260 let directory = tempfile::tempdir().unwrap();
20261 let database = Database::create(directory.path()).unwrap();
20262 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
20263 *database.next_txn_id.lock() = generation << 32;
20264 let before = crate::wal::SharedWal::replay(directory.path())
20265 .unwrap()
20266 .len();
20267 let bridge = NoopExternalBridge;
20268
20269 assert_txn_namespace_full(database.begin().commit());
20270 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
20271 assert_txn_namespace_full(
20272 database
20273 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
20274 .commit(),
20275 );
20276 assert_txn_namespace_full(
20277 database
20278 .begin_with_external_trigger_bridge(&bridge)
20279 .commit(),
20280 );
20281 assert_txn_namespace_full(
20282 database
20283 .begin_with_external_trigger_bridge_as(&bridge, None)
20284 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
20285 );
20286
20287 assert_eq!(
20288 crate::wal::SharedWal::replay(directory.path())
20289 .unwrap()
20290 .len(),
20291 before
20292 );
20293 drop(database);
20294 Database::open(directory.path()).unwrap();
20295 }
20296
20297 #[test]
20298 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
20299 let directory = tempfile::tempdir().unwrap();
20300 let table_dir = directory.path().join("7");
20301 crate::durable_file::create_directory_all(&table_dir).unwrap();
20302 let original_schema = test_schema();
20303 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
20304 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
20305 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
20306 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
20307 let original_bytes = std::fs::read(&schema_path).unwrap();
20308
20309 let mut replacement_schema = original_schema;
20310 replacement_schema.schema_id += 1;
20311 assert!(matches!(
20312 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
20313 Err(MongrelError::Conflict(_))
20314 ));
20315
20316 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
20317 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
20318 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
20319 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
20320 }
20321
20322 #[test]
20323 fn catalog_table_missing_storage_fails_without_recreating_it() {
20324 let directory = tempfile::tempdir().unwrap();
20325 let table_dir = {
20326 let database = Database::create(directory.path()).unwrap();
20327 database.create_table("docs", test_schema()).unwrap();
20328 directory
20329 .path()
20330 .join(TABLES_DIR)
20331 .join(database.table_id("docs").unwrap().to_string())
20332 };
20333 std::fs::remove_dir_all(&table_dir).unwrap();
20334
20335 assert!(matches!(
20336 Database::open(directory.path()),
20337 Err(MongrelError::NotFound(_))
20338 ));
20339 assert!(!table_dir.exists());
20340 }
20341
20342 #[test]
20343 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
20344 let directory = tempfile::tempdir().unwrap();
20345 let database = std::sync::Arc::new(
20346 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
20347 );
20348 database.create_user("alice", "old-password").unwrap();
20349 let old_identity = database.user_identity("alice").unwrap();
20350 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
20351 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
20352 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
20353 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
20354
20355 std::thread::scope(|scope| {
20356 let authenticate = {
20357 let database = std::sync::Arc::clone(&database);
20358 scope.spawn(move || {
20359 database.authenticate_principal_inner("alice", "old-password", || {
20360 verified_tx.send(()).unwrap();
20361 resume_rx.recv().unwrap();
20362 })
20363 })
20364 };
20365 verified_rx.recv().unwrap();
20366 let mutate = {
20367 let database = std::sync::Arc::clone(&database);
20368 scope.spawn(move || {
20369 mutation_started_tx.send(()).unwrap();
20370 database.drop_user("alice").unwrap();
20371 database.create_user("alice", "new-password").unwrap();
20372 mutation_done_tx.send(()).unwrap();
20373 })
20374 };
20375 mutation_started_rx.recv().unwrap();
20376 assert!(mutation_done_rx
20377 .recv_timeout(std::time::Duration::from_millis(50))
20378 .is_err());
20379 resume_tx.send(()).unwrap();
20380 let principal = authenticate.join().unwrap().unwrap().unwrap();
20381 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
20382 mutate.join().unwrap();
20383 });
20384
20385 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
20386 assert!(database
20387 .authenticate_principal("alice", "old-password")
20388 .unwrap()
20389 .is_none());
20390 assert!(database
20391 .authenticate_principal("alice", "new-password")
20392 .unwrap()
20393 .is_some());
20394 }
20395
20396 #[test]
20397 fn homogeneous_batch_summarizes_to_one_permission_decision() {
20398 let staging = (0..10_050)
20399 .map(|_| {
20400 (
20401 7,
20402 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
20403 )
20404 })
20405 .collect::<Vec<_>>();
20406
20407 let needs = summarize_write_permissions(&staging);
20408 let table = needs.get(&7).unwrap();
20409 assert_eq!(needs.len(), 1);
20410 assert!(table.insert);
20411 assert_eq!(table.insert_columns, [1, 2]);
20412 assert!(!table.update);
20413 assert!(!table.delete);
20414 assert!(!table.truncate);
20415 }
20416
20417 #[test]
20418 fn mixed_writes_union_columns_and_preserve_empty_operations() {
20419 let staging = vec![
20420 (7, Staged::Put(vec![(2, Value::Int64(2))])),
20421 (7, Staged::Put(vec![(1, Value::Int64(1))])),
20422 (
20423 7,
20424 Staged::Update {
20425 row_id: RowId(1),
20426 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
20427 changed_columns: vec![2],
20428 },
20429 ),
20430 (7, Staged::Delete(RowId(2))),
20431 (8, Staged::Truncate),
20432 ];
20433
20434 let needs = summarize_write_permissions(&staging);
20435 let table = needs.get(&7).unwrap();
20436 assert_eq!(table.insert_columns, [1, 2]);
20437 assert!(table.update);
20438 assert_eq!(table.update_columns, [2]);
20439 assert!(table.delete);
20440 assert!(needs.get(&8).unwrap().truncate);
20441 }
20442
20443 #[test]
20444 fn final_permission_decisions_do_not_scale_with_rows() {
20445 let credentialless_dir = tempfile::tempdir().unwrap();
20446 let credentialless = Database::create(credentialless_dir.path()).unwrap();
20447 credentialless.create_table("docs", test_schema()).unwrap();
20448 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20449 credentialless
20450 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
20451 .unwrap();
20452 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
20453
20454 let authenticated_dir = tempfile::tempdir().unwrap();
20455 let authenticated =
20456 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
20457 .unwrap();
20458 authenticated.create_table("docs", test_schema()).unwrap();
20459 let admin = authenticated.resolve_principal("admin").unwrap();
20460 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20461 authenticated
20462 .validate_write_permissions(
20463 &puts(authenticated.table_id("docs").unwrap()),
20464 Some(&admin),
20465 None,
20466 )
20467 .unwrap();
20468 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20469 }
20470
20471 #[test]
20472 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
20473 let dir = tempfile::tempdir().unwrap();
20474 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20475 db.create_table("docs", test_schema()).unwrap();
20476 let admin = db.resolve_principal("admin").unwrap();
20477 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20478
20479 let mut transaction = db.begin_as(Some(admin));
20480 transaction
20481 .delete_batch("docs", (0..100).map(RowId).collect())
20482 .unwrap();
20483 transaction.commit().unwrap();
20484
20485 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
20486 }
20487
20488 #[test]
20489 fn truncate_validation_checks_admin_once_for_all_tables() {
20490 let dir = tempfile::tempdir().unwrap();
20491 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20492 db.create_table("first", test_schema()).unwrap();
20493 db.create_table("second", test_schema()).unwrap();
20494 let admin = db.resolve_principal("admin").unwrap();
20495 let staging = vec![
20496 (db.table_id("first").unwrap(), Staged::Truncate),
20497 (db.table_id("second").unwrap(), Staged::Truncate),
20498 ];
20499
20500 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20501 db.validate_write_permissions(&staging, Some(&admin), None)
20502 .unwrap();
20503 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20504 }
20505
20506 #[test]
20507 fn one_table_commit_batches_structural_work() {
20508 let dir = tempfile::tempdir().unwrap();
20509 let db = Database::create(dir.path()).unwrap();
20510 db.create_table("docs", test_schema()).unwrap();
20511 let table_id = db.table_id("docs").unwrap();
20512
20513 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
20514 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20515 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20516 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20517 db.transaction(|transaction| {
20518 for id in 0..100 {
20519 transaction.put("docs", vec![(1, Value::Int64(id))])?;
20520 }
20521 Ok(())
20522 })
20523 .unwrap();
20524
20525 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
20526 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20527 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20528 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20529
20530 let puts = crate::wal::SharedWal::replay(dir.path())
20531 .unwrap()
20532 .into_iter()
20533 .filter_map(|record| match record.op {
20534 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
20535 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
20536 .unwrap()
20537 .len(),
20538 ),
20539 _ => None,
20540 })
20541 .collect::<Vec<_>>();
20542 assert_eq!(puts, [100]);
20543
20544 let row_ids = db
20545 .table("docs")
20546 .unwrap()
20547 .lock()
20548 .visible_rows(db.snapshot().0)
20549 .unwrap()
20550 .into_iter()
20551 .take(2)
20552 .map(|row| row.row_id)
20553 .collect::<Vec<_>>();
20554 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20555 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20556 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20557 db.transaction(|transaction| {
20558 for row_id in row_ids {
20559 transaction.delete("docs", row_id)?;
20560 }
20561 Ok(())
20562 })
20563 .unwrap();
20564 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20565 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20566 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20567
20568 let deletes = crate::wal::SharedWal::replay(dir.path())
20569 .unwrap()
20570 .into_iter()
20571 .filter_map(|record| match record.op {
20572 crate::wal::Op::Delete {
20573 table_id: id,
20574 row_ids,
20575 } if id == table_id => Some(row_ids.len()),
20576 _ => None,
20577 })
20578 .collect::<Vec<_>>();
20579 assert_eq!(deletes, [2]);
20580 }
20581
20582 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
20583 (0..10_050)
20584 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
20585 .collect()
20586 }
20587
20588 fn test_schema() -> Schema {
20589 Schema {
20590 columns: vec![ColumnDef {
20591 id: 1,
20592 name: "id".into(),
20593 ty: TypeId::Int64,
20594 flags: crate::schema::ColumnFlags::empty()
20595 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20596 default_value: None,
20597 embedding_source: None,
20598 }],
20599 ..Schema::default()
20600 }
20601 }
20602}
20603
20604#[cfg(test)]
20605mod cdc_bounds_tests {
20606 use super::*;
20607
20608 #[test]
20609 fn retained_byte_limit_rejects_without_allocating_payload() {
20610 let mut retained = 0;
20611 let error = charge_cdc_bytes(
20612 &mut retained,
20613 CDC_MAX_RETAINED_BYTES.saturating_add(1),
20614 "CDC retained bytes",
20615 )
20616 .unwrap_err();
20617 assert!(matches!(
20618 error,
20619 MongrelError::ResourceLimitExceeded {
20620 resource: "CDC retained bytes",
20621 ..
20622 }
20623 ));
20624 }
20625
20626 #[test]
20627 fn row_json_estimate_accounts_for_byte_array_expansion() {
20628 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
20629 .with_column(1, Value::Bytes(vec![0; 1024]));
20630 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
20631 }
20632}
20633
20634#[cfg(test)]
20635mod generation_metrics_tests {
20636 use super::*;
20637 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20638
20639 #[test]
20640 fn legacy_cow_fallback_is_measured() {
20641 let dir = tempfile::tempdir().unwrap();
20642 let table = Table::create(
20643 dir.path(),
20644 Schema {
20645 columns: vec![ColumnDef {
20646 id: 1,
20647 name: "id".into(),
20648 ty: TypeId::Int64,
20649 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20650 default_value: None,
20651 embedding_source: None,
20652 }],
20653 ..Schema::default()
20654 },
20655 1,
20656 )
20657 .unwrap();
20658 let handle = TableHandle::from_table(table);
20659 let held = match &handle.inner {
20660 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
20661 TableHandleInner::Direct(_) => unreachable!(),
20662 };
20663
20664 handle.lock().set_sync_byte_threshold(1);
20665
20666 let stats = handle.generation_stats();
20667 assert_eq!(stats.cow_clone_count, 1);
20668 assert!(stats.estimated_cow_clone_bytes > 0);
20669 drop(held);
20670 }
20671}
20672
20673#[cfg(test)]
20674mod trigger_engine_tests {
20675 use super::*;
20676
20677 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
20678 WriteEvent {
20679 table: "test".into(),
20680 kind: TriggerEvent::Insert,
20681 new: Some(TriggerRowImage {
20682 columns: new_cells.iter().cloned().collect(),
20683 }),
20684 old: Some(TriggerRowImage {
20685 columns: old_cells.iter().cloned().collect(),
20686 }),
20687 changed_columns: Vec::new(),
20688 op_indices: Vec::new(),
20689 put_idx: None,
20690 trigger_stack: Vec::new(),
20691 }
20692 }
20693
20694 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
20695 WriteEvent {
20696 table: "test".into(),
20697 kind: TriggerEvent::Insert,
20698 new: Some(TriggerRowImage {
20699 columns: new_cells.iter().cloned().collect(),
20700 }),
20701 old: None,
20702 changed_columns: Vec::new(),
20703 op_indices: Vec::new(),
20704 put_idx: None,
20705 trigger_stack: Vec::new(),
20706 }
20707 }
20708
20709 #[test]
20710 fn value_order_int64_vs_float64() {
20711 assert_eq!(
20712 value_order(&Value::Int64(5), &Value::Float64(5.0)),
20713 Some(std::cmp::Ordering::Equal)
20714 );
20715 assert_eq!(
20716 value_order(&Value::Int64(5), &Value::Float64(3.0)),
20717 Some(std::cmp::Ordering::Greater)
20718 );
20719 assert_eq!(
20720 value_order(&Value::Int64(2), &Value::Float64(3.0)),
20721 Some(std::cmp::Ordering::Less)
20722 );
20723 }
20724
20725 #[test]
20726 fn value_order_null_returns_none() {
20727 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
20728 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
20729 assert_eq!(value_order(&Value::Null, &Value::Null), None);
20730 }
20731
20732 #[test]
20733 fn value_order_cross_group_returns_none() {
20734 assert_eq!(
20735 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
20736 None
20737 );
20738 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
20739 assert_eq!(
20740 value_order(
20741 &Value::Embedding(vec![1.0, 2.0]),
20742 &Value::Embedding(vec![1.0, 2.0])
20743 ),
20744 None
20745 );
20746 }
20747
20748 #[test]
20749 fn eval_trigger_expr_ranges_and_booleans() {
20750 let expr = TriggerExpr::And {
20751 left: Box::new(TriggerExpr::Gt {
20752 left: TriggerValue::NewColumn(1),
20753 right: TriggerValue::Literal(Value::Int64(0)),
20754 }),
20755 right: Box::new(TriggerExpr::Lte {
20756 left: TriggerValue::NewColumn(1),
20757 right: TriggerValue::Literal(Value::Int64(100)),
20758 }),
20759 };
20760 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
20761 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
20762 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
20763
20764 let or_expr = TriggerExpr::Or {
20765 left: Box::new(TriggerExpr::Lt {
20766 left: TriggerValue::NewColumn(1),
20767 right: TriggerValue::Literal(Value::Int64(0)),
20768 }),
20769 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
20770 TriggerValue::OldColumn(2),
20771 )))),
20772 };
20773 assert!(eval_trigger_expr(
20774 &or_expr,
20775 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
20776 )
20777 .unwrap());
20778 assert!(!eval_trigger_expr(
20779 &or_expr,
20780 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
20781 )
20782 .unwrap());
20783
20784 assert!(eval_trigger_expr(
20785 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
20786 &event_insert(&[])
20787 )
20788 .unwrap());
20789 assert!(!eval_trigger_expr(
20790 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
20791 &event_insert(&[])
20792 )
20793 .unwrap());
20794 assert!(!eval_trigger_expr(
20795 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
20796 &event_insert(&[])
20797 )
20798 .unwrap());
20799 }
20800}
20801
20802#[cfg(test)]
20803mod core_resource_tests {
20804 use super::*;
20805
20806 fn int_pk_schema() -> Schema {
20807 Schema {
20808 columns: vec![ColumnDef {
20809 id: 1,
20810 name: "id".into(),
20811 ty: TypeId::Int64,
20812 flags: crate::schema::ColumnFlags::empty()
20813 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20814 default_value: None,
20815 embedding_source: None,
20816 }],
20817 ..Schema::default()
20818 }
20819 }
20820
20821 #[test]
20822 fn open_constructs_governor_spill_and_jobs_with_documented_defaults() {
20823 let dir = tempfile::tempdir().unwrap();
20824 let db = Database::create(dir.path()).unwrap();
20825 assert_eq!(
20826 db.memory_governor().max_bytes(),
20827 DEFAULT_MEMORY_BUDGET_BYTES
20828 );
20829 assert_eq!(
20830 db.spill_manager().config().global_bytes,
20831 DEFAULT_TEMP_DISK_BUDGET_BYTES
20832 );
20833 assert!(db.job_registry().list().is_empty());
20834 assert_eq!(
20836 db.resource_groups().len(),
20837 crate::resource::WorkloadClass::ALL.len()
20838 );
20839 assert!(db.resource_groups().get("control").is_some());
20840 assert!(db.embedding_providers().list_ids().is_empty());
20841 let err = db
20843 .embedding_providers()
20844 .embed(
20845 &crate::embedding::EmbeddingSource::SuppliedByApplication,
20846 &["text"],
20847 4,
20848 )
20849 .unwrap_err();
20850 assert!(matches!(
20851 err,
20852 crate::embedding::EmbeddingError::SuppliedByApplication
20853 ));
20854 }
20855
20856 #[test]
20857 fn lock_rows_for_update_acquires_exclusive_row_locks() {
20858 use crate::locks::LockKey;
20859 use crate::rowid::RowId;
20860
20861 let dir = tempfile::tempdir().unwrap();
20862 let db = Database::create(dir.path()).unwrap();
20863 let txn_id = db.allocate_lock_txn_id().unwrap();
20864 let rid = RowId(42);
20865 db.lock_rows_for_update(txn_id, 7, &[rid], None).unwrap();
20866 assert!(db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20867 db.release_txn_locks(txn_id);
20868 assert!(!db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20869 }
20870
20871 #[test]
20872 fn open_with_options_sizes_the_core_budgets() {
20873 let dir = tempfile::tempdir().unwrap();
20874 let db = Database::create(dir.path()).unwrap();
20875 drop(db);
20876 let db = Database::open_with_options(
20877 dir.path(),
20878 OpenOptions::default()
20879 .with_memory_budget_bytes(256 * 1024 * 1024)
20880 .with_temp_disk_budget_bytes(16 * 1024 * 1024),
20881 )
20882 .unwrap();
20883 assert_eq!(db.memory_governor().max_bytes(), 256 * 1024 * 1024);
20884 assert_eq!(db.spill_manager().config().global_bytes, 16 * 1024 * 1024);
20885 }
20886
20887 #[test]
20888 fn zero_budgets_are_rejected() {
20889 let dir = tempfile::tempdir().unwrap();
20890 let db = Database::create(dir.path()).unwrap();
20891 drop(db);
20892 let result = Database::open_with_options(
20893 dir.path(),
20894 OpenOptions::default().with_memory_budget_bytes(0),
20895 );
20896 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20897 let result = Database::open_with_options(
20898 dir.path(),
20899 OpenOptions::default().with_temp_disk_budget_bytes(0),
20900 );
20901 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20902 }
20903
20904 #[test]
20905 fn page_caches_reserve_under_the_governor() {
20906 let dir = tempfile::tempdir().unwrap();
20907 let db = Database::create(dir.path()).unwrap();
20908 db.create_table("t", int_pk_schema()).unwrap();
20909 let mut txn = db.begin();
20910 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20911 txn.commit().unwrap();
20912 let stats = db.memory_governor().stats();
20916 assert_eq!(
20917 stats.usage_for(crate::memory::MemoryClass::PageCache),
20918 db.page_cache.used_bytes()
20919 );
20920 assert_eq!(
20921 stats.usage_for(crate::memory::MemoryClass::DecodedCache),
20922 db.decoded_cache.used_bytes()
20923 );
20924 assert_eq!(
20925 db.memory_governor().reclaimable_bytes(),
20926 db.page_cache.used_bytes() + db.decoded_cache.used_bytes()
20927 );
20928 let _ = db.memory_governor().evict_reclaimable(1024 * 1024);
20930 }
20931
20932 #[test]
20933 fn job_registry_persists_across_reopen() {
20934 let dir = tempfile::tempdir().unwrap();
20935 let db = Database::create(dir.path()).unwrap();
20936 db.create_table("t", int_pk_schema()).unwrap();
20937 let job_id = db
20938 .job_registry()
20939 .submit(
20940 crate::jobs::JobKind::IndexBuild,
20941 crate::jobs::JobTarget {
20942 table: "t".to_string(),
20943 index: Some("idx".to_string()),
20944 },
20945 )
20946 .unwrap();
20947 drop(db);
20948 let db = Database::open(dir.path()).unwrap();
20949 let record = db.job_registry().get(job_id).expect("job survives reopen");
20950 assert_eq!(record.state, crate::jobs::JobState::Pending);
20951 }
20952
20953 #[test]
20954 fn spill_manager_open_sweeps_stale_temp_tree() {
20955 let dir = tempfile::tempdir().unwrap();
20956 let db = Database::create(dir.path()).unwrap();
20957 let stale = dir.path().join("temp").join("spill").join("q-deadbeef");
20958 std::fs::create_dir_all(&stale).unwrap();
20959 std::fs::write(stale.join("chunk-0"), b"stale").unwrap();
20960 drop(db);
20961 let db = Database::open(dir.path()).unwrap();
20962 assert!(
20963 !stale.exists(),
20964 "the startup sweep removes stale spill files (S1E-004)"
20965 );
20966 let session = db
20968 .spill_manager()
20969 .begin_query(
20970 mongreldb_types::ids::QueryId::from_bytes([7u8; 16]),
20971 1024 * 1024,
20972 )
20973 .unwrap();
20974 assert_eq!(session.used(), 0);
20975 }
20976}
20977
20978#[cfg(test)]
20979mod version_pin_tests {
20980 use super::*;
20981
20982 fn int_pk_schema() -> Schema {
20983 Schema {
20984 columns: vec![ColumnDef {
20985 id: 1,
20986 name: "id".into(),
20987 ty: TypeId::Int64,
20988 flags: crate::schema::ColumnFlags::empty()
20989 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20990 default_value: None,
20991 embedding_source: None,
20992 }],
20993 ..Schema::default()
20994 }
20995 }
20996
20997 fn pins_for(
20998 report: &[TablePinsReport],
20999 table: &str,
21000 source: crate::retention::PinSource,
21001 ) -> Option<crate::retention::PinInfo> {
21002 report
21003 .iter()
21004 .find(|entry| entry.table == table)
21005 .and_then(|entry| entry.pins.get(source).cloned())
21006 }
21007
21008 #[test]
21009 fn backup_boundary_registers_backup_pitr_pin() {
21010 let source = tempfile::tempdir().unwrap();
21011 let destination_parent = tempfile::tempdir().unwrap();
21012 let destination = destination_parent.path().join("backup");
21013 let db = Arc::new(Database::create(source.path()).unwrap());
21014 db.create_table("t", int_pk_schema()).unwrap();
21015 let mut txn = db.begin();
21016 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
21017 let boundary_epoch = txn.commit().unwrap();
21018
21019 let hold = Arc::new(std::sync::Barrier::new(2));
21020 let resume = Arc::new(std::sync::Barrier::new(2));
21021 db.__set_backup_hook({
21022 let hold = Arc::clone(&hold);
21023 let resume = Arc::clone(&resume);
21024 move || {
21025 hold.wait();
21026 resume.wait();
21027 }
21028 });
21029
21030 let backup = {
21031 let db = Arc::clone(&db);
21032 let destination = destination.clone();
21033 std::thread::spawn(move || db.hot_backup(destination))
21034 };
21035 hold.wait();
21036 let report = db.version_pins_report();
21039 let pin = pins_for(&report, "t", crate::retention::PinSource::BackupPitr)
21040 .expect("backup boundary must register a BackupPitr pin");
21041 assert_eq!(pin.oldest_epoch, boundary_epoch);
21042 assert!(pin.pin_count >= 1);
21043 resume.wait();
21044 backup.join().unwrap().unwrap();
21045
21046 let report = db.version_pins_report();
21047 assert!(
21048 pins_for(&report, "t", crate::retention::PinSource::BackupPitr).is_none(),
21049 "the BackupPitr pin releases when the backup finishes"
21050 );
21051 }
21052
21053 #[test]
21054 fn snapshot_and_read_generation_pins_surface_in_report() {
21055 let dir = tempfile::tempdir().unwrap();
21056 let db = Database::create(dir.path()).unwrap();
21057 db.create_table("t", int_pk_schema()).unwrap();
21058 let mut txn = db.begin();
21059 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
21060 txn.commit().unwrap();
21061
21062 let (_snapshot, guard) = db.snapshot();
21063 let report = db.version_pins_report();
21064 assert!(
21065 pins_for(
21066 &report,
21067 "t",
21068 crate::retention::PinSource::TransactionSnapshot
21069 )
21070 .is_some(),
21071 "a database snapshot projects the TransactionSnapshot source"
21072 );
21073 drop(guard);
21074
21075 let handle = db.table("t").unwrap();
21076 let (generation, _snapshot) = handle.read_generation_with_context(None).unwrap();
21077 let report = db.version_pins_report();
21078 assert!(
21079 pins_for(&report, "t", crate::retention::PinSource::ReadGeneration).is_some(),
21080 "a cloned read generation registers a ReadGeneration pin"
21081 );
21082 drop(generation);
21083
21084 let report = db.version_pins_report();
21085 let entry = report.iter().find(|entry| entry.table == "t").unwrap();
21086 assert!(
21087 entry
21088 .pins
21089 .get(crate::retention::PinSource::BackupPitr)
21090 .is_none()
21091 && entry
21092 .pins
21093 .get(crate::retention::PinSource::Replication)
21094 .is_none()
21095 && entry
21096 .pins
21097 .get(crate::retention::PinSource::OnlineIndexBuild)
21098 .is_none(),
21099 "untaken sources stay absent from the report"
21100 );
21101 }
21102}
21103
21104#[cfg(test)]
21105mod lock_manager_tests {
21106 use super::*;
21107 use crate::locks::LockKey;
21108
21109 fn col(id: u16, name: &str, ty: TypeId, flags: crate::schema::ColumnFlags) -> ColumnDef {
21110 ColumnDef {
21111 id,
21112 name: name.into(),
21113 ty,
21114 flags,
21115 default_value: None,
21116 embedding_source: None,
21117 }
21118 }
21119
21120 fn unique_schema() -> Schema {
21121 let mut constraints = crate::constraint::TableConstraints::default();
21122 constraints
21123 .uniques
21124 .push(crate::constraint::UniqueConstraint {
21125 id: 1,
21126 name: "users_email_unique".into(),
21127 columns: vec![1],
21128 });
21129 Schema {
21130 columns: vec![
21131 col(
21132 0,
21133 "id",
21134 TypeId::Int64,
21135 crate::schema::ColumnFlags::empty()
21136 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21137 ),
21138 col(
21139 1,
21140 "email",
21141 TypeId::Bytes,
21142 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
21143 ),
21144 ],
21145 constraints,
21146 ..Schema::default()
21147 }
21148 }
21149
21150 fn parent_schema() -> Schema {
21151 Schema {
21152 columns: vec![col(
21153 0,
21154 "id",
21155 TypeId::Int64,
21156 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::PRIMARY_KEY),
21157 )],
21158 ..Schema::default()
21159 }
21160 }
21161
21162 fn child_schema() -> Schema {
21163 let mut constraints = crate::constraint::TableConstraints::default();
21164 constraints
21165 .foreign_keys
21166 .push(crate::constraint::ForeignKey {
21167 id: 1,
21168 name: "child_parent_fk".into(),
21169 columns: vec![1],
21170 ref_table: "parent".into(),
21171 ref_columns: vec![0],
21172 on_delete: crate::constraint::FkAction::Restrict,
21173 on_update: crate::constraint::FkAction::Restrict,
21174 });
21175 Schema {
21176 columns: vec![
21177 col(
21178 0,
21179 "id",
21180 TypeId::Int64,
21181 crate::schema::ColumnFlags::empty()
21182 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21183 ),
21184 col(
21185 1,
21186 "pid",
21187 TypeId::Int64,
21188 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
21189 ),
21190 ],
21191 constraints,
21192 ..Schema::default()
21193 }
21194 }
21195
21196 fn auto_inc_schema() -> Schema {
21197 Schema {
21198 columns: vec![col(
21199 0,
21200 "id",
21201 TypeId::Int64,
21202 crate::schema::ColumnFlags::empty()
21203 .with(crate::schema::ColumnFlags::PRIMARY_KEY)
21204 .with(crate::schema::ColumnFlags::AUTO_INCREMENT),
21205 )],
21206 ..Schema::default()
21207 }
21208 }
21209
21210 fn pk_lock_key(table_id: u64, value: i64) -> LockKey {
21211 let mut key = b"pk:".to_vec();
21212 key.extend_from_slice(&Value::Int64(value).encode_key());
21213 LockKey::key(table_id, key)
21214 }
21215
21216 #[test]
21217 fn unique_claims_serialize_concurrent_commits() {
21218 let dir = tempfile::tempdir().unwrap();
21219 let db = Arc::new(Database::create(dir.path()).unwrap());
21220 let table_id = db.create_table("users", unique_schema()).unwrap();
21221 let pk_key = pk_lock_key(table_id, 1);
21222 let entered = Arc::new(std::sync::Barrier::new(2));
21223 let resume = Arc::new(std::sync::Barrier::new(2));
21224 let parked = Arc::new(AtomicBool::new(false));
21225 db.__set_catalog_commit_hook({
21226 let entered = Arc::clone(&entered);
21227 let resume = Arc::clone(&resume);
21228 let parked = Arc::clone(&parked);
21229 move || {
21230 if !parked.swap(true, Ordering::SeqCst) {
21233 entered.wait();
21234 resume.wait();
21235 }
21236 }
21237 });
21238
21239 let mut txn_a = db.begin();
21240 txn_a
21241 .put(
21242 "users",
21243 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21244 )
21245 .unwrap();
21246 let a_id = txn_a.txn_id();
21247 let (a_tx, a_rx) = std::sync::mpsc::channel();
21248 let (b_tx, b_rx) = std::sync::mpsc::channel();
21249 std::thread::scope(|scope| {
21250 scope.spawn(|| {
21251 a_tx.send(txn_a.commit()).unwrap();
21252 });
21253 entered.wait();
21254 assert!(
21256 db.lock_manager().holds(a_id, &pk_key),
21257 "primary-key claim must be held until the commit ends"
21258 );
21259 let mut uq_key = format!("uq{}:", 1).into_bytes();
21260 let cells_map: HashMap<u16, Value> = [(1u16, Value::Bytes(b"a@x".to_vec()))]
21261 .into_iter()
21262 .collect();
21263 uq_key.extend_from_slice(
21264 &crate::constraint::encode_composite_key(&[1], &cells_map).unwrap(),
21265 );
21266 assert!(
21267 db.lock_manager()
21268 .holds(a_id, &LockKey::key(table_id, uq_key)),
21269 "declared-unique claim must be held until the commit ends"
21270 );
21271
21272 let mut txn_b = db.begin();
21273 txn_b
21274 .put(
21275 "users",
21276 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"b@x".to_vec()))],
21277 )
21278 .unwrap();
21279 scope.spawn(|| {
21280 b_tx.send(txn_b.commit()).unwrap();
21281 });
21282 std::thread::sleep(std::time::Duration::from_millis(100));
21283 assert!(
21284 b_rx.try_recv().is_err(),
21285 "the concurrent claim must block until A ends its transaction"
21286 );
21287 resume.wait();
21288 assert!(a_rx.recv().unwrap().is_ok());
21289 let b_result = b_rx.recv().unwrap();
21290 assert!(
21291 matches!(b_result, Err(MongrelError::Conflict(_))),
21292 "the loser surfaces a conflict after serializing: {b_result:?}"
21293 );
21294 });
21295 assert!(
21296 !db.lock_manager().holds(a_id, &pk_key),
21297 "no phantom holds remain after the commit"
21298 );
21299 }
21300
21301 #[test]
21302 fn ddl_waits_for_inflight_dml_commit_on_schema_barrier() {
21303 let dir = tempfile::tempdir().unwrap();
21304 let db = Arc::new(Database::create(dir.path()).unwrap());
21305 db.create_table("parent", parent_schema()).unwrap();
21306 db.create_table("child", child_schema()).unwrap();
21307 let mut seed = db.begin();
21308 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21309 seed.commit().unwrap();
21310
21311 let entered = Arc::new(std::sync::Barrier::new(2));
21312 let resume = Arc::new(std::sync::Barrier::new(2));
21313 db.__set_fk_lock_hook({
21314 let entered = Arc::clone(&entered);
21315 let resume = Arc::clone(&resume);
21316 move || {
21317 entered.wait();
21318 resume.wait();
21319 }
21320 });
21321
21322 let mut txn_a = db.begin();
21323 txn_a
21324 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(1))])
21325 .unwrap();
21326 let a_id = txn_a.txn_id();
21327 let (a_tx, a_rx) = std::sync::mpsc::channel();
21328 let (ddl_tx, ddl_rx) = std::sync::mpsc::channel();
21329 std::thread::scope(|scope| {
21330 scope.spawn(|| {
21331 a_tx.send(txn_a.commit()).unwrap();
21332 });
21333 entered.wait();
21334 assert!(
21336 db.lock_manager().holds(a_id, &LockKey::schema_barrier()),
21337 "DML holds the schema barrier Shared for its commit"
21338 );
21339 let db = Arc::clone(&db);
21340 scope.spawn(move || {
21341 ddl_tx.send(db.drop_table("parent")).unwrap();
21342 });
21343 std::thread::sleep(std::time::Duration::from_millis(100));
21344 assert!(
21345 ddl_rx.try_recv().is_err(),
21346 "DDL must wait on the Exclusive schema barrier while DML is in flight"
21347 );
21348 resume.wait();
21349 let a_result = a_rx.recv().unwrap();
21355 match &a_result {
21356 Ok(_) => {}
21357 Err(MongrelError::Conflict(message)) => {
21358 assert!(
21359 message.contains("security policy changed during write"),
21360 "unexpected commit conflict: {message}"
21361 );
21362 }
21363 other => panic!("unexpected commit outcome: {other:?}"),
21364 }
21365 assert!(ddl_rx.recv().unwrap().is_ok());
21366 });
21367 assert!(!db.lock_manager().holds(a_id, &LockKey::schema_barrier()));
21368 }
21369
21370 #[test]
21371 fn auto_increment_sequence_barrier_held_until_commit() {
21372 let dir = tempfile::tempdir().unwrap();
21373 let db = Database::create(dir.path()).unwrap();
21374 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
21375 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
21376 let entered = Arc::new(std::sync::Barrier::new(2));
21377 let resume = Arc::new(std::sync::Barrier::new(2));
21378 let parked = Arc::new(AtomicBool::new(false));
21379 db.__set_catalog_commit_hook({
21380 let entered = Arc::clone(&entered);
21381 let resume = Arc::clone(&resume);
21382 let parked = Arc::clone(&parked);
21383 move || {
21384 if !parked.swap(true, Ordering::SeqCst) {
21385 entered.wait();
21386 resume.wait();
21387 }
21388 }
21389 });
21390
21391 let mut txn_a = db.begin();
21392 txn_a.put("seq_t", vec![(0, Value::Null)]).unwrap();
21393 let a_id = txn_a.txn_id();
21394 assert!(
21396 db.lock_manager().holds(a_id, &barrier_key),
21397 "sequence allocation takes the barrier at stage time"
21398 );
21399 let (a_tx, a_rx) = std::sync::mpsc::channel();
21400 std::thread::scope(|scope| {
21401 scope.spawn(|| {
21402 a_tx.send(txn_a.commit()).unwrap();
21403 });
21404 entered.wait();
21405 assert!(
21406 db.lock_manager().holds(a_id, &barrier_key),
21407 "the barrier is held through the commit"
21408 );
21409 resume.wait();
21410 assert!(a_rx.recv().unwrap().is_ok());
21411 });
21412 assert!(
21413 !db.lock_manager().holds(a_id, &barrier_key),
21414 "the barrier releases when the commit ends"
21415 );
21416 }
21417
21418 #[test]
21419 fn fk_wait_for_cycle_surfaces_deadlock_victim() {
21420 let dir = tempfile::tempdir().unwrap();
21421 let db = Database::create(dir.path()).unwrap();
21422 db.create_table("parent", parent_schema()).unwrap();
21423 db.create_table("child", child_schema()).unwrap();
21424 let mut seed = db.begin();
21425 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21426 seed.put("parent", vec![(0, Value::Int64(2))]).unwrap();
21427 seed.commit().unwrap();
21428 let (rid1, rid2) = {
21429 let handle = db.table("parent").unwrap();
21430 let table = handle.lock();
21431 let rid = |pk: i64| {
21432 table
21433 .lookup_pk(&Value::Int64(pk).encode_key())
21434 .expect("seeded parent row")
21435 };
21436 (rid(1), rid(2))
21437 };
21438
21439 let rendezvous = Arc::new(std::sync::Barrier::new(2));
21444 let calls = Arc::new(AtomicUsize::new(0));
21445 db.__set_fk_lock_hook({
21446 let rendezvous = Arc::clone(&rendezvous);
21447 let calls = Arc::clone(&calls);
21448 move || {
21449 if calls.fetch_add(1, Ordering::SeqCst) < 2 {
21450 rendezvous.wait();
21451 }
21452 }
21453 });
21454
21455 let mut txn_a = db.begin();
21458 txn_a.delete("parent", rid1).unwrap();
21459 txn_a
21460 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(2))])
21461 .unwrap();
21462 let mut txn_b = db.begin();
21463 txn_b.delete("parent", rid2).unwrap();
21464 txn_b
21465 .put("child", vec![(0, Value::Int64(101)), (1, Value::Int64(1))])
21466 .unwrap();
21467 let b_id = txn_b.txn_id();
21468
21469 let (a_tx, a_rx) = std::sync::mpsc::channel();
21470 let (b_tx, b_rx) = std::sync::mpsc::channel();
21471 std::thread::scope(|scope| {
21472 scope.spawn(|| {
21473 a_tx.send(txn_a.commit()).unwrap();
21474 });
21475 scope.spawn(|| {
21476 b_tx.send(txn_b.commit()).unwrap();
21477 });
21478 let a_result = a_rx.recv().unwrap();
21479 let b_result = b_rx.recv().unwrap();
21480 assert!(
21481 a_result.is_ok(),
21482 "the survivor commits once the victim releases: {a_result:?}"
21483 );
21484 match b_result {
21485 Err(MongrelError::Deadlock { victim, .. }) => {
21486 assert_eq!(victim, b_id, "the youngest transaction is the victim");
21487 }
21488 other => panic!("the victim must surface a deadlock, got {other:?}"),
21489 }
21490 });
21491 let fk_key = |table: &str, pk: i64| {
21493 let table_id = db.table_id(table).unwrap();
21494 let mut key = b"fk:".to_vec();
21495 key.extend_from_slice(&Value::Int64(pk).encode_key());
21496 LockKey::key(table_id, key)
21497 };
21498 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 2)));
21499 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 1)));
21500 }
21501
21502 #[test]
21503 fn locks_release_after_commit_rollback_and_failed_commit() {
21504 let dir = tempfile::tempdir().unwrap();
21505 let db = Database::create(dir.path()).unwrap();
21506 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
21507 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
21508
21509 let mut txn = db.begin();
21511 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21512 let committed_id = txn.txn_id();
21513 txn.commit().unwrap();
21514 assert!(!db.lock_manager().holds(committed_id, &barrier_key));
21515
21516 let mut txn = db.begin();
21518 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21519 let rolled_back_id = txn.txn_id();
21520 assert!(db.lock_manager().holds(rolled_back_id, &barrier_key));
21521 txn.rollback();
21522 assert!(
21523 !db.lock_manager().holds(rolled_back_id, &barrier_key),
21524 "rollback must not leave phantom holds"
21525 );
21526
21527 db.create_table("users", unique_schema()).unwrap();
21530 let users_id = db.table_id("users").unwrap();
21531 let mut seed = db.begin();
21532 seed.put(
21533 "users",
21534 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21535 )
21536 .unwrap();
21537 seed.commit().unwrap();
21538 let mut txn = db.begin();
21539 txn.put(
21542 "users",
21543 vec![(0, Value::Int64(2)), (1, Value::Bytes(b"a@x".to_vec()))],
21544 )
21545 .unwrap();
21546 let failed_id = txn.txn_id();
21547 let result = txn.commit();
21548 assert!(matches!(result, Err(MongrelError::Conflict(_))));
21549 assert!(
21550 !db.lock_manager()
21551 .holds(failed_id, &pk_lock_key(users_id, 2)),
21552 "a failed commit must not leave phantom holds"
21553 );
21554 }
21555}
21556
21557#[cfg(test)]
21558mod lifecycle_tests {
21559 use super::*;
21560
21561 fn int_pk_schema() -> Schema {
21562 Schema {
21563 columns: vec![ColumnDef {
21564 id: 1,
21565 name: "id".into(),
21566 ty: TypeId::Int64,
21567 flags: crate::schema::ColumnFlags::empty()
21568 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21569 default_value: None,
21570 embedding_source: None,
21571 }],
21572 ..Schema::default()
21573 }
21574 }
21575
21576 #[test]
21577 fn poisoned_core_rejects_operations_with_typed_errors() {
21578 let dir = tempfile::tempdir().unwrap();
21579 let db = Database::create(dir.path()).unwrap();
21580 db.create_table("t", int_pk_schema()).unwrap();
21581 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Open);
21582
21583 db.poisoned.store(true, Ordering::Relaxed);
21588 db.lifecycle.poison();
21589 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Poisoned);
21590
21591 let error = db.gc().unwrap_err();
21594 assert!(
21595 matches!(error, MongrelError::Conflict(_)),
21596 "gc must reject on a poisoned core: {error:?}"
21597 );
21598 let error = db.compact().unwrap_err();
21599 assert!(
21600 matches!(error, MongrelError::Conflict(_)),
21601 "compact must reject on a poisoned core: {error:?}"
21602 );
21603 assert!(db.operation_guard().is_err());
21604 let error = db.create_table("t2", int_pk_schema()).unwrap_err();
21607 assert!(
21608 error.to_string().contains("database poisoned"),
21609 "the legacy poison error still wins where it existed: {error:?}"
21610 );
21611 let mut txn = db.begin();
21612 txn.put("t", vec![(1, Value::Int64(2))]).unwrap();
21613 assert!(txn
21614 .commit()
21615 .unwrap_err()
21616 .to_string()
21617 .contains("database poisoned"));
21618 }
21619
21620 #[test]
21621 fn shutdown_waits_for_operation_guards_to_drain() {
21622 let dir = tempfile::tempdir().unwrap();
21623 let db = Arc::new(Database::create(dir.path()).unwrap());
21624 db.create_table("t", int_pk_schema()).unwrap();
21625 let guard = db.operation_guard().unwrap();
21628 let (started_tx, started_rx) = std::sync::mpsc::channel();
21629 let (done_tx, done_rx) = std::sync::mpsc::channel();
21630 let shutdown_thread = std::thread::spawn(move || {
21631 started_tx.send(()).unwrap();
21632 let result = db.shutdown();
21633 let _ = done_tx.send(result);
21634 });
21635 started_rx.recv().unwrap();
21636 std::thread::sleep(std::time::Duration::from_millis(100));
21637 assert!(
21638 done_rx.try_recv().is_err(),
21639 "shutdown must wait for the outstanding guard to drain"
21640 );
21641 drop(guard);
21642 shutdown_thread.join().unwrap();
21643 assert!(
21644 done_rx.recv().unwrap().is_ok(),
21645 "shutdown completes once the guard drops"
21646 );
21647 }
21648}
21649
21650#[cfg(test)]
21651mod commit_ts_ledger_tests {
21652 use super::*;
21653 use crate::memtable::Row;
21654
21655 fn int_pk_schema() -> Schema {
21656 Schema {
21657 columns: vec![ColumnDef {
21658 id: 1,
21659 name: "id".into(),
21660 ty: TypeId::Int64,
21661 flags: crate::schema::ColumnFlags::empty()
21662 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21663 default_value: None,
21664 embedding_source: None,
21665 }],
21666 ..Schema::default()
21667 }
21668 }
21669
21670 fn commit_one(db: &Database) -> (Epoch, mongreldb_types::hlc::HlcTimestamp) {
21671 let mut txn = db.begin();
21672 let handle = txn.state_handle();
21673 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
21674 let epoch = txn.commit().unwrap();
21675 let crate::txn::TransactionState::Committed(receipt) = handle.state() else {
21676 panic!("expected Committed, got {:?}", handle.state());
21677 };
21678 (epoch, receipt.commit_ts)
21679 }
21680
21681 #[test]
21682 fn commit_ts_for_epoch_returns_the_exact_receipt_within_one_open() {
21683 let dir = tempfile::tempdir().unwrap();
21684 let db = Database::create(dir.path()).unwrap();
21685 db.create_table("t", int_pk_schema()).unwrap();
21686
21687 let (epoch, commit_ts) = commit_one(&db);
21688 assert_eq!(db.commit_ts_for_epoch(epoch), Some(commit_ts));
21689 assert_eq!(db.commit_ts_for_epoch(Epoch(epoch.0 + 100)), None);
21691 }
21692
21693 #[test]
21694 fn commit_ts_for_epoch_survives_reopen_with_the_physical_component() {
21695 let dir = tempfile::tempdir().unwrap();
21696 let (epoch, commit_ts) = {
21697 let db = Database::create(dir.path()).unwrap();
21698 db.create_table("t", int_pk_schema()).unwrap();
21699 commit_one(&db)
21700 };
21701
21702 let db = Database::open(dir.path()).unwrap();
21703 let reconstructed = db
21704 .commit_ts_for_epoch(epoch)
21705 .expect("the durable WAL CommitTimestamp ledger reconstructs the epoch");
21706 assert_eq!(reconstructed.physical_micros, commit_ts.physical_micros);
21707 assert_eq!(reconstructed.logical, 0);
21710 assert_eq!(reconstructed.node_tiebreaker, 0);
21711 }
21712
21713 #[test]
21714 fn recovery_ledger_keeps_only_newest_epochs_and_ignores_aborted_txns() {
21715 use crate::wal::Op;
21716 let records = vec![
21717 crate::wal::Record::new(Epoch(1), 7, Op::CommitTimestamp { unix_nanos: 1_000 }),
21718 crate::wal::Record::new(
21719 Epoch(2),
21720 7,
21721 Op::TxnCommit {
21722 epoch: 41,
21723 added_runs: vec![],
21724 },
21725 ),
21726 crate::wal::Record::new(
21728 Epoch(3),
21729 8,
21730 Op::TxnCommit {
21731 epoch: 42,
21732 added_runs: vec![],
21733 },
21734 ),
21735 crate::wal::Record::new(Epoch(4), 9, Op::CommitTimestamp { unix_nanos: 9_000 }),
21737 ];
21738 let ledger = commit_ts_ledger_from_recovery(&records);
21739 assert_eq!(ledger.len(), 1);
21740 assert_eq!(
21741 ledger.get(&41),
21742 Some(&mongreldb_types::hlc::HlcTimestamp {
21743 physical_micros: 1,
21744 logical: 0,
21745 node_tiebreaker: 0,
21746 })
21747 );
21748 }
21749
21750 #[test]
21751 fn new_writes_always_have_some_commit_ts() {
21752 let dir = tempfile::tempdir().unwrap();
21753 let db = Database::create(dir.path()).unwrap();
21754 db.create_table("t", int_pk_schema()).unwrap();
21755 let mut txn = db.begin();
21756 let state = txn.state_handle();
21757 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
21758 let epoch = txn.commit().unwrap();
21759 let crate::txn::TransactionState::Committed(receipt) = state.state() else {
21760 panic!("expected Committed receipt");
21761 };
21762 let handle = db.table("t").unwrap();
21763 let table = handle.lock();
21764 let (snap, _g) = db.snapshot();
21767 let rows = table.visible_rows(snap).expect("visible rows");
21768 assert_eq!(
21769 rows.len(),
21770 1,
21771 "committed put must be visible under HLC snapshot"
21772 );
21773 assert_eq!(rows[0].commit_ts, Some(receipt.commit_ts));
21774 assert_eq!(db.commit_ts_for_epoch(epoch), Some(receipt.commit_ts));
21775 }
21776
21777 #[test]
21778 fn same_transaction_identical_hlc_on_apply() {
21779 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21780
21781 let dir = tempfile::tempdir().unwrap();
21782 let db = Database::create_cluster_replica(
21783 dir.path(),
21784 ClusterId::from_bytes([1; 16]),
21785 NodeId::from_bytes([2; 16]),
21786 DatabaseId::from_bytes([3; 16]),
21787 )
21788 .unwrap();
21789 db.apply_replicated_catalog_command(&crate::catalog_cmds::CatalogCommandRecord {
21790 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21791 catalog_version: 1,
21792 command: crate::catalog_cmds::CatalogCommand::CreateTable {
21793 name: "t".into(),
21794 schema: int_pk_schema(),
21795 created_epoch: 1,
21796 },
21797 })
21798 .unwrap();
21799 let table_id = db.table_id("t").unwrap();
21800 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
21801 physical_micros: 42_000,
21802 logical: 3,
21803 node_tiebreaker: 9,
21804 };
21805 let staged = vec![StagedTxnWrite::Put {
21808 table_id,
21809 rows: bincode::serialize(&vec![
21810 Row::new(crate::RowId(1), Epoch(0)).with_column(1, Value::Int64(1))
21811 ])
21812 .unwrap(),
21813 }
21814 .encode()
21815 .unwrap()];
21816 assert!(db
21817 .apply_committed_transaction(1 << 63, commit_ts, &staged)
21818 .unwrap());
21819 let staged2 = vec![StagedTxnWrite::Put {
21820 table_id,
21821 rows: bincode::serialize(&vec![
21822 Row::new(crate::RowId(2), Epoch(0)).with_column(1, Value::Int64(2))
21823 ])
21824 .unwrap(),
21825 }
21826 .encode()
21827 .unwrap()];
21828 assert!(db
21829 .apply_committed_transaction((1 << 63) + 1, commit_ts, &staged2)
21830 .unwrap());
21831 let handle = db.table("t").unwrap();
21832 let table = handle.lock();
21833 let row1 = table
21834 .get(crate::RowId(1), Snapshot::unbounded())
21835 .expect("applied row 1");
21836 let row2 = table
21837 .get(crate::RowId(2), Snapshot::unbounded())
21838 .expect("applied row 2");
21839 let recovery_form = mongreldb_types::hlc::HlcTimestamp {
21844 physical_micros: commit_ts.physical_micros,
21845 logical: 0,
21846 node_tiebreaker: 0,
21847 };
21848 assert_eq!(row1.commit_ts, Some(recovery_form));
21849 assert_eq!(row2.commit_ts, Some(recovery_form));
21850 assert_eq!(row1.commit_ts, row2.commit_ts);
21851 drop(table);
21852 let epoch = db.visible_epoch();
21855 assert_eq!(
21856 db.commit_ts_for_epoch(epoch).map(|ts| ts.physical_micros),
21857 Some(commit_ts.physical_micros)
21858 );
21859 }
21860
21861 #[test]
21862 fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
21863 use mongreldb_types::hlc::HlcTimestamp;
21864 let early = HlcTimestamp {
21865 physical_micros: 100,
21866 logical: 0,
21867 node_tiebreaker: 1,
21868 };
21869 let late = HlcTimestamp {
21870 physical_micros: 200,
21871 logical: 0,
21872 node_tiebreaker: 1,
21873 };
21874 let snap = Snapshot::at_hlc(Epoch(99), early);
21876 assert!(!snap.observes_version(Epoch(1), Some(late)));
21877 assert!(snap.observes_version(Epoch(1), Some(early)));
21878 let dir = tempfile::tempdir().unwrap();
21880 let db = Database::create(dir.path()).unwrap();
21881 let (snap, _g) = db.snapshot();
21882 assert_ne!(
21883 snap.commit_ts,
21884 HlcTimestamp::ZERO,
21885 "Database::snapshot must pin live HLC via at_hlc"
21886 );
21887 }
21888
21889 #[test]
21890 fn hlc_stamped_row_visible_at_hlc_snapshot_not_epoch_only() {
21891 let dir = tempfile::tempdir().unwrap();
21892 let db = Database::create(dir.path()).unwrap();
21893 db.create_table("t", int_pk_schema()).unwrap();
21894 let (_epoch, commit_ts) = commit_one(&db);
21895 assert_ne!(commit_ts, mongreldb_types::hlc::HlcTimestamp::ZERO);
21896
21897 let handle = db.table("t").unwrap();
21898 let table = handle.lock();
21899 let hlc_snap = Snapshot::at_hlc(Epoch(u64::MAX), commit_ts);
21901 let rows = table.visible_rows(hlc_snap).expect("visible");
21902 assert_eq!(rows.len(), 1);
21903 assert_eq!(rows[0].commit_ts, Some(commit_ts));
21904 assert!(table.get(rows[0].row_id, hlc_snap).is_some());
21905
21906 let legacy = Snapshot::at(Epoch(u64::MAX));
21908 assert!(!legacy.uses_hlc_authority());
21909 assert_eq!(
21910 table.visible_rows(legacy).expect("visible").len(),
21911 1,
21912 "epoch pin sees HLC-stamped rows by epoch during dual-model migration"
21913 );
21914 assert!(table.get(rows[0].row_id, legacy).is_some());
21915 }
21916
21917 #[test]
21918 fn hlc_gc_floor_reports_named_sources() {
21919 let dir = tempfile::tempdir().unwrap();
21920 let db = Database::create(dir.path()).unwrap();
21921 db.create_table("t", int_pk_schema()).unwrap();
21922 let (epoch, commit_ts) = commit_one(&db);
21923
21924 let empty = db.hlc_gc_floor();
21926 assert_eq!(empty.floor(), mongreldb_types::hlc::HlcTimestamp::ZERO);
21927 assert_eq!(empty.sources().len(), 6);
21928
21929 let (_snap, guard) = db.snapshot();
21931 let with_pin = db.hlc_gc_floor();
21932 let projected = db.commit_ts_for_epoch(epoch);
21934 if let Some(ts) = projected {
21935 assert_eq!(with_pin.transaction_snapshot, ts);
21937 assert_eq!(with_pin.floor(), ts);
21938 assert_eq!(ts.physical_micros, commit_ts.physical_micros);
21939 } else {
21940 assert_eq!(
21941 with_pin.transaction_snapshot,
21942 mongreldb_types::hlc::HlcTimestamp::ZERO
21943 );
21944 }
21945 drop(guard);
21946 }
21947}
21948
21949#[cfg(test)]
21950mod stage2e_storage_mode_tests {
21951 use super::*;
21952 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21953 use crate::storage_mode::{StorageMode, STORAGE_MODE_FILENAME};
21954 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21955
21956 fn identity(seed: u8) -> (ClusterId, NodeId, DatabaseId) {
21957 (
21958 ClusterId::from_bytes([seed; 16]),
21959 NodeId::from_bytes([seed + 1; 16]),
21960 DatabaseId::from_bytes([seed + 2; 16]),
21961 )
21962 }
21963
21964 fn marker(root: &Path) -> Option<StorageMode> {
21965 let durable = crate::durable_file::DurableRoot::open(root).unwrap();
21966 crate::storage_mode::read(&durable).unwrap()
21967 }
21968
21969 fn simple_schema() -> Schema {
21970 Schema {
21971 columns: vec![ColumnDef {
21972 id: 1,
21973 name: "id".into(),
21974 ty: TypeId::Int64,
21975 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21976 default_value: None,
21977 embedding_source: None,
21978 }],
21979 ..Schema::default()
21980 }
21981 }
21982
21983 #[test]
21984 fn standalone_create_writes_marker_and_reopens() {
21985 let dir = tempfile::tempdir().unwrap();
21986 let root = dir.path().join("db");
21987 let db = Database::create(&root).unwrap();
21988 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21989 assert_eq!(db.storage_mode().unwrap(), Some(StorageMode::Standalone));
21990 drop(db);
21991 let db = Database::open(&root).unwrap();
21992 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21993 drop(db);
21994 }
21995
21996 #[test]
21997 fn legacy_database_without_marker_opens_and_gains_marker() {
21998 let dir = tempfile::tempdir().unwrap();
21999 let root = dir.path().join("db");
22000 let db = Database::create(&root).unwrap();
22001 drop(db);
22002 std::fs::remove_file(root.join(META_DIR).join(STORAGE_MODE_FILENAME)).unwrap();
22004 assert_eq!(marker(&root), None);
22005 let db = Database::open(&root).unwrap();
22006 assert_eq!(marker(&root), Some(StorageMode::Standalone));
22007 drop(db);
22008 }
22009
22010 #[test]
22011 fn server_owned_standalone_opens_embedded() {
22012 let dir = tempfile::tempdir().unwrap();
22013 let root = dir.path().join("db");
22014 let db = Database::create(&root).unwrap();
22015 drop(db);
22016 let durable = crate::durable_file::DurableRoot::open(&root).unwrap();
22017 crate::storage_mode::rewrite(&durable, &StorageMode::ServerOwnedStandalone).unwrap();
22018 let db = Database::open(&root).unwrap();
22019 assert_eq!(marker(&root), Some(StorageMode::ServerOwnedStandalone));
22020 drop(db);
22021 }
22022
22023 #[test]
22024 fn cluster_replica_is_rejected_by_normal_opens() {
22025 let dir = tempfile::tempdir().unwrap();
22026 let root = dir.path().join("db");
22027 let (cluster_id, node_id, database_id) = identity(10);
22028 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
22029 assert_eq!(
22030 marker(&root),
22031 Some(StorageMode::ClusterReplica {
22032 cluster_id,
22033 node_id,
22034 database_id,
22035 })
22036 );
22037 drop(db);
22038
22039 let error = Database::open(&root).unwrap_err();
22040 let message = error.to_string();
22041 assert!(
22042 matches!(error, MongrelError::InvalidArgument(_)),
22043 "unexpected error: {message}"
22044 );
22045 assert!(message.contains("cluster node runtime"), "{message}");
22046 assert!(message.contains(&cluster_id.to_hex()), "{message}");
22047 assert!(message.contains(&node_id.to_hex()), "{message}");
22048 assert!(message.contains(&database_id.to_hex()), "{message}");
22049
22050 let error = Database::open_with_options(&root, OpenOptions::default()).unwrap_err();
22051 assert!(error.to_string().contains("cluster node runtime"));
22052 assert_eq!(
22054 marker(&root),
22055 Some(StorageMode::ClusterReplica {
22056 cluster_id,
22057 node_id,
22058 database_id,
22059 })
22060 );
22061 }
22062
22063 #[test]
22064 fn offline_validation_opens_cluster_replica_read_only() {
22065 let dir = tempfile::tempdir().unwrap();
22066 let root = dir.path().join("db");
22067 let (cluster_id, node_id, database_id) = identity(20);
22068 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
22069 drop(db);
22070
22071 let options = OpenOptions::default().with_offline_validation(true);
22072 let db = Database::open_with_options(&root, options).unwrap();
22073 assert!(db.is_read_only_replica());
22074 let error = db.create_table("t", simple_schema()).unwrap_err();
22075 assert!(matches!(error, MongrelError::ReadOnlyReplica));
22076 drop(db);
22077 assert_eq!(
22079 marker(&root),
22080 Some(StorageMode::ClusterReplica {
22081 cluster_id,
22082 node_id,
22083 database_id,
22084 })
22085 );
22086 }
22087
22088 #[test]
22089 fn cluster_runtime_open_requires_exact_identity() {
22090 let dir = tempfile::tempdir().unwrap();
22091 let root = dir.path().join("db");
22092 let (cluster_id, node_id, database_id) = identity(30);
22093 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
22094 drop(db);
22095
22096 let error = Database::open_cluster_replica(&root, &StorageMode::Standalone).unwrap_err();
22098 assert!(matches!(error, MongrelError::InvalidArgument(_)));
22099 let wrong = StorageMode::ClusterReplica {
22101 cluster_id,
22102 node_id,
22103 database_id: DatabaseId::from_bytes([99; 16]),
22104 };
22105 let error = Database::open_cluster_replica(&root, &wrong).unwrap_err();
22106 assert!(error.to_string().contains("identity mismatch"), "{error}");
22107 let legacy = dir.path().join("legacy");
22109 let legacy_db = Database::create(&legacy).unwrap();
22110 drop(legacy_db);
22111 let expected = StorageMode::ClusterReplica {
22112 cluster_id,
22113 node_id,
22114 database_id,
22115 };
22116 let error = Database::open_cluster_replica(&legacy, &expected).unwrap_err();
22117 assert!(error.to_string().contains("identity mismatch"), "{error}");
22118
22119 let db = Database::open_cluster_replica(&root, &expected).unwrap();
22122 assert!(db.is_read_only_replica());
22123 let error = db.create_table("t", simple_schema()).unwrap_err();
22124 assert!(matches!(error, MongrelError::ReadOnlyReplica));
22125 drop(db);
22126 }
22127}
22128
22129#[cfg(test)]
22130mod stage2e_replicated_apply_tests {
22131 use super::*;
22132 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord, CatalogDelta};
22133 use crate::memtable::{Row, Value};
22134 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
22135 use crate::wal::{Op, Record};
22136 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
22137 use std::sync::Arc;
22138
22139 fn ids() -> (ClusterId, NodeId, DatabaseId) {
22140 (
22141 ClusterId::from_bytes([1; 16]),
22142 NodeId::from_bytes([2; 16]),
22143 DatabaseId::from_bytes([3; 16]),
22144 )
22145 }
22146
22147 fn expected_mode() -> crate::storage_mode::StorageMode {
22148 let (cluster_id, node_id, database_id) = ids();
22149 crate::storage_mode::StorageMode::ClusterReplica {
22150 cluster_id,
22151 node_id,
22152 database_id,
22153 }
22154 }
22155
22156 fn simple_schema() -> Schema {
22157 Schema {
22158 columns: vec![ColumnDef {
22159 id: 1,
22160 name: "id".into(),
22161 ty: TypeId::Int64,
22162 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
22163 default_value: None,
22164 embedding_source: None,
22165 }],
22166 ..Schema::default()
22167 }
22168 }
22169
22170 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
22171 CatalogCommandRecord {
22172 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
22173 catalog_version,
22174 command: CatalogCommand::CreateTable {
22175 name: name.to_string(),
22176 schema: simple_schema(),
22177 created_epoch: 1,
22178 },
22179 }
22180 }
22181
22182 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
22183 let rows: Vec<Row> = values
22184 .iter()
22185 .map(|value| {
22186 Row::new(crate::RowId(*value as u64), Epoch(epoch))
22189 .with_column(1, Value::Int64(*value))
22190 })
22191 .collect();
22192 vec![
22193 Record::new(
22194 Epoch(0),
22195 txn_id,
22196 Op::Put {
22197 table_id,
22198 rows: bincode::serialize(&rows).unwrap(),
22199 },
22200 ),
22201 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
22202 Record::new(
22203 Epoch(0),
22204 txn_id,
22205 Op::TxnCommit {
22206 epoch,
22207 added_runs: Vec::new(),
22208 },
22209 ),
22210 ]
22211 }
22212
22213 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22214 let handle = db.table(table).unwrap();
22215 let snap = db.visible_snapshot();
22217 let rows = handle.lock().visible_rows(snap).unwrap();
22218 let mut values: Vec<i64> = rows
22219 .iter()
22220 .map(|row| match row.columns.get(&1) {
22221 Some(Value::Int64(value)) => *value,
22222 other => panic!("unexpected column: {other:?}"),
22223 })
22224 .collect();
22225 values.sort_unstable();
22226 values
22227 }
22228
22229 #[test]
22230 fn catalog_command_mounts_table_and_replays_as_noop() {
22231 let dir = tempfile::tempdir().unwrap();
22232 let (cluster_id, node_id, database_id) = ids();
22233 let db =
22234 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22235
22236 let record = create_table_record("items", 1);
22237 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22238 assert!(matches!(delta, CatalogDelta::TableCreated { .. }));
22239 assert_eq!(db.table_names(), vec!["items".to_string()]);
22240 assert_eq!(db.catalog_version(), 1);
22241
22242 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22244 assert!(matches!(delta, CatalogDelta::NoOp));
22245 assert_eq!(db.table_names().len(), 1);
22246 drop(db);
22247
22248 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22250 assert_eq!(db.table_names(), vec!["items".to_string()]);
22251 assert_eq!(db.catalog_version(), 1);
22252 }
22253
22254 #[test]
22255 fn records_apply_rows_and_skip_replays_across_restart() {
22256 let dir = tempfile::tempdir().unwrap();
22257 let (cluster_id, node_id, database_id) = ids();
22258 let db =
22259 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22260 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22261 .unwrap();
22262
22263 let records = put_records(1, 0, 2, &[10, 20, 30]);
22264 assert!(db.apply_replicated_records(&records).unwrap());
22265 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22266 assert_eq!(db.visible_epoch(), Epoch(2));
22267
22268 assert!(!db.apply_replicated_records(&records).unwrap());
22271 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22272
22273 let later = put_records(2, 0, 3, &[40]);
22275 assert!(db.apply_replicated_records(&later).unwrap());
22276 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22277 let db = Arc::new(db);
22278 db.shutdown().unwrap();
22279
22280 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22283 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22284 assert!(!db.apply_replicated_records(&later).unwrap());
22285 assert!(!db.apply_replicated_records(&records).unwrap());
22286 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22287 }
22288
22289 #[test]
22290 fn spilled_run_commits_fail_closed_this_wave() {
22291 let dir = tempfile::tempdir().unwrap();
22292 let (cluster_id, node_id, database_id) = ids();
22293 let db =
22294 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22295 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22296 .unwrap();
22297 let mut records = put_records(1, 0, 2, &[10]);
22298 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22299 panic!("put_records ends in TxnCommit");
22300 };
22301 added_runs.push(crate::wal::AddedRun {
22302 table_id: 0,
22303 run_id: 7,
22304 row_count: 1,
22305 level: 0,
22306 min_row_id: 1,
22307 max_row_id: 1,
22308 content_hash: [0; 32],
22309 });
22310 let error = db.apply_replicated_records(&records).unwrap_err();
22311 assert!(
22312 error.to_string().contains("spilled-run"),
22313 "unexpected error: {error}"
22314 );
22315 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22316 }
22317
22318 #[test]
22319 fn records_without_commit_marker_fail_closed() {
22320 let dir = tempfile::tempdir().unwrap();
22321 let (cluster_id, node_id, database_id) = ids();
22322 let db =
22323 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22324 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22325 .unwrap();
22326 let mut records = put_records(1, 0, 2, &[10]);
22327 records.pop(); let error = db.apply_replicated_records(&records).unwrap_err();
22329 assert!(matches!(error, MongrelError::InvalidArgument(_)));
22330 assert!(db.apply_replicated_records(&[]).is_err());
22331 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22332 }
22333}
22334
22335#[cfg(test)]
22336mod stage2c_spill_translation_tests {
22337 use super::*;
22338 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord};
22339 use crate::memtable::{Row, Value};
22340 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
22341 use crate::wal::{Op, Record};
22342 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
22343
22344 fn simple_schema() -> Schema {
22345 Schema {
22346 columns: vec![ColumnDef {
22347 id: 1,
22348 name: "id".into(),
22349 ty: TypeId::Int64,
22350 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
22351 default_value: None,
22352 embedding_source: None,
22353 }],
22354 ..Schema::default()
22355 }
22356 }
22357
22358 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
22359 CatalogCommandRecord {
22360 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
22361 catalog_version,
22362 command: CatalogCommand::CreateTable {
22363 name: name.to_string(),
22364 schema: simple_schema(),
22365 created_epoch: 1,
22366 },
22367 }
22368 }
22369
22370 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22371 let handle = db.table(table).unwrap();
22372 let snap = db.visible_snapshot();
22374 let rows = handle.lock().visible_rows(snap).unwrap();
22375 let mut values: Vec<i64> = rows
22376 .iter()
22377 .map(|row| match row.columns.get(&1) {
22378 Some(Value::Int64(value)) => *value,
22379 other => panic!("unexpected column: {other:?}"),
22380 })
22381 .collect();
22382 values.sort_unstable();
22383 values
22384 }
22385
22386 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
22387 let rows: Vec<Row> = values
22388 .iter()
22389 .map(|value| {
22390 Row::new(crate::RowId(*value as u64), Epoch(epoch))
22391 .with_column(1, Value::Int64(*value))
22392 })
22393 .collect();
22394 vec![
22395 Record::new(
22396 Epoch(0),
22397 txn_id,
22398 Op::Put {
22399 table_id,
22400 rows: bincode::serialize(&rows).unwrap(),
22401 },
22402 ),
22403 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
22404 Record::new(
22405 Epoch(0),
22406 txn_id,
22407 Op::TxnCommit {
22408 epoch,
22409 added_runs: Vec::new(),
22410 },
22411 ),
22412 ]
22413 }
22414
22415 fn added_run(
22416 table_id: u64,
22417 row_count: u64,
22418 min_row_id: u64,
22419 max_row_id: u64,
22420 ) -> crate::wal::AddedRun {
22421 crate::wal::AddedRun {
22422 table_id,
22423 run_id: 7,
22424 row_count,
22425 level: 0,
22426 min_row_id,
22427 max_row_id,
22428 content_hash: [0; 32],
22429 }
22430 }
22431
22432 fn spilled_commit_records(db: &Database) -> Vec<Record> {
22435 let records = crate::wal::SharedWal::replay_with_dek(&db.root, None).unwrap();
22436 let txn_id = records
22437 .iter()
22438 .find_map(|record| match &record.op {
22439 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty() => Some(record.txn_id),
22440 _ => None,
22441 })
22442 .expect("a spilled commit is present in the WAL");
22443 records
22444 .into_iter()
22445 .filter(|record| record.txn_id == txn_id)
22446 .collect()
22447 }
22448
22449 #[test]
22450 fn non_spilled_records_translate_byte_identical() {
22451 let records = put_records(1, 0, 2, &[10, 20, 30]);
22452 let translated = translate_records_for_replication(&records).unwrap();
22453 assert_eq!(
22454 bincode::serialize(&translated).unwrap(),
22455 bincode::serialize(&records).unwrap(),
22456 "a commit without spill links must pass through byte-identical"
22457 );
22458 }
22459
22460 #[test]
22461 fn translation_rejects_uncovered_or_malformed_spills() {
22462 let mut records = put_records(1, 0, 2, &[10]);
22464 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22465 panic!("put_records ends in TxnCommit");
22466 };
22467 added_runs.push(added_run(0, 1, 10, 10));
22468 let error = translate_records_for_replication(&records).unwrap_err();
22469 assert!(
22470 error.to_string().contains("no logical row records"),
22471 "unexpected error: {error}"
22472 );
22473
22474 let mut records = put_records(1, 0, 2, &[10]);
22476 let spilled: Vec<Row> = (0..3_u64)
22477 .map(|value| {
22478 Row::new(crate::RowId(value), Epoch(2)).with_column(1, Value::Int64(value as i64))
22479 })
22480 .collect();
22481 records.insert(
22482 0,
22483 Record::new(
22484 Epoch(0),
22485 1,
22486 Op::SpilledRows {
22487 table_id: 0,
22488 rows: bincode::serialize(&spilled).unwrap(),
22489 },
22490 ),
22491 );
22492 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22493 panic!("put_records ends in TxnCommit");
22494 };
22495 added_runs.push(added_run(0, 4, 0, 3));
22496 let error = translate_records_for_replication(&records).unwrap_err();
22497 assert!(
22498 error.to_string().contains("cover 3 rows"),
22499 "unexpected error: {error}"
22500 );
22501
22502 let mut records = put_records(1, 0, 2, &[10]);
22504 records.insert(
22505 0,
22506 Record::new(
22507 Epoch(0),
22508 1,
22509 Op::SpilledRows {
22510 table_id: 0,
22511 rows: vec![0xFF, 0x01, 0x02],
22512 },
22513 ),
22514 );
22515 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22516 panic!("put_records ends in TxnCommit");
22517 };
22518 added_runs.push(added_run(0, 1, 0, 0));
22519 assert!(translate_records_for_replication(&records).is_err());
22520
22521 assert!(translate_records_for_replication(&[]).is_err());
22523 let mut mixed = put_records(1, 0, 2, &[10]);
22524 mixed[0].txn_id = 99;
22525 assert!(translate_records_for_replication(&mixed).is_err());
22526 let mut no_commit = put_records(1, 0, 2, &[10]);
22527 no_commit.pop();
22528 assert!(translate_records_for_replication(&no_commit).is_err());
22529 }
22530
22531 #[test]
22532 fn spilled_commit_translates_to_logical_rows_and_applies_on_replica() {
22533 let leader_dir = tempfile::tempdir().unwrap();
22535 let leader = Database::create(leader_dir.path()).unwrap();
22536 leader.create_table("t", simple_schema()).unwrap();
22537 leader.set_spill_threshold(1);
22538 let table_id = leader.table_id("t").unwrap();
22539 let values: Vec<i64> = (0..60).collect();
22540 leader
22541 .transaction(|txn| {
22542 for value in &values {
22543 txn.put("t", vec![(1, Value::Int64(*value))])?;
22544 }
22545 Ok(())
22546 })
22547 .unwrap();
22548 assert_eq!(visible_ids(&leader, "t"), values);
22549
22550 let records = spilled_commit_records(&leader);
22553 assert!(records
22554 .iter()
22555 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22556 let Some(Op::TxnCommit { added_runs, epoch }) = records.last().map(|r| &r.op) else {
22557 panic!("a commit sequence ends in TxnCommit");
22558 };
22559 assert!(!added_runs.is_empty());
22560 let commit_epoch = *epoch;
22561
22562 let translated = translate_records_for_replication(&records).unwrap();
22565 assert!(records
22566 .iter()
22567 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22568 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
22569 panic!("a commit sequence ends in TxnCommit");
22570 };
22571 assert!(!added_runs.is_empty(), "input records must be unchanged");
22572 assert_eq!(translated.len(), records.len());
22573 assert!(translated
22574 .iter()
22575 .all(|record| !matches!(record.op, Op::SpilledRows { .. })));
22576 assert!(translated
22577 .iter()
22578 .any(|record| matches!(record.op, Op::Put { .. })));
22579 let Some(Op::TxnCommit { added_runs, epoch }) = translated.last().map(|r| &r.op) else {
22580 panic!("a commit sequence ends in TxnCommit");
22581 };
22582 assert!(added_runs.is_empty(), "no added_runs may reach a replica");
22583 assert_eq!(*epoch, commit_epoch);
22584
22585 let replica_dir = tempfile::tempdir().unwrap();
22587 let replica = Database::create_cluster_replica(
22588 replica_dir.path(),
22589 ClusterId::from_bytes([1; 16]),
22590 NodeId::from_bytes([2; 16]),
22591 DatabaseId::from_bytes([3; 16]),
22592 )
22593 .unwrap();
22594 replica
22595 .apply_replicated_catalog_command(&create_table_record("t", 1))
22596 .unwrap();
22597 assert_eq!(replica.table_id("t").unwrap(), table_id);
22598 assert!(replica.apply_replicated_records(&translated).unwrap());
22599 assert_eq!(visible_ids(&replica, "t"), values);
22600 assert_eq!(visible_ids(&replica, "t"), visible_ids(&leader, "t"));
22601
22602 drop(leader);
22605 let leader = Database::open(leader_dir.path()).unwrap();
22606 assert_eq!(visible_ids(&leader, "t"), values);
22607 }
22608
22609 #[test]
22610 fn staged_txn_writes_validate_and_apply() {
22611 let dir = tempfile::tempdir().unwrap();
22612 let db = Database::create_cluster_replica(
22613 dir.path(),
22614 ClusterId::from_bytes([1; 16]),
22615 NodeId::from_bytes([2; 16]),
22616 DatabaseId::from_bytes([3; 16]),
22617 )
22618 .unwrap();
22619 db.apply_replicated_catalog_command(&create_table_record("t", 1))
22620 .unwrap();
22621 let table_id = db.table_id("t").unwrap();
22622
22623 assert!(db.validate_staged_txn_writes(&[vec![0xFF]]).is_err());
22625 let unknown_table = StagedTxnWrite::Put {
22626 table_id: 99,
22627 rows: bincode::serialize(&Vec::<Row>::new()).unwrap(),
22628 }
22629 .encode()
22630 .unwrap();
22631 assert!(db.validate_staged_txn_writes(&[unknown_table]).is_err());
22632 let good: Vec<Vec<u8>> = [10_i64, 20, 30]
22633 .iter()
22634 .map(|value| {
22635 let rows = vec![Row::new(crate::RowId(*value as u64), Epoch(0))
22636 .with_column(1, Value::Int64(*value))];
22637 StagedTxnWrite::Put {
22638 table_id,
22639 rows: bincode::serialize(&rows).unwrap(),
22640 }
22641 .encode()
22642 .unwrap()
22643 })
22644 .collect();
22645 db.validate_staged_txn_writes(&good).unwrap();
22646
22647 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
22650 physical_micros: 5_000,
22651 logical: 0,
22652 node_tiebreaker: 0,
22653 };
22654 assert!(db
22655 .apply_staged_txn_writes(1 << 63, &good, commit_ts)
22656 .unwrap());
22657 assert_eq!(visible_ids(&db, "t"), vec![10, 20, 30]);
22658 let delete = StagedTxnWrite::Delete {
22659 table_id,
22660 row_ids: vec![20],
22661 }
22662 .encode()
22663 .unwrap();
22664 assert!(db
22665 .apply_staged_txn_writes((1 << 63) + 1, &[delete], commit_ts)
22666 .unwrap());
22667 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22668
22669 let db = Arc::new(db);
22672 db.shutdown().unwrap();
22673 let expected = crate::storage_mode::StorageMode::ClusterReplica {
22674 cluster_id: ClusterId::from_bytes([1; 16]),
22675 node_id: NodeId::from_bytes([2; 16]),
22676 database_id: DatabaseId::from_bytes([3; 16]),
22677 };
22678 let db = Database::open_cluster_replica(dir.path(), &expected).unwrap();
22679 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22680 }
22681}