1#[path = "index_ddl.rs"]
13mod index_ddl;
14
15use crate::catalog::{self, Catalog, CatalogEntry, TableState, META_DEK_LEN};
16use crate::engine::{SharedCtx, Table};
17use crate::epoch::{Epoch, EpochAuthority, EpochGuard, MaintenanceReceipt, Snapshot};
18use crate::error::{MongrelError, Result};
19use crate::external_table::ExternalTableEntry;
20use crate::memtable::Value;
21use crate::procedure::{
22 ProcedureCallOutput, ProcedureCallResult, ProcedureCallRow, ProcedureCondition, ProcedureStep,
23 ProcedureValue, StoredProcedure,
24};
25use crate::retention::{OwnedSnapshotGuard, SnapshotGuard, SnapshotRegistry};
26use crate::rowid::RowId;
27use crate::schema::{AlterColumn, ColumnDef, ColumnFlags, Schema, TypeId};
28use crate::trigger::{
29 StoredTrigger, TriggerCondition, TriggerConfig, TriggerEntry, TriggerEvent, TriggerExpr,
30 TriggerRaiseAction, TriggerStep, TriggerTarget, TriggerTiming, TriggerValue,
31};
32use parking_lot::{Mutex, RwLock};
33use sha2::{Digest, Sha256};
34use std::collections::{HashMap, HashSet, VecDeque};
35use std::io::{Read, Write};
36use std::path::{Path, PathBuf};
37use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
38use std::sync::Arc;
39use subtle::ConstantTimeEq;
40
41pub const TABLES_DIR: &str = "tables";
42pub const VTAB_DIR: &str = "_vtab";
43pub const META_DIR: &str = "_meta";
44pub const KEYS_FILENAME: &str = "keys";
45pub const KMS_KEY_FILENAME: &str = "kms_key.json";
46const MAX_KMS_KEY_ENVELOPE_BYTES: usize = 1024 * 1024;
47pub const HISTORY_RETENTION_FILENAME: &str = "history_retention";
48pub const CTAS_BUILD_TABLE_PREFIX: &str = "__mongreldb_ctas_build_";
49
50pub const WAL_TABLE_ID: u64 = u64::MAX;
54pub const EXTERNAL_TABLE_ID: u64 = u64::MAX - 1;
57
58fn advance_security_version(catalog: &mut Catalog) -> Result<()> {
59 catalog.security_version = catalog.security_version.checked_add(1).ok_or_else(|| {
60 MongrelError::Conflict("security catalog version space is exhausted".into())
61 })?;
62 Ok(())
63}
64
65type OpenLeaseId = u64;
66
67static DATABASE_OPEN_WAIT_COUNT: AtomicU64 = AtomicU64::new(0);
68static DATABASE_OPEN_FAILURE_COUNT: AtomicU64 = AtomicU64::new(0);
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct DatabaseOpenMetrics {
72 pub lock_waits: u64,
73 pub failures: u64,
74}
75
76#[derive(Clone, Debug, Eq, Hash, PartialEq)]
77enum DatabaseOpenKey {
78 IntendedPath(PathBuf),
79 FileIdentity(crate::durable_file::DurableFileIdentity),
80}
81
82#[derive(Debug)]
83enum ProcessOpenState {
84 Opening { lease_id: OpenLeaseId },
85 Open { lease_id: OpenLeaseId },
86 Closing { lease_id: OpenLeaseId },
87}
88
89impl ProcessOpenState {
90 fn lease_id(&self) -> OpenLeaseId {
91 match self {
92 Self::Opening { lease_id } | Self::Open { lease_id } | Self::Closing { lease_id } => {
93 *lease_id
94 }
95 }
96 }
97}
98
99fn embedding_error(error: crate::embedding::EmbeddingError) -> MongrelError {
100 match error {
101 crate::embedding::EmbeddingError::LimitExceeded {
102 resource,
103 requested,
104 limit,
105 } => MongrelError::ResourceLimitExceeded {
106 resource,
107 requested,
108 limit,
109 },
110 crate::embedding::EmbeddingError::Execution(message) => {
111 if message.contains("deadline") {
112 MongrelError::DeadlineExceeded
113 } else {
114 MongrelError::Cancelled
115 }
116 }
117 error => MongrelError::Other(error.to_string()),
118 }
119}
120
121fn render_embedding_input(
122 schema: &Schema,
123 spec: &crate::embedding::GeneratedEmbeddingSpec,
124 cells: &[(u16, crate::memtable::Value)],
125) -> Result<String> {
126 let mut values = Vec::with_capacity(spec.source_columns.len());
127 for source_id in &spec.source_columns {
128 let column = schema
129 .columns
130 .iter()
131 .find(|column| column.id == *source_id)
132 .ok_or_else(|| MongrelError::Schema(format!("unknown embedding source {source_id}")))?;
133 let value = cells
134 .iter()
135 .find(|(id, _)| id == source_id)
136 .map(|(_, value)| embedding_input_value(value))
137 .transpose()?
138 .unwrap_or_default();
139 values.push((column.name.as_str(), value));
140 }
141 if spec.input_template.is_empty() {
142 return Ok(values
143 .into_iter()
144 .map(|(_, value)| value)
145 .collect::<Vec<_>>()
146 .join("\n"));
147 }
148 let mut rendered = spec.input_template.clone();
149 for (name, value) in values {
150 rendered = rendered.replace(&format!("{{{name}}}"), &value);
151 }
152 if rendered.contains('{') || rendered.contains('}') {
153 return Err(MongrelError::Schema(
154 "embedding input template contains an unknown placeholder".into(),
155 ));
156 }
157 Ok(rendered)
158}
159
160fn embedding_input_value(value: &crate::memtable::Value) -> Result<String> {
161 use crate::memtable::Value;
162 match value {
163 Value::Null => Ok(String::new()),
164 Value::Bool(value) => Ok(value.to_string()),
165 Value::Int64(value) => Ok(value.to_string()),
166 Value::Float64(value) => Ok(value.to_string()),
167 Value::Bytes(value) | Value::Json(value) => String::from_utf8(value.clone())
168 .map_err(|_| MongrelError::InvalidArgument("embedding source is not UTF-8".into())),
169 Value::Decimal(value) => Ok(value.to_string()),
170 Value::Interval {
171 months,
172 days,
173 nanos,
174 } => Ok(format!("{months}:{days}:{nanos}")),
175 Value::Uuid(value) => Ok(value.iter().map(|byte| format!("{byte:02x}")).collect()),
176 Value::Embedding(_) | Value::GeneratedEmbedding(_) => Err(MongrelError::InvalidArgument(
177 "embedding columns cannot be embedding text sources".into(),
178 )),
179 }
180}
181
182#[derive(Default)]
183struct ProcessOpenRegistry {
184 next_lease_id: OpenLeaseId,
185 entries: HashMap<DatabaseOpenKey, ProcessOpenState>,
186}
187
188fn process_open_registry() -> &'static Mutex<ProcessOpenRegistry> {
189 static REGISTRY: std::sync::OnceLock<Mutex<ProcessOpenRegistry>> = std::sync::OnceLock::new();
190 REGISTRY.get_or_init(|| Mutex::new(ProcessOpenRegistry::default()))
191}
192
193fn same_process_locked(path: &Path) -> MongrelError {
194 MongrelError::DatabaseLocked {
195 path: path.to_path_buf(),
196 message: "database is already open in this process; reuse the existing Arc<Database>"
197 .into(),
198 }
199}
200
201struct OpenReservation {
202 lease_id: OpenLeaseId,
203 keys: Vec<DatabaseOpenKey>,
204 committed: bool,
205}
206
207impl OpenReservation {
208 fn acquire(key: DatabaseOpenKey, display_path: &Path) -> Result<Self> {
209 let mut registry = process_open_registry().lock();
210 if registry.entries.contains_key(&key) {
211 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
212 return Err(same_process_locked(display_path));
213 }
214 registry.next_lease_id = registry.next_lease_id.checked_add(1).ok_or_else(|| {
215 MongrelError::Full("process database-open lease namespace exhausted".into())
216 })?;
217 let lease_id = registry.next_lease_id;
218 registry
219 .entries
220 .insert(key.clone(), ProcessOpenState::Opening { lease_id });
221 Ok(Self {
222 lease_id,
223 keys: vec![key],
224 committed: false,
225 })
226 }
227
228 fn into_lease(
229 mut self,
230 bootstrap_file: std::fs::File,
231 canonical_path: PathBuf,
232 ) -> ExclusiveDatabaseLease {
233 self.committed = true;
234 ExclusiveDatabaseLease {
235 lease_id: self.lease_id,
236 keys: std::mem::take(&mut self.keys),
237 bootstrap_file,
238 legacy_file: None,
239 canonical_path,
240 durable_root: None,
241 owner_pid: std::process::id(),
242 opened: false,
243 }
244 }
245}
246
247impl Drop for OpenReservation {
248 fn drop(&mut self) {
249 if self.committed {
250 return;
251 }
252 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
253 let mut registry = process_open_registry().lock();
254 for key in &self.keys {
255 if registry
256 .entries
257 .get(key)
258 .is_some_and(|state| state.lease_id() == self.lease_id)
259 {
260 registry.entries.remove(key);
261 }
262 }
263 }
264}
265
266struct ExclusiveDatabaseLease {
267 lease_id: OpenLeaseId,
268 keys: Vec<DatabaseOpenKey>,
269 bootstrap_file: std::fs::File,
270 legacy_file: Option<std::fs::File>,
271 canonical_path: PathBuf,
272 durable_root: Option<Arc<crate::durable_file::DurableRoot>>,
273 owner_pid: u32,
274 opened: bool,
275}
276
277impl ExclusiveDatabaseLease {
278 fn claim_root_identity(&mut self, root: &crate::durable_file::DurableRoot) -> Result<()> {
279 let key = DatabaseOpenKey::FileIdentity(root.file_identity()?);
280 if self.keys.contains(&key) {
281 return Ok(());
282 }
283 let mut registry = process_open_registry().lock();
284 if registry.entries.contains_key(&key) {
285 return Err(same_process_locked(&self.canonical_path));
286 }
287 registry.entries.insert(
288 key.clone(),
289 ProcessOpenState::Opening {
290 lease_id: self.lease_id,
291 },
292 );
293 self.keys.push(key);
294 Ok(())
295 }
296
297 fn mark_open(&mut self) -> Result<()> {
298 let mut registry = process_open_registry().lock();
299 if self.keys.iter().any(|key| {
300 registry
301 .entries
302 .get(key)
303 .is_none_or(|state| state.lease_id() != self.lease_id)
304 }) {
305 return Err(MongrelError::Conflict(
306 "database-open reservation changed during initialization".into(),
307 ));
308 }
309 for key in &self.keys {
310 registry.entries.insert(
311 key.clone(),
312 ProcessOpenState::Open {
313 lease_id: self.lease_id,
314 },
315 );
316 }
317 self.opened = true;
318 Ok(())
319 }
320}
321
322impl Drop for ExclusiveDatabaseLease {
323 fn drop(&mut self) {
324 if std::process::id() != self.owner_pid {
325 return;
326 }
327 if !self.opened {
328 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
329 }
330 {
331 let mut registry = process_open_registry().lock();
332 for key in &self.keys {
333 if registry
334 .entries
335 .get(key)
336 .is_some_and(|state| state.lease_id() == self.lease_id)
337 {
338 registry.entries.insert(
339 key.clone(),
340 ProcessOpenState::Closing {
341 lease_id: self.lease_id,
342 },
343 );
344 }
345 }
346 }
347 if let Some(file) = &self.legacy_file {
348 let _ = fs2::FileExt::unlock(file);
349 }
350 let _ = fs2::FileExt::unlock(&self.bootstrap_file);
351 let mut registry = process_open_registry().lock();
352 for key in &self.keys {
353 if registry
354 .entries
355 .get(key)
356 .is_some_and(|state| state.lease_id() == self.lease_id)
357 {
358 registry.entries.remove(key);
359 }
360 }
361 }
362}
363
364fn commit_prepare_checkpoint(
365 control: Option<&crate::ExecutionControl>,
366 index: usize,
367) -> Result<()> {
368 if index.is_multiple_of(256) {
369 if let Some(control) = control {
370 control.checkpoint()?;
371 }
372 }
373 Ok(())
374}
375
376fn finish_controlled_commit_attempt(
377 result: Result<Epoch>,
378 after_commit: &mut Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
379) -> Result<Epoch> {
380 let Some(after_commit) = after_commit.as_mut() else {
381 return result;
382 };
383 match result {
384 Ok(epoch) => match (**after_commit)(Some(epoch)) {
385 Ok(()) => Ok(epoch),
386 Err(error) => Err(MongrelError::DurableCommit {
387 epoch: epoch.0,
388 message: error.to_string(),
389 }),
390 },
391 Err(MongrelError::DurableCommit { epoch, message }) => {
392 let callback_error = (**after_commit)(Some(Epoch(epoch))).err();
393 Err(MongrelError::DurableCommit {
394 epoch,
395 message: callback_error
396 .map(|error| format!("{message}; commit callback: {error}"))
397 .unwrap_or(message),
398 })
399 }
400 Err(error) => match (**after_commit)(None) {
401 Ok(()) => Err(error),
402 Err(callback_error) => Err(MongrelError::Other(format!(
403 "{error}; commit callback: {callback_error}"
404 ))),
405 },
406 }
407}
408
409fn current_unix_nanos() -> u64 {
410 std::time::SystemTime::now()
411 .duration_since(std::time::UNIX_EPOCH)
412 .unwrap_or_default()
413 .as_nanos() as u64
414}
415
416fn read_encryption_salt(
417 root: &crate::durable_file::DurableRoot,
418) -> Result<[u8; crate::encryption::SALT_LEN]> {
419 let mut file = root
420 .open_regular(Path::new(META_DIR).join(KEYS_FILENAME))
421 .map_err(|error| MongrelError::NotFound(format!("encryption salt file: {error}")))?;
422 let length = file.metadata()?.len();
423 if length != crate::encryption::SALT_LEN as u64 {
424 return Err(MongrelError::Encryption(format!(
425 "invalid encryption salt length: got {length}, expected {}",
426 crate::encryption::SALT_LEN
427 )));
428 }
429 let mut salt = [0_u8; crate::encryption::SALT_LEN];
430 file.read_exact(&mut salt)?;
431 Ok(salt)
432}
433
434fn read_kms_key_envelope(
435 root: &crate::durable_file::DurableRoot,
436) -> Result<crate::security_hardening::KmsDatabaseKeyEnvelope> {
437 let file = root
438 .open_regular(Path::new(META_DIR).join(KMS_KEY_FILENAME))
439 .map_err(|error| MongrelError::NotFound(format!("KMS key envelope: {error}")))?;
440 let mut bytes = Vec::new();
441 file.take((MAX_KMS_KEY_ENVELOPE_BYTES + 1) as u64)
442 .read_to_end(&mut bytes)?;
443 if bytes.len() > MAX_KMS_KEY_ENVELOPE_BYTES {
444 return Err(MongrelError::Encryption(
445 "KMS key envelope exceeds 1 MiB".into(),
446 ));
447 }
448 serde_json::from_slice(&bytes)
449 .map_err(|error| MongrelError::Encryption(format!("invalid KMS key envelope: {error}")))
450}
451
452fn write_kms_key_envelope(
453 root: &Path,
454 envelope: &crate::security_hardening::KmsDatabaseKeyEnvelope,
455) -> Result<()> {
456 let bytes = serde_json::to_vec(envelope)
457 .map_err(|error| MongrelError::Encryption(format!("encode KMS key envelope: {error}")))?;
458 if bytes.len() > MAX_KMS_KEY_ENVELOPE_BYTES {
459 return Err(MongrelError::Encryption(
460 "KMS key envelope exceeds 1 MiB".into(),
461 ));
462 }
463 Ok(crate::durable_file::write_atomic(
464 &root.join(META_DIR).join(KMS_KEY_FILENAME),
465 &bytes,
466 )?)
467}
468
469fn unwrap_kms_database_key(
470 provider: &dyn crate::security_hardening::KeyManagementProvider,
471 envelope: &crate::security_hardening::KmsDatabaseKeyEnvelope,
472) -> Result<zeroize::Zeroizing<Vec<u8>>> {
473 if provider.provider_id() != envelope.provider_id {
474 return Err(MongrelError::Encryption(format!(
475 "KMS provider mismatch: database requires {:?}, got {:?}",
476 envelope.provider_id,
477 provider.provider_id()
478 )));
479 }
480 let key = provider
481 .unwrap_key(&envelope.wrapped_key)
482 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
483 if key.len() != crate::encryption::DEK_LEN {
484 return Err(MongrelError::Encryption(format!(
485 "KMS returned {} key bytes, expected {}",
486 key.len(),
487 crate::encryption::DEK_LEN
488 )));
489 }
490 Ok(key)
491}
492
493fn create_kms_kek(
494 root: &Path,
495 provider: &dyn crate::security_hardening::KeyManagementProvider,
496 kms_key_id: &str,
497) -> Result<Arc<crate::encryption::Kek>> {
498 if kms_key_id.is_empty() {
499 return Err(MongrelError::InvalidArgument(
500 "KMS key id must not be empty".into(),
501 ));
502 }
503 let salt = crate::encryption::random_salt()?;
504 let mut raw_key = zeroize::Zeroizing::new([0_u8; crate::encryption::DEK_LEN]);
505 crate::encryption::fill_random(raw_key.as_mut())?;
506 let wrapped_key = provider
507 .wrap_key(kms_key_id, raw_key.as_ref())
508 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
509 let envelope = crate::security_hardening::KmsDatabaseKeyEnvelope {
510 provider_id: provider.provider_id().to_owned(),
511 wrapped_key,
512 };
513 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
514 write_kms_key_envelope(root, &envelope)?;
515 Ok(Arc::new(crate::encryption::Kek::from_raw_key(
516 raw_key.as_ref(),
517 &salt,
518 )?))
519}
520
521fn open_kms_kek(
522 root: &crate::durable_file::DurableRoot,
523 provider: &dyn crate::security_hardening::KeyManagementProvider,
524) -> Result<Arc<crate::encryption::Kek>> {
525 let salt = read_encryption_salt(root)?;
526 let envelope = read_kms_key_envelope(root)?;
527 let raw_key = unwrap_kms_database_key(provider, &envelope)?;
528 Ok(Arc::new(crate::encryption::Kek::from_raw_key(
529 raw_key.as_ref(),
530 &salt,
531 )?))
532}
533
534fn persist_key_rotation(
535 journal: &crate::security_hardening::KeyRotationJournal,
536 record: &crate::security_hardening::KeyRotationRecord,
537) -> Result<()> {
538 journal
539 .persist(record)
540 .map_err(|error| MongrelError::Encryption(error.to_string()))
541}
542
543fn fail_key_rotation<T>(
544 journal: &crate::security_hardening::KeyRotationJournal,
545 record: &mut crate::security_hardening::KeyRotationRecord,
546 error: impl ToString,
547) -> Result<T> {
548 let message = error.to_string();
549 record.fail(&message);
550 persist_key_rotation(journal, record)?;
551 Err(MongrelError::Encryption(message))
552}
553
554fn advance_key_rotation(
555 journal: &crate::security_hardening::KeyRotationJournal,
556 record: &mut crate::security_hardening::KeyRotationRecord,
557) -> Result<()> {
558 let now = std::time::SystemTime::now()
559 .duration_since(std::time::UNIX_EPOCH)
560 .unwrap_or_default()
561 .as_micros() as u64;
562 record
563 .advance(now)
564 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
565 persist_key_rotation(journal, record)?;
566 let hook = match record.phase {
567 crate::security_hardening::KeyRotationPhase::WrappingNewKey => "kms.rotation.phase.1",
568 crate::security_hardening::KeyRotationPhase::DualRead => "kms.rotation.phase.2",
569 crate::security_hardening::KeyRotationPhase::Reencrypting => "kms.rotation.phase.3",
570 crate::security_hardening::KeyRotationPhase::Validating => "kms.rotation.phase.4",
571 crate::security_hardening::KeyRotationPhase::Published => "kms.rotation.phase.5",
572 crate::security_hardening::KeyRotationPhase::RetiringOldKey => "kms.rotation.phase.6",
573 crate::security_hardening::KeyRotationPhase::Succeeded => "kms.rotation.phase.7",
574 crate::security_hardening::KeyRotationPhase::Pending
575 | crate::security_hardening::KeyRotationPhase::Failed => return Ok(()),
576 };
577 mongreldb_fault::inject(hook).map_err(|error| MongrelError::Encryption(error.to_string()))
578}
579
580fn incremental_aggregate_cache_key(
581 table: &str,
582 conditions: &[crate::query::Condition],
583 column: Option<u16>,
584 agg: crate::engine::NativeAgg,
585 principal: Option<&crate::auth::Principal>,
586 security_version: u64,
587) -> u64 {
588 use std::hash::{Hash, Hasher};
589 let projection = column.as_ref().map(std::slice::from_ref);
590 let query_key = crate::query::canonical_query_key(conditions, projection, security_version);
591 let mut hasher = std::collections::hash_map::DefaultHasher::new();
592 table.hash(&mut hasher);
593 query_key.hash(&mut hasher);
594 match agg {
595 crate::engine::NativeAgg::Count => 0u8,
596 crate::engine::NativeAgg::Sum => 1,
597 crate::engine::NativeAgg::Min => 2,
598 crate::engine::NativeAgg::Max => 3,
599 crate::engine::NativeAgg::Avg => 4,
600 }
601 .hash(&mut hasher);
602 if let Some(principal) = principal {
603 principal.user_id.hash(&mut hasher);
604 principal.created_epoch.hash(&mut hasher);
605 principal.username.hash(&mut hasher);
606 principal.is_admin.hash(&mut hasher);
607 let mut roles = principal.roles.clone();
608 roles.sort_unstable();
609 roles.hash(&mut hasher);
610 }
611 hasher.finish()
612}
613
614fn read_history_retention(
615 root: &crate::durable_file::DurableRoot,
616 current_epoch: Epoch,
617) -> Result<(u64, Epoch)> {
618 const MAX_HISTORY_RETENTION_BYTES: u64 = 128;
619 let file = match root.open_regular(Path::new(META_DIR).join(HISTORY_RETENTION_FILENAME)) {
620 Ok(file) => file,
621 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
622 return Ok((0, current_epoch));
623 }
624 Err(error) => return Err(error.into()),
625 };
626 let length = file.metadata()?.len();
627 if length > MAX_HISTORY_RETENTION_BYTES {
628 return Err(MongrelError::ResourceLimitExceeded {
629 resource: "history retention bytes",
630 requested: usize::try_from(length).unwrap_or(usize::MAX),
631 limit: MAX_HISTORY_RETENTION_BYTES as usize,
632 });
633 }
634 let mut bytes = Vec::with_capacity(length as usize);
635 file.take(MAX_HISTORY_RETENTION_BYTES + 1)
636 .read_to_end(&mut bytes)?;
637 if bytes.len() as u64 != length {
638 return Err(MongrelError::Other(
639 "history retention length changed while reading".into(),
640 ));
641 }
642 let text = std::str::from_utf8(&bytes)
643 .map_err(|error| MongrelError::Other(format!("history retention encoding: {error}")))?;
644 let mut fields = text.split_whitespace();
645 let epochs = fields
646 .next()
647 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
648 .parse::<u64>()
649 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
650 let start = fields
651 .next()
652 .ok_or_else(|| MongrelError::Other("history retention start is missing".into()))?
653 .parse::<u64>()
654 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
655 if fields.next().is_some() || start > current_epoch.0 {
656 return Err(MongrelError::Other(
657 "history retention file has trailing fields or a future start epoch".into(),
658 ));
659 }
660 Ok((epochs, Epoch(start)))
661}
662
663fn write_history_retention<F>(
664 root: &Path,
665 epochs: u64,
666 start: Epoch,
667 after_publish: F,
668) -> Result<()>
669where
670 F: FnOnce(),
671{
672 let meta = root.join(META_DIR);
673 let path = meta.join(HISTORY_RETENTION_FILENAME);
674 let bytes = format!("{epochs} {}\n", start.0);
675 crate::durable_file::write_atomic_with_after(&path, bytes.as_bytes(), after_publish)?;
676 Ok(())
677}
678
679struct PreparedBackupDestination {
680 parent: crate::durable_file::DurableRoot,
681 destination_name: std::ffi::OsString,
682 destination_path: PathBuf,
683 stage_name: std::ffi::OsString,
684 stage: Option<Box<crate::durable_file::DurableRoot>>,
685}
686
687fn prepare_backup_destination(
688 source: &Path,
689 destination: &Path,
690) -> Result<PreparedBackupDestination> {
691 let destination_name = destination
692 .file_name()
693 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?
694 .to_os_string();
695 let requested_parent = destination
696 .parent()
697 .filter(|path| !path.as_os_str().is_empty())
698 .unwrap_or_else(|| Path::new("."));
699 crate::durable_file::create_directory_all(requested_parent)?;
700 let parent = crate::durable_file::DurableRoot::open(requested_parent)?;
701 prepare_backup_destination_in(source, &parent, &destination_name)
702}
703
704fn prepare_backup_destination_in(
705 source: &Path,
706 parent: &crate::durable_file::DurableRoot,
707 destination_name: &std::ffi::OsStr,
708) -> Result<PreparedBackupDestination> {
709 let source = source.canonicalize()?;
710 if parent.canonical_path().starts_with(&source) {
711 return Err(MongrelError::InvalidArgument(
712 "backup destination must not be inside the source database".into(),
713 ));
714 }
715 if parent.entry_exists(Path::new(&destination_name))? {
716 return Err(MongrelError::Conflict(format!(
717 "backup destination already exists: {}",
718 parent.canonical_path().join(destination_name).display()
719 )));
720 }
721 let mut stage_name = None;
722 for _ in 0..128 {
723 let mut nonce = [0_u8; 8];
724 crate::encryption::fill_random(&mut nonce)?;
725 let suffix = nonce
726 .iter()
727 .map(|byte| format!("{byte:02x}"))
728 .collect::<String>();
729 let name = std::ffi::OsString::from(format!(
730 ".{}.backup-stage-{}-{suffix}",
731 destination_name.to_string_lossy(),
732 std::process::id()
733 ));
734 match parent.create_directory_new(Path::new(&name)) {
735 Ok(()) => {
736 stage_name = Some(name);
737 break;
738 }
739 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
740 Err(error) => return Err(error.into()),
741 }
742 }
743 let stage_name = stage_name
744 .ok_or_else(|| MongrelError::Conflict("could not allocate backup staging path".into()))?;
745 let stage = parent.open_directory(Path::new(&stage_name))?;
746 Ok(PreparedBackupDestination {
747 destination_path: parent.canonical_path().join(destination_name),
748 destination_name: destination_name.to_os_string(),
749 stage_name,
750 stage: Some(Box::new(stage)),
751 parent: parent.try_clone()?,
752 })
753}
754
755fn copy_backup_boundary(
756 source_root: &Path,
757 destination_root: &crate::durable_file::DurableRoot,
758 deferred_runs: &HashSet<PathBuf>,
759 copied: &mut Vec<PathBuf>,
760 control: Option<&crate::ExecutionControl>,
761) -> Result<()> {
762 let mut visited = 0;
763 crate::durable_file::walk_regular_files_nofollow(
764 source_root,
765 |relative, is_directory| {
766 if visited % 256 == 0 {
767 if let Some(control) = control {
768 control.checkpoint()?;
769 }
770 }
771 visited += 1;
772 if backup_path_excluded(relative) {
773 return Ok(false);
774 }
775 if is_directory {
776 return Ok(true);
777 }
778 if deferred_runs.contains(relative) {
779 return Ok(false);
780 }
781 Ok(!(relative
782 .parent()
783 .and_then(Path::file_name)
784 .is_some_and(|parent| parent == "_runs")
785 && relative
786 .extension()
787 .is_some_and(|extension| extension == "sr")))
788 },
789 |relative| {
790 destination_root.create_directory_all(relative)?;
791 Ok(())
792 },
793 |relative, source| {
794 destination_root.copy_new_from(relative, source)?;
795 copied.push(relative.to_path_buf());
796 Ok(())
797 },
798 )
799}
800
801fn backup_path_excluded(relative: &Path) -> bool {
802 relative == Path::new("_meta/.lock")
803 || relative == Path::new("_meta/replica")
804 || relative == Path::new("_meta/repl_epoch")
805 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
806 || relative.components().any(|component| {
807 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
808 })
809}
810
811#[derive(Debug, Clone)]
812pub enum ExternalTriggerWrite {
813 Insert {
814 table: String,
815 cells: Vec<(u16, Value)>,
816 },
817 UpdateByPk {
818 table: String,
819 pk: Value,
820 cells: Vec<(u16, Value)>,
821 },
822 DeleteByPk {
823 table: String,
824 pk: Value,
825 },
826}
827
828impl ExternalTriggerWrite {
829 fn table(&self) -> &str {
830 match self {
831 Self::Insert { table, .. }
832 | Self::UpdateByPk { table, .. }
833 | Self::DeleteByPk { table, .. } => table,
834 }
835 }
836}
837
838#[derive(Debug, Clone, PartialEq)]
839pub enum ExternalTriggerBaseWrite {
840 Put {
841 table: String,
842 cells: Vec<(u16, Value)>,
843 },
844 Delete {
845 table: String,
846 row_id: RowId,
847 },
848}
849
850#[derive(Debug, Clone, PartialEq)]
851pub struct ExternalTriggerWriteResult {
852 pub state: Vec<u8>,
853 pub base_writes: Vec<ExternalTriggerBaseWrite>,
854}
855
856impl ExternalTriggerWriteResult {
857 pub fn new(state: Vec<u8>) -> Self {
858 Self {
859 state,
860 base_writes: Vec::new(),
861 }
862 }
863}
864
865pub trait ExternalTriggerBridge: Send + Sync {
866 fn apply_trigger_external_write(
867 &self,
868 entry: &ExternalTableEntry,
869 base_state: Vec<u8>,
870 op: ExternalTriggerWrite,
871 ) -> Result<ExternalTriggerWriteResult>;
872}
873
874struct SpilledRun {
876 table_id: u64,
877 run_id: u128,
878 pending_path: PathBuf,
879 final_path: PathBuf,
880 rows: Vec<crate::memtable::Row>,
881 row_count: u64,
882 min_rid: u64,
883 max_rid: u64,
884 content_hash: [u8; 32],
885}
886
887const SPILLED_WAL_PAYLOAD_MAX_BYTES: usize = 24 * 1024 * 1024;
888const SPILLED_WAL_TOTAL_MAX_BYTES: usize = 256 * 1024 * 1024;
889
890fn encode_spilled_row_chunks(
891 rows: &[crate::memtable::Row],
892 total_bytes: &mut usize,
893 total_limit: usize,
894 control: Option<&crate::ExecutionControl>,
895) -> Result<Vec<Vec<u8>>> {
896 let mut output = Vec::new();
897 let mut start = 0;
898 while start < rows.len() {
899 let mut estimated_bytes = std::mem::size_of::<u64>();
903 let mut end = start;
904 while end < rows.len() {
905 if end % 256 == 0 {
906 if let Some(control) = control {
907 control.checkpoint()?;
908 }
909 }
910 let row_bytes =
911 usize::try_from(bincode::serialized_size(&rows[end])?).map_err(|_| {
912 MongrelError::ResourceLimitExceeded {
913 resource: "spilled WAL row bytes",
914 requested: usize::MAX,
915 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
916 }
917 })?;
918 let next_bytes = estimated_bytes.checked_add(row_bytes).ok_or(
919 MongrelError::ResourceLimitExceeded {
920 resource: "spilled WAL row bytes",
921 requested: usize::MAX,
922 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
923 },
924 )?;
925 if next_bytes > SPILLED_WAL_PAYLOAD_MAX_BYTES {
926 break;
927 }
928 estimated_bytes = next_bytes;
929 end += 1;
930 }
931 if end == start {
932 return Err(MongrelError::ResourceLimitExceeded {
933 resource: "spilled WAL row bytes",
934 requested: estimated_bytes.saturating_add(1),
935 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
936 });
937 }
938 let payload = bincode::serialize(&rows[start..end])?;
939 if payload.len() > SPILLED_WAL_PAYLOAD_MAX_BYTES {
940 return Err(MongrelError::ResourceLimitExceeded {
941 resource: "spilled WAL row bytes",
942 requested: payload.len(),
943 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
944 });
945 }
946 let requested = total_bytes.checked_add(payload.len()).unwrap_or(usize::MAX);
947 if requested > total_limit {
948 return Err(MongrelError::ResourceLimitExceeded {
949 resource: "spilled WAL transaction bytes",
950 requested,
951 limit: total_limit,
952 });
953 }
954 *total_bytes = requested;
955 output.push(payload);
956 start = end;
957 }
958 Ok(output)
959}
960
961#[cfg(test)]
962mod spilled_wal_encoding_tests {
963 use super::*;
964
965 #[test]
966 fn logical_spill_payload_has_a_total_bound() {
967 let rows = (0..4)
968 .map(|row_id| crate::memtable::Row {
969 row_id: crate::rowid::RowId(row_id),
970 committed_epoch: Epoch::ZERO,
971 columns: [(1, Value::Bytes(vec![0; 64]))].into_iter().collect(),
972 deleted: false,
973 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
1228#[derive(Default)]
1229struct RlsCache {
1230 entries: HashMap<RlsCacheKey, (Arc<HashSet<RowId>>, usize)>,
1231 lru: VecDeque<RlsCacheKey>,
1232 bytes: usize,
1233 hits: u64,
1234 misses: u64,
1235 evictions: u64,
1236 build_nanos: u64,
1237 rows_evaluated: u64,
1238}
1239
1240impl RlsCache {
1241 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
1242 let value = self.entries.get(key).map(|(value, _)| Arc::clone(value));
1243 if value.is_some() {
1244 self.hits = self.hits.saturating_add(1);
1245 self.touch(key);
1246 } else {
1247 self.misses = self.misses.saturating_add(1);
1248 }
1249 value
1250 }
1251
1252 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
1253 let bytes = key
1254 .0
1255 .len()
1256 .saturating_add(key.3.len())
1257 .saturating_add(
1258 value
1259 .capacity()
1260 .saturating_mul(std::mem::size_of::<RowId>() * 3),
1261 )
1262 .saturating_add(std::mem::size_of::<RlsCacheKey>());
1263 if bytes > RLS_CACHE_MAX_BYTES {
1264 return;
1265 }
1266 if let Some((_, old_bytes)) = self.entries.remove(&key) {
1267 self.bytes = self.bytes.saturating_sub(old_bytes);
1268 }
1269 self.lru.retain(|candidate| candidate != &key);
1270 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
1271 let Some(oldest) = self.lru.pop_front() else {
1272 break;
1273 };
1274 if let Some((_, old_bytes)) = self.entries.remove(&oldest) {
1275 self.bytes = self.bytes.saturating_sub(old_bytes);
1276 self.evictions = self.evictions.saturating_add(1);
1277 }
1278 }
1279 self.bytes = self.bytes.saturating_add(bytes);
1280 self.lru.push_back(key.clone());
1281 self.entries.insert(key, (value, bytes));
1282 }
1283
1284 fn touch(&mut self, key: &RlsCacheKey) {
1285 self.lru.retain(|candidate| candidate != key);
1286 self.lru.push_back(key.clone());
1287 }
1288
1289 fn stats(&self) -> RlsCacheStats {
1290 RlsCacheStats {
1291 entries: self.entries.len(),
1292 bytes: self.bytes,
1293 hits: self.hits,
1294 misses: self.misses,
1295 evictions: self.evictions,
1296 build_nanos: self.build_nanos,
1297 rows_evaluated: self.rows_evaluated,
1298 }
1299 }
1300}
1301
1302#[derive(Clone)]
1304pub struct TableHandle {
1305 inner: TableHandleInner,
1306 generation_metrics: Arc<TableGenerationMetrics>,
1307}
1308
1309#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1310pub struct TableGenerationStats {
1311 pub active_read_generations: usize,
1312 pub max_live_read_generations: usize,
1313 pub cow_clone_count: u64,
1314 pub cow_clone_nanos: u64,
1315 pub estimated_cow_clone_bytes: u64,
1316 pub writer_wait_nanos: u64,
1317}
1318
1319#[derive(Default)]
1320#[doc(hidden)]
1321pub struct TableGenerationMetrics {
1322 active_read_generations: AtomicUsize,
1323 max_live_read_generations: AtomicUsize,
1324 cow_clone_count: AtomicU64,
1325 cow_clone_nanos: AtomicU64,
1326 estimated_cow_clone_bytes: AtomicU64,
1327 writer_wait_nanos: AtomicU64,
1328}
1329
1330impl TableGenerationMetrics {
1331 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
1332 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
1333 self.max_live_read_generations
1334 .fetch_max(active, Ordering::Relaxed);
1335 Arc::new(TableReadGeneration {
1336 table,
1337 metrics: Arc::clone(self),
1338 })
1339 }
1340
1341 fn stats(&self) -> TableGenerationStats {
1342 TableGenerationStats {
1343 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
1344 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
1345 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
1346 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
1347 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
1348 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
1349 }
1350 }
1351}
1352
1353pub struct TableReadGeneration {
1355 table: Table,
1356 metrics: Arc<TableGenerationMetrics>,
1357}
1358
1359impl std::ops::Deref for TableReadGeneration {
1360 type Target = Table;
1361
1362 fn deref(&self) -> &Self::Target {
1363 &self.table
1364 }
1365}
1366
1367impl Drop for TableReadGeneration {
1368 fn drop(&mut self) {
1369 self.metrics
1370 .active_read_generations
1371 .fetch_sub(1, Ordering::Relaxed);
1372 }
1373}
1374
1375#[derive(Clone)]
1376enum TableHandleInner {
1377 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
1378 Direct(Arc<Mutex<Table>>),
1379}
1380
1381pub enum TableGuard<'a> {
1382 CopyOnWrite {
1383 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
1384 metrics: Arc<TableGenerationMetrics>,
1385 },
1386 Direct {
1387 table: parking_lot::MutexGuard<'a, Table>,
1388 },
1389}
1390
1391impl TableHandle {
1392 fn new(table: Table) -> Self {
1393 Self {
1394 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
1395 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1396 }
1397 }
1398
1399 pub fn from_table(table: Table) -> Self {
1400 Self::new(table)
1401 }
1402
1403 pub fn lock(&self) -> TableGuard<'_> {
1404 let started = std::time::Instant::now();
1405 let guard = match &self.inner {
1406 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
1407 table: table.write(),
1408 metrics: Arc::clone(&self.generation_metrics),
1409 },
1410 TableHandleInner::Direct(table) => TableGuard::Direct {
1411 table: table.lock(),
1412 },
1413 };
1414 self.generation_metrics.writer_wait_nanos.fetch_add(
1415 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1416 Ordering::Relaxed,
1417 );
1418 guard
1419 }
1420
1421 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
1422 let started = std::time::Instant::now();
1423 let guard = match &self.inner {
1424 TableHandleInner::CopyOnWrite(table) => {
1425 table
1426 .try_write_for(timeout)
1427 .map(|table| TableGuard::CopyOnWrite {
1428 table,
1429 metrics: Arc::clone(&self.generation_metrics),
1430 })
1431 }
1432 TableHandleInner::Direct(table) => table
1433 .try_lock_for(timeout)
1434 .map(|table| TableGuard::Direct { table }),
1435 };
1436 self.generation_metrics.writer_wait_nanos.fetch_add(
1437 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1438 Ordering::Relaxed,
1439 );
1440 guard
1441 }
1442
1443 pub fn ptr_eq(&self, other: &Self) -> bool {
1444 match (&self.inner, &other.inner) {
1445 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1446 Arc::ptr_eq(left, right)
1447 }
1448 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1449 Arc::ptr_eq(left, right)
1450 }
1451 _ => false,
1452 }
1453 }
1454
1455 pub fn read_generation_with_context(
1456 &self,
1457 context: Option<&crate::query::AiExecutionContext>,
1458 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1459 let mut table = if let Some(context) = context {
1460 loop {
1461 context.checkpoint()?;
1462 let wait = context
1463 .remaining_duration()
1464 .unwrap_or(std::time::Duration::from_millis(5))
1465 .min(std::time::Duration::from_millis(5));
1466 if let Some(table) = self.try_lock_for(wait) {
1467 break table;
1468 }
1469 }
1470 } else {
1471 self.lock()
1472 };
1473 let snapshot = table.snapshot();
1474 let generation = table.clone_read_generation()?;
1475 Ok((self.generation_metrics.activate(generation), snapshot))
1476 }
1477
1478 pub fn generation_stats(&self) -> TableGenerationStats {
1479 self.generation_metrics.stats()
1480 }
1481}
1482
1483impl From<Arc<Mutex<Table>>> for TableHandle {
1484 fn from(table: Arc<Mutex<Table>>) -> Self {
1485 Self {
1486 inner: TableHandleInner::Direct(table),
1487 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1488 }
1489 }
1490}
1491
1492impl std::ops::Deref for TableGuard<'_> {
1493 type Target = Table;
1494
1495 fn deref(&self) -> &Self::Target {
1496 match self {
1497 Self::CopyOnWrite { table, .. } => table.as_ref(),
1498 Self::Direct { table } => table,
1499 }
1500 }
1501}
1502
1503impl std::ops::DerefMut for TableGuard<'_> {
1504 fn deref_mut(&mut self) -> &mut Self::Target {
1505 match self {
1506 Self::CopyOnWrite { table, metrics } => {
1507 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1508 let estimated_bytes = table.estimated_clone_bytes();
1509 let started = std::time::Instant::now();
1510 let table = Arc::make_mut(table);
1511 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1512 metrics.cow_clone_nanos.fetch_add(
1513 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1514 Ordering::Relaxed,
1515 );
1516 metrics
1517 .estimated_cow_clone_bytes
1518 .fetch_add(estimated_bytes, Ordering::Relaxed);
1519 table
1520 } else {
1521 Arc::make_mut(table)
1522 }
1523 }
1524 Self::Direct { table } => table,
1525 }
1526 }
1527}
1528
1529#[derive(Clone, Debug)]
1530pub struct ReadAuthorization {
1531 pub operation: crate::auth::ColumnOperation,
1532 pub columns: Vec<u16>,
1533 pub permissions: Vec<crate::auth::Permission>,
1534}
1535
1536#[derive(Default, Debug)]
1537struct TableWritePermissionNeeds {
1538 insert: bool,
1539 insert_columns: Vec<u16>,
1540 update: bool,
1541 update_columns: Vec<u16>,
1542 delete: bool,
1543 truncate: bool,
1544}
1545
1546#[cfg(test)]
1547thread_local! {
1548 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1549 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1550 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1551 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1552 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1553 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1554}
1555
1556fn summarize_write_permissions(
1557 staging: &[(u64, crate::txn::Staged)],
1558) -> HashMap<u64, TableWritePermissionNeeds> {
1559 use crate::txn::Staged;
1560
1561 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1562 for (table_id, operation) in staging {
1563 let table = needs.entry(*table_id).or_default();
1564 match operation {
1565 Staged::Put(cells) => {
1566 table.insert = true;
1567 table
1568 .insert_columns
1569 .extend(cells.iter().map(|(column, _)| *column));
1570 }
1571 Staged::Update {
1572 changed_columns, ..
1573 } => {
1574 table.update = true;
1575 table.update_columns.extend(changed_columns);
1576 }
1577 Staged::Delete(_) => table.delete = true,
1578 Staged::Truncate => table.truncate = true,
1579 }
1580 }
1581 for table in needs.values_mut() {
1582 table.insert_columns.sort_unstable();
1583 table.insert_columns.dedup();
1584 table.update_columns.sort_unstable();
1585 table.update_columns.dedup();
1586 }
1587 needs
1588}
1589
1590struct SecurityCoordinator {
1591 gate: RwLock<()>,
1593 version: AtomicU64,
1594}
1595
1596fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1597 static COORDINATORS: std::sync::OnceLock<
1598 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1599 > = std::sync::OnceLock::new();
1600
1601 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1602 let mut coordinators = COORDINATORS
1603 .get_or_init(|| Mutex::new(HashMap::new()))
1604 .lock();
1605 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1606 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1607 return coordinator;
1608 }
1609 let coordinator = Arc::new(SecurityCoordinator {
1610 gate: RwLock::new(()),
1611 version: AtomicU64::new(version),
1612 });
1613 coordinators.insert(root, Arc::downgrade(&coordinator));
1614 coordinator
1615}
1616
1617pub fn lock_table_with_context<'a>(
1618 handle: &'a TableHandle,
1619 context: Option<&crate::query::AiExecutionContext>,
1620) -> Result<TableGuard<'a>> {
1621 let Some(context) = context else {
1622 return Ok(handle.lock());
1623 };
1624 loop {
1625 context.checkpoint()?;
1626 let wait = context
1627 .remaining_duration()
1628 .unwrap_or(std::time::Duration::from_millis(5))
1629 .min(std::time::Duration::from_millis(5));
1630 if let Some(guard) = handle.try_lock_for(wait) {
1631 return Ok(guard);
1632 }
1633 }
1634}
1635
1636#[derive(Clone, Debug, Default)]
1642pub struct OpenOptions {
1643 pub lock_timeout_ms: u32,
1659 pub memory_budget_bytes: Option<u64>,
1664 pub temp_disk_budget_bytes: Option<u64>,
1669 pub offline_validation: bool,
1676}
1677
1678impl OpenOptions {
1679 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1682 self.lock_timeout_ms = ms;
1683 self
1684 }
1685
1686 pub fn with_memory_budget_bytes(mut self, bytes: u64) -> Self {
1688 self.memory_budget_bytes = Some(bytes);
1689 self
1690 }
1691
1692 pub fn with_temp_disk_budget_bytes(mut self, bytes: u64) -> Self {
1694 self.temp_disk_budget_bytes = Some(bytes);
1695 self
1696 }
1697
1698 pub fn with_offline_validation(mut self, offline_validation: bool) -> Self {
1701 self.offline_validation = offline_validation;
1702 self
1703 }
1704}
1705
1706#[derive(Debug, Clone)]
1710pub(crate) enum OpenModeGate {
1711 Normal,
1714 SharedCore,
1717 OfflineValidation,
1719 ClusterRuntime {
1723 cluster_id: mongreldb_types::ids::ClusterId,
1725 node_id: mongreldb_types::ids::NodeId,
1727 database_id: mongreldb_types::ids::DatabaseId,
1729 },
1730 Create(crate::storage_mode::StorageMode),
1732}
1733
1734pub const DEFAULT_MEMORY_BUDGET_BYTES: u64 = 1024 * 1024 * 1024;
1738
1739pub const DEFAULT_TEMP_DISK_BUDGET_BYTES: u64 = 4 * 1024 * 1024 * 1024;
1742
1743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1747pub(crate) struct CoreResourceConfig {
1748 pub memory_budget_bytes: u64,
1749 pub temp_disk_budget_bytes: u64,
1750}
1751
1752impl Default for CoreResourceConfig {
1753 fn default() -> Self {
1754 Self {
1755 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
1756 temp_disk_budget_bytes: DEFAULT_TEMP_DISK_BUDGET_BYTES,
1757 }
1758 }
1759}
1760
1761impl CoreResourceConfig {
1762 fn from_options(options: &OpenOptions) -> Result<Self> {
1763 let config = Self {
1764 memory_budget_bytes: options
1765 .memory_budget_bytes
1766 .unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES),
1767 temp_disk_budget_bytes: options
1768 .temp_disk_budget_bytes
1769 .unwrap_or(DEFAULT_TEMP_DISK_BUDGET_BYTES),
1770 };
1771 if config.memory_budget_bytes == 0 {
1772 return Err(MongrelError::InvalidArgument(
1773 "memory_budget_bytes must be nonzero".into(),
1774 ));
1775 }
1776 if config.temp_disk_budget_bytes == 0 {
1777 return Err(MongrelError::InvalidArgument(
1778 "temp_disk_budget_bytes must be nonzero".into(),
1779 ));
1780 }
1781 Ok(config)
1782 }
1783}
1784
1785#[derive(Debug, Clone)]
1788pub struct TablePinsReport {
1789 pub table_id: u64,
1791 pub table: String,
1793 pub pins: crate::retention::PinsReport,
1795}
1796
1797pub struct DatabaseCore {
1810 root: PathBuf,
1811 durable_root: Arc<crate::durable_file::DurableRoot>,
1812 lifecycle: Arc<crate::core::LifecycleController>,
1815 registry: std::sync::OnceLock<crate::manager::CoreRegistration>,
1819 read_only: bool,
1821 catalog: RwLock<Catalog>,
1822 security_coordinator: Arc<SecurityCoordinator>,
1823 security_catalog_disk_reads: AtomicU64,
1824 rls_cache: Mutex<RlsCache>,
1825 epoch: Arc<EpochAuthority>,
1826 snapshots: Arc<SnapshotRegistry>,
1827 memory_governor: crate::memory::MemoryGovernor,
1832 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1833 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1834 spill_manager: crate::spill::SpillManager,
1838 resource_groups: crate::resource::ResourceGroupRegistry,
1842 embedding_providers: crate::embedding::EmbeddingProviderRegistry,
1846 pub(crate) service_principals: crate::service_principal::ServicePrincipalStore,
1850 job_registry: Arc<crate::jobs::JobRegistry>,
1853 commit_lock: Arc<Mutex<()>>,
1854 kms_rotation_lock: Mutex<()>,
1855 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1860 next_txn_id: Arc<Mutex<u64>>,
1864 tables: RwLock<HashMap<u64, TableHandle>>,
1865 kek: Option<Arc<crate::encryption::Kek>>,
1866 ddl_lock: Mutex<()>,
1869 meta_dek: Option<[u8; META_DEK_LEN]>,
1870 spill_threshold: std::sync::atomic::AtomicU64,
1873 conflicts: crate::txn::ConflictIndex,
1876 active_txns: crate::txn::ActiveTxns,
1879 lock_manager: Arc<crate::locks::LockManager>,
1885 poisoned: Arc<std::sync::atomic::AtomicBool>,
1888 group: Arc<crate::txn::GroupCommit>,
1892 commit_log: RwLock<Arc<dyn mongreldb_log::CommitLog>>,
1896 standalone_commit_log: Arc<crate::commit_log::StandaloneCommitLog>,
1900 hlc: Arc<mongreldb_types::hlc::HlcClock>,
1906 idempotency: crate::txn::IdempotencyLedger,
1909 commit_ts_ledger: Mutex<std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp>>,
1917 active_spills: Arc<crate::retention::ActiveSpills>,
1920 replication_barrier: parking_lot::RwLock<()>,
1923 replication_wal_retention_segments: AtomicUsize,
1925 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1928 #[doc(hidden)]
1932 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1933 #[doc(hidden)]
1935 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1936 #[doc(hidden)]
1939 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1940 #[doc(hidden)]
1943 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1944 #[doc(hidden)]
1950 fk_lock_hook: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
1951 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1952 trigger_recursive: AtomicBool,
1953 trigger_max_depth: AtomicU32,
1954 trigger_max_loop_iterations: AtomicU32,
1955 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
1958 change_wake: tokio::sync::broadcast::Sender<()>,
1961 _lock: Mutex<Option<ExclusiveDatabaseLease>>,
1965}
1966
1967pub struct Database {
1977 core: Arc<DatabaseCore>,
1978 principal: RwLock<Option<crate::auth::Principal>>,
1988 auth_state: crate::auth_state::AuthState,
1994 shared: bool,
2001}
2002
2003impl std::ops::Deref for Database {
2004 type Target = DatabaseCore;
2005
2006 fn deref(&self) -> &DatabaseCore {
2007 &self.core
2008 }
2009}
2010
2011struct RunPins {
2012 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
2013 runs: Vec<(u64, u128)>,
2014}
2015
2016struct TxnLockGuard<'a> {
2021 locks: &'a crate::locks::LockManager,
2022 txn_id: u64,
2023}
2024
2025impl Drop for TxnLockGuard<'_> {
2026 fn drop(&mut self) {
2027 self.locks.release_all(self.txn_id);
2028 }
2029}
2030
2031struct BackupFilePins {
2032 root: PathBuf,
2033}
2034
2035struct PendingTableDir {
2036 path: PathBuf,
2037 armed: bool,
2038}
2039
2040impl PendingTableDir {
2041 fn new(path: PathBuf) -> Self {
2042 Self { path, armed: true }
2043 }
2044
2045 fn disarm(&mut self) {
2046 self.armed = false;
2047 }
2048}
2049
2050impl Drop for PendingTableDir {
2051 fn drop(&mut self) {
2052 if self.armed {
2053 let _ = std::fs::remove_dir_all(&self.path);
2054 }
2055 }
2056}
2057
2058impl Drop for BackupFilePins {
2059 fn drop(&mut self) {
2060 let _ = std::fs::remove_dir_all(&self.root);
2061 }
2062}
2063
2064impl Drop for RunPins {
2065 fn drop(&mut self) {
2066 let mut pins = self.pins.lock();
2067 for run in &self.runs {
2068 if let Some(count) = pins.get_mut(run) {
2069 *count -= 1;
2070 if *count == 0 {
2071 pins.remove(run);
2072 }
2073 }
2074 }
2075 }
2076}
2077
2078#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2081pub struct ChangeEvent {
2082 pub id: Option<String>,
2083 pub channel: String,
2084 pub table_id: Option<u64>,
2085 pub table: String,
2086 pub op: String,
2087 pub epoch: u64,
2088 pub txn_id: Option<u64>,
2089 pub message: Option<String>,
2090 pub data: Option<serde_json::Value>,
2091}
2092
2093#[derive(Debug, Clone)]
2094pub struct CdcBatch {
2095 pub events: Vec<ChangeEvent>,
2096 pub current_epoch: u64,
2097 pub earliest_epoch: Option<u64>,
2098 pub gap: bool,
2099}
2100
2101impl std::fmt::Debug for Database {
2108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2109 let cat = self.catalog.read();
2110 let principal_guard = self.principal.read();
2111 let principal: &str = principal_guard
2112 .as_ref()
2113 .map(|p| p.username.as_str())
2114 .unwrap_or("<none>");
2115 f.debug_struct("Database")
2116 .field("root", &self.root)
2117 .field("db_epoch", &cat.db_epoch)
2118 .field("open_generation", &"sidecar")
2119 .field("tables", &cat.tables.len())
2120 .field("visible_epoch", &self.epoch.visible().0)
2121 .field("encrypted", &self.kek.is_some())
2122 .field("require_auth", &cat.require_auth)
2123 .field("principal", &principal)
2124 .finish()
2125 }
2126}
2127
2128impl std::fmt::Debug for DatabaseCore {
2131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2132 let cat = self.catalog.read();
2133 f.debug_struct("DatabaseCore")
2134 .field("root", &self.root)
2135 .field("lifecycle", &self.lifecycle.state())
2136 .field("db_epoch", &cat.db_epoch)
2137 .field("tables", &cat.tables.len())
2138 .field("visible_epoch", &self.epoch.visible().0)
2139 .field("encrypted", &self.kek.is_some())
2140 .field("require_auth", &cat.require_auth)
2141 .finish()
2142 }
2143}
2144
2145impl DatabaseCore {
2146 fn ensure_owner_process(&self) -> Result<()> {
2147 let current_pid = std::process::id();
2148 let owner_pid = self
2149 ._lock
2150 .lock()
2151 .as_ref()
2152 .map(|lease| lease.owner_pid)
2153 .unwrap_or(current_pid);
2154 if current_pid == owner_pid {
2155 Ok(())
2156 } else {
2157 Err(MongrelError::ForkedProcess {
2158 owner_pid,
2159 current_pid,
2160 })
2161 }
2162 }
2163
2164 pub fn root(&self) -> &Path {
2166 self.durable_root.canonical_path()
2167 }
2168
2169 pub fn file_identity(&self) -> Result<crate::core::DatabaseFileIdentity> {
2171 crate::core::DatabaseFileIdentity::from_durable_root(&self.durable_root)
2172 }
2173
2174 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2176 self.lifecycle.state()
2177 }
2178
2179 pub fn is_open(&self) -> bool {
2181 self.lifecycle.is_open()
2182 }
2183
2184 pub fn operation_guard(self: &Arc<Self>) -> Result<crate::core::OperationGuard> {
2188 self.lifecycle.begin_operation()
2189 }
2190
2191 pub(crate) fn set_registry(
2195 &self,
2196 registration: crate::manager::CoreRegistration,
2197 ) -> Result<()> {
2198 self.registry
2199 .set(registration)
2200 .map_err(|_| MongrelError::Conflict("database core is already registry-bound".into()))
2201 }
2202
2203 pub fn shutdown(self: &Arc<Self>, drain_deadline: std::time::Duration) -> Result<()> {
2223 self.ensure_owner_process()?;
2224 let initiated = self.lifecycle.begin_shutdown();
2225 if !initiated {
2226 match self.lifecycle.state() {
2227 crate::core::LifecycleState::Closed => return Ok(()),
2228 crate::core::LifecycleState::Draining => {}
2231 state => {
2232 return Err(MongrelError::Conflict(format!(
2233 "database core cannot shut down from lifecycle state {state}"
2234 )))
2235 }
2236 }
2237 }
2238 if let Some(registration) = self.registry.get() {
2239 registration
2240 .manager
2241 .mark_closing(®istration.identity, self);
2242 }
2243 self.lifecycle.wait_drained(drain_deadline)?;
2244 let sync = self.shared_wal.lock().group_sync().map(|_| ());
2245 self.lifecycle.mark_closing();
2246 let lease = self._lock.lock().take();
2247 drop(lease);
2248 if let Some(registration) = self.registry.get() {
2249 registration
2250 .manager
2251 .entry_closed(®istration.identity, self);
2252 }
2253 self.lifecycle.mark_closed();
2254 sync
2255 }
2256}
2257
2258impl Database {
2259 pub fn open_metrics() -> DatabaseOpenMetrics {
2260 DatabaseOpenMetrics {
2261 lock_waits: DATABASE_OPEN_WAIT_COUNT.load(Ordering::Relaxed),
2262 failures: DATABASE_OPEN_FAILURE_COUNT.load(Ordering::Relaxed),
2263 }
2264 }
2265
2266 pub fn core(&self) -> Arc<DatabaseCore> {
2268 Arc::clone(&self.core)
2269 }
2270
2271 pub fn identity(&self) -> crate::handle::HandleIdentity {
2274 match self.principal.read().as_ref() {
2275 Some(principal) => crate::handle::HandleIdentity::CatalogUser {
2276 username: principal.username.clone(),
2277 user_id: principal.user_id,
2278 created_version: principal.created_epoch,
2279 },
2280 None => crate::handle::HandleIdentity::Credentialless,
2281 }
2282 }
2283
2284 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2286 self.core.lifecycle_state()
2287 }
2288
2289 pub fn operation_guard(&self) -> Result<crate::core::OperationGuard> {
2293 self.core.operation_guard()
2294 }
2295
2296 pub(crate) fn from_core(
2300 core: Arc<DatabaseCore>,
2301 principal: Option<crate::auth::Principal>,
2302 shared: bool,
2303 ) -> Self {
2304 let require_auth = core.catalog.read().require_auth;
2305 Self {
2306 core,
2307 principal: RwLock::new(principal.clone()),
2308 auth_state: crate::auth_state::AuthState::new(require_auth, principal),
2309 shared,
2310 }
2311 }
2312
2313 pub(crate) fn rebind_service_principal(
2317 &self,
2318 def: &crate::service_principal::ServicePrincipalDefinition,
2319 ) {
2320 let principal = crate::auth::Principal {
2321 user_id: 0,
2322 created_epoch: def.creation_version,
2323 username: format!("service:{}", def.token_id),
2324 is_admin: false,
2325 roles: Vec::new(),
2326 permissions: def.permissions.clone(),
2327 };
2328 *self.principal.write() = Some(principal.clone());
2329 self.auth_state.set_principal(Some(principal));
2330 }
2331
2332 pub(crate) fn into_core(self) -> Arc<DatabaseCore> {
2334 self.core
2335 }
2336
2337 pub(crate) fn open_for_shared_core(root: impl AsRef<Path>) -> Result<Self> {
2342 Self::open_inner_with_lock_timeout(
2343 root,
2344 None,
2345 None,
2346 0,
2347 CoreResourceConfig::default(),
2348 OpenModeGate::SharedCore,
2349 )
2350 }
2351
2352 pub fn shutdown(self: Arc<Self>) -> Result<()> {
2358 if self.shared {
2359 return Err(MongrelError::Conflict(
2360 "shared-core facades do not own storage; use DatabaseHandle::shutdown".into(),
2361 ));
2362 }
2363 match Arc::try_unwrap(self) {
2364 Ok(database) => {
2365 database.ensure_owner_process()?;
2366 database.core.shutdown(std::time::Duration::from_secs(30))?;
2370 drop(database);
2371 Ok(())
2372 }
2373 Err(database) => Err(MongrelError::DatabaseBusy {
2374 strong_handles: Arc::strong_count(&database),
2375 }),
2376 }
2377 }
2378
2379 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
2380 if let Ok(canonical) = root.canonicalize() {
2381 let lock_dir = canonical.parent().ok_or_else(|| {
2382 std::io::Error::new(
2383 std::io::ErrorKind::InvalidInput,
2384 "database root must have a parent directory",
2385 )
2386 })?;
2387 return Ok((canonical.clone(), lock_dir.to_path_buf()));
2388 }
2389
2390 let absolute = if root.is_absolute() {
2391 root.to_path_buf()
2392 } else {
2393 std::env::current_dir()?.join(root)
2394 };
2395 let mut cursor = absolute.as_path();
2396 let mut suffix = Vec::new();
2397 while !cursor.exists() {
2398 let name = cursor.file_name().ok_or_else(|| {
2399 std::io::Error::new(
2400 std::io::ErrorKind::NotFound,
2401 format!("no existing ancestor for database root {}", root.display()),
2402 )
2403 })?;
2404 suffix.push(name.to_os_string());
2405 cursor = cursor.parent().ok_or_else(|| {
2406 std::io::Error::new(
2407 std::io::ErrorKind::NotFound,
2408 format!("no existing ancestor for database root {}", root.display()),
2409 )
2410 })?;
2411 }
2412 let lock_dir = cursor.canonicalize()?;
2413 let mut canonical = lock_dir.clone();
2414 for component in suffix.iter().rev() {
2415 canonical.push(component);
2416 }
2417 Ok((canonical, lock_dir))
2418 }
2419
2420 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<ExclusiveDatabaseLease> {
2421 use std::hash::{Hash, Hasher};
2422
2423 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
2424 let reservation =
2425 OpenReservation::acquire(DatabaseOpenKey::IntendedPath(canonical_path.clone()), root)?;
2426
2427 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2428 canonical_path.hash(&mut hasher);
2429 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
2430 let file = std::fs::OpenOptions::new()
2431 .create(true)
2432 .truncate(false)
2433 .write(true)
2434 .open(lock_path)?;
2435 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2436 return Err(MongrelError::DatabaseLocked {
2437 path: root.to_path_buf(),
2438 message: error.to_string(),
2439 });
2440 }
2441 Ok(reservation.into_lease(file, canonical_path))
2442 }
2443
2444 fn acquire_legacy_database_lock(
2445 lock: &mut ExclusiveDatabaseLease,
2446 root: &Path,
2447 timeout_ms: u32,
2448 ) -> Result<()> {
2449 let durable_root = lock
2450 .durable_root
2451 .as_ref()
2452 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
2453 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
2454 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2455 return Err(MongrelError::DatabaseLocked {
2456 path: root.to_path_buf(),
2457 message: error.to_string(),
2458 });
2459 }
2460 lock.legacy_file = Some(file);
2461 Ok(())
2462 }
2463
2464 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
2465 if path.exists() {
2466 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
2467 }
2468 let mut ancestor = path;
2469 while !ancestor.exists() {
2470 ancestor = ancestor.parent().ok_or_else(|| {
2471 MongrelError::NotFound(format!(
2472 "no existing ancestor for database root {}",
2473 path.display()
2474 ))
2475 })?;
2476 }
2477 let relative = path.strip_prefix(ancestor).map_err(|error| {
2478 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
2479 })?;
2480 crate::durable_file::DurableRoot::open(ancestor)?
2481 .create_directory_all_pinned(relative)
2482 .map_err(Into::into)
2483 }
2484
2485 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2486 let requested_root = root.as_ref();
2487 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
2488 let root = lock.canonical_path.clone();
2489 Self::reject_existing_database(&root)?;
2490 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
2491 if durable_root.canonical_path() != lock.canonical_path {
2492 return Err(MongrelError::Conflict(
2493 "database root changed while it was being created".into(),
2494 ));
2495 }
2496 lock.claim_root_identity(&durable_root)?;
2497 durable_root.create_directory_all(META_DIR)?;
2498 lock.durable_root = Some(durable_root);
2499 let io_root = lock
2500 .durable_root
2501 .as_ref()
2502 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2503 .io_path()?;
2504 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
2505 Self::reject_existing_database(&io_root)?;
2506 Ok((io_root, lock))
2507 }
2508
2509 fn begin_open(
2510 root: impl AsRef<Path>,
2511 lock_timeout_ms: u32,
2512 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2513 let root = root.as_ref();
2514 let canonical_root = root.canonicalize().map_err(|error| {
2515 if error.kind() == std::io::ErrorKind::NotFound {
2516 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
2517 } else {
2518 error.into()
2519 }
2520 })?;
2521 let durable_root = crate::durable_file::DurableRoot::open(&canonical_root)?;
2522 Self::begin_open_durable(durable_root, lock_timeout_ms)
2523 }
2524
2525 fn begin_open_durable(
2526 durable_root: crate::durable_file::DurableRoot,
2527 lock_timeout_ms: u32,
2528 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2529 let io_root = durable_root.io_path()?;
2530 let current_root = io_root.canonicalize()?;
2531 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
2532 lock.claim_root_identity(&durable_root)?;
2533 lock.durable_root = Some(Arc::new(durable_root));
2534 let io_root = lock
2535 .durable_root
2536 .as_ref()
2537 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2538 .io_path()?;
2539 if lock
2540 .durable_root
2541 .as_ref()
2542 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2543 .open_directory(META_DIR)
2544 .is_err()
2545 {
2546 return Err(MongrelError::NotFound(format!(
2547 "no database metadata found at {:?}",
2548 current_root
2549 )));
2550 }
2551 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
2552 Ok((io_root, lock))
2553 }
2554
2555 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
2557 let (root, lock) = Self::begin_create(root)?;
2558 Self::create_inner(
2559 root,
2560 None,
2561 lock,
2562 crate::storage_mode::StorageMode::Standalone,
2563 )
2564 }
2565
2566 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2569 let (root, lock) = Self::begin_create(root)?;
2570 let salt = crate::encryption::random_salt()?;
2571 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2572 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2573 Self::create_inner(
2574 root,
2575 Some(kek),
2576 lock,
2577 crate::storage_mode::StorageMode::Standalone,
2578 )
2579 }
2580
2581 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2584 let (root, lock) = Self::begin_create(root)?;
2585 let salt = crate::encryption::random_salt()?;
2586 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2587 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2588 Self::create_inner(
2589 root,
2590 Some(kek),
2591 lock,
2592 crate::storage_mode::StorageMode::Standalone,
2593 )
2594 }
2595
2596 pub fn create_with_kms(
2599 root: impl AsRef<Path>,
2600 provider: &dyn crate::security_hardening::KeyManagementProvider,
2601 kms_key_id: &str,
2602 ) -> Result<Self> {
2603 let (root, lock) = Self::begin_create(root)?;
2604 let kek = create_kms_kek(&root, provider, kms_key_id)?;
2605 Self::create_inner(
2606 root,
2607 Some(kek),
2608 lock,
2609 crate::storage_mode::StorageMode::Standalone,
2610 )
2611 }
2612
2613 pub fn create_cluster_replica(
2621 root: impl AsRef<Path>,
2622 cluster_id: mongreldb_types::ids::ClusterId,
2623 node_id: mongreldb_types::ids::NodeId,
2624 database_id: mongreldb_types::ids::DatabaseId,
2625 ) -> Result<Self> {
2626 let (root, lock) = Self::begin_create(root)?;
2627 Self::create_inner(
2628 root,
2629 None,
2630 lock,
2631 crate::storage_mode::StorageMode::ClusterReplica {
2632 cluster_id,
2633 node_id,
2634 database_id,
2635 },
2636 )
2637 }
2638
2639 pub fn open_cluster_replica(
2646 root: impl AsRef<Path>,
2647 expected: &crate::storage_mode::StorageMode,
2648 ) -> Result<Self> {
2649 let Some((cluster_id, node_id, database_id)) = expected.cluster_identity() else {
2650 return Err(MongrelError::InvalidArgument(format!(
2651 "open_cluster_replica requires a ClusterReplica identity, got {expected:?}"
2652 )));
2653 };
2654 let (root, lock) = Self::begin_open(root, 0)?;
2655 Self::open_inner_locked(
2656 root,
2657 None,
2658 lock,
2659 CoreResourceConfig::default(),
2660 OpenModeGate::ClusterRuntime {
2661 cluster_id,
2662 node_id,
2663 database_id,
2664 },
2665 )
2666 }
2667
2668 fn create_inner(
2669 root: PathBuf,
2670 kek: Option<Arc<crate::encryption::Kek>>,
2671 lock: ExclusiveDatabaseLease,
2672 storage_mode: crate::storage_mode::StorageMode,
2673 ) -> Result<Self> {
2674 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2675 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2676 let cat = Catalog::empty();
2677 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2678 Self::finish_open(
2679 root,
2680 cat,
2681 kek,
2682 meta_dek,
2683 false,
2684 None,
2685 None,
2686 None,
2687 lock,
2688 CoreResourceConfig::default(),
2689 OpenModeGate::Create(storage_mode),
2690 )
2691 }
2692
2693 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
2695 Self::open_inner(root, None, None)
2696 }
2697
2698 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2700 let (root, lock) = Self::begin_open(root, 0)?;
2701 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2702 MongrelError::Other("database root descriptor was not pinned".into())
2703 })?)?;
2704 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2705 Self::open_inner_locked(
2706 root,
2707 Some(kek),
2708 lock,
2709 CoreResourceConfig::default(),
2710 OpenModeGate::Normal,
2711 )
2712 }
2713
2714 pub fn open_encrypted_with_options(
2717 root: impl AsRef<Path>,
2718 passphrase: &str,
2719 options: OpenOptions,
2720 ) -> Result<Self> {
2721 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2722 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2723 MongrelError::Other("database root descriptor was not pinned".into())
2724 })?)?;
2725 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2726 let gate = if options.offline_validation {
2727 OpenModeGate::OfflineValidation
2728 } else {
2729 OpenModeGate::Normal
2730 };
2731 Self::open_inner_locked(
2732 root,
2733 Some(kek),
2734 lock,
2735 CoreResourceConfig::from_options(&options)?,
2736 gate,
2737 )
2738 }
2739
2740 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2742 let (root, lock) = Self::begin_open(root, 0)?;
2743 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2744 MongrelError::Other("database root descriptor was not pinned".into())
2745 })?)?;
2746 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2747 Self::open_inner_locked(
2748 root,
2749 Some(kek),
2750 lock,
2751 CoreResourceConfig::default(),
2752 OpenModeGate::Normal,
2753 )
2754 }
2755
2756 pub fn open_with_kms(
2759 root: impl AsRef<Path>,
2760 provider: &dyn crate::security_hardening::KeyManagementProvider,
2761 ) -> Result<Self> {
2762 Self::open_with_kms_and_options(root, provider, OpenOptions::default())
2763 }
2764
2765 pub fn open_with_kms_and_options(
2767 root: impl AsRef<Path>,
2768 provider: &dyn crate::security_hardening::KeyManagementProvider,
2769 options: OpenOptions,
2770 ) -> Result<Self> {
2771 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2772 let kek = open_kms_kek(
2773 lock.durable_root.as_deref().ok_or_else(|| {
2774 MongrelError::Other("database root descriptor was not pinned".into())
2775 })?,
2776 provider,
2777 )?;
2778 let gate = if options.offline_validation {
2779 OpenModeGate::OfflineValidation
2780 } else {
2781 OpenModeGate::Normal
2782 };
2783 Self::open_inner_locked(
2784 root,
2785 Some(kek),
2786 lock,
2787 CoreResourceConfig::from_options(&options)?,
2788 gate,
2789 )
2790 }
2791
2792 pub fn rotate_kms_key(
2796 &self,
2797 provider: &dyn crate::security_hardening::KeyManagementProvider,
2798 new_kms_key_id: &str,
2799 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2800 let _rotation_guard = self.core.kms_rotation_lock.lock();
2801 if new_kms_key_id.is_empty() {
2802 return Err(MongrelError::InvalidArgument(
2803 "KMS key id must not be empty".into(),
2804 ));
2805 }
2806 let durable_root = Arc::clone(&self.core.durable_root);
2807 let root = durable_root.io_path()?;
2808 let envelope = read_kms_key_envelope(&durable_root)?;
2809 if envelope.provider_id != provider.provider_id() {
2810 return Err(MongrelError::Encryption(format!(
2811 "KMS provider mismatch: database requires {:?}, got {:?}",
2812 envelope.provider_id,
2813 provider.provider_id()
2814 )));
2815 }
2816 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2817 if let Some(record) = journal
2818 .load()
2819 .map_err(|error| MongrelError::Encryption(error.to_string()))?
2820 .filter(|record| record.phase != crate::security_hardening::KeyRotationPhase::Succeeded)
2821 {
2822 if record.to_key_id != new_kms_key_id {
2823 return Err(MongrelError::Conflict(format!(
2824 "KMS rotation to {:?} is already in progress",
2825 record.to_key_id
2826 )));
2827 }
2828 return self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record);
2829 }
2830 let now_micros = std::time::SystemTime::now()
2831 .duration_since(std::time::UNIX_EPOCH)
2832 .unwrap_or_default()
2833 .as_micros() as u64;
2834 let mut record = crate::security_hardening::KeyRotationRecord::new(
2835 envelope.wrapped_key.key_version.clone(),
2836 String::new(),
2837 now_micros,
2838 );
2839 record.from_key_id = envelope.wrapped_key.kms_key_id.clone();
2840 record.to_key_id = new_kms_key_id.to_owned();
2841 persist_key_rotation(&journal, &record)?;
2842 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
2843 }
2844
2845 pub fn retry_kms_key_rotation(
2847 &self,
2848 provider: &dyn crate::security_hardening::KeyManagementProvider,
2849 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2850 let _rotation_guard = self.core.kms_rotation_lock.lock();
2851 let durable_root = Arc::clone(&self.core.durable_root);
2852 let root = durable_root.io_path()?;
2853 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2854 let mut record = journal
2855 .load()
2856 .map_err(|error| MongrelError::Encryption(error.to_string()))?
2857 .ok_or_else(|| MongrelError::NotFound("KMS rotation journal".into()))?;
2858 if record.phase != crate::security_hardening::KeyRotationPhase::Failed {
2859 return Err(MongrelError::Conflict(
2860 "KMS rotation is not in Failed state".into(),
2861 ));
2862 }
2863 record.retry();
2864 persist_key_rotation(&journal, &record)?;
2865 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
2866 }
2867
2868 fn run_kms_key_rotation(
2869 &self,
2870 provider: &dyn crate::security_hardening::KeyManagementProvider,
2871 durable_root: &crate::durable_file::DurableRoot,
2872 root: &Path,
2873 journal: &crate::security_hardening::KeyRotationJournal,
2874 mut record: crate::security_hardening::KeyRotationRecord,
2875 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2876 use crate::security_hardening::{KeyRotationPhase, KmsDatabaseKeyEnvelope};
2877 loop {
2878 match record.phase {
2879 KeyRotationPhase::Pending => advance_key_rotation(journal, &mut record)?,
2880 KeyRotationPhase::WrappingNewKey => {
2881 if record.staged_wrapped_key.is_none() {
2882 let current = read_kms_key_envelope(durable_root)?;
2883 let wrapped =
2884 match provider.rewrap_key(¤t.wrapped_key, &record.to_key_id) {
2885 Ok(wrapped) => wrapped,
2886 Err(error) => {
2887 return fail_key_rotation(journal, &mut record, error);
2888 }
2889 };
2890 record.to_version = wrapped.key_version.clone();
2891 record.staged_wrapped_key = Some(wrapped);
2892 persist_key_rotation(journal, &record)?;
2893 }
2894 advance_key_rotation(journal, &mut record)?;
2895 }
2896 KeyRotationPhase::DualRead => {
2897 let old_envelope = read_kms_key_envelope(durable_root)?;
2898 let new_envelope = KmsDatabaseKeyEnvelope {
2899 provider_id: provider.provider_id().to_owned(),
2900 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
2901 MongrelError::Encryption(
2902 "KMS rotation has no staged wrapped key".into(),
2903 )
2904 })?,
2905 };
2906 let old_raw = match unwrap_kms_database_key(provider, &old_envelope) {
2907 Ok(key) => key,
2908 Err(error) => {
2909 return fail_key_rotation(journal, &mut record, error);
2910 }
2911 };
2912 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
2913 Ok(key) => key,
2914 Err(error) => {
2915 return fail_key_rotation(journal, &mut record, error);
2916 }
2917 };
2918 if !bool::from(old_raw.as_slice().ct_eq(new_raw.as_slice())) {
2919 return fail_key_rotation(
2920 journal,
2921 &mut record,
2922 "KMS rewrap changed the database root key",
2923 );
2924 }
2925 advance_key_rotation(journal, &mut record)?;
2926 }
2927 KeyRotationPhase::Reencrypting => {
2928 advance_key_rotation(journal, &mut record)?;
2931 }
2932 KeyRotationPhase::Validating => {
2933 let new_envelope = KmsDatabaseKeyEnvelope {
2934 provider_id: provider.provider_id().to_owned(),
2935 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
2936 MongrelError::Encryption(
2937 "KMS rotation has no staged wrapped key".into(),
2938 )
2939 })?,
2940 };
2941 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
2942 Ok(key) => key,
2943 Err(error) => {
2944 return fail_key_rotation(journal, &mut record, error);
2945 }
2946 };
2947 let salt = read_encryption_salt(durable_root)?;
2948 let candidate = crate::encryption::Kek::from_raw_key(new_raw.as_ref(), &salt)?;
2949 let candidate_meta = candidate.derive_meta_key();
2950 let Some(active_meta) = self.core.meta_dek.as_ref() else {
2951 return fail_key_rotation(
2952 journal,
2953 &mut record,
2954 "KMS metadata exists on a plaintext database",
2955 );
2956 };
2957 if !bool::from(candidate_meta.as_ref().ct_eq(active_meta.as_slice())) {
2958 return fail_key_rotation(
2959 journal,
2960 &mut record,
2961 "KMS rewrap validation failed",
2962 );
2963 }
2964 advance_key_rotation(journal, &mut record)?;
2965 }
2966 KeyRotationPhase::Published => {
2967 let envelope = KmsDatabaseKeyEnvelope {
2968 provider_id: provider.provider_id().to_owned(),
2969 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
2970 MongrelError::Encryption(
2971 "KMS rotation has no staged wrapped key".into(),
2972 )
2973 })?,
2974 };
2975 write_kms_key_envelope(root, &envelope)?;
2976 advance_key_rotation(journal, &mut record)?;
2977 }
2978 KeyRotationPhase::RetiringOldKey => {
2979 advance_key_rotation(journal, &mut record)?;
2980 }
2981 KeyRotationPhase::Succeeded => return Ok(record),
2982 KeyRotationPhase::Failed => {
2983 return Err(MongrelError::Encryption(
2984 "failed KMS rotation must be explicitly retried".into(),
2985 ));
2986 }
2987 }
2988 }
2989 }
2990
2991 pub fn open_with_credentials(
3003 root: impl AsRef<Path>,
3004 username: &str,
3005 password: &str,
3006 ) -> Result<Self> {
3007 Self::open_inner_with_credentials(root, None, username, password)
3008 }
3009
3010 pub fn open_with_credentials_and_options(
3014 root: impl AsRef<Path>,
3015 username: &str,
3016 password: &str,
3017 options: OpenOptions,
3018 ) -> Result<Self> {
3019 let gate = if options.offline_validation {
3020 OpenModeGate::OfflineValidation
3021 } else {
3022 OpenModeGate::Normal
3023 };
3024 Self::open_inner_with_credentials_and_lock_timeout(
3025 root,
3026 None,
3027 username,
3028 password,
3029 options.lock_timeout_ms,
3030 CoreResourceConfig::from_options(&options)?,
3031 gate,
3032 )
3033 }
3034
3035 pub fn open_encrypted_with_credentials(
3038 root: impl AsRef<Path>,
3039 passphrase: &str,
3040 username: &str,
3041 password: &str,
3042 ) -> Result<Self> {
3043 let (root, lock) = Self::begin_open(root, 0)?;
3044 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3045 MongrelError::Other("database root descriptor was not pinned".into())
3046 })?)?;
3047 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3048 Self::open_inner_with_credentials_locked(
3049 root,
3050 Some(kek),
3051 username,
3052 password,
3053 lock,
3054 CoreResourceConfig::default(),
3055 OpenModeGate::Normal,
3056 )
3057 }
3058
3059 pub fn open_encrypted_with_credentials_and_options(
3063 root: impl AsRef<Path>,
3064 passphrase: &str,
3065 username: &str,
3066 password: &str,
3067 options: OpenOptions,
3068 ) -> Result<Self> {
3069 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
3070 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3071 MongrelError::Other("database root descriptor was not pinned".into())
3072 })?)?;
3073 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3074 let gate = if options.offline_validation {
3075 OpenModeGate::OfflineValidation
3076 } else {
3077 OpenModeGate::Normal
3078 };
3079 Self::open_inner_with_credentials_locked(
3080 root,
3081 Some(kek),
3082 username,
3083 password,
3084 lock,
3085 CoreResourceConfig::from_options(&options)?,
3086 gate,
3087 )
3088 }
3089
3090 pub fn open_with_kms_and_credentials(
3092 root: impl AsRef<Path>,
3093 provider: &dyn crate::security_hardening::KeyManagementProvider,
3094 username: &str,
3095 password: &str,
3096 ) -> Result<Self> {
3097 let (root, lock) = Self::begin_open(root, 0)?;
3098 let kek = open_kms_kek(
3099 lock.durable_root.as_deref().ok_or_else(|| {
3100 MongrelError::Other("database root descriptor was not pinned".into())
3101 })?,
3102 provider,
3103 )?;
3104 Self::open_inner_with_credentials_locked(
3105 root,
3106 Some(kek),
3107 username,
3108 password,
3109 lock,
3110 CoreResourceConfig::default(),
3111 OpenModeGate::Normal,
3112 )
3113 }
3114
3115 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
3122 let gate = if options.offline_validation {
3125 OpenModeGate::OfflineValidation
3126 } else {
3127 OpenModeGate::Normal
3128 };
3129 Self::open_inner_with_lock_timeout(
3130 root,
3131 None,
3132 None,
3133 options.lock_timeout_ms,
3134 CoreResourceConfig::from_options(&options)?,
3135 gate,
3136 )
3137 }
3138
3139 fn open_inner_with_lock_timeout(
3140 root: impl AsRef<Path>,
3141 kek: Option<Arc<crate::encryption::Kek>>,
3142 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3143 lock_timeout_ms: u32,
3144 resources: CoreResourceConfig,
3145 mode_gate: OpenModeGate,
3146 ) -> Result<Self> {
3147 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3148 Self::open_inner_locked(root, kek, lock, resources, mode_gate)
3149 }
3150
3151 fn open_inner_locked(
3152 root: PathBuf,
3153 kek: Option<Arc<crate::encryption::Kek>>,
3154 lock: ExclusiveDatabaseLease,
3155 resources: CoreResourceConfig,
3156 mode_gate: OpenModeGate,
3157 ) -> Result<Self> {
3158 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3159 let mut cat = catalog::read_durable(
3160 lock.durable_root.as_deref().ok_or_else(|| {
3161 MongrelError::Other("database root descriptor was not pinned".into())
3162 })?,
3163 meta_dek.as_ref(),
3164 )?
3165 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3166 let recovery_checkpoint = cat.clone();
3167
3168 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3171 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3172 lock.durable_root.as_deref().ok_or_else(|| {
3173 MongrelError::Other("database root descriptor was not pinned".into())
3174 })?,
3175 wal_dek.as_ref(),
3176 )?;
3177 recover_ddl_from_records(
3178 &root,
3179 Some(lock.durable_root.as_deref().ok_or_else(|| {
3180 MongrelError::Other("database root descriptor was not pinned".into())
3181 })?),
3182 &mut cat,
3183 meta_dek.as_ref(),
3184 false,
3185 None,
3186 &recovery_records,
3187 )?;
3188 Self::finish_open(
3189 root,
3190 cat,
3191 kek,
3192 meta_dek,
3193 true,
3194 Some(recovery_checkpoint),
3195 Some(recovery_records),
3196 None,
3197 lock,
3198 resources,
3199 mode_gate,
3200 )
3201 }
3202
3203 fn open_inner_with_credentials(
3210 root: impl AsRef<Path>,
3211 kek: Option<Arc<crate::encryption::Kek>>,
3212 username: &str,
3213 password: &str,
3214 ) -> Result<Self> {
3215 Self::open_inner_with_credentials_and_lock_timeout(
3216 root,
3217 kek,
3218 username,
3219 password,
3220 0,
3221 CoreResourceConfig::default(),
3222 OpenModeGate::Normal,
3223 )
3224 }
3225
3226 #[allow(clippy::too_many_arguments)]
3230 fn open_inner_with_credentials_and_lock_timeout(
3231 root: impl AsRef<Path>,
3232 kek: Option<Arc<crate::encryption::Kek>>,
3233 username: &str,
3234 password: &str,
3235 lock_timeout_ms: u32,
3236 resources: CoreResourceConfig,
3237 mode_gate: OpenModeGate,
3238 ) -> Result<Self> {
3239 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3240 Self::open_inner_with_credentials_locked(
3241 root, kek, username, password, lock, resources, mode_gate,
3242 )
3243 }
3244
3245 #[allow(clippy::too_many_arguments)]
3246 fn open_inner_with_credentials_locked(
3247 root: PathBuf,
3248 kek: Option<Arc<crate::encryption::Kek>>,
3249 username: &str,
3250 password: &str,
3251 lock: ExclusiveDatabaseLease,
3252 resources: CoreResourceConfig,
3253 mode_gate: OpenModeGate,
3254 ) -> Result<Self> {
3255 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3256 let mut cat = catalog::read_durable(
3257 lock.durable_root.as_deref().ok_or_else(|| {
3258 MongrelError::Other("database root descriptor was not pinned".into())
3259 })?,
3260 meta_dek.as_ref(),
3261 )?
3262 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3263 let recovery_checkpoint = cat.clone();
3264
3265 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3268 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3269 lock.durable_root.as_deref().ok_or_else(|| {
3270 MongrelError::Other("database root descriptor was not pinned".into())
3271 })?,
3272 wal_dek.as_ref(),
3273 )?;
3274 recover_ddl_from_records(
3275 &root,
3276 Some(lock.durable_root.as_deref().ok_or_else(|| {
3277 MongrelError::Other("database root descriptor was not pinned".into())
3278 })?),
3279 &mut cat,
3280 meta_dek.as_ref(),
3281 false,
3282 None,
3283 &recovery_records,
3284 )?;
3285
3286 if !cat.require_auth {
3289 return Err(MongrelError::AuthNotRequired);
3290 }
3291
3292 let user = cat
3297 .users
3298 .iter()
3299 .find(|u| u.username == username)
3300 .filter(|u| !u.password_hash.is_empty())
3301 .ok_or_else(|| MongrelError::InvalidCredentials {
3302 username: username.to_string(),
3303 })?;
3304 let password_ok = crate::auth::verify_password(password, &user.password_hash)
3305 .map_err(MongrelError::Other)?;
3306 if !password_ok {
3307 return Err(MongrelError::InvalidCredentials {
3308 username: username.to_string(),
3309 });
3310 }
3311
3312 let principal =
3314 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
3315 MongrelError::InvalidCredentials {
3316 username: username.to_string(),
3317 }
3318 })?;
3319
3320 Self::finish_open(
3321 root,
3322 cat,
3323 kek,
3324 meta_dek,
3325 true,
3326 Some(recovery_checkpoint),
3327 Some(recovery_records),
3328 Some(principal),
3329 lock,
3330 resources,
3331 mode_gate,
3332 )
3333 }
3334
3335 pub fn create_with_credentials(
3345 root: impl AsRef<Path>,
3346 admin_username: &str,
3347 admin_password: &str,
3348 ) -> Result<Self> {
3349 let (root, lock) = Self::begin_create(root)?;
3350 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
3351 }
3352
3353 pub fn create_encrypted_with_credentials(
3357 root: impl AsRef<Path>,
3358 passphrase: &str,
3359 admin_username: &str,
3360 admin_password: &str,
3361 ) -> Result<Self> {
3362 let (root, lock) = Self::begin_create(root)?;
3363 let salt = crate::encryption::random_salt()?;
3364 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
3365 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3366 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3367 }
3368
3369 pub fn create_with_kms_and_credentials(
3371 root: impl AsRef<Path>,
3372 provider: &dyn crate::security_hardening::KeyManagementProvider,
3373 kms_key_id: &str,
3374 admin_username: &str,
3375 admin_password: &str,
3376 ) -> Result<Self> {
3377 let (root, lock) = Self::begin_create(root)?;
3378 let kek = create_kms_kek(&root, provider, kms_key_id)?;
3379 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3380 }
3381
3382 fn create_inner_with_credentials(
3383 root: PathBuf,
3384 kek: Option<Arc<crate::encryption::Kek>>,
3385 admin_username: &str,
3386 admin_password: &str,
3387 lock: ExclusiveDatabaseLease,
3388 ) -> Result<Self> {
3389 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
3390 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3391
3392 let password_hash =
3394 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
3395 let scram_sha_256 =
3396 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
3397 let mysql_caching_sha2 =
3398 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
3399 let mut cat = Catalog::empty();
3400 cat.require_auth = true;
3401 cat.next_user_id = 2;
3402 cat.users.push(crate::auth::UserEntry {
3403 id: 1,
3404 username: admin_username.to_string(),
3405 password_hash,
3406 scram_sha_256: Some(scram_sha_256),
3407 mysql_caching_sha2: Some(mysql_caching_sha2),
3408 roles: Vec::new(),
3409 is_admin: true,
3410 created_epoch: 0,
3411 });
3412 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3413
3414 let admin_principal = crate::auth::Principal {
3417 user_id: 1,
3418 created_epoch: 0,
3419 username: admin_username.to_string(),
3420 is_admin: true,
3421 roles: Vec::new(),
3422 permissions: Vec::new(),
3423 };
3424 Self::finish_open(
3425 root,
3426 cat,
3427 kek,
3428 meta_dek,
3429 false,
3430 None,
3431 None,
3432 Some(admin_principal),
3433 lock,
3434 CoreResourceConfig::default(),
3435 OpenModeGate::Create(crate::storage_mode::StorageMode::Standalone),
3436 )
3437 }
3438
3439 fn reject_existing_database(root: &Path) -> Result<()> {
3440 if root.join(catalog::CATALOG_FILENAME).exists() {
3443 return Err(MongrelError::InvalidArgument(format!(
3444 "database already exists at {}; use Database::open() to open it, \
3445 or remove the directory first",
3446 root.display()
3447 )));
3448 }
3449 Ok(())
3450 }
3451
3452 fn open_inner(
3453 root: impl AsRef<Path>,
3454 kek: Option<Arc<crate::encryption::Kek>>,
3455 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3456 ) -> Result<Self> {
3457 Self::open_inner_with_lock_timeout(
3458 root,
3459 kek,
3460 None,
3461 0,
3462 CoreResourceConfig::default(),
3463 OpenModeGate::Normal,
3464 )
3465 }
3466
3467 pub(crate) fn open_offline_validation(root: impl AsRef<Path>) -> Result<Self> {
3470 Self::open_inner_with_lock_timeout(
3471 root,
3472 None,
3473 None,
3474 0,
3475 CoreResourceConfig::default(),
3476 OpenModeGate::OfflineValidation,
3477 )
3478 }
3479
3480 pub(crate) fn open_replica_recovery_durable(
3484 root: &crate::durable_file::DurableRoot,
3485 ) -> Result<Self> {
3486 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3487 Self::open_replica_recovery_inner(root, None, lock)
3488 }
3489
3490 pub(crate) fn open_encrypted_replica_recovery_durable(
3491 root: &crate::durable_file::DurableRoot,
3492 passphrase: &str,
3493 ) -> Result<Self> {
3494 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3495 let salt = read_encryption_salt(root)?;
3496 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3497 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
3498 }
3499
3500 fn open_replica_recovery_inner(
3501 root: PathBuf,
3502 kek: Option<Arc<crate::encryption::Kek>>,
3503 lock: ExclusiveDatabaseLease,
3504 ) -> Result<Self> {
3505 if !root.join(META_DIR).join("replica").is_file() {
3506 return Err(MongrelError::InvalidArgument(
3507 "recovery auth bypass requires a marked replica staging directory".into(),
3508 ));
3509 }
3510 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3511 let mut cat = catalog::read_durable(
3512 lock.durable_root.as_deref().ok_or_else(|| {
3513 MongrelError::Other("database root descriptor was not pinned".into())
3514 })?,
3515 meta_dek.as_ref(),
3516 )?
3517 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3518 let recovery_checkpoint = cat.clone();
3519 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3520 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3521 lock.durable_root.as_deref().ok_or_else(|| {
3522 MongrelError::Other("database root descriptor was not pinned".into())
3523 })?,
3524 wal_dek.as_ref(),
3525 )?;
3526 recover_ddl_from_records(
3527 &root,
3528 Some(lock.durable_root.as_deref().ok_or_else(|| {
3529 MongrelError::Other("database root descriptor was not pinned".into())
3530 })?),
3531 &mut cat,
3532 meta_dek.as_ref(),
3533 false,
3534 None,
3535 &recovery_records,
3536 )?;
3537 let principal = if cat.require_auth {
3538 cat.users
3539 .iter()
3540 .find(|user| user.is_admin)
3541 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
3542 .ok_or_else(|| {
3543 MongrelError::Schema(
3544 "authenticated replica catalog has no recoverable admin".into(),
3545 )
3546 })?
3547 .into()
3548 } else {
3549 None
3550 };
3551 Self::finish_open(
3552 root,
3553 cat,
3554 kek,
3555 meta_dek,
3556 true,
3557 Some(recovery_checkpoint),
3558 Some(recovery_records),
3559 principal,
3560 lock,
3561 CoreResourceConfig::default(),
3562 OpenModeGate::Normal,
3563 )
3564 }
3565
3566 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
3578 use fs2::FileExt;
3579 if timeout_ms == 0 {
3580 return f.try_lock_exclusive();
3581 }
3582 let deadline =
3584 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
3585 let mut next_sleep = std::time::Duration::from_millis(1);
3586 let mut recorded_wait = false;
3587 loop {
3588 match f.try_lock_exclusive() {
3589 Ok(()) => return Ok(()),
3590 Err(e) if Self::is_file_lock_contended(&e) => {
3591 if !recorded_wait {
3592 DATABASE_OPEN_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
3593 recorded_wait = true;
3594 }
3595 let now = std::time::Instant::now();
3596 if now >= deadline {
3597 return Err(std::io::Error::new(
3598 std::io::ErrorKind::WouldBlock,
3599 format!("could not acquire database lock within {timeout_ms}ms"),
3600 ));
3601 }
3602 let remaining = deadline - now;
3603 let sleep = next_sleep.min(remaining);
3604 std::thread::sleep(sleep);
3605 next_sleep = next_sleep
3608 .saturating_mul(10)
3609 .min(std::time::Duration::from_millis(50));
3610 }
3611 Err(e) => return Err(e),
3612 }
3613 }
3614 }
3615
3616 fn is_file_lock_contended(error: &std::io::Error) -> bool {
3617 if error.kind() == std::io::ErrorKind::WouldBlock {
3618 return true;
3619 }
3620 #[cfg(windows)]
3621 {
3622 return error.raw_os_error() == Some(33);
3625 }
3626 #[cfg(not(windows))]
3627 false
3628 }
3629
3630 #[allow(clippy::too_many_arguments)]
3631 fn finish_open(
3632 root: PathBuf,
3633 cat: Catalog,
3634 kek: Option<Arc<crate::encryption::Kek>>,
3635 meta_dek: Option<[u8; META_DEK_LEN]>,
3636 existing: bool,
3637 recovery_checkpoint: Option<Catalog>,
3638 recovery_records: Option<Vec<crate::wal::Record>>,
3639 principal: Option<crate::auth::Principal>,
3640 lock: ExclusiveDatabaseLease,
3641 resources: CoreResourceConfig,
3642 mode_gate: OpenModeGate,
3643 ) -> Result<Self> {
3644 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
3645 MongrelError::Other("database root descriptor was not pinned".into())
3646 })?);
3647 let storage_mode = if existing {
3652 crate::storage_mode::read(&durable_root)?
3653 } else {
3654 None
3655 };
3656 match (&mode_gate, &storage_mode) {
3657 (OpenModeGate::Create(_), _) => {}
3658 (OpenModeGate::Normal | OpenModeGate::SharedCore, mode) => {
3659 crate::storage_mode::check_open(mode.as_ref(), false)?
3660 }
3661 (OpenModeGate::OfflineValidation, mode) => {
3662 crate::storage_mode::check_open(mode.as_ref(), true)?
3663 }
3664 (
3665 OpenModeGate::ClusterRuntime {
3666 cluster_id,
3667 node_id,
3668 database_id,
3669 },
3670 Some(actual),
3671 ) => {
3672 let expected = crate::storage_mode::StorageMode::ClusterReplica {
3673 cluster_id: *cluster_id,
3674 node_id: *node_id,
3675 database_id: *database_id,
3676 };
3677 if *actual != expected {
3678 return Err(
3679 crate::storage_mode::StorageModeError::IdentityMismatch(format!(
3680 "expected {expected:?}, marker holds {actual:?}"
3681 ))
3682 .into(),
3683 );
3684 }
3685 }
3686 (OpenModeGate::ClusterRuntime { .. }, None) => {
3687 return Err(crate::storage_mode::StorageModeError::IdentityMismatch(
3688 "expected a ClusterReplica marker; directory has none".into(),
3689 )
3690 .into());
3691 }
3692 }
3693 let read_only = match &mode_gate {
3694 OpenModeGate::OfflineValidation | OpenModeGate::ClusterRuntime { .. } => true,
3695 OpenModeGate::Create(mode) => mode.cluster_identity().is_some(),
3698 OpenModeGate::Normal | OpenModeGate::SharedCore => false,
3699 } || if existing {
3700 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
3701 Ok(_) => true,
3702 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
3703 Err(error) => return Err(error.into()),
3704 }
3705 } else {
3706 false
3707 };
3708 let recovered_catalog = cat;
3709 let mut cat = recovered_catalog.clone();
3710 let abandoned = if existing && !read_only {
3711 let abandoned = cat
3712 .tables
3713 .iter()
3714 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
3715 .map(|entry| entry.table_id)
3716 .collect::<Vec<_>>();
3717 for entry in &mut cat.tables {
3718 if abandoned.contains(&entry.table_id) {
3719 entry.state = TableState::Dropped {
3720 at_epoch: cat.db_epoch,
3721 };
3722 }
3723 }
3724 abandoned
3725 } else {
3726 Vec::new()
3727 };
3728 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3729 let recovery_records = match (existing, recovery_records) {
3730 (true, Some(records)) => records,
3731 (true, None) => {
3732 return Err(MongrelError::Other(
3733 "existing open has no validated WAL recovery plan".into(),
3734 ))
3735 }
3736 (false, _) => Vec::new(),
3737 };
3738 let (history_epochs, history_start) =
3739 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
3740 let open_generation = if existing {
3741 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
3742 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3743 })?;
3744 let recovered_table_ids = cat
3745 .tables
3746 .iter()
3747 .filter(|entry| {
3748 checkpoint
3749 .tables
3750 .iter()
3751 .all(|checkpoint| checkpoint.table_id != entry.table_id)
3752 })
3753 .map(|entry| entry.table_id)
3754 .collect::<HashSet<_>>();
3755 let reconciled_table_ids = cat
3756 .tables
3757 .iter()
3758 .filter(|entry| {
3759 checkpoint
3760 .tables
3761 .iter()
3762 .find(|checkpoint| checkpoint.table_id == entry.table_id)
3763 .is_some_and(|checkpoint| {
3764 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
3765 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
3766 })
3767 })
3768 .map(|entry| entry.table_id)
3769 .collect::<HashSet<_>>();
3770 validate_shared_wal_recovery_plan(
3771 &durable_root,
3772 &cat,
3773 &recovered_table_ids,
3774 &reconciled_table_ids,
3775 meta_dek.as_ref(),
3776 kek.clone(),
3777 &recovery_records,
3778 )?;
3779 let retained_generation = recovery_records
3780 .iter()
3781 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
3782 .map(|record| record.txn_id >> 32)
3783 .max()
3784 .unwrap_or(0);
3785 let head_generation =
3786 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
3787 let durable_floor = match head_generation {
3788 Some(head) if retained_generation > head => {
3789 return Err(MongrelError::CorruptWal {
3790 offset: retained_generation,
3791 reason: format!(
3792 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
3793 ),
3794 })
3795 }
3796 Some(head) => head,
3797 None => retained_generation,
3798 };
3799 let stored = catalog::read_generation(&durable_root)?;
3800 if stored.is_some_and(|generation| generation < durable_floor) {
3801 return Err(MongrelError::Other(format!(
3802 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
3803 )));
3804 }
3805 let bumped = stored
3806 .unwrap_or(durable_floor)
3807 .max(durable_floor)
3808 .checked_add(1)
3809 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
3810 if bumped > u32::MAX as u64 {
3811 return Err(MongrelError::Full(
3812 "open-generation namespace exhausted".into(),
3813 ));
3814 }
3815 bumped
3816 } else {
3817 0
3818 };
3819 let principal = if cat.require_auth && !matches!(&mode_gate, OpenModeGate::SharedCore) {
3820 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3821 Some(
3822 Self::resolve_bound_principal_from_catalog(&cat, supplied)
3823 .ok_or(MongrelError::AuthRequired)?,
3824 )
3825 } else {
3826 principal
3827 };
3828 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
3829 if existing {
3830 for entry in &cat.tables {
3831 if !matches!(entry.state, TableState::Live) {
3832 continue;
3833 }
3834 match durable_root
3835 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
3836 {
3837 Ok(root) => {
3838 table_roots.insert(entry.table_id, Arc::new(root));
3839 }
3840 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3841 Err(error) => return Err(error.into()),
3842 }
3843 }
3844 }
3845
3846 if existing {
3850 let mut applied = recovery_checkpoint.ok_or_else(|| {
3851 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3852 })?;
3853 recover_ddl_from_records(
3854 &root,
3855 Some(&durable_root),
3856 &mut applied,
3857 meta_dek.as_ref(),
3858 true,
3859 Some(&table_roots),
3860 &recovery_records,
3861 )?;
3862 let catalog_value = |catalog: &Catalog| {
3863 serde_json::to_value(catalog)
3864 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
3865 };
3866 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
3867 return Err(MongrelError::CorruptWal {
3868 offset: 0,
3869 reason: "validated and applied DDL recovery plans differ".into(),
3870 });
3871 }
3872 if catalog_value(&cat)? != catalog_value(&applied)? {
3873 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3874 }
3875 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
3876 if !read_only {
3877 sweep_unreferenced_table_dirs(&root, &cat)?;
3878 }
3879 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
3880 Ok(()) => {}
3881 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3882 Err(error) => return Err(error.into()),
3883 }
3884 }
3885
3886 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
3887 let snapshots = Arc::new(SnapshotRegistry::new());
3888 snapshots.configure_history(history_epochs, history_start);
3889 let memory_governor = crate::memory::MemoryGovernor::new(
3894 crate::memory::GovernorConfig::new(resources.memory_budget_bytes),
3895 )
3896 .map_err(|error| MongrelError::InvalidArgument(format!("memory governor: {error}")))?;
3897 let spill_manager = crate::spill::SpillManager::open(
3901 &durable_root,
3902 crate::spill::SpillConfig::new(resources.temp_disk_budget_bytes),
3903 meta_dek,
3904 )?;
3905 let job_registry = Arc::new(crate::jobs::JobRegistry::open(&root, meta_dek.as_ref())?);
3909 let page_cache = Arc::new(crate::cache::Sharded::new(
3910 crate::cache::CACHE_SHARDS,
3911 || {
3912 crate::cache::PageCache::new(
3913 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3914 )
3915 .with_governor(
3916 memory_governor.clone(),
3917 crate::memory::MemoryClass::PageCache,
3918 )
3919 },
3920 ));
3921 memory_governor.register_reclaimable(&page_cache);
3922 let decoded_cache = Arc::new(crate::cache::Sharded::new(
3923 crate::cache::CACHE_SHARDS,
3924 || {
3925 crate::cache::DecodedPageCache::new(
3926 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
3927 )
3928 .with_governor(
3929 memory_governor.clone(),
3930 crate::memory::MemoryClass::DecodedCache,
3931 )
3932 },
3933 ));
3934 memory_governor.register_reclaimable(&decoded_cache);
3935 let commit_lock = Arc::new(Mutex::new(()));
3936 let shared_wal = Arc::new(Mutex::new(if existing {
3937 crate::wal::SharedWal::open_durable_root_validated(
3938 Arc::clone(&durable_root),
3939 Epoch(cat.db_epoch),
3940 wal_dek.clone(),
3941 Some(&recovery_records),
3942 )?
3943 } else {
3944 crate::wal::SharedWal::create_with_durable_root(
3945 Arc::clone(&durable_root),
3946 Epoch(cat.db_epoch),
3947 wal_dek.clone(),
3948 )?
3949 }));
3950 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
3954 let lifecycle = Arc::new(crate::core::LifecycleController::new());
3959 let group = Arc::new(
3960 crate::txn::GroupCommit::new(shared_wal.lock().durable_seq())
3961 .with_lifecycle(Arc::clone(&lifecycle)),
3962 );
3963 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
3964 let txn_ids = Arc::new(Mutex::new(1u64));
3968 let _ = abandoned;
3969 let hlc = Arc::new(mongreldb_types::hlc::HlcClock::new(
3976 0,
3977 std::time::Duration::from_millis(500),
3978 ));
3979 let idempotency = crate::txn::IdempotencyLedger::open(Arc::clone(&durable_root), meta_dek)?;
3982 let standalone_commit_log = Arc::new(crate::commit_log::StandaloneCommitLog::new(
3983 Arc::clone(&shared_wal),
3984 Arc::clone(&group),
3985 Arc::clone(&epoch),
3986 Arc::clone(&commit_lock),
3987 Arc::clone(&txn_ids),
3988 root.clone(),
3989 wal_dek.clone(),
3990 Arc::clone(&hlc),
3991 ));
3992 let commit_log: Arc<dyn mongreldb_log::CommitLog> = standalone_commit_log.clone();
3993
3994 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
3999 let security_coordinator = security_coordinator(&root, cat.security_version);
4000 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> =
4001 if matches!(&mode_gate, OpenModeGate::SharedCore) {
4002 None
4006 } else {
4007 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
4008 auth_state.clone(),
4009 )))
4010 };
4011
4012 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
4018 for entry in &cat.tables {
4019 if !matches!(entry.state, TableState::Live) {
4020 continue;
4021 }
4022 let table_root = match table_roots.remove(&entry.table_id) {
4023 Some(root) => root,
4024 None => Arc::new(
4025 durable_root
4026 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
4027 ),
4028 };
4029 let tdir = table_root.io_path()?;
4030 let ctx = SharedCtx {
4031 root_guard: Some(table_root),
4032 epoch: Arc::clone(&epoch),
4033 page_cache: Arc::clone(&page_cache),
4034 decoded_cache: Arc::clone(&decoded_cache),
4035 snapshots: Arc::clone(&snapshots),
4036 kek: kek.clone(),
4037 commit_lock: Arc::clone(&commit_lock),
4038 shared: Some(crate::engine::SharedWalCtx {
4039 wal: Arc::clone(&shared_wal),
4040 group: Arc::clone(&group),
4041 poisoned: Arc::clone(&poisoned),
4042 txn_ids: Arc::clone(&txn_ids),
4043 change_wake: change_wake.clone(),
4044 lifecycle: Arc::clone(&lifecycle),
4045 hlc: Arc::clone(&hlc),
4046 }),
4047 table_name: Some(entry.name.clone()),
4048 auth: auth_checker.clone(),
4049 read_only,
4050 };
4051 let t = Table::open_in(&tdir, ctx)?;
4052 tables.insert(entry.table_id, TableHandle::new(t));
4053 }
4054
4055 if existing {
4061 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
4062 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
4063 if read_only {
4064 crate::replication::reconcile_replica_epoch_durable(
4065 &durable_root,
4066 epoch.visible().0,
4067 )?;
4068 }
4069 sweep_pending_txn_dirs(&root, &cat);
4072 }
4073
4074 catalog::write_generation(&durable_root, open_generation)?;
4076 match &mode_gate {
4081 OpenModeGate::Create(mode) => crate::storage_mode::write(&durable_root, mode)?,
4082 _ if existing && storage_mode.is_none() => {
4083 crate::storage_mode::write(
4084 &durable_root,
4085 &crate::storage_mode::StorageMode::Standalone,
4086 )?;
4087 }
4088 _ => {}
4089 }
4090 shared_wal.lock().seal_open_generation(open_generation)?;
4091 crate::replication::replication_identity_durable(&durable_root)?;
4092 let next_txn_id = (open_generation << 32) | 1;
4093 *txn_ids.lock() = next_txn_id;
4095 let mut lock = lock;
4096 lock.mark_open()?;
4097
4098 lifecycle.mark_open();
4102 let core = DatabaseCore {
4103 root,
4104 durable_root,
4105 lifecycle,
4106 registry: std::sync::OnceLock::new(),
4107 read_only,
4108 catalog: RwLock::new(cat),
4109 security_coordinator,
4110 security_catalog_disk_reads: AtomicU64::new(0),
4111 rls_cache: Mutex::new(RlsCache::default()),
4112 epoch,
4113 snapshots,
4114 memory_governor,
4115 page_cache,
4116 decoded_cache,
4117 spill_manager,
4118 resource_groups: crate::resource::ResourceGroupRegistry::with_defaults(),
4119 embedding_providers: crate::embedding::EmbeddingProviderRegistry::new(),
4120 service_principals: crate::service_principal::ServicePrincipalStore::new(),
4121 job_registry,
4122 commit_lock,
4123 kms_rotation_lock: Mutex::new(()),
4124 shared_wal,
4125 next_txn_id: txn_ids,
4126 tables: RwLock::new(tables),
4127 kek,
4128 ddl_lock: Mutex::new(()),
4129 meta_dek,
4130 conflicts: crate::txn::ConflictIndex::new(),
4131 active_txns: crate::txn::ActiveTxns::new(),
4132 lock_manager: Arc::new(crate::locks::LockManager::new()),
4133 poisoned,
4134 group,
4135 commit_log: RwLock::new(commit_log),
4136 standalone_commit_log,
4137 hlc,
4138 idempotency,
4139 commit_ts_ledger: Mutex::new(commit_ts_ledger_from_recovery(&recovery_records)),
4140 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
4141 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
4142 replication_barrier: parking_lot::RwLock::new(()),
4143 replication_wal_retention_segments: AtomicUsize::new(0),
4144 backup_pins: Arc::new(Mutex::new(HashMap::new())),
4145 spill_hook: Mutex::new(None),
4146 security_commit_hook: Mutex::new(None),
4147 catalog_commit_hook: Mutex::new(None),
4148 backup_hook: Mutex::new(None),
4149 fk_lock_hook: Mutex::new(None),
4150 replication_hook: Mutex::new(None),
4151 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
4152 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
4153 trigger_max_loop_iterations: AtomicU32::new(
4154 TriggerConfig::default().max_loop_iterations,
4155 ),
4156 notify: {
4157 let (tx, _rx) = tokio::sync::broadcast::channel(256);
4158 tx
4159 },
4160 change_wake,
4161 _lock: Mutex::new(Some(lock)),
4162 };
4163 Ok(Self {
4164 core: Arc::new(core),
4165 principal: RwLock::new(principal),
4166 auth_state,
4167 shared: false,
4168 })
4169 }
4170
4171 pub fn visible_epoch(&self) -> Epoch {
4173 self.epoch.visible()
4174 }
4175
4176 pub fn catalog_version(&self) -> u64 {
4178 self.catalog.read().catalog_version
4179 }
4180
4181 pub fn storage_mode(&self) -> Result<Option<crate::storage_mode::StorageMode>> {
4185 Ok(crate::storage_mode::read(&self.durable_root)?)
4186 }
4187
4188 pub fn memory_governor(&self) -> &crate::memory::MemoryGovernor {
4191 &self.memory_governor
4192 }
4193
4194 pub fn resource_groups(&self) -> &crate::resource::ResourceGroupRegistry {
4197 &self.resource_groups
4198 }
4199
4200 pub fn embedding_providers(&self) -> &crate::embedding::EmbeddingProviderRegistry {
4204 &self.embedding_providers
4205 }
4206
4207 pub fn unregister_embedding_provider(&self, provider_id: &str) -> Result<bool> {
4209 let references = self
4210 .catalog
4211 .read()
4212 .tables
4213 .iter()
4214 .flat_map(|table| &table.schema.columns)
4215 .filter(|column| {
4216 column
4217 .embedding_source
4218 .as_ref()
4219 .and_then(crate::embedding::EmbeddingSource::provider_id)
4220 == Some(provider_id)
4221 })
4222 .count();
4223 self.embedding_providers
4224 .unregister_unreferenced(provider_id, references)
4225 .map_err(embedding_error)
4226 }
4227
4228 fn materialize_generated_embeddings(
4229 &self,
4230 staging: &mut [(u64, crate::txn::Staged)],
4231 control: Option<&crate::ExecutionControl>,
4232 ) -> Result<()> {
4233 let schemas = {
4234 let catalog = self.catalog.read();
4235 catalog
4236 .tables
4237 .iter()
4238 .map(|table| (table.table_id, table.schema.clone()))
4239 .collect::<HashMap<_, _>>()
4240 };
4241 let fallback_control = crate::ExecutionControl::new(None);
4242 let control = control.unwrap_or(&fallback_control);
4243 for (table_id, staged) in &mut *staging {
4244 let (cells, mut update_changed_columns) = match staged {
4245 crate::txn::Staged::Put(cells) => (cells, None),
4246 crate::txn::Staged::Update {
4247 new_row,
4248 changed_columns,
4249 ..
4250 } => (new_row, Some(changed_columns)),
4251 crate::txn::Staged::Delete(_) | crate::txn::Staged::Truncate => continue,
4252 };
4253 let schema = schemas.get(table_id).ok_or_else(|| {
4254 MongrelError::Schema(format!("table id {table_id} disappeared during embedding"))
4255 })?;
4256 for target in &schema.columns {
4257 let Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) =
4258 target.embedding_source.as_ref()
4259 else {
4260 continue;
4261 };
4262 let text = render_embedding_input(schema, spec, cells)?;
4263 let reservation_bytes = text
4264 .len()
4265 .saturating_add((spec.dimension as usize).saturating_mul(4));
4266 let _reservation = self
4267 .memory_governor
4268 .try_reserve(
4269 reservation_bytes as u64,
4270 crate::memory::MemoryClass::AiCandidates,
4271 )
4272 .map_err(|error| MongrelError::ResourceLimitExceeded {
4273 resource: "embedding memory",
4274 requested: reservation_bytes,
4275 limit: match error {
4276 crate::memory::MemoryError::Exhausted { available, .. } => {
4277 available as usize
4278 }
4279 _ => 0,
4280 },
4281 })?;
4282 let source =
4283 crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec: spec.clone() };
4284 let (registry_generation, provider) = self
4285 .embedding_providers
4286 .resolve_with_generation(&source)
4287 .map_err(embedding_error)?;
4288 let semantic_identity = provider.semantic_identity();
4289 self.ensure_ann_semantic_identity_compatible(
4290 *table_id,
4291 target.id,
4292 &semantic_identity,
4293 )?;
4294 let vectors = self
4295 .embedding_providers
4296 .embed_controlled(
4297 &source,
4298 &[text.as_str()],
4299 spec.dimension,
4300 control,
4301 "generated-column-write",
4302 crate::embedding::EmbeddingLimits::default(),
4303 )
4304 .map_err(embedding_error)?;
4305 cells.retain(|(column, _)| *column != target.id);
4306 cells.push((
4307 target.id,
4308 crate::memtable::Value::GeneratedEmbedding(Box::new(
4309 crate::embedding::GeneratedEmbeddingValue {
4310 vector: vectors.into_iter().next().ok_or_else(|| {
4311 MongrelError::Other("embedding provider returned no vector".into())
4312 })?,
4313 metadata: crate::embedding::GeneratedEmbeddingMetadata {
4314 provider_id: provider.provider_id().to_owned(),
4315 model_id: provider.model_id().to_owned(),
4316 model_version: provider.model_version().to_owned(),
4317 preprocessing_version: provider.preprocessing_version().to_owned(),
4318 source_fingerprint: Sha256::digest(text.as_bytes()).into(),
4319 status: crate::embedding::EmbeddingGenerationStatus::Ready,
4320 last_error_category: None,
4321 attempt_count: 1,
4322 semantic_identity,
4323 provider_registry_generation: registry_generation,
4324 },
4325 },
4326 )),
4327 ));
4328 if let Some(changed_columns) = update_changed_columns.as_deref_mut() {
4329 changed_columns.push(target.id);
4330 }
4331 }
4332 if let Some(changed_columns) = update_changed_columns {
4333 changed_columns.sort_unstable();
4334 changed_columns.dedup();
4335 }
4336 }
4337 self.validate_staged_generated_embedding_identities(staging)?;
4338 Ok(())
4339 }
4340
4341 fn ensure_ann_semantic_identity_compatible(
4342 &self,
4343 table_id: u64,
4344 column_id: u16,
4345 identity: &crate::embedding::EmbeddingProviderRef,
4346 ) -> Result<()> {
4347 let tables = self.tables.read();
4348 let Some(handle) = tables.get(&table_id) else {
4349 return Ok(());
4350 };
4351 let table = handle.lock();
4352 let Some(ann) = table.ann_index(column_id) else {
4353 return Ok(());
4354 };
4355 if let Some(existing) = ann.semantic_identity() {
4356 if existing != identity {
4357 return Err(embedding_error(
4358 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
4359 column_id,
4360 expected: existing.fingerprint_sha256(),
4361 got: identity.fingerprint_sha256(),
4362 },
4363 ));
4364 }
4365 }
4366 Ok(())
4367 }
4368
4369 fn validate_staged_generated_embedding_identities(
4370 &self,
4371 staging: &[(u64, crate::txn::Staged)],
4372 ) -> Result<()> {
4373 for (table_id, staged) in staging {
4374 let cells = match staged {
4375 crate::txn::Staged::Put(cells) => cells.as_slice(),
4376 crate::txn::Staged::Update { new_row, .. } => new_row.as_slice(),
4377 crate::txn::Staged::Delete(_) | crate::txn::Staged::Truncate => continue,
4378 };
4379 for (column_id, value) in cells {
4380 let Some(meta) = value.generated_embedding_metadata() else {
4381 continue;
4382 };
4383 self.ensure_ann_semantic_identity_compatible(
4384 *table_id,
4385 *column_id,
4386 &meta.semantic_identity,
4387 )?;
4388 }
4389 }
4390 Ok(())
4391 }
4392
4393 pub fn lock_rows_for_update(
4397 &self,
4398 txn_id: u64,
4399 table_id: u64,
4400 row_ids: &[crate::rowid::RowId],
4401 control: Option<&crate::ExecutionControl>,
4402 ) -> Result<()> {
4403 for &row_id in row_ids {
4404 self.acquire_txn_lock(
4405 txn_id,
4406 crate::locks::LockKey::row(table_id, row_id),
4407 crate::locks::LockMode::Exclusive,
4408 control,
4409 )?;
4410 }
4411 Ok(())
4412 }
4413
4414 pub fn spill_manager(&self) -> &crate::spill::SpillManager {
4418 &self.spill_manager
4419 }
4420
4421 pub fn job_registry(&self) -> &Arc<crate::jobs::JobRegistry> {
4423 &self.job_registry
4424 }
4425
4426 pub fn version_pins_report(&self) -> Vec<TablePinsReport> {
4431 let names: HashMap<u64, String> = self
4432 .catalog
4433 .read()
4434 .tables
4435 .iter()
4436 .map(|entry| (entry.table_id, entry.name.clone()))
4437 .collect();
4438 let handles: Vec<_> = self
4439 .tables
4440 .read()
4441 .iter()
4442 .map(|(table_id, handle)| (*table_id, handle.clone()))
4443 .collect();
4444 handles
4445 .into_iter()
4446 .map(|(table_id, handle)| {
4447 let pins = handle.lock().version_pins_report();
4448 TablePinsReport {
4449 table_id,
4450 table: names.get(&table_id).cloned().unwrap_or_default(),
4451 pins,
4452 }
4453 })
4454 .collect()
4455 }
4456
4457 pub fn hlc_gc_floor(&self) -> crate::epoch::GcFloor {
4465 let mut combined = crate::epoch::GcFloor::ZERO;
4466 if let Some(epoch) = self.snapshots.min_pinned() {
4468 if let Some(ts) = self.commit_ts_for_epoch(epoch) {
4469 combined.transaction_snapshot = ts;
4470 }
4471 }
4472 if let Some(epoch) = self.snapshots.history_floor(self.epoch.visible()) {
4473 if let Some(ts) = self.commit_ts_for_epoch(epoch) {
4474 combined.history_retention = ts;
4475 }
4476 }
4477 let handles: Vec<_> = self.tables.read().values().cloned().collect();
4479 for handle in handles {
4480 let table_floor = handle
4481 .lock()
4482 .hlc_gc_floor(|epoch| self.commit_ts_for_epoch(epoch));
4483 for (label, ts) in table_floor.sources() {
4484 if ts == mongreldb_types::hlc::HlcTimestamp::ZERO {
4485 continue;
4486 }
4487 let slot = match label {
4488 "transaction_snapshot" => &mut combined.transaction_snapshot,
4489 "history_retention" => &mut combined.history_retention,
4490 "backup_pitr" => &mut combined.backup_pitr,
4491 "replication" => &mut combined.replication,
4492 "read_generation" => &mut combined.read_generation,
4493 "online_index_build" => &mut combined.online_index_build,
4494 _ => continue,
4495 };
4496 if *slot == mongreldb_types::hlc::HlcTimestamp::ZERO || ts < *slot {
4497 *slot = ts;
4498 }
4499 }
4500 }
4501 combined
4502 }
4503
4504 pub fn lock_manager(&self) -> &Arc<crate::locks::LockManager> {
4506 &self.lock_manager
4507 }
4508
4509 pub fn release_txn_locks(&self, txn_id: u64) {
4514 self.lock_manager.release_all(txn_id);
4515 }
4516
4517 fn txn_lock_request(
4520 txn_id: u64,
4521 mode: crate::locks::LockMode,
4522 control: Option<&crate::ExecutionControl>,
4523 ) -> crate::locks::LockRequest {
4524 let control = control
4525 .cloned()
4526 .unwrap_or_else(|| crate::ExecutionControl::new(None));
4527 crate::locks::LockRequest::new(txn_id, mode, control)
4528 }
4529
4530 pub(crate) fn acquire_txn_lock(
4534 &self,
4535 txn_id: u64,
4536 key: crate::locks::LockKey,
4537 mode: crate::locks::LockMode,
4538 control: Option<&crate::ExecutionControl>,
4539 ) -> Result<()> {
4540 self.lock_manager
4541 .acquire(key, Self::txn_lock_request(txn_id, mode, control))
4542 .map_err(MongrelError::from)
4543 }
4544
4545 fn acquire_schema_barrier_exclusive(&self) -> Result<TxnLockGuard<'_>> {
4550 let txn_id = self.alloc_txn_id()?;
4551 self.acquire_txn_lock(
4552 txn_id,
4553 crate::locks::LockKey::schema_barrier(),
4554 crate::locks::LockMode::Exclusive,
4555 None,
4556 )?;
4557 Ok(TxnLockGuard {
4558 locks: &self.lock_manager,
4559 txn_id,
4560 })
4561 }
4562
4563 pub fn commit_log(&self) -> Arc<dyn mongreldb_log::CommitLog> {
4568 Arc::clone(&self.commit_log.read())
4569 }
4570
4571 pub fn bind_cluster_commit_log(
4578 &self,
4579 commit_log: Arc<dyn mongreldb_log::CommitLog>,
4580 ) -> Result<()> {
4581 match self.storage_mode()? {
4582 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4583 *self.commit_log.write() = commit_log;
4584 Ok(())
4585 }
4586 mode => Err(MongrelError::InvalidArgument(format!(
4587 "commit-log substitution requires ClusterReplica storage, got {mode:?}"
4588 ))),
4589 }
4590 }
4591
4592 pub fn detach_cluster_commit_log(&self) -> Result<()> {
4598 match self.storage_mode()? {
4599 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4600 let standalone: Arc<dyn mongreldb_log::CommitLog> =
4601 self.standalone_commit_log.clone();
4602 *self.commit_log.write() = standalone;
4603 Ok(())
4604 }
4605 mode => Err(MongrelError::InvalidArgument(format!(
4606 "commit-log detachment requires ClusterReplica storage, got {mode:?}"
4607 ))),
4608 }
4609 }
4610
4611 pub fn hlc(&self) -> &mongreldb_types::hlc::HlcClock {
4617 self.hlc_clock()
4618 }
4619
4620 pub(crate) fn hlc_clock(&self) -> &mongreldb_types::hlc::HlcClock {
4621 &self.hlc
4622 }
4623
4624 pub fn catalog_snapshot(&self) -> Catalog {
4626 self.catalog.read().clone()
4627 }
4628
4629 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
4631 let catalog = self.catalog.read();
4632 match key {
4633 "user_version" => Ok(catalog.user_version),
4634 "application_id" => Ok(catalog.application_id),
4635 _ => Err(MongrelError::InvalidArgument(format!(
4636 "unsupported persistent SQL pragma {key:?}"
4637 ))),
4638 }
4639 }
4640
4641 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
4644 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
4645 }
4646
4647 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
4648 &self,
4649 key: &str,
4650 value: i64,
4651 mut before_commit: F,
4652 ) -> Result<Option<Epoch>>
4653 where
4654 F: FnMut() -> Result<()>,
4655 {
4656 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
4657 }
4658
4659 fn set_sql_pragma_i64_with_epoch_inner(
4660 &self,
4661 key: &str,
4662 value: i64,
4663 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
4664 ) -> Result<Option<Epoch>> {
4665 use crate::wal::DdlOp;
4666
4667 self.require(&crate::auth::Permission::Ddl)?;
4668 if self.read_only {
4669 return Err(MongrelError::ReadOnlyReplica);
4670 }
4671 if self.poisoned.load(Ordering::Relaxed) {
4672 return Err(MongrelError::Other(
4673 "database poisoned by fsync error".into(),
4674 ));
4675 }
4676 let _ddl = self.ddl_lock.lock();
4677 let _security_write = self.security_write()?;
4678 self.require(&crate::auth::Permission::Ddl)?;
4679 let mut next_catalog = self.catalog.read().clone();
4680 let target = match key {
4681 "user_version" => &mut next_catalog.user_version,
4682 "application_id" => &mut next_catalog.application_id,
4683 _ => {
4684 return Err(MongrelError::InvalidArgument(format!(
4685 "unsupported persistent SQL pragma {key:?}"
4686 )))
4687 }
4688 };
4689 if *target == Some(value) {
4690 return Ok(None);
4691 }
4692 *target = Some(value);
4693
4694 let _commit = self.commit_lock.lock();
4695 let epoch = self.epoch.bump_assigned();
4696 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4697 let txn_id = self.alloc_txn_id()?;
4698 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4699 let commit_seq = {
4700 let mut wal = self.shared_wal.lock();
4701 if let Some(before_commit) = before_commit {
4702 before_commit()?;
4703 }
4704 let append: Result<u64> = (|| {
4705 wal.append(
4706 txn_id,
4707 WAL_TABLE_ID,
4708 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
4709 key: key.to_string(),
4710 value,
4711 }),
4712 )?;
4713 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4714 wal.append_commit(txn_id, epoch, &[])
4715 })();
4716 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4717 };
4718 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4719 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4720 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
4721 Ok(Some(epoch))
4722 }
4723
4724 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
4725 self.catalog
4726 .read()
4727 .materialized_views
4728 .iter()
4729 .find(|definition| definition.name == name)
4730 .cloned()
4731 }
4732
4733 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
4734 self.catalog.read().materialized_views.clone()
4735 }
4736
4737 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
4738 self.catalog.read().security.clone()
4739 }
4740
4741 pub fn security_active_for(&self, table: &str) -> bool {
4742 self.catalog.read().security.table_has_security(table)
4743 }
4744
4745 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
4746 if self.catalog.read().security_version == expected_version {
4747 return Ok(());
4748 }
4749 self.security_catalog_disk_reads
4750 .fetch_add(1, Ordering::Relaxed);
4751 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
4752 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
4753 let principal = self.principal.read().clone();
4754 let principal = if fresh.require_auth
4755 && principal
4756 .as_ref()
4757 .is_some_and(|principal| principal.user_id != 0)
4758 {
4759 principal
4760 .as_ref()
4761 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
4762 } else {
4763 principal
4764 };
4765 self.auth_state.set_require_auth(fresh.require_auth);
4766 *self.catalog.write() = fresh;
4767 *self.principal.write() = principal.clone();
4768 self.auth_state.set_principal(principal);
4769 Ok(())
4770 }
4771
4772 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
4773 let guard = self.security_coordinator.gate.write();
4774 let version = self.security_coordinator.version.load(Ordering::Acquire);
4775 self.refresh_security_catalog_if_stale(version)?;
4776 Ok(guard)
4777 }
4778
4779 pub(crate) fn admit_operation(&self) -> Result<crate::core::OperationGuard> {
4806 self.core.operation_guard()
4807 }
4808
4809 fn apply_catalog_command_to(
4815 &self,
4816 next_catalog: &mut Catalog,
4817 command: crate::catalog_cmds::CatalogCommand,
4818 ) -> Result<crate::catalog_cmds::CatalogDelta> {
4819 let record = crate::catalog_cmds::CatalogCommandRecord::next(next_catalog, command);
4820 next_catalog.apply_command(&record)
4821 }
4822
4823 pub fn apply_replicated_records(&self, records: &[crate::wal::Record]) -> Result<bool> {
4841 use crate::wal::Op;
4842 use std::sync::atomic::Ordering;
4843
4844 let _operation = self.admit_operation()?;
4845 if self.poisoned.load(Ordering::Relaxed) {
4846 return Err(MongrelError::Other(
4847 "database poisoned by fsync error".into(),
4848 ));
4849 }
4850 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
4853 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
4854 })?;
4855 if records.iter().any(|record| record.txn_id != txn_id) {
4856 return Err(MongrelError::InvalidArgument(
4857 "replicated transaction payload mixes transaction ids".into(),
4858 ));
4859 }
4860 let commits = records
4861 .iter()
4862 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
4863 .count();
4864 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
4865 return Err(MongrelError::InvalidArgument(
4866 "replicated transaction payload must end in exactly one commit marker".into(),
4867 ));
4868 }
4869 let commit_epoch = match records.last().map(|r| &r.op) {
4870 Some(Op::TxnCommit { epoch, .. }) => *epoch,
4871 _ => unreachable!("validated above"),
4872 };
4873 if let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) {
4882 if !added_runs.is_empty() {
4883 return Err(MongrelError::InvalidArgument(
4884 "replicated spilled-run commits are not appliable: the leader must translate \
4885 the commit through translate_records_for_replication before proposal"
4886 .into(),
4887 ));
4888 }
4889 }
4890 if commit_epoch <= self.epoch.visible().0 {
4894 return Ok(false);
4895 }
4896 {
4901 let mut wal = self.shared_wal.lock();
4902 for record in records {
4903 wal.append(record.txn_id, 0, record.op.clone())?;
4904 }
4905 if let Err(error) = wal.group_sync() {
4906 self.poisoned.store(true, Ordering::Relaxed);
4907 return Err(error);
4908 }
4909 }
4910 let replication_pins: Vec<crate::retention::PinGuard> = {
4913 let tables = self.tables.read();
4914 let floor = Epoch(self.epoch.visible().0);
4915 tables
4916 .values()
4917 .map(|handle| {
4918 let t = handle.lock();
4919 Arc::clone(t.pin_registry())
4920 .pin(crate::retention::PinSource::Replication, floor)
4921 })
4922 .collect()
4923 };
4924 let tables = self.tables.read().clone();
4925 let catalog = self.catalog.read().clone();
4926 recover_shared_wal(&self.durable_root, &tables, &catalog, &self.epoch, records)?;
4927 if let Some(Op::TxnCommit { epoch, .. }) = records.last().map(|r| &r.op) {
4932 let commit_ts = records
4933 .iter()
4934 .rev()
4935 .find_map(|record| match &record.op {
4936 Op::CommitTimestamp { unix_nanos } => {
4937 Some(mongreldb_types::hlc::HlcTimestamp {
4938 physical_micros: unix_nanos / 1_000,
4939 logical: 0,
4940 node_tiebreaker: 0,
4941 })
4942 }
4943 _ => None,
4944 })
4945 .unwrap_or_else(|| {
4946 self.hlc
4947 .now()
4948 .unwrap_or(mongreldb_types::hlc::HlcTimestamp {
4949 physical_micros: 0,
4950 logical: 0,
4951 node_tiebreaker: 0,
4952 })
4953 });
4954 self.record_commit_ts(Epoch(*epoch), commit_ts);
4955 }
4956 drop(replication_pins);
4957 Ok(true)
4958 }
4959
4960 pub fn validate_staged_txn_writes(&self, staged: &[Vec<u8>]) -> Result<()> {
4970 let tables = self.tables.read();
4971 for payload in staged {
4972 match StagedTxnWrite::decode(payload)? {
4973 StagedTxnWrite::Put { table_id, rows } => {
4974 let handle = tables.get(&table_id).ok_or_else(|| {
4975 MongrelError::InvalidArgument(format!(
4976 "staged write targets unmounted table {table_id}"
4977 ))
4978 })?;
4979 let rows: Vec<crate::memtable::Row> =
4980 bincode::deserialize(&rows).map_err(|error| {
4981 MongrelError::InvalidArgument(format!(
4982 "staged put payload for table {table_id} cannot decode: {error}"
4983 ))
4984 })?;
4985 let schema = handle.lock().schema().clone();
4986 for row in &rows {
4987 validate_recovered_row(&schema, row).map_err(|error| {
4988 MongrelError::InvalidArgument(format!(
4989 "staged row for table {table_id} is not appliable: {error}"
4990 ))
4991 })?;
4992 }
4993 }
4994 StagedTxnWrite::Delete { table_id, row_ids } => {
4995 if !tables.contains_key(&table_id) {
4996 return Err(MongrelError::InvalidArgument(format!(
4997 "staged delete targets unmounted table {table_id}"
4998 )));
4999 }
5000 if row_ids.contains(&u64::MAX) {
5001 return Err(MongrelError::InvalidArgument(format!(
5002 "staged delete for table {table_id} names an exhausted row id"
5003 )));
5004 }
5005 }
5006 }
5007 }
5008 Ok(())
5009 }
5010
5011 pub fn apply_staged_txn_writes(
5033 &self,
5034 txn_tag: u64,
5035 staged: &[Vec<u8>],
5036 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5037 ) -> Result<bool> {
5038 use crate::wal::Op;
5039
5040 let mut writes = Vec::with_capacity(staged.len());
5042 for payload in staged {
5043 writes.push(StagedTxnWrite::decode(payload)?);
5044 }
5045 let generation = *self.next_txn_id.lock() >> 32;
5051 let txn_id = (generation << 32) | (txn_tag & 0x7FFF_FFFF) | 0x8000_0000;
5052 let mut records = Vec::with_capacity(writes.len() + 2);
5053 for write in writes {
5054 let op = match write {
5055 StagedTxnWrite::Put { table_id, rows } => {
5056 let mut decoded: Vec<crate::memtable::Row> = bincode::deserialize(&rows)
5060 .map_err(|e| {
5061 MongrelError::Other(format!(
5062 "staged txn put rows could not be decoded: {e}"
5063 ))
5064 })?;
5065 for row in &mut decoded {
5066 if commit_ts != mongreldb_types::hlc::HlcTimestamp::ZERO {
5069 row.commit_ts = Some(commit_ts);
5070 }
5071 }
5072 let stamped = bincode::serialize(&decoded).map_err(|e| {
5073 MongrelError::Other(format!("staged txn put rows serialize: {e}"))
5074 })?;
5075 Op::Put {
5076 table_id,
5077 rows: stamped,
5078 }
5079 }
5080 StagedTxnWrite::Delete { table_id, row_ids } => Op::Delete {
5081 table_id,
5082 row_ids: row_ids.into_iter().map(crate::RowId).collect(),
5083 },
5084 };
5085 records.push(crate::wal::Record::new(Epoch(0), txn_id, op));
5086 }
5087 let epoch = self.epoch.visible().0 + 1;
5088 let unix_nanos = commit_ts.physical_micros.saturating_mul(1_000);
5092 records.push(crate::wal::Record::new(
5093 Epoch(0),
5094 txn_id,
5095 Op::CommitTimestamp { unix_nanos },
5096 ));
5097 records.push(crate::wal::Record::new(
5098 Epoch(0),
5099 txn_id,
5100 Op::TxnCommit {
5101 epoch,
5102 added_runs: Vec::new(),
5103 },
5104 ));
5105 let applied = self.apply_replicated_records(&records)?;
5106 if applied && commit_ts != mongreldb_types::hlc::HlcTimestamp::ZERO {
5107 self.record_commit_ts(Epoch(epoch), commit_ts);
5110 }
5111 Ok(applied)
5112 }
5113
5114 pub fn apply_committed_transaction(
5119 &self,
5120 transaction_id: u64,
5121 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5122 operations: &[Vec<u8>],
5123 ) -> Result<bool> {
5124 self.apply_staged_txn_writes(transaction_id, operations, commit_ts)
5125 }
5126
5127 pub fn apply_replicated_catalog_command(
5136 &self,
5137 record: &crate::catalog_cmds::CatalogCommandRecord,
5138 ) -> Result<crate::catalog_cmds::CatalogDelta> {
5139 let _operation = self.admit_operation()?;
5140 let _g = self.ddl_lock.lock();
5141 let mut next_catalog = self.catalog.read().clone();
5142 let delta = next_catalog.apply_command(record)?;
5143 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
5144 return Ok(delta);
5145 }
5146 let referenced_epoch = match &delta {
5152 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => Some(entry.created_epoch),
5153 crate::catalog_cmds::CatalogDelta::TableDropped { at_epoch, .. }
5154 | crate::catalog_cmds::CatalogDelta::TableRenamed { at_epoch, .. } => Some(*at_epoch),
5155 _ => None,
5156 };
5157 if let Some(referenced) = referenced_epoch {
5158 self.epoch.advance_recovered(Epoch(referenced));
5159 next_catalog.db_epoch = next_catalog.db_epoch.max(referenced);
5160 }
5161 match &delta {
5162 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => {
5163 if !self.tables.read().contains_key(&entry.table_id) {
5168 self.mount_catalog_entry(entry)?;
5169 }
5170 }
5171 crate::catalog_cmds::CatalogDelta::TableDropped { table_id, .. } => {
5172 self.tables.write().remove(table_id);
5173 }
5174 _ => {}
5175 }
5176 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref())?;
5180 *self.catalog.write() = next_catalog;
5181 Ok(delta)
5182 }
5183
5184 fn mount_catalog_entry(&self, entry: &crate::catalog::CatalogEntry) -> Result<()> {
5189 let table_relative = Path::new(TABLES_DIR).join(entry.table_id.to_string());
5190 let table_root = Arc::new(
5191 self.durable_root
5192 .create_directory_all_pinned(&table_relative)?,
5193 );
5194 let tdir = table_root.io_path()?;
5195 let ctx = SharedCtx {
5196 root_guard: Some(table_root),
5197 epoch: Arc::clone(&self.epoch),
5198 page_cache: Arc::clone(&self.page_cache),
5199 decoded_cache: Arc::clone(&self.decoded_cache),
5200 snapshots: Arc::clone(&self.snapshots),
5201 kek: self.kek.clone(),
5202 commit_lock: Arc::clone(&self.commit_lock),
5203 shared: Some(crate::engine::SharedWalCtx {
5204 wal: Arc::clone(&self.shared_wal),
5205 group: Arc::clone(&self.group),
5206 poisoned: Arc::clone(&self.poisoned),
5207 txn_ids: Arc::clone(&self.next_txn_id),
5208 change_wake: self.change_wake.clone(),
5209 lifecycle: Arc::clone(&self.lifecycle),
5210 hlc: Arc::clone(&self.hlc),
5211 }),
5212 table_name: Some(entry.name.clone()),
5213 auth: self.table_auth_checker(),
5214 read_only: self.read_only,
5215 };
5216 let table = Table::create_in(&tdir, entry.schema.clone(), entry.table_id, ctx)?;
5217 self.tables
5218 .write()
5219 .insert(entry.table_id, TableHandle::new(table));
5220 Ok(())
5221 }
5222
5223 fn publish_catalog_candidate(
5224 &self,
5225 catalog: Catalog,
5226 epoch: Epoch,
5227 epoch_guard: &mut EpochGuard<'_>,
5228 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5229 ) -> Result<()> {
5230 self.publish_catalog_candidate_with_prelude(
5231 catalog,
5232 epoch,
5233 epoch_guard,
5234 before_publish,
5235 Vec::new(),
5236 )
5237 }
5238
5239 fn publish_catalog_candidate_with_prelude(
5240 &self,
5241 catalog: Catalog,
5242 epoch: Epoch,
5243 epoch_guard: &mut EpochGuard<'_>,
5244 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5245 prelude: Vec<(u64, crate::wal::Op)>,
5246 ) -> Result<()> {
5247 use crate::wal::DdlOp;
5248
5249 if self.read_only {
5250 return Err(MongrelError::ReadOnlyReplica);
5251 }
5252 if self.poisoned.load(Ordering::Relaxed) {
5253 return Err(MongrelError::Other(
5254 "database poisoned by fsync error".into(),
5255 ));
5256 }
5257 let _operation = self.admit_operation()?;
5259 if let Some(before_publish) = before_publish.as_mut() {
5260 (**before_publish)()?;
5261 }
5262 if catalog.db_epoch != epoch.0 {
5263 return Err(MongrelError::InvalidArgument(format!(
5264 "catalog epoch {} does not match commit epoch {}",
5265 catalog.db_epoch, epoch.0
5266 )));
5267 }
5268 {
5269 let current = self.catalog.read();
5270 validate_catalog_transition(¤t, &catalog)?;
5271 }
5272 validate_recovered_catalog(&catalog)?;
5273 let catalog_json = DdlOp::encode_catalog(&catalog)?;
5274 let txn_id = self.alloc_txn_id()?;
5275 let commit_seq = {
5276 let mut wal = self.shared_wal.lock();
5277 let append: Result<u64> = (|| {
5278 for (table_id, op) in prelude {
5279 wal.append(txn_id, table_id, op)?;
5280 }
5281 wal.append(
5282 txn_id,
5283 WAL_TABLE_ID,
5284 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
5285 )?;
5286 wal.append_commit(txn_id, epoch, &[])
5287 })();
5288 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5289 };
5290 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5291 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
5292 self.finish_durable_publish(epoch, epoch_guard, &receipt, checkpoint)
5293 }
5294
5295 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
5299 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
5300 let version = catalog.security_version;
5301 let principal = self.principal.read().clone();
5302 let principal = if catalog.require_auth
5303 && principal
5304 .as_ref()
5305 .is_some_and(|principal| principal.user_id != 0)
5306 {
5307 principal.as_ref().and_then(|principal| {
5308 Self::resolve_bound_principal_from_catalog(&catalog, principal)
5309 })
5310 } else {
5311 principal
5312 };
5313 *self.catalog.write() = catalog;
5314 self.security_coordinator
5315 .version
5316 .store(version, Ordering::Release);
5317 self.auth_state
5318 .set_require_auth(self.catalog.read().require_auth);
5319 *self.principal.write() = principal.clone();
5320 self.auth_state.set_principal(principal);
5321 checkpoint
5322 }
5323
5324 fn finish_durable_publish(
5325 &self,
5326 epoch: Epoch,
5327 epoch_guard: &mut EpochGuard<'_>,
5328 receipt: &mongreldb_log::CommitReceipt,
5329 post_step: Result<()>,
5330 ) -> Result<()> {
5331 if let Err(error) = self.publish_committed(receipt, epoch) {
5332 self.poisoned.store(true, Ordering::Relaxed);
5337 self.lifecycle.poison();
5338 return Err(MongrelError::DurableCommit {
5339 epoch: epoch.0,
5340 message: error.to_string(),
5341 });
5342 }
5343 epoch_guard.disarm();
5344 match post_step {
5345 Ok(()) => Ok(()),
5346 Err(error) => {
5347 self.poisoned.store(true, Ordering::Relaxed);
5348 self.lifecycle.poison();
5349 Err(MongrelError::DurableCommit {
5350 epoch: epoch.0,
5351 message: error.to_string(),
5352 })
5353 }
5354 }
5355 }
5356
5357 fn publish_committed(
5363 &self,
5364 receipt: &mongreldb_log::CommitReceipt,
5365 epoch: Epoch,
5366 ) -> Result<()> {
5367 debug_assert_eq!(
5368 receipt.log_position.index, epoch.0,
5369 "commit receipt position must match the published epoch"
5370 );
5371 mongreldb_fault::inject("commit.publish.before").map_err(crate::commit_log::fault_as_io)?;
5372 self.epoch.publish_in_order(epoch);
5373 mongreldb_fault::inject("commit.publish.after").map_err(crate::commit_log::fault_as_io)?;
5374 Ok(())
5375 }
5376
5377 fn await_durable_commit(
5386 &self,
5387 txn_id: u64,
5388 commit_seq: u64,
5389 epoch: Epoch,
5390 ) -> Result<mongreldb_log::CommitReceipt> {
5391 match self
5392 .standalone_commit_log
5393 .seal_transaction(txn_id, epoch, commit_seq, None)
5394 {
5395 Ok(receipt) => {
5396 self.record_commit_ts(epoch, receipt.commit_ts);
5397 Ok(receipt)
5398 }
5399 Err(error) => {
5400 self.poisoned.store(true, Ordering::Relaxed);
5401 self.lifecycle.poison();
5402 Err(MongrelError::CommitOutcomeUnknown {
5403 epoch: epoch.0,
5404 message: error.to_string(),
5405 })
5406 }
5407 }
5408 }
5409
5410 fn await_durable_commit_with_ts(
5415 &self,
5416 txn_id: u64,
5417 commit_seq: u64,
5418 epoch: Epoch,
5419 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5420 ) -> Result<mongreldb_log::CommitReceipt> {
5421 match self.standalone_commit_log.seal_transaction(
5422 txn_id,
5423 epoch,
5424 commit_seq,
5425 Some(commit_ts),
5426 ) {
5427 Ok(receipt) => {
5428 self.record_commit_ts(epoch, receipt.commit_ts);
5429 Ok(receipt)
5430 }
5431 Err(error) => {
5432 self.poisoned.store(true, Ordering::Relaxed);
5433 self.lifecycle.poison();
5434 Err(MongrelError::CommitOutcomeUnknown {
5435 epoch: epoch.0,
5436 message: error.to_string(),
5437 })
5438 }
5439 }
5440 }
5441
5442 fn record_commit_ts(&self, epoch: Epoch, commit_ts: mongreldb_types::hlc::HlcTimestamp) {
5447 let mut ledger = self.commit_ts_ledger.lock();
5448 ledger.insert(epoch.0, commit_ts);
5449 while ledger.len() > COMMIT_TS_LEDGER_CAP {
5450 ledger.pop_first();
5451 }
5452 }
5453
5454 pub fn commit_ts_for_epoch(&self, epoch: Epoch) -> Option<mongreldb_types::hlc::HlcTimestamp> {
5468 self.commit_ts_ledger.lock().get(&epoch.0).copied()
5469 }
5470
5471 pub fn epoch_at_or_before_commit_ts(
5477 &self,
5478 timestamp: mongreldb_types::hlc::HlcTimestamp,
5479 ) -> Option<Epoch> {
5480 self.commit_ts_ledger
5481 .lock()
5482 .iter()
5483 .rev()
5484 .find_map(|(epoch, commit_ts)| (*commit_ts <= timestamp).then_some(Epoch(*epoch)))
5485 }
5486
5487 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
5488 self.poisoned.store(true, Ordering::Relaxed);
5489 self.lifecycle.poison();
5490 MongrelError::CommitOutcomeUnknown {
5491 epoch: epoch.0,
5492 message: error.to_string(),
5493 }
5494 }
5495
5496 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
5498 self.set_security_catalog_as_with_epoch(security, None)
5499 .map(|_| ())
5500 }
5501
5502 pub fn set_security_catalog_as(
5504 &self,
5505 security: crate::security::SecurityCatalog,
5506 principal: Option<&crate::auth::Principal>,
5507 ) -> Result<()> {
5508 self.set_security_catalog_as_with_epoch(security, principal)
5509 .map(|_| ())
5510 }
5511
5512 pub fn set_security_catalog_as_with_epoch(
5514 &self,
5515 security: crate::security::SecurityCatalog,
5516 principal: Option<&crate::auth::Principal>,
5517 ) -> Result<Epoch> {
5518 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
5519 }
5520
5521 pub fn set_security_catalog_as_with_epoch_controlled<F>(
5524 &self,
5525 security: crate::security::SecurityCatalog,
5526 principal: Option<&crate::auth::Principal>,
5527 mut before_commit: F,
5528 ) -> Result<Epoch>
5529 where
5530 F: FnMut() -> Result<()>,
5531 {
5532 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
5533 }
5534
5535 fn set_security_catalog_as_with_epoch_inner(
5536 &self,
5537 security: crate::security::SecurityCatalog,
5538 principal: Option<&crate::auth::Principal>,
5539 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
5540 ) -> Result<Epoch> {
5541 use crate::wal::DdlOp;
5542 use std::sync::atomic::Ordering;
5543
5544 let command = crate::catalog_cmds::CatalogCommand::SetSecurityCatalog {
5548 security: security.clone(),
5549 };
5550 self.require_for(
5551 principal,
5552 &crate::catalog_cmds::required_permission(&command),
5553 )?;
5554 if self.poisoned.load(Ordering::Relaxed) {
5555 return Err(MongrelError::Other(
5556 "database poisoned by fsync error".into(),
5557 ));
5558 }
5559 let _operation = self.admit_operation()?;
5562 let _ddl = self.ddl_lock.lock();
5563 let _security_write = self.security_write()?;
5566 self.require_for(
5567 principal,
5568 &crate::catalog_cmds::required_permission(&command),
5569 )?;
5570 let mut next_catalog = self.catalog.read().clone();
5571 validate_security_catalog(&next_catalog, &security)?;
5572 let payload = DdlOp::encode_security(&security)?;
5573 let _commit = self.commit_lock.lock();
5574 let epoch = self.epoch.bump_assigned();
5575 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5576 let txn_id = self.alloc_txn_id()?;
5577 self.apply_catalog_command_to(&mut next_catalog, command)?;
5578 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
5579 let commit_seq = {
5580 let mut wal = self.shared_wal.lock();
5581 if let Some(before_commit) = before_commit {
5582 before_commit()?;
5583 }
5584 let append: Result<u64> = (|| {
5585 wal.append(
5586 txn_id,
5587 WAL_TABLE_ID,
5588 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
5589 security_json: payload,
5590 }),
5591 )?;
5592 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
5593 wal.append_commit(txn_id, epoch, &[])
5594 })();
5595 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5596 };
5597 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5598 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
5599 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
5600 Ok(epoch)
5601 }
5602
5603 pub fn require_for(
5604 &self,
5605 principal: Option<&crate::auth::Principal>,
5606 permission: &crate::auth::Permission,
5607 ) -> Result<()> {
5608 let Some(principal) = principal else {
5609 return self.require(permission);
5610 };
5611 let resolved;
5612 let principal = if principal.user_id != 0 {
5613 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5614 .ok_or(MongrelError::AuthRequired)?;
5615 &resolved
5616 } else {
5617 principal
5618 };
5619 #[cfg(test)]
5620 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5621 if principal.has_permission(permission) {
5622 Ok(())
5623 } else {
5624 Err(MongrelError::PermissionDenied {
5625 required: permission.clone(),
5626 principal: principal.username.clone(),
5627 })
5628 }
5629 }
5630
5631 fn require_exact_principal_current(
5635 &self,
5636 principal: Option<&crate::auth::Principal>,
5637 permission: &crate::auth::Permission,
5638 ) -> Result<()> {
5639 let catalog = self.catalog.read();
5640 if !catalog.require_auth {
5641 return Ok(());
5642 }
5643 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
5644 let current = if supplied.user_id == 0 {
5645 supplied.clone()
5646 } else {
5647 Self::resolve_bound_principal_from_catalog(&catalog, supplied)
5648 .ok_or(MongrelError::AuthRequired)?
5649 };
5650 if current.has_permission(permission) {
5651 Ok(())
5652 } else {
5653 Err(MongrelError::PermissionDenied {
5654 required: permission.clone(),
5655 principal: current.username,
5656 })
5657 }
5658 }
5659
5660 pub(crate) fn with_exact_principal_current<T, F>(
5661 &self,
5662 principal: Option<&crate::auth::Principal>,
5663 permission: &crate::auth::Permission,
5664 operation: F,
5665 ) -> Result<T>
5666 where
5667 F: FnOnce() -> Result<T>,
5668 {
5669 let _security = self.security_coordinator.gate.read();
5670 self.require_exact_principal_current(principal, permission)?;
5671 operation()
5672 }
5673
5674 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
5675 self.principal.read().clone()
5676 }
5677
5678 #[cfg(test)]
5679 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
5680 *self.principal.write() = principal.clone();
5681 self.auth_state.set_principal(principal);
5682 }
5683
5684 pub fn require_columns_for(
5685 &self,
5686 table: &str,
5687 operation: crate::auth::ColumnOperation,
5688 column_ids: &[u16],
5689 principal: Option<&crate::auth::Principal>,
5690 ) -> Result<()> {
5691 if principal.is_none() && !self.auth_state.require_auth() {
5692 return Ok(());
5693 }
5694 let cached = self.principal.read().clone();
5695 let principal = principal.or(cached.as_ref());
5696 let Some(principal) = principal else {
5697 let permission = match operation {
5698 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5699 table: table.to_string(),
5700 },
5701 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5702 table: table.to_string(),
5703 },
5704 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5705 table: table.to_string(),
5706 },
5707 };
5708 return self.require(&permission);
5709 };
5710 let catalog = self.catalog.read();
5711 let resolved;
5712 let principal = if principal.user_id != 0 {
5713 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
5714 .ok_or(MongrelError::AuthRequired)?;
5715 &resolved
5716 } else {
5717 principal
5718 };
5719 let schema = &catalog
5720 .live(table)
5721 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5722 .schema;
5723 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
5724 }
5725
5726 fn require_columns_for_principal(
5727 table: &str,
5728 schema: &Schema,
5729 operation: crate::auth::ColumnOperation,
5730 column_ids: &[u16],
5731 principal: &crate::auth::Principal,
5732 ) -> Result<()> {
5733 #[cfg(test)]
5734 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5735 match principal.column_access(table, operation) {
5736 crate::auth::ColumnAccess::All => Ok(()),
5737 crate::auth::ColumnAccess::Columns(allowed) => {
5738 let denied = column_ids.iter().find_map(|column_id| {
5739 schema
5740 .columns
5741 .iter()
5742 .find(|column| column.id == *column_id)
5743 .filter(|column| !allowed.contains(&column.name))
5744 });
5745 if denied.is_none() {
5746 Ok(())
5747 } else {
5748 Err(MongrelError::PermissionDenied {
5749 required: match operation {
5750 crate::auth::ColumnOperation::Select => {
5751 crate::auth::Permission::SelectColumns {
5752 table: table.to_string(),
5753 columns: denied
5754 .into_iter()
5755 .map(|column| column.name.clone())
5756 .collect(),
5757 }
5758 }
5759 crate::auth::ColumnOperation::Insert => {
5760 crate::auth::Permission::InsertColumns {
5761 table: table.to_string(),
5762 columns: denied
5763 .into_iter()
5764 .map(|column| column.name.clone())
5765 .collect(),
5766 }
5767 }
5768 crate::auth::ColumnOperation::Update => {
5769 crate::auth::Permission::UpdateColumns {
5770 table: table.to_string(),
5771 columns: denied
5772 .into_iter()
5773 .map(|column| column.name.clone())
5774 .collect(),
5775 }
5776 }
5777 },
5778 principal: principal.username.clone(),
5779 })
5780 }
5781 }
5782 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5783 required: match operation {
5784 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5785 table: table.to_string(),
5786 },
5787 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5788 table: table.to_string(),
5789 },
5790 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5791 table: table.to_string(),
5792 },
5793 },
5794 principal: principal.username.clone(),
5795 }),
5796 }
5797 }
5798
5799 pub fn select_column_ids_for(
5800 &self,
5801 table: &str,
5802 principal: Option<&crate::auth::Principal>,
5803 ) -> Result<Vec<u16>> {
5804 let catalog = self.catalog.read();
5805 let columns = catalog
5806 .live(table)
5807 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5808 .schema
5809 .columns
5810 .iter()
5811 .map(|column| (column.id, column.name.clone()))
5812 .collect::<Vec<_>>();
5813 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
5814 drop(catalog);
5815 let Some(principal) = principal.as_ref() else {
5816 self.require(&crate::auth::Permission::Select {
5817 table: table.to_string(),
5818 })?;
5819 return Ok(columns.iter().map(|(id, _)| *id).collect());
5820 };
5821 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
5822 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
5823 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
5824 .iter()
5825 .filter(|(_, name)| allowed.contains(name))
5826 .map(|(id, _)| *id)
5827 .collect()),
5828 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5829 required: crate::auth::Permission::Select {
5830 table: table.to_string(),
5831 },
5832 principal: principal.username.clone(),
5833 }),
5834 }
5835 }
5836
5837 pub fn secure_rows_for(
5838 &self,
5839 table: &str,
5840 rows: Vec<crate::memtable::Row>,
5841 principal: Option<&crate::auth::Principal>,
5842 ) -> Result<Vec<crate::memtable::Row>> {
5843 self.secure_rows_for_with_context(table, rows, principal, None)
5844 }
5845
5846 pub fn secure_rows_for_with_context(
5847 &self,
5848 table: &str,
5849 rows: Vec<crate::memtable::Row>,
5850 principal: Option<&crate::auth::Principal>,
5851 context: Option<&crate::query::AiExecutionContext>,
5852 ) -> Result<Vec<crate::memtable::Row>> {
5853 let (security, principal) = {
5854 let catalog = self.catalog.read();
5855 (
5856 catalog.security.clone(),
5857 self.principal_for_authorized_read(&catalog, principal, false)?,
5858 )
5859 };
5860 if !security.table_has_security(table) {
5861 return Ok(rows);
5862 }
5863 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5864 let mut output = Vec::new();
5865 for mut row in rows {
5866 if let Some(context) = context {
5867 context.consume(1)?;
5868 }
5869 if security.row_allowed(
5870 table,
5871 crate::security::PolicyCommand::Select,
5872 &row,
5873 principal,
5874 false,
5875 ) {
5876 security.apply_masks(table, &mut row, principal);
5877 output.push(row);
5878 }
5879 }
5880 Ok(output)
5881 }
5882
5883 pub fn mask_search_hits_for(
5886 &self,
5887 table: &str,
5888 hits: &mut [crate::query::SearchHit],
5889 principal: Option<&crate::auth::Principal>,
5890 ) -> Result<()> {
5891 let (security, principal) = {
5892 let catalog = self.catalog.read();
5893 (
5894 catalog.security.clone(),
5895 self.principal_for_authorized_read(&catalog, principal, false)?,
5896 )
5897 };
5898 if !security.table_has_security(table) {
5899 return Ok(());
5900 }
5901 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5902 for hit in hits {
5903 security.apply_masks_to_cells(table, &mut hit.cells, principal);
5904 }
5905 Ok(())
5906 }
5907
5908 pub fn mask_rows_for(
5910 &self,
5911 table: &str,
5912 rows: &mut [crate::memtable::Row],
5913 principal: Option<&crate::auth::Principal>,
5914 ) -> Result<()> {
5915 let (security, principal) = {
5916 let catalog = self.catalog.read();
5917 (
5918 catalog.security.clone(),
5919 self.principal_for_authorized_read(&catalog, principal, false)?,
5920 )
5921 };
5922 if !security.table_has_security(table) {
5923 return Ok(());
5924 }
5925 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5926 for row in rows {
5927 security.apply_masks(table, row, principal);
5928 }
5929 Ok(())
5930 }
5931
5932 pub fn authorized_candidate_ids_for(
5934 &self,
5935 table: &str,
5936 principal: Option<&crate::auth::Principal>,
5937 ) -> Result<Option<std::collections::HashSet<RowId>>> {
5938 Ok(self
5939 .authorized_read_snapshot(table, principal)?
5940 .allowed_row_ids)
5941 }
5942
5943 fn allowed_row_ids_locked(
5944 &self,
5945 table_name: &str,
5946 table: &Table,
5947 table_snapshot: Snapshot,
5948 security_state: (&crate::security::SecurityCatalog, u64),
5949 principal: Option<&crate::auth::Principal>,
5950 context: Option<&crate::query::AiExecutionContext>,
5951 ) -> Result<Option<Arc<HashSet<RowId>>>> {
5952 let (security, security_version) = security_state;
5953 if !security.rls_enabled(table_name) {
5954 return Ok(None);
5955 }
5956 let authorization_started = std::time::Instant::now();
5957 let principal = principal.ok_or(MongrelError::AuthRequired)?;
5958 let mut roles = principal.roles.clone();
5959 roles.sort_unstable();
5960 let principal_key = format!(
5961 "{}:{}:{}:{}:{roles:?}",
5962 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
5963 );
5964 let cache_key = (
5965 table_name.to_string(),
5966 table.data_generation(),
5967 security_version,
5968 principal_key,
5969 );
5970 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
5971 crate::trace::QueryTrace::record(|trace| {
5972 trace.rls_cache_hit = true;
5973 trace.authorization_nanos = trace
5974 .authorization_nanos
5975 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5976 });
5977 return Ok(Some(allowed));
5978 }
5979 if let Some(context) = context {
5980 context.checkpoint()?;
5981 }
5982 let started = std::time::Instant::now();
5984 let rows = table.visible_rows(table_snapshot)?;
5985 let rows_evaluated = rows.len() as u64;
5986 let mut allowed = HashSet::new();
5987 for chunk in rows.chunks(256) {
5988 if let Some(context) = context {
5989 context.consume(chunk.len())?;
5990 }
5991 allowed.extend(chunk.iter().filter_map(|row| {
5992 security
5993 .row_allowed(
5994 table_name,
5995 crate::security::PolicyCommand::Select,
5996 row,
5997 principal,
5998 false,
5999 )
6000 .then_some(row.row_id)
6001 }));
6002 }
6003 let allowed = Arc::new(allowed);
6004 let mut cache = self.rls_cache.lock();
6005 cache.build_nanos = cache
6006 .build_nanos
6007 .saturating_add(started.elapsed().as_nanos() as u64);
6008 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
6009 cache.insert(cache_key, Arc::clone(&allowed));
6010 crate::trace::QueryTrace::record(|trace| {
6011 trace.rls_rows_evaluated = trace
6012 .rls_rows_evaluated
6013 .saturating_add(rows_evaluated as usize);
6014 trace.authorization_nanos = trace
6015 .authorization_nanos
6016 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
6017 });
6018 Ok(Some(allowed))
6019 }
6020
6021 fn principal_for_authorized_read(
6022 &self,
6023 catalog: &Catalog,
6024 principal: Option<&crate::auth::Principal>,
6025 catalog_bound: bool,
6026 ) -> Result<Option<crate::auth::Principal>> {
6027 let principal = principal.cloned().or_else(|| self.principal.read().clone());
6028 let Some(principal) = principal else {
6029 return Ok(None);
6030 };
6031 if catalog_bound || principal.user_id != 0 {
6032 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
6033 .map(Some)
6034 .ok_or(MongrelError::AuthRequired);
6035 }
6036 Ok(Some(principal))
6037 }
6038
6039 pub fn with_authorized_read<T, F>(
6043 &self,
6044 table_name: &str,
6045 principal: Option<&crate::auth::Principal>,
6046 catalog_bound: bool,
6047 read: F,
6048 ) -> Result<T>
6049 where
6050 F: FnMut(
6051 &mut Table,
6052 Snapshot,
6053 Option<&HashSet<RowId>>,
6054 Option<&crate::auth::Principal>,
6055 ) -> Result<T>,
6056 {
6057 self.with_authorized_read_context(
6058 table_name,
6059 principal,
6060 catalog_bound,
6061 None,
6062 None,
6063 None,
6064 read,
6065 )
6066 }
6067
6068 #[allow(clippy::too_many_arguments)]
6069 pub fn with_authorized_read_context<T, F>(
6070 &self,
6071 table_name: &str,
6072 principal: Option<&crate::auth::Principal>,
6073 catalog_bound: bool,
6074 authorization: Option<&ReadAuthorization>,
6075 context: Option<&crate::query::AiExecutionContext>,
6076 snapshot_override: Option<Snapshot>,
6077 read: F,
6078 ) -> Result<T>
6079 where
6080 F: FnMut(
6081 &mut Table,
6082 Snapshot,
6083 Option<&HashSet<RowId>>,
6084 Option<&crate::auth::Principal>,
6085 ) -> Result<T>,
6086 {
6087 self.with_authorized_read_context_stamped(
6088 table_name,
6089 principal,
6090 catalog_bound,
6091 authorization,
6092 context,
6093 snapshot_override,
6094 read,
6095 )
6096 .map(|(result, _)| result)
6097 }
6098
6099 #[allow(clippy::too_many_arguments)]
6100 pub fn with_authorized_read_context_stamped<T, F>(
6101 &self,
6102 table_name: &str,
6103 principal: Option<&crate::auth::Principal>,
6104 catalog_bound: bool,
6105 authorization: Option<&ReadAuthorization>,
6106 context: Option<&crate::query::AiExecutionContext>,
6107 snapshot_override: Option<Snapshot>,
6108 mut read: F,
6109 ) -> Result<(T, AuthorizedReadStamp)>
6110 where
6111 F: FnMut(
6112 &mut Table,
6113 Snapshot,
6114 Option<&HashSet<RowId>>,
6115 Option<&crate::auth::Principal>,
6116 ) -> Result<T>,
6117 {
6118 if principal.is_none() && self.principal.read().is_some() {
6119 self.refresh_principal()?;
6120 }
6121 const RETRIES: usize = 3;
6122 let handle = self.table(table_name)?;
6123 for attempt in 0..RETRIES {
6124 crate::trace::QueryTrace::record(|trace| {
6125 trace.authorization_retries = attempt;
6126 });
6127 let (security, security_version, effective_principal) = {
6128 let catalog = self.catalog.read();
6129 (
6130 catalog.security.clone(),
6131 catalog.security_version,
6132 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6133 )
6134 };
6135 if let Some(authorization) = authorization {
6136 for permission in &authorization.permissions {
6137 self.require_for(effective_principal.as_ref(), permission)?;
6138 }
6139 self.require_columns_for(
6140 table_name,
6141 authorization.operation,
6142 &authorization.columns,
6143 effective_principal.as_ref(),
6144 )?;
6145 }
6146 let result = {
6147 let mut table = lock_table_with_context(&handle, context)?;
6148 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
6149 let allowed = self.allowed_row_ids_locked(
6150 table_name,
6151 &table,
6152 snapshot,
6153 (&security, security_version),
6154 effective_principal.as_ref(),
6155 context,
6156 )?;
6157 let stamp = AuthorizedReadStamp {
6158 table_id: table.table_id(),
6159 schema_id: table.schema().schema_id,
6160 data_generation: table.data_generation(),
6161 security_version,
6162 snapshot,
6163 };
6164 let result = read(
6165 &mut table,
6166 snapshot,
6167 allowed.as_deref(),
6168 effective_principal.as_ref(),
6169 )?;
6170 (result, stamp)
6171 };
6172 if let Some(context) = context {
6173 context.checkpoint()?;
6174 }
6175 if self.catalog.read().security_version == security_version {
6176 return Ok(result);
6177 }
6178 if attempt + 1 == RETRIES {
6179 return Err(MongrelError::Conflict(
6180 "security policy changed during scored read".into(),
6181 ));
6182 }
6183 }
6184 Err(MongrelError::Conflict(
6185 "authorization retry loop exhausted".into(),
6186 ))
6187 }
6188
6189 fn with_authorized_aggregate_table<T, F>(
6190 &self,
6191 table_name: &str,
6192 columns: &[u16],
6193 principal: Option<&crate::auth::Principal>,
6194 catalog_bound: bool,
6195 allow_table_security: bool,
6196 mut aggregate: F,
6197 ) -> Result<T>
6198 where
6199 F: FnMut(
6200 &mut Table,
6201 Option<&crate::security::CandidateAuthorization<'_>>,
6202 Option<&crate::auth::Principal>,
6203 u64,
6204 ) -> Result<T>,
6205 {
6206 if principal.is_none() && self.principal.read().is_some() {
6207 self.refresh_principal()?;
6208 }
6209 const RETRIES: usize = 3;
6210 let handle = self.table(table_name)?;
6211 for attempt in 0..RETRIES {
6212 let (security, security_version, effective_principal) = {
6213 let catalog = self.catalog.read();
6214 (
6215 catalog.security.clone(),
6216 catalog.security_version,
6217 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6218 )
6219 };
6220 self.require_columns_for(
6221 table_name,
6222 crate::auth::ColumnOperation::Select,
6223 columns,
6224 effective_principal.as_ref(),
6225 )?;
6226 if !allow_table_security && security.table_has_security(table_name) {
6227 return Err(MongrelError::InvalidArgument(
6228 "incremental aggregate is unsupported while RLS or column masks are active"
6229 .into(),
6230 ));
6231 }
6232 let result = {
6233 let mut table = handle.lock();
6234 let authorization = if security.rls_enabled(table_name) {
6235 Some(crate::security::CandidateAuthorization {
6236 table: table_name,
6237 security: &security,
6238 principal: effective_principal
6239 .as_ref()
6240 .ok_or(MongrelError::AuthRequired)?,
6241 })
6242 } else {
6243 None
6244 };
6245 aggregate(
6246 &mut table,
6247 authorization.as_ref(),
6248 effective_principal.as_ref(),
6249 security_version,
6250 )?
6251 };
6252 if self.catalog.read().security_version == security_version {
6253 return Ok(result);
6254 }
6255 if attempt + 1 == RETRIES {
6256 return Err(MongrelError::Conflict(
6257 "security policy changed during aggregate read".into(),
6258 ));
6259 }
6260 }
6261 Err(MongrelError::Conflict(
6262 "aggregate authorization retry loop exhausted".into(),
6263 ))
6264 }
6265
6266 pub fn with_authorized_scored_read_context<T, F>(
6270 &self,
6271 table_name: &str,
6272 principal: Option<&crate::auth::Principal>,
6273 catalog_bound: bool,
6274 authorization: Option<&ReadAuthorization>,
6275 context: Option<&crate::query::AiExecutionContext>,
6276 mut read: F,
6277 ) -> Result<T>
6278 where
6279 F: FnMut(
6280 &mut Table,
6281 Snapshot,
6282 Option<&crate::security::CandidateAuthorization<'_>>,
6283 Option<&crate::auth::Principal>,
6284 ) -> Result<T>,
6285 {
6286 self.with_authorized_scored_read_context_at(
6287 table_name,
6288 principal,
6289 catalog_bound,
6290 authorization,
6291 context,
6292 None,
6293 |table, snapshot, authorization, principal| {
6294 let mut table = table.clone();
6295 read(&mut table, snapshot, authorization, principal)
6296 },
6297 )
6298 }
6299
6300 #[allow(clippy::too_many_arguments)]
6301 pub fn with_authorized_scored_read_context_at<T, F>(
6302 &self,
6303 table_name: &str,
6304 principal: Option<&crate::auth::Principal>,
6305 catalog_bound: bool,
6306 authorization: Option<&ReadAuthorization>,
6307 context: Option<&crate::query::AiExecutionContext>,
6308 snapshot_override: Option<Snapshot>,
6309 read: F,
6310 ) -> Result<T>
6311 where
6312 F: FnMut(
6313 &Table,
6314 Snapshot,
6315 Option<&crate::security::CandidateAuthorization<'_>>,
6316 Option<&crate::auth::Principal>,
6317 ) -> Result<T>,
6318 {
6319 self.with_authorized_scored_read_context_at_stamped(
6320 table_name,
6321 principal,
6322 catalog_bound,
6323 authorization,
6324 context,
6325 snapshot_override,
6326 read,
6327 )
6328 .map(|(result, _)| result)
6329 }
6330
6331 #[allow(clippy::too_many_arguments)]
6332 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
6333 &self,
6334 table_name: &str,
6335 principal: Option<&crate::auth::Principal>,
6336 catalog_bound: bool,
6337 authorization: Option<&ReadAuthorization>,
6338 context: Option<&crate::query::AiExecutionContext>,
6339 snapshot_override: Option<Snapshot>,
6340 mut read: F,
6341 ) -> Result<(T, AuthorizedReadStamp)>
6342 where
6343 F: FnMut(
6344 &Table,
6345 Snapshot,
6346 Option<&crate::security::CandidateAuthorization<'_>>,
6347 Option<&crate::auth::Principal>,
6348 ) -> Result<T>,
6349 {
6350 if principal.is_none() && self.principal.read().is_some() {
6351 self.refresh_principal()?;
6352 }
6353 const RETRIES: usize = 3;
6354 let handle = self.table(table_name)?;
6355 for attempt in 0..RETRIES {
6356 if let Some(context) = context {
6357 context.checkpoint()?;
6358 }
6359 crate::trace::QueryTrace::record(|trace| {
6360 trace.authorization_retries = attempt;
6361 });
6362 let (security, security_version, effective_principal) = {
6363 let catalog = self.catalog.read();
6364 (
6365 catalog.security.clone(),
6366 catalog.security_version,
6367 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6368 )
6369 };
6370 if let Some(authorization) = authorization {
6371 for permission in &authorization.permissions {
6372 self.require_for(effective_principal.as_ref(), permission)?;
6373 }
6374 self.require_columns_for(
6375 table_name,
6376 authorization.operation,
6377 &authorization.columns,
6378 effective_principal.as_ref(),
6379 )?;
6380 }
6381 let result = {
6382 let (table, snapshot, _snapshot_guard, _run_pins) =
6383 self.scored_read_generation(&handle, context, snapshot_override)?;
6384 let candidate_authorization = if security.rls_enabled(table_name) {
6385 Some(crate::security::CandidateAuthorization {
6386 table: table_name,
6387 security: &security,
6388 principal: effective_principal
6389 .as_ref()
6390 .ok_or(MongrelError::AuthRequired)?,
6391 })
6392 } else {
6393 None
6394 };
6395 let stamp = AuthorizedReadStamp {
6396 table_id: table.table_id(),
6397 schema_id: table.schema().schema_id,
6398 data_generation: table.data_generation(),
6399 security_version,
6400 snapshot,
6401 };
6402 let result = read(
6403 table.as_ref(),
6404 snapshot,
6405 candidate_authorization.as_ref(),
6406 effective_principal.as_ref(),
6407 )?;
6408 (result, stamp)
6409 };
6410 if let Some(context) = context {
6411 context.checkpoint()?;
6412 }
6413 if self.catalog.read().security_version == security_version {
6414 return Ok(result);
6415 }
6416 if attempt + 1 == RETRIES {
6417 return Err(MongrelError::Conflict(
6418 "security policy changed during scored read".into(),
6419 ));
6420 }
6421 }
6422 Err(MongrelError::Conflict(
6423 "scored-read authorization retry loop exhausted".into(),
6424 ))
6425 }
6426
6427 fn scored_read_generation(
6428 &self,
6429 handle: &TableHandle,
6430 context: Option<&crate::query::AiExecutionContext>,
6431 snapshot_override: Option<Snapshot>,
6432 ) -> Result<(
6433 Arc<TableReadGeneration>,
6434 Snapshot,
6435 crate::retention::OwnedSnapshotGuard,
6436 RunPins,
6437 )> {
6438 let mut table = if let Some(context) = context {
6439 loop {
6440 context.checkpoint()?;
6441 let wait = context
6442 .remaining_duration()
6443 .unwrap_or(std::time::Duration::from_millis(5))
6444 .min(std::time::Duration::from_millis(5));
6445 if let Some(table) = handle.try_lock_for(wait) {
6446 break table;
6447 }
6448 }
6449 } else {
6450 handle.lock()
6451 };
6452 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
6453 self.snapshot_at_owned(snapshot.epoch)?
6454 } else {
6455 let snapshot = table.snapshot();
6456 let guard = self.snapshots.register_owned(snapshot.epoch);
6457 (snapshot, guard)
6458 };
6459 let table_id = table.table_id();
6460 let run_keys: Vec<_> = table
6461 .active_run_ids()
6462 .map(|run_id| (table_id, run_id))
6463 .collect();
6464 let generation = handle
6465 .generation_metrics
6466 .activate(table.clone_read_generation()?);
6467 let run_pins = self.pin_runs(&run_keys);
6468 Ok((generation, snapshot, snapshot_guard, run_pins))
6469 }
6470
6471 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
6472 let mut pins = self.backup_pins.lock();
6473 for run in runs {
6474 *pins.entry(*run).or_insert(0) += 1;
6475 }
6476 drop(pins);
6477 RunPins {
6478 pins: Arc::clone(&self.backup_pins),
6479 runs: runs.to_vec(),
6480 }
6481 }
6482
6483 pub fn query_for_current_principal(
6487 &self,
6488 table_name: &str,
6489 query: &crate::query::Query,
6490 projection: Option<&[u16]>,
6491 ) -> Result<Vec<crate::memtable::Row>> {
6492 let condition_columns = crate::query::condition_columns(&query.conditions);
6493 let catalog_bound = self
6494 .principal
6495 .read()
6496 .as_ref()
6497 .is_some_and(|principal| principal.user_id != 0);
6498 self.with_authorized_read(
6499 table_name,
6500 None,
6501 catalog_bound,
6502 |table, snapshot, allowed, principal| {
6503 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6504 self.require_columns_for(
6505 table_name,
6506 crate::auth::ColumnOperation::Select,
6507 &condition_columns,
6508 principal,
6509 )?;
6510 if let Some(projection) = projection {
6511 self.require_columns_for(
6512 table_name,
6513 crate::auth::ColumnOperation::Select,
6514 projection,
6515 principal,
6516 )?;
6517 }
6518 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
6519 let projection =
6520 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6521 for row in &mut rows {
6522 row.columns.retain(|column, _| {
6523 allowed_columns.contains(column)
6524 && projection
6525 .as_ref()
6526 .is_none_or(|projection| projection.contains(column))
6527 });
6528 }
6529 self.secure_rows_for(table_name, rows, principal)
6530 },
6531 )
6532 }
6533
6534 pub fn query_for_current_principal_controlled(
6538 &self,
6539 table_name: &str,
6540 query: &crate::query::Query,
6541 projection: Option<&[u16]>,
6542 control: &crate::ExecutionControl,
6543 ) -> Result<Vec<crate::memtable::Row>> {
6544 let catalog_bound = self
6545 .principal
6546 .read()
6547 .as_ref()
6548 .is_some_and(|principal| principal.user_id != 0);
6549 self.query_for_principal_controlled(
6550 table_name,
6551 query,
6552 projection,
6553 None,
6554 catalog_bound,
6555 control,
6556 )
6557 }
6558
6559 pub fn query_as_principal_controlled(
6563 &self,
6564 table_name: &str,
6565 query: &crate::query::Query,
6566 projection: Option<&[u16]>,
6567 principal: Option<&crate::auth::Principal>,
6568 control: &crate::ExecutionControl,
6569 ) -> Result<Vec<crate::memtable::Row>> {
6570 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6571 self.query_for_principal_controlled(
6572 table_name,
6573 query,
6574 projection,
6575 principal,
6576 catalog_bound,
6577 control,
6578 )
6579 }
6580
6581 fn query_for_principal_controlled(
6582 &self,
6583 table_name: &str,
6584 query: &crate::query::Query,
6585 projection: Option<&[u16]>,
6586 principal: Option<&crate::auth::Principal>,
6587 catalog_bound: bool,
6588 control: &crate::ExecutionControl,
6589 ) -> Result<Vec<crate::memtable::Row>> {
6590 control.checkpoint()?;
6591 let context = crate::query::AiExecutionContext::with_control(
6592 control.clone(),
6593 usize::MAX,
6594 crate::query::MAX_FUSED_CANDIDATES,
6595 );
6596 let condition_columns = crate::query::condition_columns(&query.conditions);
6597 self.with_authorized_read_context(
6598 table_name,
6599 principal,
6600 catalog_bound,
6601 None,
6602 Some(&context),
6603 None,
6604 |table, snapshot, allowed, principal| {
6605 control.checkpoint()?;
6606 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6607 self.require_columns_for(
6608 table_name,
6609 crate::auth::ColumnOperation::Select,
6610 &condition_columns,
6611 principal,
6612 )?;
6613 if let Some(projection) = projection {
6614 self.require_columns_for(
6615 table_name,
6616 crate::auth::ColumnOperation::Select,
6617 projection,
6618 principal,
6619 )?;
6620 }
6621 let rows =
6622 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
6623 let projection =
6624 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6625 let mut projected = Vec::with_capacity(rows.len());
6626 for (index, mut row) in rows.into_iter().enumerate() {
6627 if index & 255 == 0 {
6628 control.checkpoint()?;
6629 }
6630 row.columns.retain(|column, _| {
6631 allowed_columns.contains(column)
6632 && projection
6633 .as_ref()
6634 .is_none_or(|projection| projection.contains(column))
6635 });
6636 projected.push(row);
6637 }
6638 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
6639 },
6640 )
6641 }
6642
6643 pub fn approx_aggregate_for_current_principal(
6646 &self,
6647 table_name: &str,
6648 conditions: &[crate::query::Condition],
6649 column: Option<u16>,
6650 agg: crate::engine::ApproxAgg,
6651 z: f64,
6652 ) -> Result<Option<crate::engine::ApproxResult>> {
6653 if !z.is_finite() || z <= 0.0 {
6654 return Err(MongrelError::InvalidArgument(
6655 "z must be finite and > 0".into(),
6656 ));
6657 }
6658 let mut columns = crate::query::condition_columns(conditions);
6659 columns.extend(column);
6660 columns.sort_unstable();
6661 columns.dedup();
6662 self.with_authorized_aggregate_table(
6663 table_name,
6664 &columns,
6665 None,
6666 true,
6667 true,
6668 |table, authorization, _, _| {
6669 table.approx_aggregate_with_candidate_authorization(
6670 conditions,
6671 column,
6672 agg,
6673 z,
6674 authorization,
6675 )
6676 },
6677 )
6678 }
6679
6680 pub fn incremental_aggregate_for_current_principal(
6684 &self,
6685 table_name: &str,
6686 conditions: &[crate::query::Condition],
6687 column: Option<u16>,
6688 agg: crate::engine::NativeAgg,
6689 ) -> Result<crate::engine::IncrementalAggResult> {
6690 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
6691 }
6692
6693 pub fn incremental_aggregate_for_principal(
6697 &self,
6698 table_name: &str,
6699 conditions: &[crate::query::Condition],
6700 column: Option<u16>,
6701 agg: crate::engine::NativeAgg,
6702 principal: Option<&crate::auth::Principal>,
6703 catalog_bound: bool,
6704 ) -> Result<crate::engine::IncrementalAggResult> {
6705 let mut columns = crate::query::condition_columns(conditions);
6706 columns.extend(column);
6707 columns.sort_unstable();
6708 columns.dedup();
6709 self.with_authorized_aggregate_table(
6710 table_name,
6711 &columns,
6712 principal,
6713 catalog_bound,
6714 false,
6715 |table, _, principal, security_version| {
6716 let cache_key = incremental_aggregate_cache_key(
6717 table_name,
6718 conditions,
6719 column,
6720 agg,
6721 principal,
6722 security_version,
6723 );
6724 table.aggregate_incremental(cache_key, conditions, column, agg)
6725 },
6726 )
6727 }
6728
6729 pub fn get_for_current_principal(
6732 &self,
6733 table_name: &str,
6734 row_id: RowId,
6735 ) -> Result<Option<crate::memtable::Row>> {
6736 self.with_authorized_read(
6737 table_name,
6738 None,
6739 true,
6740 |table, snapshot, allowed, principal| {
6741 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6742 let Some(row) = table.get(row_id, snapshot) else {
6743 return Ok(None);
6744 };
6745 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
6746 return Ok(None);
6747 }
6748 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6749 if let Some(row) = rows.first_mut() {
6750 row.columns
6751 .retain(|column, _| allowed_columns.contains(column));
6752 }
6753 Ok(rows.pop())
6754 },
6755 )
6756 }
6757
6758 pub fn ann_rerank_for_current_principal(
6761 &self,
6762 table_name: &str,
6763 request: &crate::query::AnnRerankRequest,
6764 ) -> Result<Vec<crate::query::AnnRerankHit>> {
6765 self.with_authorized_scored_read_context_at(
6766 table_name,
6767 None,
6768 true,
6769 Some(&ReadAuthorization {
6770 operation: crate::auth::ColumnOperation::Select,
6771 columns: vec![request.column_id],
6772 permissions: Vec::new(),
6773 }),
6774 None,
6775 None,
6776 |table, snapshot, authorization, principal| {
6777 self.require_columns_for(
6778 table_name,
6779 crate::auth::ColumnOperation::Select,
6780 &[request.column_id],
6781 principal,
6782 )?;
6783 table.ann_rerank_at_with_candidate_authorization_on_generation(
6784 request,
6785 snapshot,
6786 authorization,
6787 None,
6788 )
6789 },
6790 )
6791 }
6792
6793 pub fn search_for_current_principal(
6797 &self,
6798 table_name: &str,
6799 request: &crate::query::SearchRequest,
6800 ) -> Result<Vec<crate::query::SearchHit>> {
6801 let principal = self.principal_snapshot();
6802 self.search_for_principal_with_context(table_name, request, principal.as_ref(), None)
6803 }
6804
6805 pub fn search_for_principal_with_context(
6811 &self,
6812 table_name: &str,
6813 request: &crate::query::SearchRequest,
6814 principal: Option<&crate::auth::Principal>,
6815 context: Option<&crate::query::AiExecutionContext>,
6816 ) -> Result<Vec<crate::query::SearchHit>> {
6817 let mut columns = crate::query::condition_columns(&request.must);
6818 for named in &request.retrievers {
6819 columns.push(named.retriever.column_id());
6820 }
6821 if let Some(crate::query::Rerank::ExactVector {
6822 embedding_column, ..
6823 }) = &request.rerank
6824 {
6825 columns.push(*embedding_column);
6826 }
6827 if let Some(proj) = &request.projection {
6828 columns.extend(proj);
6829 }
6830 columns.sort_unstable();
6831 columns.dedup();
6832 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6833 self.with_authorized_scored_read_context_at(
6834 table_name,
6835 principal,
6836 catalog_bound,
6837 Some(&ReadAuthorization {
6838 operation: crate::auth::ColumnOperation::Select,
6839 columns: columns.clone(),
6840 permissions: Vec::new(),
6841 }),
6842 context,
6843 None,
6844 |table, snapshot, authorization, principal| {
6845 self.require_columns_for(
6846 table_name,
6847 crate::auth::ColumnOperation::Select,
6848 &columns,
6849 principal,
6850 )?;
6851 let hits = table.search_at_with_candidate_authorization_on_generation(
6852 request,
6853 snapshot,
6854 authorization,
6855 context,
6856 )?;
6857 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6858 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
6859 for mut hit in hits {
6860 let row = crate::memtable::Row {
6861 row_id: hit.row_id,
6862 committed_epoch: crate::Epoch(0),
6863 columns: hit
6864 .cells
6865 .iter()
6866 .cloned()
6867 .collect::<std::collections::HashMap<u16, crate::Value>>(),
6868 deleted: false,
6869 commit_ts: None,
6870 };
6871 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6872 if let Some(mut row) = rows.pop() {
6873 row.columns
6874 .retain(|column, _| allowed_columns.contains(column));
6875 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
6876 hit.cells.sort_by_key(|(column, _)| *column);
6877 secured.push(hit);
6878 }
6879 }
6880 Ok(secured)
6881 },
6882 )
6883 }
6884
6885 pub fn retrieve_text(
6888 &self,
6889 table: &str,
6890 embedding_column: u16,
6891 text: &str,
6892 search_options: crate::embedding::TextSearchOptions,
6893 ) -> Result<crate::embedding::TextRetrieveResult> {
6894 let principal = self.principal_snapshot();
6895 self.retrieve_text_for_principal(
6896 table,
6897 embedding_column,
6898 text,
6899 search_options,
6900 principal.as_ref(),
6901 )
6902 }
6903
6904 pub fn retrieve_text_for_principal(
6905 &self,
6906 table: &str,
6907 embedding_column: u16,
6908 text: &str,
6909 search_options: crate::embedding::TextSearchOptions,
6910 principal: Option<&crate::auth::Principal>,
6911 ) -> Result<crate::embedding::TextRetrieveResult> {
6912 if search_options.k == 0 {
6913 return Err(MongrelError::InvalidArgument(
6914 "retrieve_text k must be greater than zero".into(),
6915 ));
6916 }
6917 let catalog_bound = principal.is_some_and(|p| p.user_id != 0);
6918 let (semantic_identity, registry_generation, expected_dim) = {
6919 let handle = self.table(table)?;
6920 let mut g = handle.lock();
6921 g.ensure_indexes_complete()?;
6922 let schema = g.schema().clone();
6923 let column = schema
6924 .columns
6925 .iter()
6926 .find(|c| c.id == embedding_column)
6927 .ok_or_else(|| {
6928 MongrelError::ColumnNotFound(format!(
6929 "embedding column {embedding_column} not found on table {table}"
6930 ))
6931 })?;
6932 let dim = match column.ty {
6933 crate::schema::TypeId::Embedding { dim } => dim,
6934 _ => {
6935 return Err(MongrelError::InvalidArgument(format!(
6936 "column {embedding_column} is not an embedding column"
6937 )))
6938 }
6939 };
6940 if let Some(identity) = g
6941 .ann_index(embedding_column)
6942 .and_then(|a| a.semantic_identity().cloned())
6943 {
6944 let (generation, provider) = self
6945 .embedding_providers
6946 .resolve_by_semantic_identity(&identity)
6947 .map_err(embedding_error)?;
6948 if provider.dimension() != dim {
6949 return Err(embedding_error(
6950 crate::embedding::EmbeddingError::DimensionMismatch {
6951 expected: dim,
6952 got: provider.dimension(),
6953 },
6954 ));
6955 }
6956 (identity, generation, dim)
6957 } else {
6958 let source = column.embedding_source.clone().ok_or_else(|| {
6959 embedding_error(crate::embedding::EmbeddingError::NoActiveAnnIdentity(
6960 embedding_column,
6961 ))
6962 })?;
6963 let (generation, provider) = self
6964 .embedding_providers
6965 .resolve_with_generation(&source)
6966 .map_err(embedding_error)?;
6967 let identity = provider.semantic_identity();
6968 if identity.dimension != dim {
6969 return Err(embedding_error(
6970 crate::embedding::EmbeddingError::DimensionMismatch {
6971 expected: dim,
6972 got: identity.dimension,
6973 },
6974 ));
6975 }
6976 (identity, generation, dim)
6977 }
6978 };
6979 let (_g, provider) = self
6980 .embedding_providers
6981 .resolve_by_semantic_identity(&semantic_identity)
6982 .map_err(embedding_error)?;
6983 let control = crate::ExecutionControl::new(None);
6984 let response = provider
6985 .embed(crate::embedding::EmbeddingRequest {
6986 texts: &[text],
6987 control: &control,
6988 trace_id: "retrieve-text",
6989 })
6990 .map_err(embedding_error)?;
6991 crate::embedding::validate_response(
6992 provider.as_ref(),
6993 1,
6994 expected_dim,
6995 crate::embedding::EmbeddingLimits::default(),
6996 &response.vectors,
6997 )
6998 .map_err(embedding_error)?;
6999 let query = response.vectors.into_iter().next().ok_or_else(|| {
7000 MongrelError::Other("embedding provider returned no query vector".into())
7001 })?;
7002 if provider.semantic_identity() != semantic_identity {
7003 return Err(embedding_error(
7004 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7005 column_id: embedding_column,
7006 expected: semantic_identity.fingerprint_sha256(),
7007 got: provider.semantic_identity().fingerprint_sha256(),
7008 },
7009 ));
7010 }
7011 let retriever = crate::query::Retriever::Ann {
7012 column_id: embedding_column,
7013 query,
7014 k: search_options.k,
7015 };
7016 let hits = self.with_authorized_scored_read_context_at(
7017 table,
7018 principal,
7019 catalog_bound,
7020 Some(&ReadAuthorization {
7021 operation: crate::auth::ColumnOperation::Select,
7022 columns: vec![embedding_column],
7023 permissions: Vec::new(),
7024 }),
7025 None,
7026 None,
7027 |table_guard, snapshot, authorization, principal| {
7028 self.require_columns_for(
7029 table,
7030 crate::auth::ColumnOperation::Select,
7031 &[embedding_column],
7032 principal,
7033 )?;
7034 if let Some(ann) = table_guard.ann_index(embedding_column) {
7035 if let Some(active) = ann.semantic_identity() {
7036 if active != &semantic_identity {
7037 return Err(embedding_error(
7038 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7039 column_id: embedding_column,
7040 expected: semantic_identity.fingerprint_sha256(),
7041 got: active.fingerprint_sha256(),
7042 },
7043 ));
7044 }
7045 }
7046 }
7047 table_guard.retrieve_at_with_candidate_authorization_on_generation(
7048 &retriever,
7049 snapshot,
7050 authorization,
7051 None,
7052 )
7053 },
7054 )?;
7055 Ok(crate::embedding::TextRetrieveResult {
7056 hits,
7057 provenance: crate::embedding::TextRetrieveProvenance {
7058 semantic_identity,
7059 provider_registry_generation: registry_generation,
7060 query_source_fingerprint: Sha256::digest(text.as_bytes()).into(),
7061 embedding_column,
7062 },
7063 })
7064 }
7065
7066 pub fn authorized_read_snapshot(
7069 &self,
7070 table: &str,
7071 principal: Option<&crate::auth::Principal>,
7072 ) -> Result<AuthorizedReadSnapshot> {
7073 let (security, security_version, effective_principal) = {
7074 let catalog = self.catalog.read();
7075 (
7076 catalog.security.clone(),
7077 catalog.security_version,
7078 self.principal_for_authorized_read(&catalog, principal, false)?,
7079 )
7080 };
7081 let handle = self.table(table)?;
7082 let (table_snapshot, data_generation, allowed_row_ids) = {
7083 let table_handle = handle.lock();
7084 let table_snapshot = table_handle.snapshot();
7085 let data_generation = table_handle.data_generation();
7086 let allowed = self.allowed_row_ids_locked(
7087 table,
7088 &table_handle,
7089 table_snapshot,
7090 (&security, security_version),
7091 effective_principal.as_ref(),
7092 None,
7093 )?;
7094 (
7095 table_snapshot,
7096 data_generation,
7097 allowed.map(|allowed| (*allowed).clone()),
7098 )
7099 };
7100 Ok(AuthorizedReadSnapshot {
7101 table: table.to_string(),
7102 table_snapshot,
7103 data_generation,
7104 security_version,
7105 allowed_row_ids,
7106 })
7107 }
7108
7109 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
7110 if self.catalog.read().security_version != snapshot.security_version {
7111 return false;
7112 }
7113 self.table(&snapshot.table)
7114 .ok()
7115 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
7116 }
7117
7118 pub fn rls_cache_stats(&self) -> RlsCacheStats {
7119 self.rls_cache.lock().stats()
7120 }
7121
7122 pub fn rows_for(
7124 &self,
7125 table: &str,
7126 principal: Option<&crate::auth::Principal>,
7127 ) -> Result<Vec<crate::memtable::Row>> {
7128 if principal.is_none() && self.principal.read().is_some() {
7129 self.refresh_principal()?;
7130 }
7131 let allowed = self.select_column_ids_for(table, principal)?;
7132 let handle = self.table(table)?;
7133 let rows = {
7134 let table = handle.lock();
7135 table.visible_rows(table.snapshot())?
7136 };
7137 let mut rows = self.secure_rows_for(table, rows, principal)?;
7138 for row in &mut rows {
7139 row.columns.retain(|column, _| allowed.contains(column));
7140 }
7141 Ok(rows)
7142 }
7143
7144 pub fn rows_at_epoch_for_current_principal(
7147 &self,
7148 table_name: &str,
7149 snapshot: Snapshot,
7150 ) -> Result<Vec<crate::memtable::Row>> {
7151 self.with_authorized_read_context(
7152 table_name,
7153 None,
7154 true,
7155 Some(&ReadAuthorization {
7156 operation: crate::auth::ColumnOperation::Select,
7157 columns: Vec::new(),
7158 permissions: Vec::new(),
7159 }),
7160 None,
7161 Some(snapshot),
7162 |table, snapshot, allowed, principal| {
7163 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
7164 let mut rows = table.visible_rows(snapshot)?;
7165 if let Some(allowed) = allowed {
7166 rows.retain(|row| allowed.contains(&row.row_id));
7167 }
7168 rows = self.secure_rows_for(table_name, rows, principal)?;
7169 for row in &mut rows {
7170 row.columns
7171 .retain(|column, _| allowed_columns.contains(column));
7172 }
7173 Ok(rows)
7174 },
7175 )
7176 }
7177
7178 pub fn count_for(
7180 &self,
7181 table: &str,
7182 principal: Option<&crate::auth::Principal>,
7183 ) -> Result<u64> {
7184 if principal.is_none() && self.principal.read().is_some() {
7185 self.refresh_principal()?;
7186 }
7187 self.select_column_ids_for(table, principal)?;
7188 if self.security_active_for(table) {
7189 Ok(self.rows_for(table, principal)?.len() as u64)
7190 } else {
7191 Ok(self.table(table)?.lock().count())
7192 }
7193 }
7194
7195 pub fn put_for(
7197 &self,
7198 table: &str,
7199 mut cells: Vec<(u16, crate::memtable::Value)>,
7200 principal: Option<&crate::auth::Principal>,
7201 ) -> Result<RowId> {
7202 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
7203 self.require_columns_for(
7204 table,
7205 crate::auth::ColumnOperation::Insert,
7206 &columns,
7207 principal,
7208 )?;
7209 let handle = self.table(table)?;
7210 let mut table_handle = handle.lock();
7211 table_handle.fill_auto_inc(&mut cells)?;
7212 table_handle.apply_defaults(&mut cells)?;
7213 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
7214 row.columns.extend(cells.iter().cloned());
7215 self.check_row_policy_for(
7216 table,
7217 crate::security::PolicyCommand::Insert,
7218 &row,
7219 true,
7220 principal,
7221 )?;
7222 table_handle.put(cells)
7223 }
7224
7225 pub fn check_row_policy_for(
7226 &self,
7227 table: &str,
7228 command: crate::security::PolicyCommand,
7229 row: &crate::memtable::Row,
7230 check_new: bool,
7231 principal: Option<&crate::auth::Principal>,
7232 ) -> Result<()> {
7233 let security = self.catalog.read().security.clone();
7234 if !security.rls_enabled(table) {
7235 return Ok(());
7236 }
7237 let cached = self.principal.read().clone();
7238 let principal = principal
7239 .or(cached.as_ref())
7240 .ok_or(MongrelError::AuthRequired)?;
7241 if security.row_allowed(table, command, row, principal, check_new) {
7242 return Ok(());
7243 }
7244 let required = match command {
7245 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
7246 table: table.to_string(),
7247 },
7248 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
7249 table: table.to_string(),
7250 },
7251 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
7252 table: table.to_string(),
7253 },
7254 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
7255 crate::auth::Permission::Delete {
7256 table: table.to_string(),
7257 }
7258 }
7259 };
7260 Err(MongrelError::PermissionDenied {
7261 required,
7262 principal: principal.username.clone(),
7263 })
7264 }
7265
7266 pub fn set_materialized_view(
7269 &self,
7270 definition: crate::catalog::MaterializedViewEntry,
7271 ) -> Result<()> {
7272 self.set_materialized_view_with_epoch(definition)
7273 .map(|_| ())
7274 }
7275
7276 pub fn set_materialized_view_with_epoch(
7278 &self,
7279 definition: crate::catalog::MaterializedViewEntry,
7280 ) -> Result<Epoch> {
7281 use crate::wal::DdlOp;
7282 use std::sync::atomic::Ordering;
7283
7284 let command = crate::catalog_cmds::CatalogCommand::CreateMaterializedView {
7285 definition: definition.clone(),
7286 };
7287 self.require(&crate::catalog_cmds::required_permission(&command))?;
7288 if self.poisoned.load(Ordering::Relaxed) {
7289 return Err(MongrelError::Other(
7290 "database poisoned by fsync error".into(),
7291 ));
7292 }
7293 if definition.name.is_empty() || definition.query.trim().is_empty() {
7294 return Err(MongrelError::InvalidArgument(
7295 "materialized view name and query must not be empty".into(),
7296 ));
7297 }
7298 let _operation = self.admit_operation()?;
7301 let _ddl = self.ddl_lock.lock();
7302 let _security_write = self.security_write()?;
7303 self.require(&crate::catalog_cmds::required_permission(&command))?;
7304 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7308 let table_id = self
7309 .catalog
7310 .read()
7311 .live(&definition.name)
7312 .ok_or_else(|| {
7313 MongrelError::NotFound(format!(
7314 "materialized view table {:?} not found",
7315 definition.name
7316 ))
7317 })?
7318 .table_id;
7319 let definition_json = DdlOp::encode_materialized_view(&definition)?;
7320 let _commit = self.commit_lock.lock();
7321 let epoch = self.epoch.bump_assigned();
7322 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7323 let txn_id = self.alloc_txn_id()?;
7324 let mut next_catalog = self.catalog.read().clone();
7325 self.apply_catalog_command_to(&mut next_catalog, command)?;
7326 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
7327 let commit_seq = {
7328 let mut wal = self.shared_wal.lock();
7329 let append: Result<u64> = (|| {
7330 wal.append(
7331 txn_id,
7332 table_id,
7333 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
7334 name: definition.name.clone(),
7335 definition_json,
7336 }),
7337 )?;
7338 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
7339 wal.append_commit(txn_id, epoch, &[])
7340 })();
7341 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
7342 };
7343 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
7344
7345 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
7346 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
7347 Ok(epoch)
7348 }
7349
7350 pub fn root(&self) -> &Path {
7352 self.durable_root.canonical_path()
7353 }
7354
7355 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
7358 Arc::clone(&self.durable_root)
7359 }
7360
7361 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
7365 self.kek
7366 .as_deref()
7367 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
7368 }
7369
7370 pub fn is_read_only_replica(&self) -> bool {
7371 self.read_only
7372 }
7373
7374 pub fn ensure_consistent_read(&self) -> Result<()> {
7378 self.ensure_owner_process()?;
7379 if self.poisoned.load(Ordering::Relaxed) {
7380 return Err(MongrelError::Other(
7381 "database poisoned by post-commit failure; reopen required".into(),
7382 ));
7383 }
7384 Ok(())
7385 }
7386
7387 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
7388 self.replication_wal_retention_segments
7389 .store(segments, std::sync::atomic::Ordering::Relaxed);
7390 }
7391
7392 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
7397 let admin = crate::auth::Permission::Admin;
7398 self.require(&admin)?;
7399 let _operation = self.admit_operation()?;
7401 let operation_principal = self.principal_snapshot();
7402 let _barrier = self.replication_barrier.write();
7403 let _ddl = self.ddl_lock.lock();
7404 let _security = self.security_coordinator.gate.read();
7405 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7406 let mut handles: Vec<_> = self
7407 .tables
7408 .read()
7409 .iter()
7410 .map(|(id, handle)| (*id, handle.clone()))
7411 .collect();
7412 handles.sort_by_key(|(id, _)| *id);
7413 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7414 let _commit = self.commit_lock.lock();
7415 let mut wal = self.shared_wal.lock();
7416 wal.group_sync()?;
7417 let io_root = self.durable_root.io_path()?;
7418 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7419 let records = crate::wal::SharedWal::replay_with_dek(&io_root, wal_dek.as_ref())?;
7420 let epoch = records
7421 .iter()
7422 .filter_map(|record| match &record.op {
7423 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
7424 _ => None,
7425 })
7426 .max()
7427 .unwrap_or(0)
7428 .max(self.epoch.committed().0);
7429 let files = crate::replication::capture_files(&io_root)?;
7430 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7431 drop(wal);
7432 Ok(crate::replication::ReplicationSnapshot::new(
7433 source_id, epoch, files,
7434 ))
7435 }
7436
7437 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
7445 let control = crate::ExecutionControl::new(None);
7446 self.hot_backup_controlled(destination, &control, || true)
7447 }
7448
7449 pub(crate) fn hot_backup_to_durable_child(
7450 &self,
7451 parent: &crate::durable_file::DurableRoot,
7452 child: &Path,
7453 control: &crate::ExecutionControl,
7454 ) -> Result<crate::backup::BackupReport> {
7455 let mut components = child.components();
7456 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
7457 || components.next().is_some()
7458 {
7459 return Err(MongrelError::InvalidArgument(
7460 "durable backup child must be one relative path component".into(),
7461 ));
7462 }
7463 let destination_name = child.file_name().ok_or_else(|| {
7464 MongrelError::InvalidArgument("durable backup child has no filename".into())
7465 })?;
7466 let source_root = self.durable_root.io_path()?;
7467 let prepared = prepare_backup_destination_in(&source_root, parent, destination_name)?;
7468 self.hot_backup_prepared(prepared, control, || true)
7469 }
7470
7471 #[doc(hidden)]
7474 pub fn hot_backup_controlled<F>(
7475 &self,
7476 destination: impl AsRef<Path>,
7477 control: &crate::ExecutionControl,
7478 before_publish: F,
7479 ) -> Result<crate::backup::BackupReport>
7480 where
7481 F: FnOnce() -> bool,
7482 {
7483 let source_root = self.durable_root.io_path()?;
7484 let prepared = prepare_backup_destination(&source_root, destination.as_ref())?;
7485 self.hot_backup_prepared(prepared, control, before_publish)
7486 }
7487
7488 fn hot_backup_prepared<F>(
7489 &self,
7490 mut prepared: PreparedBackupDestination,
7491 control: &crate::ExecutionControl,
7492 before_publish: F,
7493 ) -> Result<crate::backup::BackupReport>
7494 where
7495 F: FnOnce() -> bool,
7496 {
7497 let admin = crate::auth::Permission::Admin;
7498 self.require(&admin)?;
7499 let _operation = self.admit_operation()?;
7502 let operation_principal = self.principal_snapshot();
7503 control.checkpoint()?;
7504 let destination = prepared.destination_path.clone();
7505 let mut before_publish = Some(before_publish);
7506
7507 let outcome = (|| {
7508 control.checkpoint()?;
7509 let barrier = self.replication_barrier.write();
7510 let ddl = self.ddl_lock.lock();
7511 let security = self.security_coordinator.gate.read();
7512 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7513 let mut handles: Vec<_> = self
7514 .tables
7515 .read()
7516 .iter()
7517 .map(|(id, handle)| (*id, handle.clone()))
7518 .collect();
7519 handles.sort_by_key(|(id, _)| *id);
7520 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7521 let commit = self.commit_lock.lock();
7522 let mut wal = self.shared_wal.lock();
7523 wal.group_sync()?;
7524 let epoch = self.epoch.committed().0;
7525 let boundary_unix_nanos = current_unix_nanos();
7526 let source_root = self.durable_root.io_path()?;
7527
7528 let pin_nonce = std::time::SystemTime::now()
7529 .duration_since(std::time::UNIX_EPOCH)
7530 .unwrap_or_default()
7531 .as_nanos();
7532 let file_pin_root = source_root
7533 .join(META_DIR)
7534 .join("backup-pins")
7535 .join(format!("{}-{pin_nonce}", std::process::id()));
7536 std::fs::create_dir_all(&file_pin_root)?;
7537 let _file_pins = BackupFilePins {
7538 root: file_pin_root.clone(),
7539 };
7540 let mut run_files = Vec::new();
7541 for (index, (table_id, _)) in handles.iter().enumerate() {
7542 if index % 256 == 0 {
7543 control.checkpoint()?;
7544 }
7545 let table = &table_guards[index];
7546 for (run_index, run) in table.run_refs().iter().enumerate() {
7547 if run_index % 256 == 0 {
7548 control.checkpoint()?;
7549 }
7550 let source = table.run_path(run.run_id as u64);
7551 let relative = Path::new(TABLES_DIR)
7552 .join(table_id.to_string())
7553 .join(crate::engine::RUNS_DIR)
7554 .join(format!("r-{}.sr", run.run_id));
7555 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
7556 if std::fs::hard_link(&source, &pinned).is_err() {
7557 crate::backup::copy_file_synced(&source, &pinned)?;
7558 }
7559 run_files.push(((*table_id, run.run_id), pinned, relative));
7560 }
7561 }
7562 crate::durable_file::sync_directory(&file_pin_root)?;
7563 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
7564 {
7565 let mut pins = self.backup_pins.lock();
7566 for key in &run_keys {
7567 *pins.entry(*key).or_insert(0) += 1;
7568 }
7569 }
7570 let _run_pins = RunPins {
7571 pins: Arc::clone(&self.backup_pins),
7572 runs: run_keys,
7573 };
7574 let _registry_pins: Vec<crate::retention::PinGuard> = table_guards
7581 .iter()
7582 .map(|table| {
7583 table
7584 .pin_registry()
7585 .pin(crate::retention::PinSource::BackupPitr, Epoch(epoch))
7586 })
7587 .collect();
7588 let deferred: HashSet<_> = run_files
7589 .iter()
7590 .map(|(_, _, relative)| relative.clone())
7591 .collect();
7592 let mut copied = Vec::new();
7593 copy_backup_boundary(
7594 &source_root,
7595 prepared.stage.as_deref().ok_or_else(|| {
7596 MongrelError::Other("backup staging root was already released".into())
7597 })?,
7598 &deferred,
7599 &mut copied,
7600 Some(control),
7601 )?;
7602
7603 drop(wal);
7604 drop(commit);
7605 drop(table_guards);
7606 drop(security);
7607 drop(ddl);
7608 drop(barrier);
7609
7610 if let Some(hook) = self.backup_hook.lock().as_ref() {
7611 hook();
7612 }
7613 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
7614 if index % 256 == 0 {
7615 control.checkpoint()?;
7616 }
7617 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
7618 prepared
7619 .stage
7620 .as_deref()
7621 .ok_or_else(|| {
7622 MongrelError::Other("backup staging root was already released".into())
7623 })?
7624 .copy_new_from(&relative, &mut source)?;
7625 copied.push(relative);
7626 }
7627
7628 let manifest = crate::backup::BackupManifest::create_controlled_durable(
7629 prepared.stage.as_deref().ok_or_else(|| {
7630 MongrelError::Other("backup staging root was already released".into())
7631 })?,
7632 epoch,
7633 &copied,
7634 control,
7635 )?;
7636 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
7637 MongrelError::Other("backup staging root was already released".into())
7638 })?)?;
7639 control.checkpoint()?;
7640 let publish = before_publish.take().ok_or_else(|| {
7641 MongrelError::Other("backup publication callback already consumed".into())
7642 })?;
7643 if !publish() {
7644 return Err(MongrelError::Cancelled);
7645 }
7646 let final_security = self.security_coordinator.gate.read();
7647 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7648 drop(prepared.stage.take().ok_or_else(|| {
7652 MongrelError::Other("backup staging root was already released".into())
7653 })?);
7654 let published = std::cell::Cell::new(false);
7655 if let Err(error) = prepared.parent.rename_directory_new_with_after(
7656 Path::new(&prepared.stage_name),
7657 &prepared.parent,
7658 Path::new(&prepared.destination_name),
7659 || published.set(true),
7660 ) {
7661 if published.get() {
7662 return Err(MongrelError::CommitOutcomeUnknown {
7663 epoch,
7664 message: format!("backup publication was not durable: {error}"),
7665 });
7666 }
7667 return Err(error.into());
7668 }
7669 drop(final_security);
7670 Ok(crate::backup::BackupReport {
7671 destination,
7672 epoch,
7673 boundary_unix_nanos,
7674 files: manifest.files.len(),
7675 bytes: manifest.total_bytes(),
7676 })
7677 })();
7678
7679 if outcome.is_err() {
7680 drop(prepared.stage.take());
7681 let _ = prepared
7682 .parent
7683 .remove_directory_all(Path::new(&prepared.stage_name));
7684 }
7685 outcome
7686 }
7687
7688 pub fn replication_batch_since(
7691 &self,
7692 since_epoch: u64,
7693 ) -> Result<crate::replication::ReplicationBatch> {
7694 use crate::wal::Op;
7695
7696 let admin = crate::auth::Permission::Admin;
7697 self.require(&admin)?;
7698 let _operation = self.admit_operation()?;
7700 let operation_principal = self.principal_snapshot();
7701
7702 let mut wal = self.shared_wal.lock();
7703 wal.group_sync()?;
7704 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7705 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
7706 drop(wal);
7707
7708 let commits: HashMap<u64, u64> = records
7709 .iter()
7710 .filter_map(|record| match &record.op {
7711 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
7712 _ => None,
7713 })
7714 .collect();
7715 let earliest_epoch = commits.values().copied().min();
7716 let current_epoch = commits
7717 .values()
7718 .copied()
7719 .max()
7720 .unwrap_or(0)
7721 .max(self.epoch.committed().0);
7722 let selected: HashSet<u64> = commits
7723 .iter()
7724 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
7725 .collect();
7726 let retention_gap = since_epoch < current_epoch
7727 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
7728 let spilled = records.iter().any(|record| {
7729 selected.contains(&record.txn_id)
7730 && matches!(
7731 &record.op,
7732 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
7733 )
7734 });
7735 let records = records
7736 .into_iter()
7737 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
7738 .filter(|record| selected.contains(&record.txn_id))
7739 .collect();
7740 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7741 let batch = crate::replication::ReplicationBatch::complete_for_source(
7742 source_id,
7743 since_epoch,
7744 current_epoch,
7745 earliest_epoch,
7746 retention_gap,
7747 spilled,
7748 records,
7749 )?;
7750 if let Some(hook) = self.replication_hook.lock().as_ref() {
7751 hook();
7752 }
7753 let _security = self.security_coordinator.gate.read();
7754 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7755 Ok(batch)
7756 }
7757
7758 pub fn append_replication_batch(
7762 &self,
7763 batch: &crate::replication::ReplicationBatch,
7764 ) -> Result<u64> {
7765 use crate::wal::Op;
7766
7767 if !self.read_only {
7768 return Err(MongrelError::InvalidArgument(
7769 "replication batches may only target a marked replica".into(),
7770 ));
7771 }
7772 let _operation = self.admit_operation()?;
7774 let current = crate::replication::replica_epoch(&self.root)?;
7775 if batch.is_source_bound() {
7776 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
7777 if batch.source_id != source_id {
7778 return Err(MongrelError::Conflict(
7779 "replication batch source does not match follower binding".into(),
7780 ));
7781 }
7782 }
7783 if batch.requires_snapshot {
7784 return Err(MongrelError::Conflict(
7785 "replication snapshot required for this batch".into(),
7786 ));
7787 }
7788 batch.validate_proof()?;
7789 if batch.from_epoch != current {
7790 if batch.from_epoch < current && batch.current_epoch == current {
7791 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7792 let _wal = self.shared_wal.lock();
7793 let existing: HashSet<(u64, u64)> =
7794 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7795 .into_iter()
7796 .filter_map(|record| match record.op {
7797 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7798 _ => None,
7799 })
7800 .collect();
7801 let already_applied = batch.records.iter().all(|record| match &record.op {
7802 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
7803 _ => true,
7804 });
7805 if already_applied {
7806 return Ok(current);
7807 }
7808 }
7809 return Err(MongrelError::Conflict(format!(
7810 "replication batch starts at epoch {}, follower is at epoch {current}",
7811 batch.from_epoch
7812 )));
7813 }
7814 if batch.current_epoch < current {
7815 return Err(MongrelError::InvalidArgument(format!(
7816 "replication batch current epoch {} precedes follower epoch {current}",
7817 batch.current_epoch
7818 )));
7819 }
7820 let records = &batch.records;
7821 let mut commits = HashMap::new();
7822 let mut commit_epochs = HashSet::new();
7823 let mut commit_timestamps = HashMap::new();
7824 for record in records {
7825 match &record.op {
7826 Op::TxnCommit { epoch, added_runs } => {
7827 if !added_runs.is_empty() {
7828 return Err(MongrelError::Conflict(
7829 "replication snapshot required for spilled-run transaction".into(),
7830 ));
7831 }
7832 if commits.insert(record.txn_id, *epoch).is_some() {
7833 return Err(MongrelError::InvalidArgument(format!(
7834 "duplicate commit for replication transaction {}",
7835 record.txn_id
7836 )));
7837 }
7838 if *epoch <= current || *epoch > batch.current_epoch {
7839 return Err(MongrelError::InvalidArgument(format!(
7840 "replication commit epoch {epoch} is outside ({current}, {}]",
7841 batch.current_epoch
7842 )));
7843 }
7844 if !commit_epochs.insert(*epoch) {
7845 return Err(MongrelError::InvalidArgument(format!(
7846 "duplicate replication commit epoch {epoch}"
7847 )));
7848 }
7849 }
7850 Op::CommitTimestamp { unix_nanos } => {
7851 commit_timestamps.insert(record.txn_id, *unix_nanos);
7852 }
7853 _ => {}
7854 }
7855 }
7856 for record in records {
7857 if record.txn_id == crate::wal::SYSTEM_TXN_ID
7858 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
7859 {
7860 return Err(MongrelError::InvalidArgument(
7861 "replication batch contains a non-committed record".into(),
7862 ));
7863 }
7864 if !commits.contains_key(&record.txn_id) {
7865 return Err(MongrelError::InvalidArgument(format!(
7866 "incomplete replication transaction {}",
7867 record.txn_id
7868 )));
7869 }
7870 }
7871 let target_epoch = commits
7872 .values()
7873 .copied()
7874 .filter(|epoch| *epoch > current)
7875 .max()
7876 .unwrap_or(current);
7877 if target_epoch != batch.current_epoch {
7878 return Err(MongrelError::InvalidArgument(format!(
7879 "replication batch ends at epoch {target_epoch}, expected {}",
7880 batch.current_epoch
7881 )));
7882 }
7883 let mut selected: HashSet<u64> = commits
7884 .iter()
7885 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
7886 .collect();
7887 if selected.is_empty() {
7888 return Ok(current);
7889 }
7890 let mut wal = self.shared_wal.lock();
7891 wal.group_sync()?;
7892 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7893 let existing: HashSet<(u64, u64)> =
7894 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7895 .into_iter()
7896 .filter_map(|record| match record.op {
7897 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7898 _ => None,
7899 })
7900 .collect();
7901 selected.retain(|txn_id| {
7902 commits
7903 .get(txn_id)
7904 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
7905 });
7906 for record in records {
7907 if !selected.contains(&record.txn_id) {
7908 continue;
7909 }
7910 match &record.op {
7911 Op::TxnCommit { epoch, added_runs } => {
7912 let timestamp = commit_timestamps
7913 .get(&record.txn_id)
7914 .copied()
7915 .unwrap_or_else(current_unix_nanos);
7916 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
7917 }
7918 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
7919 op => {
7920 wal.append(record.txn_id, 0, op.clone())?;
7921 }
7922 }
7923 }
7924 if !selected.is_empty() {
7925 wal.group_sync()?;
7926 }
7927 drop(wal);
7928
7929 let mut recovered_catalog = self.catalog.read().clone();
7933 if let Err(error) = recover_ddl_from_wal(
7934 &self.root,
7935 Some(&self.durable_root),
7936 &mut recovered_catalog,
7937 self.meta_dek.as_ref(),
7938 wal_dek.as_ref(),
7939 true,
7940 None,
7941 ) {
7942 return Err(MongrelError::DurableCommit {
7943 epoch: target_epoch,
7944 message: format!(
7945 "replication WAL is durable but catalog checkpoint failed: {error}"
7946 ),
7947 });
7948 }
7949 let _security = self.security_coordinator.gate.write();
7950 let old_security_version = self.catalog.read().security_version;
7951 let security_changed = old_security_version != recovered_catalog.security_version
7952 || self.catalog.read().require_auth != recovered_catalog.require_auth;
7953 let require_auth = recovered_catalog.require_auth;
7954 let principal = if security_changed {
7955 None
7956 } else {
7957 self.principal.read().as_ref().and_then(|principal| {
7958 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
7959 })
7960 };
7961 if require_auth {
7962 self.auth_state.set_require_auth(true);
7963 }
7964 self.auth_state.set_principal(principal.clone());
7965 *self.principal.write() = principal;
7966 let security_version = recovered_catalog.security_version;
7967 *self.catalog.write() = recovered_catalog;
7968 self.security_coordinator
7969 .version
7970 .store(security_version, Ordering::Release);
7971 if !require_auth {
7972 self.auth_state.set_require_auth(false);
7973 }
7974 if let Err(error) =
7975 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
7976 {
7977 return Err(MongrelError::DurableCommit {
7978 epoch: target_epoch,
7979 message: format!(
7980 "replication WAL and catalog are durable but follower watermark failed: {error}"
7981 ),
7982 });
7983 }
7984 Ok(target_epoch)
7985 }
7986
7987 pub fn table_id(&self, name: &str) -> Result<u64> {
7990 let cat = self.catalog.read();
7991 cat.live(name)
7992 .map(|e| e.table_id)
7993 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
7994 }
7995
7996 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
8001 let catalog = self.catalog.read();
8002 catalog
8003 .live(name)
8004 .map(|entry| (entry.table_id, entry.schema.schema_id))
8005 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
8006 }
8007
8008 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
8009 self.catalog
8010 .read()
8011 .building(name)
8012 .map(|entry| entry.table_id)
8013 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
8014 }
8015
8016 pub fn procedures(&self) -> Vec<StoredProcedure> {
8017 self.catalog
8018 .read()
8019 .procedures
8020 .iter()
8021 .map(|p| p.procedure.clone())
8022 .collect()
8023 }
8024
8025 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
8026 self.catalog
8027 .read()
8028 .procedures
8029 .iter()
8030 .find(|p| p.procedure.name == name)
8031 .map(|p| p.procedure.clone())
8032 }
8033
8034 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
8035 self.create_procedure_inner(procedure, None)
8036 }
8037
8038 pub fn create_procedure_controlled<F>(
8039 &self,
8040 procedure: StoredProcedure,
8041 mut before_publish: F,
8042 ) -> Result<StoredProcedure>
8043 where
8044 F: FnMut() -> Result<()>,
8045 {
8046 self.create_procedure_inner(procedure, Some(&mut before_publish))
8047 }
8048
8049 fn create_procedure_inner(
8050 &self,
8051 mut procedure: StoredProcedure,
8052 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8053 ) -> Result<StoredProcedure> {
8054 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8055 procedure: procedure.clone(),
8056 };
8057 self.require(&crate::catalog_cmds::required_permission(&command))?;
8058 let _g = self.ddl_lock.lock();
8059 let _security_write = self.security_write()?;
8060 self.require(&crate::catalog_cmds::required_permission(&command))?;
8061 procedure.validate()?;
8062 self.validate_procedure_references(&procedure)?;
8063 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8067 let commit_lock = Arc::clone(&self.commit_lock);
8068 let _c = commit_lock.lock();
8069 let epoch = self.epoch.bump_assigned();
8070 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8071 procedure.created_epoch = epoch.0;
8072 procedure.updated_epoch = epoch.0;
8073 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8074 procedure: procedure.clone(),
8075 };
8076 let mut next_catalog = self.catalog.read().clone();
8077 self.apply_catalog_command_to(&mut next_catalog, command)?;
8078 next_catalog.db_epoch = epoch.0;
8079 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8080 Ok(procedure)
8081 }
8082
8083 pub fn create_or_replace_procedure(
8084 &self,
8085 procedure: StoredProcedure,
8086 ) -> Result<StoredProcedure> {
8087 self.create_or_replace_procedure_inner(procedure, None)
8088 }
8089
8090 pub fn create_or_replace_procedure_controlled<F>(
8091 &self,
8092 procedure: StoredProcedure,
8093 mut before_publish: F,
8094 ) -> Result<StoredProcedure>
8095 where
8096 F: FnMut() -> Result<()>,
8097 {
8098 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
8099 }
8100
8101 fn create_or_replace_procedure_inner(
8102 &self,
8103 procedure: StoredProcedure,
8104 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8105 ) -> Result<StoredProcedure> {
8106 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8107 procedure: procedure.clone(),
8108 };
8109 self.require(&crate::catalog_cmds::required_permission(&command))?;
8110 let _g = self.ddl_lock.lock();
8111 let _security_write = self.security_write()?;
8112 self.require(&crate::catalog_cmds::required_permission(&command))?;
8113 procedure.validate()?;
8114 self.validate_procedure_references(&procedure)?;
8115 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8119 let commit_lock = Arc::clone(&self.commit_lock);
8120 let _c = commit_lock.lock();
8121 let epoch = self.epoch.bump_assigned();
8122 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8123 let mut next_catalog = self.catalog.read().clone();
8124 let replaced = match next_catalog
8129 .procedures
8130 .iter()
8131 .position(|p| p.procedure.name == procedure.name)
8132 {
8133 Some(idx) => next_catalog.procedures[idx]
8134 .procedure
8135 .replaced(procedure.clone(), epoch.0)?,
8136 None => {
8137 let mut next = procedure;
8138 next.created_epoch = epoch.0;
8139 next.updated_epoch = epoch.0;
8140 next
8141 }
8142 };
8143 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8144 procedure: replaced.clone(),
8145 };
8146 self.apply_catalog_command_to(&mut next_catalog, command)?;
8147 next_catalog.db_epoch = epoch.0;
8148 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8149 Ok(replaced)
8150 }
8151
8152 pub fn drop_procedure(&self, name: &str) -> Result<()> {
8153 self.drop_procedure_with_epoch(name).map(|_| ())
8154 }
8155
8156 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
8157 self.drop_procedure_with_epoch_inner(name, None)
8158 }
8159
8160 pub fn drop_procedure_with_epoch_controlled<F>(
8161 &self,
8162 name: &str,
8163 mut before_publish: F,
8164 ) -> Result<Epoch>
8165 where
8166 F: FnMut() -> Result<()>,
8167 {
8168 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
8169 }
8170
8171 fn drop_procedure_with_epoch_inner(
8172 &self,
8173 name: &str,
8174 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8175 ) -> Result<Epoch> {
8176 let command = crate::catalog_cmds::CatalogCommand::DropProcedure {
8177 name: name.to_string(),
8178 };
8179 self.require(&crate::catalog_cmds::required_permission(&command))?;
8180 let _g = self.ddl_lock.lock();
8181 let _security_write = self.security_write()?;
8182 self.require(&crate::catalog_cmds::required_permission(&command))?;
8183 let commit_lock = Arc::clone(&self.commit_lock);
8184 let _c = commit_lock.lock();
8185 let epoch = self.epoch.bump_assigned();
8186 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8187 let mut next_catalog = self.catalog.read().clone();
8188 self.apply_catalog_command_to(&mut next_catalog, command)?;
8189 next_catalog.db_epoch = epoch.0;
8190 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8191 Ok(epoch)
8192 }
8193
8194 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
8199 self.catalog.read().users.clone()
8200 }
8201
8202 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
8205 self.catalog
8206 .read()
8207 .users
8208 .iter()
8209 .find(|user| user.username == username)
8210 .map(|user| (user.id, user.created_epoch))
8211 }
8212
8213 pub fn user_scram_verifier(
8215 &self,
8216 username: &str,
8217 ) -> Option<crate::security_hardening::ScramVerifier> {
8218 self.catalog
8219 .read()
8220 .users
8221 .iter()
8222 .find(|user| user.username == username)
8223 .and_then(|user| user.scram_sha_256.clone())
8224 }
8225
8226 pub fn security_version(&self) -> u64 {
8229 self.catalog.read().security_version
8230 }
8231
8232 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
8234 self.catalog.read().roles.clone()
8235 }
8236
8237 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
8239 self.require(&crate::auth::Permission::Admin)?;
8240 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
8241 let scram = crate::auth::scram_verifier(password).map_err(MongrelError::Other)?;
8242 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(password);
8243 self.create_user_with_password_hash_inner(username, hash, Some((scram, mysql)), None)
8244 }
8245
8246 pub fn create_user_with_password_hash(
8248 &self,
8249 username: &str,
8250 hash: String,
8251 ) -> Result<crate::auth::UserEntry> {
8252 self.create_user_with_password_hash_inner(username, hash, None, None)
8253 }
8254
8255 pub fn create_user_with_password_hash_controlled<F>(
8256 &self,
8257 username: &str,
8258 hash: String,
8259 mut before_publish: F,
8260 ) -> Result<crate::auth::UserEntry>
8261 where
8262 F: FnMut() -> Result<()>,
8263 {
8264 self.create_user_with_password_hash_inner(username, hash, None, Some(&mut before_publish))
8265 }
8266
8267 fn create_user_with_password_hash_inner(
8268 &self,
8269 username: &str,
8270 hash: String,
8271 verifiers: Option<(
8272 crate::security_hardening::ScramVerifier,
8273 crate::auth::MysqlCachingSha2Verifier,
8274 )>,
8275 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8276 ) -> Result<crate::auth::UserEntry> {
8277 let command = match &verifiers {
8281 Some((scram_sha_256, mysql_caching_sha2)) => {
8282 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8283 username: username.to_string(),
8284 password_hash: hash.clone(),
8285 scram_sha_256: scram_sha_256.clone(),
8286 mysql_caching_sha2: mysql_caching_sha2.clone(),
8287 is_admin: false,
8288 created_epoch: 0,
8289 }
8290 }
8291 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8292 username: username.to_string(),
8293 password_hash: hash.clone(),
8294 is_admin: false,
8295 created_epoch: 0,
8296 },
8297 };
8298 self.require(&crate::catalog_cmds::required_permission(&command))?;
8299 let _ddl = self.ddl_lock.lock();
8300 let _security_write = self.security_write()?;
8301 self.require(&crate::catalog_cmds::required_permission(&command))?;
8302 let _commit = self.commit_lock.lock();
8303 let epoch = self.epoch.bump_assigned();
8304 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8305 let command = match verifiers {
8306 Some((scram_sha_256, mysql_caching_sha2)) => {
8307 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8308 username: username.to_string(),
8309 password_hash: hash,
8310 scram_sha_256,
8311 mysql_caching_sha2,
8312 is_admin: false,
8313 created_epoch: epoch.0,
8314 }
8315 }
8316 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8317 username: username.to_string(),
8318 password_hash: hash,
8319 is_admin: false,
8320 created_epoch: epoch.0,
8321 },
8322 };
8323 let mut next_catalog = self.catalog.read().clone();
8324 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8325 let crate::catalog_cmds::CatalogDelta::UserUpserted(entry) = delta else {
8326 return Err(MongrelError::Other(
8327 "CreateUser resolved to an unexpected catalog delta".into(),
8328 ));
8329 };
8330 next_catalog.db_epoch = epoch.0;
8331 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8332 Ok(entry)
8333 }
8334
8335 pub fn drop_user(&self, username: &str) -> Result<()> {
8337 self.drop_user_with_epoch(username).map(|_| ())
8338 }
8339
8340 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
8341 self.drop_user_with_epoch_inner(username, None)
8342 }
8343
8344 pub fn drop_user_with_epoch_controlled<F>(
8345 &self,
8346 username: &str,
8347 mut before_publish: F,
8348 ) -> Result<Epoch>
8349 where
8350 F: FnMut() -> Result<()>,
8351 {
8352 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
8353 }
8354
8355 fn drop_user_with_epoch_inner(
8356 &self,
8357 username: &str,
8358 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8359 ) -> Result<Epoch> {
8360 let command = crate::catalog_cmds::CatalogCommand::DropUser {
8361 username: username.to_string(),
8362 };
8363 self.require(&crate::catalog_cmds::required_permission(&command))?;
8364 let _ddl = self.ddl_lock.lock();
8365 let _security_write = self.security_write()?;
8366 self.require(&crate::catalog_cmds::required_permission(&command))?;
8367 let _commit = self.commit_lock.lock();
8368 let epoch = self.epoch.bump_assigned();
8369 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8370 let mut next_catalog = self.catalog.read().clone();
8371 self.apply_catalog_command_to(&mut next_catalog, command)?;
8372 next_catalog.db_epoch = epoch.0;
8373 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8374 Ok(epoch)
8375 }
8376
8377 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
8379 self.alter_user_password_with_epoch(username, new_password)
8380 .map(|_| ())
8381 }
8382
8383 pub fn alter_user_password_with_epoch(
8384 &self,
8385 username: &str,
8386 new_password: &str,
8387 ) -> Result<Epoch> {
8388 self.require(&crate::auth::Permission::Admin)?;
8389 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
8390 let scram = crate::auth::scram_verifier(new_password).map_err(MongrelError::Other)?;
8391 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(new_password);
8392 self.alter_user_password_hash_with_epoch_inner(username, hash, Some((scram, mysql)), None)
8393 }
8394
8395 pub fn alter_user_password_hash_with_epoch(
8396 &self,
8397 username: &str,
8398 hash: String,
8399 ) -> Result<Epoch> {
8400 self.alter_user_password_hash_with_epoch_inner(username, hash, None, None)
8401 }
8402
8403 pub fn alter_user_password_hash_with_epoch_controlled<F>(
8404 &self,
8405 username: &str,
8406 hash: String,
8407 mut before_publish: F,
8408 ) -> Result<Epoch>
8409 where
8410 F: FnMut() -> Result<()>,
8411 {
8412 self.alter_user_password_hash_with_epoch_inner(
8413 username,
8414 hash,
8415 None,
8416 Some(&mut before_publish),
8417 )
8418 }
8419
8420 fn alter_user_password_hash_with_epoch_inner(
8421 &self,
8422 username: &str,
8423 hash: String,
8424 verifiers: Option<(
8425 crate::security_hardening::ScramVerifier,
8426 crate::auth::MysqlCachingSha2Verifier,
8427 )>,
8428 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8429 ) -> Result<Epoch> {
8430 let command = match verifiers {
8431 Some((scram_sha_256, mysql_caching_sha2)) => {
8432 crate::catalog_cmds::CatalogCommand::AlterUserPasswordWithAuthVerifiers {
8433 username: username.to_string(),
8434 password_hash: hash,
8435 scram_sha_256,
8436 mysql_caching_sha2,
8437 }
8438 }
8439 None => crate::catalog_cmds::CatalogCommand::AlterUserPassword {
8440 username: username.to_string(),
8441 password_hash: hash,
8442 },
8443 };
8444 self.require(&crate::catalog_cmds::required_permission(&command))?;
8445 let _ddl = self.ddl_lock.lock();
8446 let _security_write = self.security_write()?;
8447 self.require(&crate::catalog_cmds::required_permission(&command))?;
8448 let _commit = self.commit_lock.lock();
8449 let epoch = self.epoch.bump_assigned();
8450 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8451 let mut next_catalog = self.catalog.read().clone();
8452 self.apply_catalog_command_to(&mut next_catalog, command)?;
8453 next_catalog.db_epoch = epoch.0;
8454 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8455 Ok(epoch)
8456 }
8457
8458 pub fn verify_user(
8461 &self,
8462 username: &str,
8463 password: &str,
8464 ) -> Result<Option<crate::auth::UserEntry>> {
8465 let cat = self.catalog.read();
8466 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
8467 return Ok(None);
8468 };
8469 if user.password_hash.is_empty() {
8470 return Ok(None);
8471 }
8472 let ok = crate::auth::verify_password(password, &user.password_hash)
8473 .map_err(MongrelError::Other)?;
8474 if ok {
8475 Ok(Some(user.clone()))
8476 } else {
8477 Ok(None)
8478 }
8479 }
8480
8481 pub fn authenticate_principal(
8485 &self,
8486 username: &str,
8487 password: &str,
8488 ) -> Result<Option<crate::auth::Principal>> {
8489 self.authenticate_principal_inner(username, password, || {})
8490 }
8491
8492 fn authenticate_principal_inner<F>(
8493 &self,
8494 username: &str,
8495 password: &str,
8496 after_verify: F,
8497 ) -> Result<Option<crate::auth::Principal>>
8498 where
8499 F: FnOnce(),
8500 {
8501 let catalog = self.catalog.read();
8502 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
8503 return Ok(None);
8504 };
8505 if user.password_hash.is_empty()
8506 || !crate::auth::verify_password(password, &user.password_hash)
8507 .map_err(MongrelError::Other)?
8508 {
8509 return Ok(None);
8510 }
8511 after_verify();
8512 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
8513 }
8514
8515 pub fn authenticate_mysql_caching_sha2_principal(
8518 &self,
8519 username: &str,
8520 nonce: &[u8],
8521 proof: &[u8],
8522 ) -> Option<crate::auth::Principal> {
8523 let catalog = self.catalog.read();
8524 let user = catalog
8525 .users
8526 .iter()
8527 .find(|user| user.username == username)?;
8528 if !user.mysql_caching_sha2.as_ref()?.verify(nonce, proof) {
8529 return None;
8530 }
8531 Self::resolve_user_principal_from_catalog(&catalog, user)
8532 }
8533
8534 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
8536 self.set_user_admin_with_epoch(username, is_admin)
8537 .map(|_| ())
8538 }
8539
8540 pub fn set_user_admin_with_epoch(
8541 &self,
8542 username: &str,
8543 is_admin: bool,
8544 ) -> Result<Option<Epoch>> {
8545 self.set_user_admin_with_epoch_inner(username, is_admin, None)
8546 }
8547
8548 pub fn set_user_admin_with_epoch_controlled<F>(
8549 &self,
8550 username: &str,
8551 is_admin: bool,
8552 mut before_publish: F,
8553 ) -> Result<Option<Epoch>>
8554 where
8555 F: FnMut() -> Result<()>,
8556 {
8557 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
8558 }
8559
8560 fn set_user_admin_with_epoch_inner(
8561 &self,
8562 username: &str,
8563 is_admin: bool,
8564 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8565 ) -> Result<Option<Epoch>> {
8566 let command = crate::catalog_cmds::CatalogCommand::SetUserAdmin {
8567 username: username.to_string(),
8568 is_admin,
8569 };
8570 self.require(&crate::catalog_cmds::required_permission(&command))?;
8571 let _ddl = self.ddl_lock.lock();
8572 let _security_write = self.security_write()?;
8573 self.require(&crate::catalog_cmds::required_permission(&command))?;
8574 let _commit = self.commit_lock.lock();
8575 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8579 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8580 return Ok(None);
8581 }
8582 let epoch = self.epoch.bump_assigned();
8583 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8584 let mut next_catalog = self.catalog.read().clone();
8585 self.apply_catalog_command_to(&mut next_catalog, command)?;
8586 next_catalog.db_epoch = epoch.0;
8587 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8588 Ok(Some(epoch))
8589 }
8590
8591 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
8593 self.create_role_inner(name, None)
8594 }
8595
8596 pub fn create_role_controlled<F>(
8597 &self,
8598 name: &str,
8599 mut before_publish: F,
8600 ) -> Result<crate::auth::RoleEntry>
8601 where
8602 F: FnMut() -> Result<()>,
8603 {
8604 self.create_role_inner(name, Some(&mut before_publish))
8605 }
8606
8607 fn create_role_inner(
8608 &self,
8609 name: &str,
8610 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8611 ) -> Result<crate::auth::RoleEntry> {
8612 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8613 name: name.to_string(),
8614 created_epoch: 0, };
8616 self.require(&crate::catalog_cmds::required_permission(&command))?;
8617 let _ddl = self.ddl_lock.lock();
8618 let _security_write = self.security_write()?;
8619 self.require(&crate::catalog_cmds::required_permission(&command))?;
8620 let _commit = self.commit_lock.lock();
8621 let epoch = self.epoch.bump_assigned();
8622 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8623 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8624 name: name.to_string(),
8625 created_epoch: epoch.0,
8626 };
8627 let mut next_catalog = self.catalog.read().clone();
8628 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8629 let crate::catalog_cmds::CatalogDelta::RoleUpserted(entry) = delta else {
8630 return Err(MongrelError::Other(
8631 "CreateRole resolved to an unexpected catalog delta".into(),
8632 ));
8633 };
8634 next_catalog.db_epoch = epoch.0;
8635 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8636 Ok(entry)
8637 }
8638
8639 pub fn drop_role(&self, name: &str) -> Result<()> {
8641 self.drop_role_with_epoch(name).map(|_| ())
8642 }
8643
8644 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
8645 self.drop_role_with_epoch_inner(name, None)
8646 }
8647
8648 pub fn drop_role_with_epoch_controlled<F>(
8649 &self,
8650 name: &str,
8651 mut before_publish: F,
8652 ) -> Result<Epoch>
8653 where
8654 F: FnMut() -> Result<()>,
8655 {
8656 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
8657 }
8658
8659 fn drop_role_with_epoch_inner(
8660 &self,
8661 name: &str,
8662 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8663 ) -> Result<Epoch> {
8664 let command = crate::catalog_cmds::CatalogCommand::DropRole {
8665 name: name.to_string(),
8666 };
8667 self.require(&crate::catalog_cmds::required_permission(&command))?;
8668 let _ddl = self.ddl_lock.lock();
8669 let _security_write = self.security_write()?;
8670 self.require(&crate::catalog_cmds::required_permission(&command))?;
8671 let _commit = self.commit_lock.lock();
8672 let epoch = self.epoch.bump_assigned();
8673 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8674 let mut next_catalog = self.catalog.read().clone();
8675 self.apply_catalog_command_to(&mut next_catalog, command)?;
8676 next_catalog.db_epoch = epoch.0;
8677 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8678 Ok(epoch)
8679 }
8680
8681 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
8683 self.grant_role_with_epoch(username, role_name).map(|_| ())
8684 }
8685
8686 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8687 self.grant_role_with_epoch_inner(username, role_name, None)
8688 }
8689
8690 pub fn grant_role_with_epoch_controlled<F>(
8691 &self,
8692 username: &str,
8693 role_name: &str,
8694 mut before_publish: F,
8695 ) -> Result<Option<Epoch>>
8696 where
8697 F: FnMut() -> Result<()>,
8698 {
8699 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8700 }
8701
8702 fn grant_role_with_epoch_inner(
8703 &self,
8704 username: &str,
8705 role_name: &str,
8706 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8707 ) -> Result<Option<Epoch>> {
8708 let command = crate::catalog_cmds::CatalogCommand::GrantRole {
8709 username: username.to_string(),
8710 role: role_name.to_string(),
8711 };
8712 self.require(&crate::catalog_cmds::required_permission(&command))?;
8713 let _ddl = self.ddl_lock.lock();
8714 let _security_write = self.security_write()?;
8715 self.require(&crate::catalog_cmds::required_permission(&command))?;
8716 let _commit = self.commit_lock.lock();
8717 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8721 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8722 return Ok(None);
8723 }
8724 let epoch = self.epoch.bump_assigned();
8725 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8726 let mut next_catalog = self.catalog.read().clone();
8727 self.apply_catalog_command_to(&mut next_catalog, command)?;
8728 next_catalog.db_epoch = epoch.0;
8729 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8730 Ok(Some(epoch))
8731 }
8732
8733 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
8735 self.revoke_role_with_epoch(username, role_name).map(|_| ())
8736 }
8737
8738 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8739 self.revoke_role_with_epoch_inner(username, role_name, None)
8740 }
8741
8742 pub fn revoke_role_with_epoch_controlled<F>(
8743 &self,
8744 username: &str,
8745 role_name: &str,
8746 mut before_publish: F,
8747 ) -> Result<Option<Epoch>>
8748 where
8749 F: FnMut() -> Result<()>,
8750 {
8751 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8752 }
8753
8754 fn revoke_role_with_epoch_inner(
8755 &self,
8756 username: &str,
8757 role_name: &str,
8758 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8759 ) -> Result<Option<Epoch>> {
8760 let command = crate::catalog_cmds::CatalogCommand::RevokeRole {
8761 username: username.to_string(),
8762 role: role_name.to_string(),
8763 };
8764 self.require(&crate::catalog_cmds::required_permission(&command))?;
8765 let _ddl = self.ddl_lock.lock();
8766 let _security_write = self.security_write()?;
8767 self.require(&crate::catalog_cmds::required_permission(&command))?;
8768 let _commit = self.commit_lock.lock();
8769 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8773 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8774 return Ok(None);
8775 }
8776 let epoch = self.epoch.bump_assigned();
8777 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8778 let mut next_catalog = self.catalog.read().clone();
8779 self.apply_catalog_command_to(&mut next_catalog, command)?;
8780 next_catalog.db_epoch = epoch.0;
8781 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8782 Ok(Some(epoch))
8783 }
8784
8785 pub fn grant_permission(
8787 &self,
8788 role_name: &str,
8789 permission: crate::auth::Permission,
8790 ) -> Result<()> {
8791 self.grant_permission_with_epoch(role_name, permission)
8792 .map(|_| ())
8793 }
8794
8795 pub fn grant_permission_with_epoch(
8796 &self,
8797 role_name: &str,
8798 permission: crate::auth::Permission,
8799 ) -> Result<Option<Epoch>> {
8800 self.grant_permission_with_epoch_inner(role_name, permission, None)
8801 }
8802
8803 pub fn grant_permission_with_epoch_controlled<F>(
8804 &self,
8805 role_name: &str,
8806 permission: crate::auth::Permission,
8807 mut before_publish: F,
8808 ) -> Result<Option<Epoch>>
8809 where
8810 F: FnMut() -> Result<()>,
8811 {
8812 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8813 }
8814
8815 fn grant_permission_with_epoch_inner(
8816 &self,
8817 role_name: &str,
8818 permission: crate::auth::Permission,
8819 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8820 ) -> Result<Option<Epoch>> {
8821 let command = crate::catalog_cmds::CatalogCommand::GrantPermission {
8822 role: role_name.to_string(),
8823 permission,
8824 };
8825 self.require(&crate::catalog_cmds::required_permission(&command))?;
8826 let _ddl = self.ddl_lock.lock();
8827 let _security_write = self.security_write()?;
8828 self.require(&crate::catalog_cmds::required_permission(&command))?;
8829 let _commit = self.commit_lock.lock();
8830 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8834 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8835 return Ok(None);
8836 }
8837 let epoch = self.epoch.bump_assigned();
8838 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8839 let mut next_catalog = self.catalog.read().clone();
8840 self.apply_catalog_command_to(&mut next_catalog, command)?;
8841 next_catalog.db_epoch = epoch.0;
8842 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8843 Ok(Some(epoch))
8844 }
8845
8846 pub fn revoke_permission(
8848 &self,
8849 role_name: &str,
8850 permission: crate::auth::Permission,
8851 ) -> Result<()> {
8852 self.revoke_permission_with_epoch(role_name, permission)
8853 .map(|_| ())
8854 }
8855
8856 pub fn revoke_permission_with_epoch(
8857 &self,
8858 role_name: &str,
8859 permission: crate::auth::Permission,
8860 ) -> Result<Option<Epoch>> {
8861 self.revoke_permission_with_epoch_inner(role_name, permission, None)
8862 }
8863
8864 pub fn revoke_permission_with_epoch_controlled<F>(
8865 &self,
8866 role_name: &str,
8867 permission: crate::auth::Permission,
8868 mut before_publish: F,
8869 ) -> Result<Option<Epoch>>
8870 where
8871 F: FnMut() -> Result<()>,
8872 {
8873 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8874 }
8875
8876 fn revoke_permission_with_epoch_inner(
8877 &self,
8878 role_name: &str,
8879 permission: crate::auth::Permission,
8880 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8881 ) -> Result<Option<Epoch>> {
8882 let command = crate::catalog_cmds::CatalogCommand::RevokePermission {
8883 role: role_name.to_string(),
8884 permission,
8885 };
8886 self.require(&crate::catalog_cmds::required_permission(&command))?;
8887 let _ddl = self.ddl_lock.lock();
8888 let _security_write = self.security_write()?;
8889 self.require(&crate::catalog_cmds::required_permission(&command))?;
8890 let _commit = self.commit_lock.lock();
8891 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8895 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8896 return Ok(None);
8897 }
8898 let epoch = self.epoch.bump_assigned();
8899 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8900 let mut next_catalog = self.catalog.read().clone();
8901 self.apply_catalog_command_to(&mut next_catalog, command)?;
8902 next_catalog.db_epoch = epoch.0;
8903 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8904 Ok(Some(epoch))
8905 }
8906
8907 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
8910 let cat = self.catalog.read();
8911 Self::resolve_principal_from_catalog(&cat, username)
8912 }
8913
8914 pub fn resolve_current_principal(
8917 &self,
8918 principal: &crate::auth::Principal,
8919 ) -> Option<crate::auth::Principal> {
8920 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
8921 }
8922
8923 fn resolve_principal_from_catalog(
8928 cat: &Catalog,
8929 username: &str,
8930 ) -> Option<crate::auth::Principal> {
8931 let user = cat.users.iter().find(|u| u.username == username)?;
8932 Self::resolve_user_principal_from_catalog(cat, user)
8933 }
8934
8935 fn resolve_bound_principal_from_catalog(
8936 cat: &Catalog,
8937 principal: &crate::auth::Principal,
8938 ) -> Option<crate::auth::Principal> {
8939 let user = cat.users.iter().find(|user| {
8940 user.id == principal.user_id
8941 && user.created_epoch == principal.created_epoch
8942 && user.username == principal.username
8943 })?;
8944 Self::resolve_user_principal_from_catalog(cat, user)
8945 }
8946
8947 fn resolve_user_principal_from_catalog(
8948 cat: &Catalog,
8949 user: &crate::auth::UserEntry,
8950 ) -> Option<crate::auth::Principal> {
8951 let mut permissions = Vec::new();
8952 for role_name in &user.roles {
8953 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
8954 permissions.extend(role.permissions.iter().cloned());
8955 }
8956 }
8957 Some(crate::auth::Principal {
8958 user_id: user.id,
8959 created_epoch: user.created_epoch,
8960 username: user.username.clone(),
8961 is_admin: user.is_admin,
8962 roles: user.roles.clone(),
8963 permissions,
8964 })
8965 }
8966
8967 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
8969 match self.resolve_principal(username) {
8970 Some(p) => p.has_permission(permission),
8971 None => false,
8972 }
8973 }
8974
8975 pub fn require_auth_enabled(&self) -> bool {
8979 self.catalog.read().require_auth
8980 }
8981
8982 pub fn principal(&self) -> Option<crate::auth::Principal> {
8986 self.principal.read().clone()
8987 }
8988
8989 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
8995 if self.shared {
8996 return None;
8997 }
8998 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
8999 self.auth_state.clone(),
9000 )))
9001 }
9002
9003 pub fn refresh_principal(&self) -> Result<()> {
9017 let previous = match self.principal.read().clone() {
9018 Some(principal) => principal,
9019 None => return Ok(()),
9020 };
9021 if previous.user_id == 0 {
9025 return Ok(());
9026 }
9027 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
9028 self.refresh_security_catalog_if_stale(observed_version)?;
9029 let cat = self.catalog.read();
9030 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
9031 Some(p) => {
9032 *self.principal.write() = Some(p.clone());
9033 self.auth_state.set_principal(Some(p));
9037 Ok(())
9038 }
9039 None => Err(MongrelError::InvalidCredentials {
9040 username: previous.username,
9041 }),
9042 }
9043 }
9044
9045 pub fn security_catalog_disk_read_count(&self) -> u64 {
9048 self.security_catalog_disk_reads.load(Ordering::Relaxed)
9049 }
9050
9051 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
9063 if self.shared {
9064 return Err(MongrelError::Conflict(
9070 "enable_auth requires an exclusive Database owner; shared cores reject \
9071 auth-mode transitions"
9072 .into(),
9073 ));
9074 }
9075 let password_hash =
9076 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
9077 let scram_sha_256 =
9078 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
9079 let mysql_caching_sha2 =
9080 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
9081 let _ddl = self.ddl_lock.lock();
9082 let _security_write = self.security_write()?;
9083 let _commit = self.commit_lock.lock();
9084 let epoch = self.epoch.bump_assigned();
9085 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9086 let mut next_catalog = self.catalog.read().clone();
9087 if next_catalog.require_auth {
9088 return Err(MongrelError::InvalidArgument(
9089 "database already has require_auth enabled".into(),
9090 ));
9091 }
9092 if next_catalog
9093 .users
9094 .iter()
9095 .any(|u| u.username == admin_username)
9096 {
9097 return Err(MongrelError::InvalidArgument(format!(
9098 "user {admin_username:?} already exists"
9099 )));
9100 }
9101 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
9102 let id = next_catalog.next_user_id;
9103 next_catalog.next_user_id = id
9104 .checked_add(1)
9105 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
9106 next_catalog.users.push(crate::auth::UserEntry {
9107 id,
9108 username: admin_username.to_string(),
9109 password_hash,
9110 scram_sha_256: Some(scram_sha_256),
9111 mysql_caching_sha2: Some(mysql_caching_sha2),
9112 roles: Vec::new(),
9113 is_admin: true,
9114 created_epoch: epoch.0,
9115 });
9116 next_catalog.require_auth = true;
9117 advance_security_version(&mut next_catalog)?;
9118 next_catalog.db_epoch = epoch.0;
9119 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9120 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9124 let principal = crate::auth::Principal {
9125 user_id: id,
9126 created_epoch: epoch.0,
9127 username: admin_username.to_string(),
9128 is_admin: true,
9129 roles: Vec::new(),
9130 permissions: Vec::new(),
9131 };
9132 *self.principal.write() = Some(principal.clone());
9133 self.auth_state.set_principal(Some(principal));
9134 }
9135 publish
9136 }
9137
9138 pub fn disable_auth(&self) -> Result<()> {
9155 let _ddl = self.ddl_lock.lock();
9156 let _security_write = self.security_write()?;
9157 let _commit = self.commit_lock.lock();
9158 let epoch = self.epoch.bump_assigned();
9159 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9160 let mut next_catalog = self.catalog.read().clone();
9161 if !next_catalog.require_auth {
9162 return Err(MongrelError::InvalidArgument(
9163 "database does not have require_auth enabled".into(),
9164 ));
9165 }
9166 next_catalog.require_auth = false;
9167 advance_security_version(&mut next_catalog)?;
9168 next_catalog.db_epoch = epoch.0;
9169 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9170 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9172 *self.principal.write() = None;
9173 }
9174 publish
9175 }
9176
9177 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
9184 self.ensure_owner_process()?;
9185 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
9186 return Err(MongrelError::ReadOnlyReplica);
9187 }
9188 if self.principal.read().is_some() {
9189 self.refresh_principal().map_err(|error| match error {
9190 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
9191 error => error,
9192 })?;
9193 }
9194 if !self.catalog.read().require_auth {
9195 return Ok(());
9196 }
9197 let guard = self.principal.read();
9198 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
9199 if p.has_permission(perm) {
9200 Ok(())
9201 } else {
9202 Err(MongrelError::PermissionDenied {
9203 required: perm.clone(),
9204 principal: p.username.clone(),
9205 })
9206 }
9207 }
9208
9209 pub fn require_table(
9214 &self,
9215 table: &str,
9216 perm: crate::auth_state::RequiredPermission,
9217 ) -> Result<()> {
9218 self.require(&perm.into_permission(table))
9219 }
9220
9221 pub fn triggers(&self) -> Vec<StoredTrigger> {
9222 self.catalog
9223 .read()
9224 .triggers
9225 .iter()
9226 .map(|t| t.trigger.clone())
9227 .collect()
9228 }
9229
9230 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
9231 self.catalog
9232 .read()
9233 .triggers
9234 .iter()
9235 .find(|t| t.trigger.name == name)
9236 .map(|t| t.trigger.clone())
9237 }
9238
9239 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9240 self.create_trigger_inner(trigger, None, None)
9241 }
9242
9243 pub fn create_trigger_controlled<F>(
9244 &self,
9245 trigger: StoredTrigger,
9246 mut before_publish: F,
9247 ) -> Result<StoredTrigger>
9248 where
9249 F: FnMut() -> Result<()>,
9250 {
9251 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
9252 }
9253
9254 pub fn create_trigger_as_controlled<F>(
9255 &self,
9256 trigger: StoredTrigger,
9257 principal: Option<&crate::auth::Principal>,
9258 mut before_publish: F,
9259 ) -> Result<StoredTrigger>
9260 where
9261 F: FnMut() -> Result<()>,
9262 {
9263 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
9264 }
9265
9266 fn create_trigger_inner(
9267 &self,
9268 mut trigger: StoredTrigger,
9269 principal: Option<&crate::auth::Principal>,
9270 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9271 ) -> Result<StoredTrigger> {
9272 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9273 trigger: trigger.clone(),
9274 };
9275 self.require_for(
9276 principal,
9277 &crate::catalog_cmds::required_permission(&command),
9278 )?;
9279 let _g = self.ddl_lock.lock();
9280 let _security_write = self.security_write()?;
9281 self.require_for(
9282 principal,
9283 &crate::catalog_cmds::required_permission(&command),
9284 )?;
9285 trigger.validate()?;
9286 self.validate_trigger_references(&trigger)
9287 .map_err(trigger_validation_error)?;
9288 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9292 let commit_lock = Arc::clone(&self.commit_lock);
9293 let _c = commit_lock.lock();
9294 let epoch = self.epoch.bump_assigned();
9295 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9296 trigger.created_epoch = epoch.0;
9297 trigger.updated_epoch = epoch.0;
9298 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9299 trigger: trigger.clone(),
9300 };
9301 let mut next_catalog = self.catalog.read().clone();
9302 self.apply_catalog_command_to(&mut next_catalog, command)?;
9303 next_catalog.db_epoch = epoch.0;
9304 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9305 Ok(trigger)
9306 }
9307
9308 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9309 self.create_or_replace_trigger_inner(trigger, None, None)
9310 }
9311
9312 pub fn create_or_replace_trigger_controlled<F>(
9313 &self,
9314 trigger: StoredTrigger,
9315 mut before_publish: F,
9316 ) -> Result<StoredTrigger>
9317 where
9318 F: FnMut() -> Result<()>,
9319 {
9320 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
9321 }
9322
9323 pub fn create_or_replace_trigger_as_controlled<F>(
9324 &self,
9325 trigger: StoredTrigger,
9326 principal: Option<&crate::auth::Principal>,
9327 mut before_publish: F,
9328 ) -> Result<StoredTrigger>
9329 where
9330 F: FnMut() -> Result<()>,
9331 {
9332 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
9333 }
9334
9335 fn create_or_replace_trigger_inner(
9336 &self,
9337 trigger: StoredTrigger,
9338 principal: Option<&crate::auth::Principal>,
9339 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9340 ) -> Result<StoredTrigger> {
9341 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9342 trigger: trigger.clone(),
9343 };
9344 self.require_for(
9345 principal,
9346 &crate::catalog_cmds::required_permission(&command),
9347 )?;
9348 let _g = self.ddl_lock.lock();
9349 let _security_write = self.security_write()?;
9350 self.require_for(
9351 principal,
9352 &crate::catalog_cmds::required_permission(&command),
9353 )?;
9354 trigger.validate()?;
9355 self.validate_trigger_references(&trigger)
9356 .map_err(trigger_validation_error)?;
9357 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9361 let commit_lock = Arc::clone(&self.commit_lock);
9362 let _c = commit_lock.lock();
9363 let epoch = self.epoch.bump_assigned();
9364 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9365 let mut next_catalog = self.catalog.read().clone();
9366 let replaced = match next_catalog
9371 .triggers
9372 .iter()
9373 .position(|t| t.trigger.name == trigger.name)
9374 {
9375 Some(idx) => next_catalog.triggers[idx]
9376 .trigger
9377 .replaced(trigger.clone(), epoch.0)?,
9378 None => {
9379 let mut next = trigger;
9380 next.created_epoch = epoch.0;
9381 next.updated_epoch = epoch.0;
9382 next
9383 }
9384 };
9385 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9386 trigger: replaced.clone(),
9387 };
9388 self.apply_catalog_command_to(&mut next_catalog, command)?;
9389 next_catalog.db_epoch = epoch.0;
9390 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9391 Ok(replaced)
9392 }
9393
9394 pub fn drop_trigger(&self, name: &str) -> Result<()> {
9395 self.drop_trigger_with_epoch(name).map(|_| ())
9396 }
9397
9398 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
9400 self.drop_triggers_with_epoch(&[name.to_string()])
9401 }
9402
9403 pub fn drop_trigger_with_epoch_controlled<F>(
9404 &self,
9405 name: &str,
9406 before_publish: F,
9407 ) -> Result<Epoch>
9408 where
9409 F: FnMut() -> Result<()>,
9410 {
9411 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
9412 }
9413
9414 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
9416 self.drop_triggers_with_epoch_inner(names, None, None)
9417 }
9418
9419 pub fn drop_triggers_with_epoch_controlled<F>(
9420 &self,
9421 names: &[String],
9422 mut before_publish: F,
9423 ) -> Result<Epoch>
9424 where
9425 F: FnMut() -> Result<()>,
9426 {
9427 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
9428 }
9429
9430 pub fn drop_triggers_with_epoch_as_controlled<F>(
9431 &self,
9432 names: &[String],
9433 principal: Option<&crate::auth::Principal>,
9434 mut before_publish: F,
9435 ) -> Result<Epoch>
9436 where
9437 F: FnMut() -> Result<()>,
9438 {
9439 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
9440 }
9441
9442 fn drop_triggers_with_epoch_inner(
9443 &self,
9444 names: &[String],
9445 principal: Option<&crate::auth::Principal>,
9446 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9447 ) -> Result<Epoch> {
9448 if names.is_empty() {
9449 return Err(MongrelError::InvalidArgument(
9450 "at least one trigger name is required".into(),
9451 ));
9452 }
9453 let commands: Vec<crate::catalog_cmds::CatalogCommand> = names
9454 .iter()
9455 .map(|name| crate::catalog_cmds::CatalogCommand::DropTrigger { name: name.clone() })
9456 .collect();
9457 for command in &commands {
9458 self.require_for(
9459 principal,
9460 &crate::catalog_cmds::required_permission(command),
9461 )?;
9462 }
9463 let _g = self.ddl_lock.lock();
9464 let _security_write = self.security_write()?;
9465 for command in &commands {
9466 self.require_for(
9467 principal,
9468 &crate::catalog_cmds::required_permission(command),
9469 )?;
9470 }
9471 {
9476 let cat = self.catalog.read();
9477 for command in &commands {
9478 crate::catalog_cmds::apply(&cat, command)?;
9479 }
9480 }
9481 let commit_lock = Arc::clone(&self.commit_lock);
9482 let _c = commit_lock.lock();
9483 let epoch = self.epoch.bump_assigned();
9484 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9485 let mut next_catalog = self.catalog.read().clone();
9486 for command in commands {
9487 self.apply_catalog_command_to(&mut next_catalog, command)?;
9488 }
9489 next_catalog.db_epoch = epoch.0;
9490 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9491 Ok(epoch)
9492 }
9493
9494 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
9495 self.catalog.read().external_tables.clone()
9496 }
9497
9498 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
9499 self.catalog
9500 .read()
9501 .external_tables
9502 .iter()
9503 .find(|t| t.name == name)
9504 .cloned()
9505 }
9506
9507 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
9508 self.create_external_table_inner(entry, None)
9509 }
9510
9511 pub fn create_external_table_controlled<F>(
9512 &self,
9513 entry: ExternalTableEntry,
9514 mut before_publish: F,
9515 ) -> Result<ExternalTableEntry>
9516 where
9517 F: FnMut() -> Result<()>,
9518 {
9519 self.create_external_table_inner(entry, Some(&mut before_publish))
9520 }
9521
9522 fn create_external_table_inner(
9523 &self,
9524 mut entry: ExternalTableEntry,
9525 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9526 ) -> Result<ExternalTableEntry> {
9527 self.require(&crate::auth::Permission::Ddl)?;
9528 let _g = self.ddl_lock.lock();
9529 let _security_write = self.security_write()?;
9530 self.require(&crate::auth::Permission::Ddl)?;
9531 entry.validate()?;
9532 {
9533 let cat = self.catalog.read();
9534 if cat.live(&entry.name).is_some()
9535 || cat.external_tables.iter().any(|t| t.name == entry.name)
9536 {
9537 return Err(MongrelError::InvalidArgument(format!(
9538 "table {:?} already exists",
9539 entry.name
9540 )));
9541 }
9542 }
9543 let commit_lock = Arc::clone(&self.commit_lock);
9544 let _c = commit_lock.lock();
9545 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
9549 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
9550 let epoch = self.epoch.bump_assigned();
9551 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9552 entry.created_epoch = epoch.0;
9553 let mut next_catalog = self.catalog.read().clone();
9554 next_catalog.external_tables.push(entry.clone());
9555 next_catalog.db_epoch = epoch.0;
9556 self.publish_catalog_candidate_with_prelude(
9557 next_catalog,
9558 epoch,
9559 &mut _epoch_guard,
9560 before_publish,
9561 vec![(
9562 EXTERNAL_TABLE_ID,
9563 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9564 name: entry.name.clone(),
9565 generation_epoch: epoch.0,
9566 }),
9567 )],
9568 )?;
9569 Ok(entry)
9570 }
9571
9572 pub fn drop_external_table(&self, name: &str) -> Result<()> {
9573 self.drop_external_table_with_epoch(name).map(|_| ())
9574 }
9575
9576 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
9578 self.drop_external_table_with_epoch_inner(name, None)
9579 }
9580
9581 pub fn drop_external_table_with_epoch_controlled<F>(
9582 &self,
9583 name: &str,
9584 mut before_publish: F,
9585 ) -> Result<Epoch>
9586 where
9587 F: FnMut() -> Result<()>,
9588 {
9589 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
9590 }
9591
9592 fn drop_external_table_with_epoch_inner(
9593 &self,
9594 name: &str,
9595 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9596 ) -> Result<Epoch> {
9597 self.require(&crate::auth::Permission::Ddl)?;
9598 let _g = self.ddl_lock.lock();
9599 let _security_write = self.security_write()?;
9600 self.require(&crate::auth::Permission::Ddl)?;
9601 let commit_lock = Arc::clone(&self.commit_lock);
9602 let _c = commit_lock.lock();
9603 let epoch = self.epoch.bump_assigned();
9604 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9605 let mut next_catalog = self.catalog.read().clone();
9606 let before = next_catalog.external_tables.len();
9607 next_catalog.external_tables.retain(|t| t.name != name);
9608 if next_catalog.external_tables.len() == before {
9609 return Err(MongrelError::NotFound(format!(
9610 "external table {name:?} not found"
9611 )));
9612 }
9613 next_catalog.db_epoch = epoch.0;
9614 self.publish_catalog_candidate_with_prelude(
9615 next_catalog,
9616 epoch,
9617 &mut _epoch_guard,
9618 before_publish,
9619 vec![(
9620 EXTERNAL_TABLE_ID,
9621 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9622 name: name.to_string(),
9623 generation_epoch: epoch.0,
9624 }),
9625 )],
9626 )?;
9627 let state_dir = self.root.join(VTAB_DIR).join(name);
9628 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
9629 return Err(MongrelError::DurableCommit {
9630 epoch: epoch.0,
9631 message: format!(
9632 "external table was dropped but connector-state cleanup failed: {error}"
9633 ),
9634 });
9635 }
9636 Ok(epoch)
9637 }
9638
9639 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
9640 let txn_id = self.alloc_txn_id()?;
9641 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9642 self.commit_transaction_with_external_states(
9643 txn_id,
9644 self.epoch.visible(),
9645 Vec::new(),
9646 vec![(name.to_string(), state.to_vec())],
9647 Vec::new(),
9648 principal,
9649 catalog_bound,
9650 None,
9651 crate::txn::TxnCommitContext::internal(),
9652 )
9653 .map(|(epoch, _)| epoch)
9654 }
9655
9656 pub fn trigger_config(&self) -> TriggerConfig {
9657 use std::sync::atomic::Ordering;
9658 TriggerConfig {
9659 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
9660 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
9661 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
9662 }
9663 }
9664
9665 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
9666 use std::sync::atomic::Ordering;
9667 if config.max_depth == 0 {
9668 return Err(MongrelError::InvalidArgument(
9669 "trigger max_depth must be greater than 0".into(),
9670 ));
9671 }
9672 self.trigger_recursive
9673 .store(config.recursive_triggers, Ordering::Relaxed);
9674 self.trigger_max_depth
9675 .store(config.max_depth, Ordering::Relaxed);
9676 self.trigger_max_loop_iterations
9677 .store(config.max_loop_iterations, Ordering::Relaxed);
9678 Ok(())
9679 }
9680
9681 pub fn set_recursive_triggers(&self, recursive: bool) {
9682 use std::sync::atomic::Ordering;
9683 self.trigger_recursive.store(recursive, Ordering::Relaxed);
9684 }
9685
9686 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
9690 self.notify.subscribe()
9691 }
9692
9693 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
9694 self.change_wake.subscribe()
9695 }
9696
9697 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
9702 let control = crate::ExecutionControl::new(None);
9703 self.change_events_since_controlled(last_event_id, &control)
9704 }
9705
9706 pub fn change_events_since_controlled(
9708 &self,
9709 last_event_id: Option<&str>,
9710 control: &crate::ExecutionControl,
9711 ) -> Result<CdcBatch> {
9712 use crate::wal::Op;
9713
9714 control.checkpoint()?;
9715 let resume = match last_event_id {
9716 Some(id) => {
9717 let (epoch, index) = id.split_once(':').ok_or_else(|| {
9718 MongrelError::InvalidArgument(format!(
9719 "invalid CDC event id {id:?}; expected <epoch>:<index>"
9720 ))
9721 })?;
9722 Some((
9723 epoch.parse::<u64>().map_err(|error| {
9724 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
9725 })?,
9726 index.parse::<u32>().map_err(|error| {
9727 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
9728 })?,
9729 ))
9730 }
9731 None => None,
9732 };
9733
9734 let mut wal = self.shared_wal.lock();
9735 wal.group_sync()?;
9736 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
9737 let records = crate::wal::SharedWal::replay_with_dek_controlled(
9738 &self.root,
9739 wal_dek.as_ref(),
9740 control,
9741 CDC_MAX_WAL_RECORDS,
9742 CDC_MAX_WAL_REPLAY_BYTES,
9743 )?;
9744 drop(wal);
9745 control.checkpoint()?;
9746
9747 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
9748 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
9749 for (index, record) in records.iter().enumerate() {
9750 if index % 256 == 0 {
9751 control.checkpoint()?;
9752 }
9753 if let Op::TxnCommit { epoch, added_runs } = &record.op {
9754 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
9755 }
9756 if let Op::SpilledRows { table_id, rows } = &record.op {
9757 spilled_payloads
9758 .entry((record.txn_id, *table_id))
9759 .or_default()
9760 .push(rows);
9761 }
9762 }
9763 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
9764 let current_epoch = self.epoch.committed().0;
9765 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
9766 let gap = resume.is_some_and(|(epoch, _)| {
9767 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
9768 });
9769 if gap {
9770 return Ok(CdcBatch {
9771 events: Vec::new(),
9772 current_epoch,
9773 earliest_epoch,
9774 gap: true,
9775 });
9776 }
9777
9778 let table_names: HashMap<u64, String> = self
9779 .catalog
9780 .read()
9781 .tables
9782 .iter()
9783 .map(|entry| (entry.table_id, entry.name.clone()))
9784 .collect();
9785 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
9786 let mut retained_bytes = 0_usize;
9787 for (index, record) in records.iter().enumerate() {
9788 if index % 256 == 0 {
9789 control.checkpoint()?;
9790 }
9791 if !commits.contains_key(&record.txn_id) {
9792 continue;
9793 }
9794 let Op::BeforeImage {
9795 table_id,
9796 row_id,
9797 row,
9798 } = &record.op
9799 else {
9800 continue;
9801 };
9802 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9803 return Err(MongrelError::ResourceLimitExceeded {
9804 resource: "CDC before-image bytes",
9805 requested: row.len(),
9806 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9807 });
9808 }
9809 let before: crate::memtable::Row = bincode::deserialize(row)?;
9810 if before_images.len() >= CDC_MAX_ROWS {
9811 return Err(MongrelError::ResourceLimitExceeded {
9812 resource: "CDC before-image rows",
9813 requested: before_images.len().saturating_add(1),
9814 limit: CDC_MAX_ROWS,
9815 });
9816 }
9817 charge_cdc_bytes(
9818 &mut retained_bytes,
9819 cdc_row_storage_bytes(&before),
9820 "CDC retained bytes",
9821 )?;
9822 before_images.insert((record.txn_id, *table_id, row_id.0), before);
9823 }
9824 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
9825 let mut events = Vec::new();
9826 let mut decoded_rows = before_images.len();
9827 for (record_index, record) in records.iter().enumerate() {
9828 if record_index % 256 == 0 {
9829 control.checkpoint()?;
9830 }
9831 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
9832 continue;
9833 };
9834 let event = match &record.op {
9835 Op::Put { table_id, rows } => {
9836 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9837 return Err(MongrelError::ResourceLimitExceeded {
9838 resource: "CDC inline row bytes",
9839 requested: rows.len(),
9840 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9841 });
9842 }
9843 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
9844 decoded_rows = decoded_rows.saturating_add(rows.len());
9845 if decoded_rows > CDC_MAX_ROWS {
9846 return Err(MongrelError::ResourceLimitExceeded {
9847 resource: "CDC decoded rows",
9848 requested: decoded_rows,
9849 limit: CDC_MAX_ROWS,
9850 });
9851 }
9852 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
9853 let mut peak_bytes = retained_bytes;
9854 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9855 let data = serde_json::to_value(rows)
9856 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
9857 Some((*table_id, "put", data, event_bytes))
9858 }
9859 Op::Delete { table_id, row_ids } => {
9860 let before = row_ids
9861 .iter()
9862 .filter_map(|row_id| {
9863 before_images
9864 .get(&(record.txn_id, *table_id, row_id.0))
9865 .cloned()
9866 })
9867 .collect::<Vec<_>>();
9868 let event_bytes = cdc_rows_json_bytes(&before)
9869 .saturating_add(
9870 row_ids
9871 .len()
9872 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
9873 )
9874 .saturating_add(512);
9875 let mut peak_bytes = retained_bytes;
9876 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9877 Some((
9878 *table_id,
9879 "delete",
9880 serde_json::json!({
9881 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
9882 "before": before,
9883 }),
9884 event_bytes,
9885 ))
9886 }
9887 Op::TruncateTable { table_id } => {
9888 Some((*table_id, "truncate", serde_json::Value::Null, 512))
9889 }
9890 _ => None,
9891 };
9892 if let Some((table_id, op, data, event_bytes)) = event {
9893 let index = operation_indices.entry(record.txn_id).or_insert(0);
9894 let event_position = (*commit_epoch, *index);
9895 *index = index.saturating_add(1);
9896 if resume.is_some_and(|position| event_position <= position) {
9897 continue;
9898 }
9899 if events.len() >= CDC_MAX_EVENTS {
9900 return Err(MongrelError::ResourceLimitExceeded {
9901 resource: "CDC events",
9902 requested: events.len().saturating_add(1),
9903 limit: CDC_MAX_EVENTS,
9904 });
9905 }
9906 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
9907 events.push(ChangeEvent {
9908 id: Some(format!("{}:{}", event_position.0, event_position.1)),
9909 channel: "changes".into(),
9910 table_id: Some(table_id),
9911 table: table_names.get(&table_id).cloned().unwrap_or_default(),
9912 op: op.into(),
9913 epoch: *commit_epoch,
9914 txn_id: Some(record.txn_id),
9915 message: None,
9916 data: Some(data),
9917 });
9918 }
9919 if let Op::TxnCommit { added_runs, .. } = &record.op {
9920 for run in added_runs {
9921 control.checkpoint()?;
9922 let index = operation_indices.entry(record.txn_id).or_insert(0);
9923 let event_position = (*commit_epoch, *index);
9924 *index = index.saturating_add(1);
9925 if resume.is_some_and(|position| event_position <= position) {
9926 continue;
9927 }
9928 let mut rows = if let Some(payloads) =
9929 spilled_payloads.get(&(record.txn_id, run.table_id))
9930 {
9931 let mut rows = Vec::new();
9932 for payload in payloads {
9933 control.checkpoint()?;
9934 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9935 return Err(MongrelError::ResourceLimitExceeded {
9936 resource: "CDC spilled row bytes",
9937 requested: payload.len(),
9938 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9939 });
9940 }
9941 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
9942 if decoded_rows
9943 .saturating_add(rows.len())
9944 .saturating_add(chunk.len())
9945 > CDC_MAX_ROWS
9946 {
9947 return Err(MongrelError::ResourceLimitExceeded {
9948 resource: "CDC decoded rows",
9949 requested: decoded_rows
9950 .saturating_add(rows.len())
9951 .saturating_add(chunk.len()),
9952 limit: CDC_MAX_ROWS,
9953 });
9954 }
9955 rows.extend(chunk);
9956 }
9957 rows
9958 } else {
9959 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
9960 return Ok(CdcBatch {
9961 events: Vec::new(),
9962 current_epoch,
9963 earliest_epoch,
9964 gap: true,
9965 });
9966 };
9967 let table = handle.lock();
9968 let mut reader = match table.open_reader(run.run_id) {
9969 Ok(reader) => reader,
9970 Err(_) => {
9971 return Ok(CdcBatch {
9972 events: Vec::new(),
9973 current_epoch,
9974 earliest_epoch,
9975 gap: true,
9976 })
9977 }
9978 };
9979 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
9980 let rows = reader.all_rows_controlled(control, remaining)?;
9981 drop(reader);
9982 drop(table);
9983 rows
9984 };
9985 for row in &mut rows {
9986 row.committed_epoch = Epoch(*commit_epoch);
9987 }
9988 decoded_rows = decoded_rows.saturating_add(rows.len());
9989 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
9990 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
9991 if events.len() >= CDC_MAX_EVENTS {
9992 return Err(MongrelError::ResourceLimitExceeded {
9993 resource: "CDC events",
9994 requested: events.len().saturating_add(1),
9995 limit: CDC_MAX_EVENTS,
9996 });
9997 }
9998 events.push(ChangeEvent {
9999 id: Some(format!("{}:{}", event_position.0, event_position.1)),
10000 channel: "changes".into(),
10001 table_id: Some(run.table_id),
10002 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
10003 op: "put_run".into(),
10004 epoch: *commit_epoch,
10005 txn_id: Some(record.txn_id),
10006 message: None,
10007 data: Some(serde_json::json!({
10008 "run_id": run.run_id.to_string(),
10009 "row_count": run.row_count,
10010 "min_row_id": run.min_row_id,
10011 "max_row_id": run.max_row_id,
10012 "rows": rows,
10013 })),
10014 });
10015 }
10016 }
10017 }
10018 control.checkpoint()?;
10019 Ok(CdcBatch {
10020 events,
10021 current_epoch,
10022 earliest_epoch,
10023 gap: false,
10024 })
10025 }
10026
10027 pub fn notify(&self, channel: &str, message: Option<String>) {
10030 let _ = self.notify.send(ChangeEvent {
10031 id: None,
10032 channel: channel.to_string(),
10033 table_id: None,
10034 table: String::new(),
10035 op: "notify".into(),
10036 epoch: self.epoch.visible().0,
10037 txn_id: None,
10038 message,
10039 data: None,
10040 });
10041 }
10042
10043 pub fn call_procedure(
10044 &self,
10045 name: &str,
10046 args: HashMap<String, crate::Value>,
10047 ) -> Result<ProcedureCallResult> {
10048 self.call_procedure_as(name, args, None)
10049 }
10050
10051 pub fn call_procedure_as(
10052 &self,
10053 name: &str,
10054 args: HashMap<String, crate::Value>,
10055 principal: Option<&crate::auth::Principal>,
10056 ) -> Result<ProcedureCallResult> {
10057 let control = crate::ExecutionControl::new(None);
10058 self.call_procedure_as_controlled(name, args, principal, &control, || true)
10059 }
10060
10061 #[doc(hidden)]
10064 pub fn call_procedure_as_bound(
10065 &self,
10066 expected: &StoredProcedure,
10067 args: HashMap<String, crate::Value>,
10068 principal: Option<&crate::auth::Principal>,
10069 ) -> Result<ProcedureCallResult> {
10070 self.require_for(principal, &crate::auth::Permission::All)?;
10071 let procedure = self.procedure(&expected.name).ok_or_else(|| {
10072 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
10073 })?;
10074 if &procedure != expected {
10075 return Err(MongrelError::Conflict(format!(
10076 "procedure {:?} changed after request authorization",
10077 expected.name
10078 )));
10079 }
10080 let control = crate::ExecutionControl::new(None);
10081 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
10082 }
10083
10084 #[doc(hidden)]
10089 pub fn call_procedure_as_controlled<F>(
10090 &self,
10091 name: &str,
10092 args: HashMap<String, crate::Value>,
10093 principal: Option<&crate::auth::Principal>,
10094 control: &crate::ExecutionControl,
10095 before_commit: F,
10096 ) -> Result<ProcedureCallResult>
10097 where
10098 F: FnOnce() -> bool,
10099 {
10100 self.require_for(principal, &crate::auth::Permission::All)?;
10104 let procedure = self
10105 .procedure(name)
10106 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
10107 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
10108 }
10109
10110 fn execute_procedure_as_controlled<F>(
10111 &self,
10112 procedure: StoredProcedure,
10113 args: HashMap<String, crate::Value>,
10114 principal: Option<&crate::auth::Principal>,
10115 control: &crate::ExecutionControl,
10116 before_commit: F,
10117 ) -> Result<ProcedureCallResult>
10118 where
10119 F: FnOnce() -> bool,
10120 {
10121 let args = bind_procedure_args(&procedure, args)?;
10122 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
10123 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
10124 if has_writes {
10125 let mut tx = self.begin_as(principal.cloned());
10126 let run = (|| {
10127 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10128 if step_index % 256 == 0 {
10129 control.checkpoint()?;
10130 }
10131 let output = self.execute_procedure_step(
10132 step,
10133 &args,
10134 &outputs,
10135 Some(&mut tx),
10136 principal,
10137 Some(control),
10138 )?;
10139 outputs.insert(step.id().to_string(), output);
10140 }
10141 control.checkpoint()?;
10142 eval_return_output(&procedure.body.return_value, &args, &outputs)
10143 })();
10144 match run {
10145 Ok(output) => {
10146 control.checkpoint()?;
10147 if !before_commit() {
10148 tx.rollback();
10149 return Err(MongrelError::Cancelled);
10150 }
10151 let epoch = tx.commit()?.0;
10152 Ok(ProcedureCallResult {
10153 epoch: Some(epoch),
10154 output,
10155 })
10156 }
10157 Err(e) => {
10158 tx.rollback();
10159 Err(e)
10160 }
10161 }
10162 } else {
10163 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10164 if step_index % 256 == 0 {
10165 control.checkpoint()?;
10166 }
10167 let output = self.execute_procedure_step(
10168 step,
10169 &args,
10170 &outputs,
10171 None,
10172 principal,
10173 Some(control),
10174 )?;
10175 outputs.insert(step.id().to_string(), output);
10176 }
10177 control.checkpoint()?;
10178 Ok(ProcedureCallResult {
10179 epoch: None,
10180 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
10181 })
10182 }
10183 }
10184
10185 fn execute_procedure_step(
10186 &self,
10187 step: &ProcedureStep,
10188 args: &HashMap<String, crate::Value>,
10189 outputs: &HashMap<String, ProcedureCallOutput>,
10190 tx: Option<&mut crate::txn::Transaction<'_>>,
10191 principal: Option<&crate::auth::Principal>,
10192 control: Option<&crate::ExecutionControl>,
10193 ) -> Result<ProcedureCallOutput> {
10194 if let Some(control) = control {
10195 control.checkpoint()?;
10196 }
10197 match step {
10198 ProcedureStep::NativeQuery {
10199 table,
10200 conditions,
10201 projection,
10202 limit,
10203 ..
10204 } => {
10205 let mut q = crate::Query::new();
10206 for condition in conditions {
10207 q = q.and(eval_condition(condition, args, outputs)?);
10208 }
10209 let fallback_control = crate::ExecutionControl::new(None);
10210 let query_control = control.unwrap_or(&fallback_control);
10211 let mut rows = self.query_for_principal_controlled(
10212 table,
10213 &q,
10214 projection.as_deref(),
10215 principal,
10216 false,
10217 query_control,
10218 )?;
10219 if let Some(limit) = limit {
10220 rows.truncate(*limit);
10221 }
10222 let mut output = Vec::with_capacity(rows.len());
10223 for (row_index, row) in rows.into_iter().enumerate() {
10224 if row_index % 256 == 0 {
10225 if let Some(control) = control {
10226 control.checkpoint()?;
10227 }
10228 }
10229 output.push(ProcedureCallRow {
10230 row_id: Some(row.row_id),
10231 columns: row.columns,
10232 });
10233 }
10234 Ok(ProcedureCallOutput::Rows(output))
10235 }
10236 ProcedureStep::Put {
10237 table,
10238 cells,
10239 returning,
10240 ..
10241 } => {
10242 let tx = tx.ok_or_else(|| {
10243 MongrelError::InvalidArgument(
10244 "write procedure step requires a transaction".into(),
10245 )
10246 })?;
10247 let cells = eval_cells(cells, args, outputs)?;
10248 if *returning {
10249 let out = tx.put_returning(table, cells)?;
10250 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10251 row_id: None,
10252 columns: out.row.columns.into_iter().collect(),
10253 }))
10254 } else {
10255 tx.put(table, cells)?;
10256 Ok(ProcedureCallOutput::Null)
10257 }
10258 }
10259 ProcedureStep::Upsert {
10260 table,
10261 cells,
10262 update_cells,
10263 returning,
10264 ..
10265 } => {
10266 let tx = tx.ok_or_else(|| {
10267 MongrelError::InvalidArgument(
10268 "write procedure step requires a transaction".into(),
10269 )
10270 })?;
10271 let cells = eval_cells(cells, args, outputs)?;
10272 let action = match update_cells {
10273 Some(update_cells) => {
10274 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
10275 }
10276 None => crate::UpsertAction::DoNothing,
10277 };
10278 let out = tx.upsert(table, cells, action)?;
10279 if *returning {
10280 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10281 row_id: None,
10282 columns: out.row.columns.into_iter().collect(),
10283 }))
10284 } else {
10285 Ok(ProcedureCallOutput::Null)
10286 }
10287 }
10288 ProcedureStep::DeleteByPk { table, pk, .. } => {
10289 let tx = tx.ok_or_else(|| {
10290 MongrelError::InvalidArgument(
10291 "write procedure step requires a transaction".into(),
10292 )
10293 })?;
10294 let pk = eval_value(pk, args, outputs)?;
10295 let handle = self.table(table)?;
10296 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
10297 MongrelError::NotFound("procedure delete_by_pk target not found".into())
10298 })?;
10299 tx.delete(table, row_id)?;
10300 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
10301 }
10302 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
10303 "DeleteRows procedure step is not supported by the core executor yet".into(),
10304 )),
10305 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
10306 "SqlQuery procedure step must be executed by mongreldb-query".into(),
10307 )),
10308 }
10309 }
10310
10311 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
10312 let cat = self.catalog.read();
10313 for step in &procedure.body.steps {
10314 let Some(table_name) = step.table() else {
10315 continue;
10316 };
10317 let schema = &cat
10318 .live(table_name)
10319 .ok_or_else(|| {
10320 MongrelError::InvalidArgument(format!(
10321 "procedure {:?} references unknown table {table_name:?}",
10322 procedure.name
10323 ))
10324 })?
10325 .schema;
10326 match step {
10327 ProcedureStep::NativeQuery {
10328 conditions,
10329 projection,
10330 ..
10331 } => {
10332 for condition in conditions {
10333 validate_condition_columns(condition, schema)?;
10334 }
10335 if let Some(projection) = projection {
10336 for id in projection {
10337 validate_column_id(*id, schema)?;
10338 }
10339 }
10340 }
10341 ProcedureStep::Put { cells, .. } => {
10342 for cell in cells {
10343 validate_column_id(cell.column_id, schema)?;
10344 }
10345 }
10346 ProcedureStep::Upsert {
10347 cells,
10348 update_cells,
10349 ..
10350 } => {
10351 for cell in cells {
10352 validate_column_id(cell.column_id, schema)?;
10353 }
10354 if let Some(update_cells) = update_cells {
10355 for cell in update_cells {
10356 validate_column_id(cell.column_id, schema)?;
10357 }
10358 }
10359 }
10360 ProcedureStep::DeleteByPk { .. } => {
10361 if schema.primary_key().is_none() {
10362 return Err(MongrelError::InvalidArgument(format!(
10363 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
10364 procedure.name
10365 )));
10366 }
10367 }
10368 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
10369 }
10370 }
10371 Ok(())
10372 }
10373
10374 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
10375 let cat = self.catalog.read();
10376 let target_schema = match &trigger.target {
10377 TriggerTarget::Table(target_name) => cat
10378 .live(target_name)
10379 .ok_or_else(|| {
10380 MongrelError::InvalidArgument(format!(
10381 "trigger {:?} references unknown target table {target_name:?}",
10382 trigger.name
10383 ))
10384 })?
10385 .schema
10386 .clone(),
10387 TriggerTarget::View(_) => Schema {
10388 columns: trigger.target_columns.clone(),
10389 ..Schema::default()
10390 },
10391 };
10392 for col in &trigger.update_of {
10393 if target_schema.column(col).is_none() {
10394 return Err(MongrelError::InvalidArgument(format!(
10395 "trigger {:?} UPDATE OF references unknown column {col:?}",
10396 trigger.name
10397 )));
10398 }
10399 }
10400 if let Some(expr) = &trigger.when {
10401 validate_trigger_expr(expr, &target_schema, trigger.event)?;
10402 }
10403 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
10404 for step in &trigger.program.steps {
10405 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
10406 {
10407 return Err(MongrelError::InvalidArgument(
10408 "SetNew trigger steps are only valid in BEFORE triggers".into(),
10409 ));
10410 }
10411 validate_trigger_step(
10412 step,
10413 &cat,
10414 &target_schema,
10415 trigger.event,
10416 &mut select_schemas,
10417 )?;
10418 }
10419 Ok(())
10420 }
10421
10422 pub fn begin(&self) -> crate::txn::Transaction<'_> {
10424 self.begin_with_isolation(crate::txn::IsolationLevel::default())
10425 }
10426
10427 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
10428 let principal = self.principal.read().clone();
10429 let catalog_bound = principal
10430 .as_ref()
10431 .is_some_and(|principal| principal.user_id != 0);
10432 (principal, catalog_bound)
10433 }
10434
10435 pub fn begin_as(
10436 &self,
10437 principal: Option<crate::auth::Principal>,
10438 ) -> crate::txn::Transaction<'_> {
10439 let catalog_bound = principal
10440 .as_ref()
10441 .is_some_and(|principal| principal.user_id != 0);
10442 let txn_id = self.alloc_txn_id();
10443 let read = self.visible_snapshot();
10444 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10445 .with_principal(principal, catalog_bound)
10446 }
10447
10448 pub fn begin_with_isolation(
10450 &self,
10451 level: crate::txn::IsolationLevel,
10452 ) -> crate::txn::Transaction<'_> {
10453 let txn_id = self.alloc_txn_id();
10454 let read = self.visible_snapshot();
10457 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10458 crate::txn::Transaction::new(self, txn_id, read, level)
10459 .with_principal(principal, catalog_bound)
10460 }
10461
10462 pub fn begin_with_external_trigger_bridge<'a>(
10465 &'a self,
10466 bridge: &'a dyn ExternalTriggerBridge,
10467 ) -> crate::txn::Transaction<'a> {
10468 let txn_id = self.alloc_txn_id();
10469 let read = self.visible_snapshot();
10470 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10471 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10472 .with_external_trigger_bridge(bridge)
10473 .with_principal(principal, catalog_bound)
10474 }
10475
10476 pub fn begin_with_external_trigger_bridge_as<'a>(
10477 &'a self,
10478 bridge: &'a dyn ExternalTriggerBridge,
10479 principal: Option<crate::auth::Principal>,
10480 ) -> crate::txn::Transaction<'a> {
10481 let catalog_bound = principal
10482 .as_ref()
10483 .is_some_and(|principal| principal.user_id != 0);
10484 let txn_id = self.alloc_txn_id();
10485 let read = self.visible_snapshot();
10486 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10487 .with_external_trigger_bridge(bridge)
10488 .with_principal(principal, catalog_bound)
10489 }
10490
10491 pub fn transaction<T>(
10493 &self,
10494 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10495 ) -> Result<T> {
10496 let mut tx = self.begin();
10497 match f(&mut tx) {
10498 Ok(out) => {
10499 tx.commit()?;
10500 Ok(out)
10501 }
10502 Err(e) => {
10503 tx.rollback();
10504 Err(e)
10505 }
10506 }
10507 }
10508
10509 pub fn transaction_with_row_ids<T>(
10510 &self,
10511 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10512 ) -> Result<(T, Vec<RowId>)> {
10513 let mut tx = self.begin();
10514 match f(&mut tx) {
10515 Ok(output) => {
10516 let (_, row_ids) = tx.commit_with_row_ids()?;
10517 Ok((output, row_ids))
10518 }
10519 Err(error) => {
10520 tx.rollback();
10521 Err(error)
10522 }
10523 }
10524 }
10525
10526 pub fn transaction_for_current_principal<T>(
10527 &self,
10528 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10529 ) -> Result<T> {
10530 if self.principal.read().is_some() {
10531 self.refresh_principal()?;
10532 }
10533 let mut transaction = self.begin_as(self.principal.read().clone());
10534 match f(&mut transaction) {
10535 Ok(output) => {
10536 transaction.commit()?;
10537 Ok(output)
10538 }
10539 Err(error) => {
10540 transaction.rollback();
10541 Err(error)
10542 }
10543 }
10544 }
10545
10546 pub fn transaction_for_current_principal_with_epoch<T>(
10547 &self,
10548 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10549 ) -> Result<(Epoch, T)> {
10550 if self.principal.read().is_some() {
10551 self.refresh_principal()?;
10552 }
10553 let mut transaction = self.begin_as(self.principal.read().clone());
10554 match f(&mut transaction) {
10555 Ok(output) => {
10556 let epoch = transaction.commit()?;
10557 Ok((epoch, output))
10558 }
10559 Err(error) => {
10560 transaction.rollback();
10561 Err(error)
10562 }
10563 }
10564 }
10565
10566 pub fn transaction_with_row_ids_for_current_principal<T>(
10567 &self,
10568 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10569 ) -> Result<(T, Vec<RowId>)> {
10570 if self.principal.read().is_some() {
10571 self.refresh_principal()?;
10572 }
10573 let mut transaction = self.begin_as(self.principal.read().clone());
10574 match f(&mut transaction) {
10575 Ok(output) => {
10576 let (_, row_ids) = transaction.commit_with_row_ids()?;
10577 Ok((output, row_ids))
10578 }
10579 Err(error) => {
10580 transaction.rollback();
10581 Err(error)
10582 }
10583 }
10584 }
10585
10586 pub fn transaction_with_external_trigger_bridge<'a, T>(
10589 &'a self,
10590 bridge: &'a dyn ExternalTriggerBridge,
10591 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10592 ) -> Result<T> {
10593 let mut tx = self.begin_with_external_trigger_bridge(bridge);
10594 match f(&mut tx) {
10595 Ok(out) => {
10596 tx.commit()?;
10597 Ok(out)
10598 }
10599 Err(e) => {
10600 tx.rollback();
10601 Err(e)
10602 }
10603 }
10604 }
10605
10606 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
10607 &'a self,
10608 bridge: &'a dyn ExternalTriggerBridge,
10609 principal: Option<crate::auth::Principal>,
10610 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10611 ) -> Result<T> {
10612 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
10613 match f(&mut tx) {
10614 Ok(output) => {
10615 tx.commit()?;
10616 Ok(output)
10617 }
10618 Err(error) => {
10619 tx.rollback();
10620 Err(error)
10621 }
10622 }
10623 }
10624
10625 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
10628 self.active_txns.register(epoch)
10629 }
10630
10631 fn fill_auto_increment_for_staging(
10632 &self,
10633 txn_id: u64,
10634 staging: &mut [(u64, crate::txn::Staged)],
10635 control: Option<&crate::ExecutionControl>,
10636 ) -> Result<()> {
10637 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
10638 for (index, (table_id, staged)) in staging.iter().enumerate() {
10639 commit_prepare_checkpoint(control, index)?;
10640 if matches!(staged, crate::txn::Staged::Put(_)) {
10641 puts_by_table.entry(*table_id).or_default().push(index);
10642 }
10643 }
10644
10645 {
10652 let mut barrier_tables: Vec<u64> = puts_by_table
10653 .iter()
10654 .filter(|(table_id, indexes)| {
10655 indexes.iter().any(|index| {
10656 matches!(
10657 &staging[*index].1,
10658 crate::txn::Staged::Put(cells)
10659 if self.table_auto_inc_would_allocate(**table_id, cells)
10660 )
10661 })
10662 })
10663 .map(|(table_id, _)| *table_id)
10664 .collect();
10665 barrier_tables.sort_unstable();
10666 for table_id in barrier_tables {
10667 self.acquire_txn_lock(
10668 txn_id,
10669 crate::locks::LockKey::sequence_barrier(&format!("auto_inc:{table_id}")),
10670 crate::locks::LockMode::Exclusive,
10671 control,
10672 )?;
10673 }
10674 }
10675
10676 let tables = self.tables.read();
10677 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
10678 commit_prepare_checkpoint(control, table_index)?;
10679 if let Some(handle) = tables.get(&table_id) {
10680 #[cfg(test)]
10681 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10682 let mut t = handle.lock();
10683 for (fill_index, index) in indexes.into_iter().enumerate() {
10684 commit_prepare_checkpoint(control, fill_index)?;
10685 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
10686 t.fill_auto_inc(cells)?;
10687 }
10688 }
10689 }
10690 }
10691 Ok(())
10692 }
10693
10694 fn expand_table_triggers(
10695 &self,
10696 txn_id: u64,
10697 staging: &mut Vec<(u64, crate::txn::Staged)>,
10698 read_epoch: Epoch,
10699 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
10700 external_states: &mut Vec<(String, Vec<u8>)>,
10701 control: Option<&crate::ExecutionControl>,
10702 ) -> Result<()> {
10703 commit_prepare_checkpoint(control, 0)?;
10704 let mut external_writes = Vec::new();
10705 let config = self.trigger_config();
10706 if config.recursive_triggers {
10707 let chunk = std::mem::take(staging);
10708 let stacks = vec![Vec::new(); chunk.len()];
10709 *staging = self.expand_trigger_chunk(
10710 txn_id,
10711 chunk,
10712 stacks,
10713 read_epoch,
10714 0,
10715 config.max_depth,
10716 &mut external_writes,
10717 &config,
10718 control,
10719 )?;
10720 self.apply_external_trigger_writes(
10721 external_writes,
10722 external_trigger_bridge,
10723 external_states,
10724 staging,
10725 control,
10726 )?;
10727 return Ok(());
10728 }
10729
10730 let mut expansion =
10731 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
10732 if !expansion.before.is_empty() {
10733 let mut final_staging = expansion.before;
10734 final_staging.extend(filter_ignored_staging(
10735 std::mem::take(staging),
10736 &expansion.ignored_indices,
10737 ));
10738 *staging = final_staging;
10739 } else if !expansion.ignored_indices.is_empty() {
10740 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
10741 }
10742 staging.append(&mut expansion.after);
10743 external_writes.append(&mut expansion.before_external);
10744 external_writes.append(&mut expansion.after_external);
10745 self.apply_external_trigger_writes(
10746 external_writes,
10747 external_trigger_bridge,
10748 external_states,
10749 staging,
10750 control,
10751 )?;
10752 Ok(())
10753 }
10754
10755 #[allow(clippy::too_many_arguments)]
10756 fn expand_trigger_chunk(
10757 &self,
10758 txn_id: u64,
10759 mut chunk: Vec<(u64, crate::txn::Staged)>,
10760 stacks: Vec<Vec<String>>,
10761 read_epoch: Epoch,
10762 depth: u32,
10763 max_depth: u32,
10764 external_writes: &mut Vec<ExternalTriggerWrite>,
10765 config: &TriggerConfig,
10766 control: Option<&crate::ExecutionControl>,
10767 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
10768 if chunk.is_empty() {
10769 return Ok(Vec::new());
10770 }
10771 commit_prepare_checkpoint(control, 0)?;
10772 self.fill_auto_increment_for_staging(txn_id, &mut chunk, control)?;
10773 let expansion = self.expand_table_triggers_once(
10774 &mut chunk,
10775 read_epoch,
10776 Some(&stacks),
10777 config,
10778 control,
10779 )?;
10780 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
10781 let stack = expansion
10782 .before_stacks
10783 .first()
10784 .or_else(|| expansion.after_stacks.first())
10785 .cloned()
10786 .unwrap_or_default();
10787 return Err(MongrelError::TriggerValidation(format!(
10788 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
10789 Self::format_trigger_stack(&stack)
10790 )));
10791 }
10792
10793 let mut out = Vec::new();
10794 external_writes.extend(expansion.before_external);
10795 out.extend(self.expand_trigger_chunk(
10796 txn_id,
10797 expansion.before,
10798 expansion.before_stacks,
10799 read_epoch,
10800 depth + 1,
10801 max_depth,
10802 external_writes,
10803 config,
10804 control,
10805 )?);
10806 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
10807 external_writes.extend(expansion.after_external);
10808 out.extend(self.expand_trigger_chunk(
10809 txn_id,
10810 expansion.after,
10811 expansion.after_stacks,
10812 read_epoch,
10813 depth + 1,
10814 max_depth,
10815 external_writes,
10816 config,
10817 control,
10818 )?);
10819 Ok(out)
10820 }
10821
10822 fn apply_external_trigger_writes(
10823 &self,
10824 writes: Vec<ExternalTriggerWrite>,
10825 bridge: Option<&dyn ExternalTriggerBridge>,
10826 external_states: &mut Vec<(String, Vec<u8>)>,
10827 staging: &mut Vec<(u64, crate::txn::Staged)>,
10828 control: Option<&crate::ExecutionControl>,
10829 ) -> Result<()> {
10830 if writes.is_empty() {
10831 return Ok(());
10832 }
10833 let bridge = bridge.ok_or_else(|| {
10834 MongrelError::TriggerValidation(
10835 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
10836 )
10837 })?;
10838 for (write_index, write) in writes.into_iter().enumerate() {
10839 commit_prepare_checkpoint(control, write_index)?;
10840 let table = write.table().to_string();
10841 let entry = self.external_table(&table).ok_or_else(|| {
10842 MongrelError::NotFound(format!("external table {table:?} not found"))
10843 })?;
10844 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
10845 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
10846 external_states.push((table, result.state));
10847 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
10848 commit_prepare_checkpoint(control, base_index)?;
10849 match base_write {
10850 ExternalTriggerBaseWrite::Put { table, cells } => {
10851 let table_id = self.table_id(&table)?;
10852 staging.push((table_id, crate::txn::Staged::Put(cells)));
10853 }
10854 ExternalTriggerBaseWrite::Delete { table, row_id } => {
10855 let table_id = self.table_id(&table)?;
10856 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
10857 }
10858 }
10859 }
10860 }
10861 dedup_external_states_in_place(external_states);
10862 Ok(())
10863 }
10864
10865 fn expand_table_triggers_once(
10866 &self,
10867 staging: &mut Vec<(u64, crate::txn::Staged)>,
10868 read_epoch: Epoch,
10869 trigger_stacks: Option<&[Vec<String>]>,
10870 config: &TriggerConfig,
10871 control: Option<&crate::ExecutionControl>,
10872 ) -> Result<TriggerExpansion> {
10873 commit_prepare_checkpoint(control, 0)?;
10874 let triggers: Vec<StoredTrigger> = self
10875 .catalog
10876 .read()
10877 .triggers
10878 .iter()
10879 .filter(|entry| {
10880 entry.trigger.enabled
10881 && matches!(
10882 entry.trigger.timing,
10883 TriggerTiming::Before | TriggerTiming::After
10884 )
10885 && matches!(entry.trigger.target, TriggerTarget::Table(_))
10886 })
10887 .map(|entry| entry.trigger.clone())
10888 .collect();
10889 if triggers.is_empty() || staging.is_empty() {
10890 return Ok(TriggerExpansion::default());
10891 }
10892
10893 let before_triggers = triggers
10894 .iter()
10895 .filter(|trigger| trigger.timing == TriggerTiming::Before)
10896 .cloned()
10897 .collect::<Vec<_>>();
10898 let after_triggers = triggers
10899 .iter()
10900 .filter(|trigger| trigger.timing == TriggerTiming::After)
10901 .cloned()
10902 .collect::<Vec<_>>();
10903
10904 let mut before_added = Vec::new();
10905 let mut before_stacks = Vec::new();
10906 let mut before_external = Vec::new();
10907 let mut ignored_indices = std::collections::BTreeSet::new();
10908 if !before_triggers.is_empty() {
10909 let before_events =
10910 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
10911 let mut out = TriggerProgramOutput {
10912 added: &mut before_added,
10913 added_stacks: &mut before_stacks,
10914 added_external: &mut before_external,
10915 ignored_indices: &mut ignored_indices,
10916 };
10917 self.execute_triggers_for_events(
10918 &before_triggers,
10919 &before_events,
10920 Some(staging),
10921 &mut out,
10922 config,
10923 read_epoch,
10924 control,
10925 )?;
10926 }
10927
10928 let after_events = if after_triggers.is_empty() {
10929 Vec::new()
10930 } else {
10931 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
10932 .into_iter()
10933 .filter(|event| {
10934 !event
10935 .op_indices
10936 .iter()
10937 .any(|idx| ignored_indices.contains(idx))
10938 })
10939 .collect()
10940 };
10941
10942 let mut after_added = Vec::new();
10943 let mut after_stacks = Vec::new();
10944 let mut after_external = Vec::new();
10945 let mut out = TriggerProgramOutput {
10946 added: &mut after_added,
10947 added_stacks: &mut after_stacks,
10948 added_external: &mut after_external,
10949 ignored_indices: &mut ignored_indices,
10950 };
10951 self.execute_triggers_for_events(
10952 &after_triggers,
10953 &after_events,
10954 None,
10955 &mut out,
10956 config,
10957 read_epoch,
10958 control,
10959 )?;
10960 Ok(TriggerExpansion {
10961 before: before_added,
10962 before_stacks,
10963 before_external,
10964 after: after_added,
10965 after_stacks,
10966 after_external,
10967 ignored_indices,
10968 })
10969 }
10970
10971 #[allow(clippy::too_many_arguments)]
10972 fn execute_triggers_for_events(
10973 &self,
10974 triggers: &[StoredTrigger],
10975 events: &[WriteEvent],
10976 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
10977 out: &mut TriggerProgramOutput<'_>,
10978 config: &TriggerConfig,
10979 read_epoch: Epoch,
10980 control: Option<&crate::ExecutionControl>,
10981 ) -> Result<()> {
10982 let mut checkpoint_index = 0_usize;
10983 for event in events {
10984 for trigger in triggers {
10985 commit_prepare_checkpoint(control, checkpoint_index)?;
10986 checkpoint_index += 1;
10987 if event
10988 .op_indices
10989 .iter()
10990 .any(|idx| out.ignored_indices.contains(idx))
10991 {
10992 break;
10993 }
10994 let matches = {
10995 let cat = self.catalog.read();
10996 trigger_matches_event(trigger, event, &cat)?
10997 };
10998 if !matches {
10999 continue;
11000 }
11001 if let Some(when) = &trigger.when {
11002 if !eval_trigger_expr(when, event)? {
11003 continue;
11004 }
11005 }
11006 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
11007 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
11008 return Err(MongrelError::TriggerValidation(format!(
11009 "trigger recursion cycle detected; trigger stack: {}",
11010 Self::format_trigger_stack(&trigger_stack)
11011 )));
11012 }
11013 let outcome = match staging.as_mut() {
11014 Some(staging) => self.execute_trigger_program(
11015 trigger,
11016 event,
11017 Some(&mut **staging),
11018 out,
11019 &trigger_stack,
11020 config,
11021 read_epoch,
11022 control,
11023 )?,
11024 None => self.execute_trigger_program(
11025 trigger,
11026 event,
11027 None,
11028 out,
11029 &trigger_stack,
11030 config,
11031 read_epoch,
11032 control,
11033 )?,
11034 };
11035 if outcome == TriggerProgramOutcome::Ignore {
11036 out.ignored_indices.extend(event.op_indices.iter().copied());
11037 break;
11038 }
11039 }
11040 }
11041 Ok(())
11042 }
11043
11044 fn trigger_events_for_staging(
11045 &self,
11046 staging: &[(u64, crate::txn::Staged)],
11047 read_epoch: Epoch,
11048 trigger_stacks: Option<&[Vec<String>]>,
11049 control: Option<&crate::ExecutionControl>,
11050 ) -> Result<Vec<WriteEvent>> {
11051 use crate::txn::Staged;
11052 use std::collections::{HashMap, VecDeque};
11053
11054 let snapshot = self.snapshot_for_epoch(read_epoch);
11055 let cat = self.catalog.read();
11056 let mut table_names = HashMap::new();
11057 let mut table_schemas = HashMap::new();
11058 for entry in cat
11059 .tables
11060 .iter()
11061 .filter(|entry| matches!(entry.state, TableState::Live))
11062 {
11063 table_names.insert(entry.table_id, entry.name.clone());
11064 table_schemas.insert(entry.table_id, entry.schema.clone());
11065 }
11066 drop(cat);
11067
11068 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
11069 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11070 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11071
11072 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11073 commit_prepare_checkpoint(control, idx)?;
11074 let Some(schema) = table_schemas.get(table_id) else {
11075 continue;
11076 };
11077 let Some(pk) = schema.primary_key() else {
11078 continue;
11079 };
11080 match staged {
11081 Staged::Delete(row_id) => {
11082 let handle = self.table_by_id(*table_id)?;
11083 let Some(row) = handle.lock().get(*row_id, snapshot) else {
11084 continue;
11085 };
11086 let Some(pk_value) = row.columns.get(&pk.id) else {
11087 continue;
11088 };
11089 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
11090 delete_by_key
11091 .entry((*table_id, pk_value.encode_key()))
11092 .or_default()
11093 .push_back(idx);
11094 }
11095 Staged::Put(cells) => {
11096 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
11097 put_by_key
11098 .entry((*table_id, value.encode_key()))
11099 .or_default()
11100 .push_back(idx);
11101 }
11102 }
11103 Staged::Update { row_id, .. } => {
11104 let handle = self.table_by_id(*table_id)?;
11105 let row = handle.lock().get(*row_id, snapshot);
11106 if let Some(row) = row {
11107 old_rows.insert(idx, TriggerRowImage::from_row(row));
11108 }
11109 }
11110 Staged::Truncate => {}
11111 }
11112 }
11113
11114 let mut paired_delete = std::collections::HashSet::new();
11115 let mut paired_put = std::collections::HashSet::new();
11116 let mut events = Vec::new();
11117
11118 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
11119 commit_prepare_checkpoint(control, pair_index)?;
11120 let Some(puts) = put_by_key.get_mut(key) else {
11121 continue;
11122 };
11123 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
11124 paired_delete.insert(delete_idx);
11125 paired_put.insert(put_idx);
11126 let (table_id, _) = &staging[put_idx];
11127 let Some(table_name) = table_names.get(table_id).cloned() else {
11128 continue;
11129 };
11130 let old = old_rows.get(&delete_idx).cloned();
11131 let new = match &staging[put_idx].1 {
11132 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
11133 _ => None,
11134 };
11135 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11136 events.push(WriteEvent {
11137 table: table_name,
11138 kind: TriggerEvent::Update,
11139 old,
11140 new,
11141 changed_columns,
11142 op_indices: vec![delete_idx, put_idx],
11143 put_idx: Some(put_idx),
11144 trigger_stack: Self::trigger_stack_for_indices(
11145 trigger_stacks,
11146 &[delete_idx, put_idx],
11147 ),
11148 });
11149 }
11150 }
11151
11152 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11153 commit_prepare_checkpoint(control, idx)?;
11154 let Some(table_name) = table_names.get(table_id).cloned() else {
11155 continue;
11156 };
11157 match staged {
11158 Staged::Put(cells) if !paired_put.contains(&idx) => {
11159 let new = Some(TriggerRowImage::from_cells(cells));
11160 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
11161 events.push(WriteEvent {
11162 table: table_name,
11163 kind: TriggerEvent::Insert,
11164 old: None,
11165 new,
11166 changed_columns,
11167 op_indices: vec![idx],
11168 put_idx: Some(idx),
11169 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11170 });
11171 }
11172 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
11173 let old = match old_rows.get(&idx).cloned() {
11174 Some(old) => Some(old),
11175 None => {
11176 let handle = self.table_by_id(*table_id)?;
11177 let row = handle.lock().get(*row_id, snapshot);
11178 row.map(TriggerRowImage::from_row)
11179 }
11180 };
11181 let Some(old) = old else {
11182 continue;
11183 };
11184 let changed_columns = old.columns.keys().copied().collect();
11185 events.push(WriteEvent {
11186 table: table_name,
11187 kind: TriggerEvent::Delete,
11188 old: Some(old),
11189 new: None,
11190 changed_columns,
11191 op_indices: vec![idx],
11192 put_idx: None,
11193 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11194 });
11195 }
11196 Staged::Update { new_row: cells, .. } => {
11197 let old = old_rows.get(&idx).cloned();
11198 let new = Some(TriggerRowImage::from_cells(cells));
11199 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11200 events.push(WriteEvent {
11201 table: table_name,
11202 kind: TriggerEvent::Update,
11203 old,
11204 new,
11205 changed_columns,
11206 op_indices: vec![idx],
11207 put_idx: Some(idx),
11208 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11209 });
11210 }
11211 Staged::Truncate => {}
11212 _ => {}
11213 }
11214 }
11215
11216 Ok(events)
11217 }
11218
11219 #[allow(clippy::too_many_arguments)]
11220 fn execute_trigger_program(
11221 &self,
11222 trigger: &StoredTrigger,
11223 event: &WriteEvent,
11224 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11225 out: &mut TriggerProgramOutput<'_>,
11226 trigger_stack: &[String],
11227 config: &TriggerConfig,
11228 read_epoch: Epoch,
11229 control: Option<&crate::ExecutionControl>,
11230 ) -> Result<TriggerProgramOutcome> {
11231 let mut event = event.clone();
11232 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
11233 self.execute_trigger_steps(
11234 trigger,
11235 &trigger.program.steps,
11236 &mut event,
11237 staging,
11238 out,
11239 trigger_stack,
11240 config,
11241 &mut select_results,
11242 0,
11243 None,
11244 read_epoch,
11245 control,
11246 )
11247 }
11248
11249 #[allow(clippy::too_many_arguments)]
11250 fn execute_trigger_steps(
11251 &self,
11252 trigger: &StoredTrigger,
11253 steps: &[TriggerStep],
11254 event: &mut WriteEvent,
11255 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11256 out: &mut TriggerProgramOutput<'_>,
11257 trigger_stack: &[String],
11258 config: &TriggerConfig,
11259 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
11260 depth: u32,
11261 selected: Option<&TriggerRowImage>,
11262 read_epoch: Epoch,
11263 control: Option<&crate::ExecutionControl>,
11264 ) -> Result<TriggerProgramOutcome> {
11265 let _ = depth;
11266 for (step_index, step) in steps.iter().enumerate() {
11267 commit_prepare_checkpoint(control, step_index)?;
11268 match step {
11269 TriggerStep::SetNew { cells } => {
11270 if trigger.timing != TriggerTiming::Before {
11271 return Err(MongrelError::InvalidArgument(
11272 "SetNew trigger step is only valid in BEFORE triggers".into(),
11273 ));
11274 }
11275 let put_idx = event.put_idx.ok_or_else(|| {
11276 MongrelError::InvalidArgument(
11277 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
11278 )
11279 })?;
11280 let staging = staging.as_deref_mut().ok_or_else(|| {
11281 MongrelError::InvalidArgument(
11282 "SetNew trigger step requires mutable trigger staging".into(),
11283 )
11284 })?;
11285 let mut update_changed_columns = None;
11286 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
11287 Some(crate::txn::Staged::Put(cells)) => cells,
11288 Some(crate::txn::Staged::Update {
11289 new_row,
11290 changed_columns,
11291 ..
11292 }) => {
11293 update_changed_columns = Some(changed_columns);
11294 new_row
11295 }
11296 _ => {
11297 return Err(MongrelError::InvalidArgument(
11298 "SetNew trigger step target row is not mutable".into(),
11299 ))
11300 }
11301 };
11302 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
11303 row_cells.retain(|(id, _)| *id != column_id);
11304 row_cells.push((column_id, value.clone()));
11305 if let Some(changed_columns) = &mut update_changed_columns {
11306 changed_columns.push(column_id);
11307 }
11308 if let Some(new) = &mut event.new {
11309 new.columns.insert(column_id, value);
11310 }
11311 }
11312 row_cells.sort_by_key(|(id, _)| *id);
11313 if let Some(changed_columns) = update_changed_columns {
11314 changed_columns.sort_unstable();
11315 changed_columns.dedup();
11316 }
11317 }
11318 TriggerStep::Insert { table, cells } => {
11319 let cells = eval_trigger_cells(cells, event, selected)?;
11320 if let Ok(table_id) = self.table_id(table) {
11321 out.added.push((table_id, crate::txn::Staged::Put(cells)));
11322 out.added_stacks.push(trigger_stack.to_vec());
11323 } else if self.external_table(table).is_some() {
11324 out.added_external.push(ExternalTriggerWrite::Insert {
11325 table: table.clone(),
11326 cells,
11327 });
11328 } else {
11329 return Err(MongrelError::NotFound(format!(
11330 "trigger {:?} insert target {table:?} not found",
11331 trigger.name
11332 )));
11333 }
11334 }
11335 TriggerStep::UpdateByPk { table, pk, cells } => {
11336 let pk = eval_trigger_value(pk, event, selected)?;
11337 let cells = eval_trigger_cells(cells, event, selected)?;
11338 if self.external_table(table).is_some() {
11339 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
11340 table: table.clone(),
11341 pk,
11342 cells,
11343 });
11344 } else {
11345 let row_id = self
11346 .table(table)?
11347 .lock()
11348 .lookup_pk(&pk.encode_key())
11349 .ok_or_else(|| {
11350 MongrelError::NotFound(format!(
11351 "trigger {:?} update target not found",
11352 trigger.name
11353 ))
11354 })?;
11355 let handle = self.table(table)?;
11356 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
11357 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
11358 MongrelError::NotFound(format!(
11359 "trigger {:?} update target not visible",
11360 trigger.name
11361 ))
11362 })?;
11363 let mut changed_columns = cells
11364 .iter()
11365 .map(|(column_id, _)| *column_id)
11366 .collect::<Vec<_>>();
11367 changed_columns.sort_unstable();
11368 changed_columns.dedup();
11369 let mut merged = old.columns;
11370 for (column_id, value) in cells {
11371 merged.insert(column_id, value);
11372 }
11373 out.added.push((
11374 self.table_id(table)?,
11375 crate::txn::Staged::Update {
11376 row_id,
11377 new_row: merged.into_iter().collect(),
11378 changed_columns,
11379 },
11380 ));
11381 out.added_stacks.push(trigger_stack.to_vec());
11382 }
11383 }
11384 TriggerStep::DeleteByPk { table, pk } => {
11385 let pk = eval_trigger_value(pk, event, selected)?;
11386 if self.external_table(table).is_some() {
11387 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
11388 table: table.clone(),
11389 pk,
11390 });
11391 } else {
11392 let row_id = self
11393 .table(table)?
11394 .lock()
11395 .lookup_pk(&pk.encode_key())
11396 .ok_or_else(|| {
11397 MongrelError::NotFound(format!(
11398 "trigger {:?} delete target not found",
11399 trigger.name
11400 ))
11401 })?;
11402 out.added
11403 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
11404 out.added_stacks.push(trigger_stack.to_vec());
11405 }
11406 }
11407 TriggerStep::Select {
11408 id,
11409 table,
11410 conditions,
11411 } => {
11412 let schema = self.table(table)?.lock().schema().clone();
11413 let snapshot = self.snapshot_for_epoch(read_epoch);
11414 let handle = self.table(table)?;
11415 let rows = match control {
11416 Some(control) => {
11417 handle.lock().visible_rows_controlled(snapshot, control)?
11418 }
11419 None => handle.lock().visible_rows(snapshot)?,
11420 };
11421 let mut matched = Vec::new();
11422 for (row_index, row) in rows.into_iter().enumerate() {
11423 commit_prepare_checkpoint(control, row_index)?;
11424 let image = TriggerRowImage::from_row(row);
11425 let passes = conditions
11426 .iter()
11427 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11428 .collect::<Result<Vec<_>>>()?
11429 .into_iter()
11430 .all(|b| b);
11431 if passes {
11432 matched.push(image);
11433 }
11434 }
11435 if let Some(pk) = schema.primary_key() {
11436 matched.sort_by(|a, b| {
11437 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
11438 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
11439 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
11440 });
11441 }
11442 select_results.insert(id.clone(), matched);
11443 }
11444 TriggerStep::Foreach { id, steps } => {
11445 let rows = select_results.get(id).ok_or_else(|| {
11446 MongrelError::InvalidArgument(format!(
11447 "trigger {:?} foreach references unknown select id {id:?}",
11448 trigger.name
11449 ))
11450 })?;
11451 if rows.len() > config.max_loop_iterations as usize {
11452 return Err(MongrelError::InvalidArgument(format!(
11453 "trigger {:?} foreach exceeded max_loop_iterations ({})",
11454 trigger.name, config.max_loop_iterations
11455 )));
11456 }
11457 for (row_index, row) in rows.clone().into_iter().enumerate() {
11458 commit_prepare_checkpoint(control, row_index)?;
11459 let result = self.execute_trigger_steps(
11460 trigger,
11461 steps,
11462 event,
11463 staging.as_deref_mut(),
11464 out,
11465 trigger_stack,
11466 config,
11467 select_results,
11468 depth + 1,
11469 Some(&row),
11470 read_epoch,
11471 control,
11472 )?;
11473 if result == TriggerProgramOutcome::Ignore {
11474 return Ok(TriggerProgramOutcome::Ignore);
11475 }
11476 }
11477 }
11478 TriggerStep::DeleteWhere { table, conditions } => {
11479 let schema = self.table(table)?.lock().schema().clone();
11480 let snapshot = self.snapshot_for_epoch(read_epoch);
11481 let handle = self.table(table)?;
11482 let rows = match control {
11483 Some(control) => {
11484 handle.lock().visible_rows_controlled(snapshot, control)?
11485 }
11486 None => handle.lock().visible_rows(snapshot)?,
11487 };
11488 let table_id = self.table_id(table)?;
11489 let mut to_delete = Vec::new();
11490 for (row_index, row) in rows.into_iter().enumerate() {
11491 commit_prepare_checkpoint(control, row_index)?;
11492 let image = TriggerRowImage::from_row(row.clone());
11493 let passes = conditions
11494 .iter()
11495 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11496 .collect::<Result<Vec<_>>>()?
11497 .into_iter()
11498 .all(|b| b);
11499 if passes {
11500 to_delete.push((table_id, row.row_id));
11501 }
11502 }
11503 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
11504 commit_prepare_checkpoint(control, row_index)?;
11505 out.added
11506 .push((table_id, crate::txn::Staged::Delete(row_id)));
11507 out.added_stacks.push(trigger_stack.to_vec());
11508 }
11509 }
11510 TriggerStep::UpdateWhere {
11511 table,
11512 conditions,
11513 cells,
11514 } => {
11515 let schema = self.table(table)?.lock().schema().clone();
11516 let snapshot = self.snapshot_for_epoch(read_epoch);
11517 let handle = self.table(table)?;
11518 let rows = match control {
11519 Some(control) => {
11520 handle.lock().visible_rows_controlled(snapshot, control)?
11521 }
11522 None => handle.lock().visible_rows(snapshot)?,
11523 };
11524 let table_id = self.table_id(table)?;
11525 let mut changed_columns =
11526 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
11527 changed_columns.sort_unstable();
11528 changed_columns.dedup();
11529 let mut to_update = Vec::new();
11530 for (row_index, row) in rows.into_iter().enumerate() {
11531 commit_prepare_checkpoint(control, row_index)?;
11532 let image = TriggerRowImage::from_row(row.clone());
11533 let passes = conditions
11534 .iter()
11535 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11536 .collect::<Result<Vec<_>>>()?
11537 .into_iter()
11538 .all(|b| b);
11539 if passes {
11540 let new_cells = cells
11541 .iter()
11542 .map(|cell| {
11543 Ok((
11544 cell.column_id,
11545 eval_trigger_value(&cell.value, event, Some(&image))?,
11546 ))
11547 })
11548 .collect::<Result<Vec<_>>>()?;
11549 let mut merged = row.columns.clone();
11550 for (column_id, value) in new_cells {
11551 merged.insert(column_id, value);
11552 }
11553 to_update.push((table_id, row.row_id, merged));
11554 }
11555 }
11556 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
11557 {
11558 commit_prepare_checkpoint(control, row_index)?;
11559 out.added.push((
11560 table_id,
11561 crate::txn::Staged::Update {
11562 row_id,
11563 new_row: merged.into_iter().collect(),
11564 changed_columns: changed_columns.clone(),
11565 },
11566 ));
11567 out.added_stacks.push(trigger_stack.to_vec());
11568 }
11569 }
11570 TriggerStep::Raise { action, message } => match action {
11571 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
11572 TriggerRaiseAction::Abort
11573 | TriggerRaiseAction::Fail
11574 | TriggerRaiseAction::Rollback => {
11575 let message = eval_trigger_value(message, event, selected)?;
11576 return Err(MongrelError::TriggerValidation(format!(
11577 "trigger {:?} raised: {}; trigger stack: {}",
11578 trigger.name,
11579 trigger_message(message),
11580 Self::format_trigger_stack(trigger_stack)
11581 )));
11582 }
11583 },
11584 }
11585 }
11586 Ok(TriggerProgramOutcome::Continue)
11587 }
11588
11589 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
11590 let Some(stacks) = stacks else {
11591 return Vec::new();
11592 };
11593 let mut out = Vec::new();
11594 for idx in indices {
11595 let Some(stack) = stacks.get(*idx) else {
11596 continue;
11597 };
11598 for name in stack {
11599 if !out.iter().any(|existing| existing == name) {
11600 out.push(name.clone());
11601 }
11602 }
11603 }
11604 out
11605 }
11606
11607 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
11608 let mut out = stack.to_vec();
11609 out.push(trigger_name.to_string());
11610 out
11611 }
11612
11613 fn format_trigger_stack(stack: &[String]) -> String {
11614 if stack.is_empty() {
11615 "<root>".into()
11616 } else {
11617 stack.join(" -> ")
11618 }
11619 }
11620
11621 fn acquire_unique_key_claims(
11642 &self,
11643 txn_id: u64,
11644 staging: &[(u64, crate::txn::Staged)],
11645 control: Option<&crate::ExecutionControl>,
11646 ) -> Result<()> {
11647 let catalog = self.catalog.read();
11648 let has_uniques = staging.iter().any(|(table_id, staged)| {
11649 matches!(
11650 staged,
11651 crate::txn::Staged::Put(_) | crate::txn::Staged::Update { .. }
11652 ) && catalog.tables.iter().any(|entry| {
11653 entry.table_id == *table_id
11654 && (entry.schema.primary_key().is_some()
11655 || !entry.schema.constraints.uniques.is_empty())
11656 })
11657 });
11658 if !has_uniques {
11659 return Ok(());
11660 }
11661 let mut claims: Vec<(u64, Vec<u8>)> = Vec::new();
11662 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11663 commit_prepare_checkpoint(control, staged_index)?;
11664 let cells = match staged {
11665 crate::txn::Staged::Put(cells) => cells,
11666 crate::txn::Staged::Update { new_row, .. } => new_row,
11667 _ => continue,
11668 };
11669 let Some(entry) = catalog
11670 .tables
11671 .iter()
11672 .find(|entry| entry.table_id == *table_id)
11673 else {
11674 continue;
11675 };
11676 for column in &entry.schema.columns {
11677 if !column
11678 .flags
11679 .contains(crate::schema::ColumnFlags::PRIMARY_KEY)
11680 {
11681 continue;
11682 }
11683 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == column.id) {
11684 let mut key = b"pk:".to_vec();
11685 key.extend_from_slice(&value.encode_key());
11686 claims.push((*table_id, key));
11687 }
11688 }
11689 let cells_map: HashMap<u16, Value> = cells.iter().cloned().collect();
11694 for uc in &entry.schema.constraints.uniques {
11695 if let Some(composite) =
11696 crate::constraint::encode_composite_key(&uc.columns, &cells_map)
11697 {
11698 let mut key = format!("uq{}:", uc.id).into_bytes();
11699 key.extend_from_slice(&composite);
11700 claims.push((*table_id, key));
11701 }
11702 }
11703 }
11704 claims.sort();
11705 claims.dedup();
11706 for (table_id, key) in claims {
11707 self.acquire_txn_lock(
11708 txn_id,
11709 crate::locks::LockKey::key(table_id, key),
11710 crate::locks::LockMode::Exclusive,
11711 control,
11712 )?;
11713 }
11714 Ok(())
11715 }
11716
11717 fn acquire_fk_lock(
11721 &self,
11722 txn_id: u64,
11723 table_id: u64,
11724 key: &[u8],
11725 mode: crate::locks::LockMode,
11726 control: Option<&crate::ExecutionControl>,
11727 ) -> Result<()> {
11728 let mut namespaced = b"fk:".to_vec();
11729 namespaced.extend_from_slice(key);
11730 self.acquire_txn_lock(
11731 txn_id,
11732 crate::locks::LockKey::key(table_id, namespaced),
11733 mode,
11734 control,
11735 )?;
11736 let hook = self.fk_lock_hook.lock().clone();
11739 if let Some(hook) = hook {
11740 hook();
11741 }
11742 Ok(())
11743 }
11744
11745 fn validate_constraints(
11746 &self,
11747 txn_id: u64,
11748 staging: &mut Vec<(u64, crate::txn::Staged)>,
11749 read_epoch: Epoch,
11750 principal: Option<&crate::auth::Principal>,
11751 control: Option<&crate::ExecutionControl>,
11752 ) -> Result<()> {
11753 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
11754 use crate::memtable::Row;
11755 use crate::txn::Staged;
11756 use std::collections::HashSet;
11757
11758 commit_prepare_checkpoint(control, 0)?;
11759 let snapshot = self.snapshot_for_epoch(read_epoch);
11762 let cat = self.catalog.read().clone();
11763
11764 let live: Vec<(u64, String, crate::schema::Schema)> = cat
11766 .tables
11767 .iter()
11768 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
11769 .map(|e| (e.table_id, e.name.clone(), e.schema.clone()))
11770 .collect();
11771
11772 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
11774 if !any_constraints {
11775 self.materialize_generated_embeddings(staging, control)?;
11776 return Ok(());
11777 }
11778
11779 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
11781 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
11782 if let Some(r) = rows_cache.get(&table_id) {
11783 return Ok(r.clone());
11784 }
11785 let handle = self.table_by_id(table_id)?;
11786 let rows = match control {
11787 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
11788 None => handle.lock().visible_rows(snapshot)?,
11789 };
11790 rows_cache.insert(table_id, rows.clone());
11791 Ok(rows)
11792 };
11793
11794 let mut processed_updates = HashSet::new();
11799 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
11800 let mut update_pass = 0_usize;
11801 loop {
11802 commit_prepare_checkpoint(control, update_pass)?;
11803 update_pass += 1;
11804 let updates: Vec<PendingUpdate> = staging
11805 .iter()
11806 .enumerate()
11807 .filter_map(|(index, (table_id, op))| match op {
11808 Staged::Update {
11809 row_id,
11810 new_row: cells,
11811 ..
11812 } if !processed_updates.contains(&index) => {
11813 Some((index, *table_id, *row_id, cells.clone()))
11814 }
11815 _ => None,
11816 })
11817 .collect();
11818 if updates.is_empty() {
11819 break;
11820 }
11821 let mut new_ops = Vec::new();
11822 for (update_index, (index, table_id, row_id, new_cells)) in
11823 updates.into_iter().enumerate()
11824 {
11825 commit_prepare_checkpoint(control, update_index)?;
11826 processed_updates.insert(index);
11827 let Some(tname) = live
11828 .iter()
11829 .find(|(id, _, _)| *id == table_id)
11830 .map(|(_, name, _)| name.as_str())
11831 else {
11832 continue;
11833 };
11834 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
11835 continue;
11836 };
11837 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
11838 for (child_id, _child_name, child_schema) in &live {
11839 for fk in &child_schema.constraints.foreign_keys {
11840 if fk.ref_table != tname {
11841 continue;
11842 }
11843 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
11844 else {
11845 continue;
11846 };
11847 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
11848 == Some(old_key.as_slice())
11849 {
11850 continue;
11851 }
11852 if fk.on_update == FkAction::Restrict {
11853 continue;
11854 }
11855 self.acquire_fk_lock(
11860 txn_id,
11861 table_id,
11862 &old_key,
11863 crate::locks::LockMode::Exclusive,
11864 control,
11865 )?;
11866 let child_rows = load_rows(*child_id)?;
11867 for (child_index, child) in child_rows.into_iter().enumerate() {
11868 commit_prepare_checkpoint(control, child_index)?;
11869 if encode_composite_key(&fk.columns, &child.columns).as_deref()
11870 != Some(old_key.as_slice())
11871 {
11872 continue;
11873 }
11874 if staging.iter().any(|(id, op)| {
11875 *id == *child_id
11876 && matches!(op, Staged::Delete(id) if *id == child.row_id)
11877 }) {
11878 continue;
11879 }
11880 let mut cells: Vec<(u16, Value)> = child
11881 .columns
11882 .iter()
11883 .map(|(column_id, value)| (*column_id, value.clone()))
11884 .collect();
11885 for (child_column, parent_column) in
11886 fk.columns.iter().zip(&fk.ref_columns)
11887 {
11888 cells.retain(|(column_id, _)| column_id != child_column);
11889 let value = match fk.on_update {
11890 FkAction::Cascade => {
11891 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
11892 }
11893 FkAction::SetNull => Value::Null,
11894 FkAction::Restrict => {
11895 return Err(MongrelError::Other(
11896 "restricted foreign-key update reached cascade preparation"
11897 .into(),
11898 ));
11899 }
11900 };
11901 cells.push((*child_column, value));
11902 }
11903 cells.sort_by_key(|(column_id, _)| *column_id);
11904 if let Some(existing_index) = staging.iter().position(|(id, op)| {
11905 *id == *child_id
11906 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
11907 }) {
11908 if let Staged::Update {
11909 new_row: existing,
11910 changed_columns,
11911 ..
11912 } = &mut staging[existing_index].1 {
11913 changed_columns.extend(fk.columns.iter().copied());
11914 changed_columns.sort_unstable();
11915 changed_columns.dedup();
11916 if *existing != cells {
11917 *existing = cells;
11918 processed_updates.remove(&existing_index);
11919 }
11920 }
11921 } else {
11922 new_ops.push((
11923 *child_id,
11924 Staged::Update {
11925 row_id: child.row_id,
11926 new_row: cells,
11927 changed_columns: fk.columns.clone(),
11928 },
11929 ));
11930 }
11931 }
11932 }
11933 }
11934 }
11935 staging.extend(new_ops);
11936 }
11937
11938 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
11943 let mut cascade_pass = 0_usize;
11944 loop {
11945 commit_prepare_checkpoint(control, cascade_pass)?;
11946 cascade_pass += 1;
11947 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
11948 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
11949 .iter()
11950 .filter_map(|(t, op)| match op {
11951 Staged::Delete(rid) => Some((*t, *rid)),
11952 _ => None,
11953 })
11954 .collect();
11955 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
11956 commit_prepare_checkpoint(control, delete_index)?;
11957 if !cascaded.insert((table_id, rid.0)) {
11958 continue;
11959 }
11960 let Some(tname) = live
11961 .iter()
11962 .find(|(t, _, _)| *t == table_id)
11963 .map(|(_, n, _)| n.as_str())
11964 else {
11965 continue;
11966 };
11967 let parent_handle = self.table_by_id(table_id)?;
11968 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
11969 continue;
11970 };
11971 for (child_id, _child_name, child_schema) in &live {
11972 for fk in &child_schema.constraints.foreign_keys {
11973 if fk.ref_table != tname {
11974 continue;
11975 }
11976 let Some(parent_key) =
11977 encode_composite_key(&fk.ref_columns, &parent_row.columns)
11978 else {
11979 continue;
11980 };
11981 let key_preserved = staging.iter().any(|(t, op)| {
11990 if *t != table_id {
11991 return false;
11992 }
11993 let Staged::Put(cells) = op else {
11994 return false;
11995 };
11996 let map: HashMap<u16, crate::memtable::Value> =
11997 cells.iter().cloned().collect();
11998 encode_composite_key(&fk.ref_columns, &map).as_deref()
11999 == Some(parent_key.as_slice())
12000 });
12001 if key_preserved {
12002 continue;
12003 }
12004 self.acquire_fk_lock(
12011 txn_id,
12012 table_id,
12013 &parent_key,
12014 crate::locks::LockMode::Exclusive,
12015 control,
12016 )?;
12017 match fk.on_delete {
12018 FkAction::Restrict => continue,
12019 FkAction::Cascade => {
12020 let child_rows = load_rows(*child_id)?;
12021 for (child_index, cr) in child_rows.iter().enumerate() {
12022 commit_prepare_checkpoint(control, child_index)?;
12023 if !cascaded.contains(&(*child_id, cr.row_id.0))
12024 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12025 == Some(parent_key.as_slice())
12026 {
12027 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
12028 }
12029 }
12030 }
12031 FkAction::SetNull => {
12032 let child_rows = load_rows(*child_id)?;
12033 for (child_index, cr) in child_rows.iter().enumerate() {
12034 commit_prepare_checkpoint(control, child_index)?;
12035 if !cascaded.contains(&(*child_id, cr.row_id.0))
12036 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12037 == Some(parent_key.as_slice())
12038 {
12039 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
12042 .columns
12043 .iter()
12044 .map(|(k, v)| (*k, v.clone()))
12045 .collect();
12046 for cid in &fk.columns {
12047 cells.retain(|(k, _)| k != cid);
12048 cells.push((*cid, crate::memtable::Value::Null));
12049 }
12050 new_ops.push((
12051 *child_id,
12052 Staged::Update {
12053 row_id: cr.row_id,
12054 new_row: cells,
12055 changed_columns: fk.columns.clone(),
12056 },
12057 ));
12058 }
12059 }
12060 }
12061 }
12062 }
12063 }
12064 }
12065 if new_ops.is_empty() {
12066 break;
12067 }
12068 staging.extend(new_ops);
12069 }
12070
12071 self.validate_write_permissions(staging, principal, control)?;
12075 self.validate_security_writes(staging, read_epoch, principal, control)?;
12076 self.materialize_generated_embeddings(staging, control)?;
12077
12078 let staged_deletes: HashSet<(u64, u64)> = staging
12082 .iter()
12083 .filter_map(|(t, op)| match op {
12084 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
12085 _ => None,
12086 })
12087 .collect();
12088
12089 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
12091
12092 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
12094 commit_prepare_checkpoint(control, operation_index)?;
12095 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id) else {
12096 continue;
12097 };
12098 let cells_map: HashMap<u16, crate::memtable::Value>;
12099 match op {
12100 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
12101 cells_map = cells.iter().cloned().collect();
12102
12103 if !schema.constraints.checks.is_empty() {
12105 validate_checks(&schema.constraints.checks, &cells_map)?;
12106 }
12107
12108 for uc in &schema.constraints.uniques {
12110 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
12111 continue; };
12113 let marker = (*table_id, uc.id, key.clone());
12114 if !seen_unique.insert(marker) {
12115 return Err(MongrelError::Conflict(format!(
12116 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
12117 uc.name
12118 )));
12119 }
12120 let rows = load_rows(*table_id)?;
12121 for (row_index, r) in rows.iter().enumerate() {
12122 commit_prepare_checkpoint(control, row_index)?;
12123 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
12126 continue;
12127 }
12128 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
12129 if theirs == key {
12130 return Err(MongrelError::Conflict(format!(
12131 "UNIQUE constraint '{}' on table '{tname}' violated",
12132 uc.name
12133 )));
12134 }
12135 }
12136 }
12137 }
12138
12139 for fk in &schema.constraints.foreign_keys {
12141 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
12142 continue; };
12144 let Some(parent_id) = cat
12145 .tables
12146 .iter()
12147 .find(|t| t.name == fk.ref_table)
12148 .map(|t| t.table_id)
12149 else {
12150 return Err(MongrelError::InvalidArgument(format!(
12151 "FOREIGN KEY '{}' references unknown table '{}'",
12152 fk.name, fk.ref_table
12153 )));
12154 };
12155 self.acquire_fk_lock(
12160 txn_id,
12161 parent_id,
12162 &child_key,
12163 crate::locks::LockMode::Shared,
12164 control,
12165 )?;
12166 let parent_rows = load_rows(parent_id)?;
12167 let mut found = false;
12168 for (row_index, r) in parent_rows.iter().enumerate() {
12169 commit_prepare_checkpoint(control, row_index)?;
12170 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
12171 continue;
12172 }
12173 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
12174 if pkey == child_key {
12175 found = true;
12176 break;
12177 }
12178 }
12179 }
12180 if !found {
12187 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
12188 commit_prepare_checkpoint(control, staged_index)?;
12189 if *st_table != parent_id {
12190 continue;
12191 }
12192 if let Staged::Put(pcells)
12193 | Staged::Update {
12194 new_row: pcells, ..
12195 } = st_op
12196 {
12197 let pmap: HashMap<u16, crate::memtable::Value> =
12198 pcells.iter().cloned().collect();
12199 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
12200 {
12201 if pkey == child_key {
12202 found = true;
12203 break;
12204 }
12205 }
12206 }
12207 }
12208 }
12209 if !found {
12210 return Err(MongrelError::Conflict(format!(
12211 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
12212 fk.name, fk.ref_table
12213 )));
12214 }
12215 }
12216
12217 if let Staged::Update { row_id, .. } = op {
12222 let parent_handle = self.table_by_id(*table_id)?;
12223 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
12224 continue;
12225 };
12226 for (child_id, child_name, child_schema) in &live {
12227 for fk in &child_schema.constraints.foreign_keys {
12228 if fk.ref_table != *tname || fk.on_update != FkAction::Restrict {
12229 continue;
12230 }
12231 let Some(old_key) =
12232 encode_composite_key(&fk.ref_columns, &old_parent.columns)
12233 else {
12234 continue;
12235 };
12236 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
12237 == Some(old_key.as_slice())
12238 {
12239 continue;
12240 }
12241 self.acquire_fk_lock(
12246 txn_id,
12247 *table_id,
12248 &old_key,
12249 crate::locks::LockMode::Exclusive,
12250 control,
12251 )?;
12252 for (child_index, child) in
12253 load_rows(*child_id)?.into_iter().enumerate()
12254 {
12255 commit_prepare_checkpoint(control, child_index)?;
12256 if encode_composite_key(&fk.columns, &child.columns).as_deref()
12257 != Some(old_key.as_slice())
12258 {
12259 continue;
12260 }
12261 let replacement = staging.iter().find_map(|(id, op)| {
12262 if *id != *child_id {
12263 return None;
12264 }
12265 match op {
12266 Staged::Delete(id) if *id == child.row_id => Some(None),
12267 Staged::Update {
12268 row_id,
12269 new_row: cells,
12270 ..
12271 } if *row_id == child.row_id => {
12272 let map: HashMap<u16, Value> =
12273 cells.iter().cloned().collect();
12274 Some(encode_composite_key(&fk.columns, &map))
12275 }
12276 _ => None,
12277 }
12278 });
12279 if replacement.is_some_and(|key| {
12280 key.as_deref() != Some(old_key.as_slice())
12281 }) {
12282 continue;
12283 }
12284 return Err(MongrelError::Conflict(format!(
12285 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
12286 fk.name
12287 )));
12288 }
12289 }
12290 }
12291 }
12292 }
12293 Staged::Delete(rid) => {
12294 let parent_handle = self.table_by_id(*table_id)?;
12298 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
12299 continue;
12300 };
12301 for (child_id, child_name, child_schema) in &live {
12302 for fk in &child_schema.constraints.foreign_keys {
12303 if fk.ref_table != *tname || fk.on_delete != FkAction::Restrict {
12304 continue;
12305 }
12306 let Some(parent_key) =
12307 encode_composite_key(&fk.ref_columns, &parent_row.columns)
12308 else {
12309 continue;
12310 };
12311 let child_rows = load_rows(*child_id)?;
12312 for (row_index, r) in child_rows.iter().enumerate() {
12313 commit_prepare_checkpoint(control, row_index)?;
12314 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
12317 continue;
12318 }
12319 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
12320 if ck == parent_key {
12321 return Err(MongrelError::Conflict(format!(
12322 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
12323 fk.name
12324 )));
12325 }
12326 }
12327 }
12328 }
12329 }
12330 }
12331 Staged::Truncate => {
12332 for (child_id, child_name, child_schema) in &live {
12336 for fk in &child_schema.constraints.foreign_keys {
12337 if fk.ref_table != *tname {
12338 continue;
12339 }
12340 let child_rows = load_rows(*child_id)?;
12341 if child_rows
12342 .iter()
12343 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
12344 {
12345 return Err(MongrelError::Conflict(format!(
12346 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
12347 fk.name
12348 )));
12349 }
12350 }
12351 }
12352 }
12353 }
12354 }
12355 Ok(())
12356 }
12357
12358 fn validate_write_permissions(
12359 &self,
12360 staging: &[(u64, crate::txn::Staged)],
12361 principal: Option<&crate::auth::Principal>,
12362 control: Option<&crate::ExecutionControl>,
12363 ) -> Result<()> {
12364 commit_prepare_checkpoint(control, 0)?;
12365 if principal.is_none() && !self.auth_state.require_auth() {
12366 return Ok(());
12367 }
12368 let principal = principal.ok_or(MongrelError::AuthRequired)?;
12369 let needs = summarize_write_permissions(staging);
12370 let catalog = self.catalog.read();
12371
12372 if needs.values().any(|need| need.truncate) {
12373 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
12374 }
12375 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
12376 commit_prepare_checkpoint(control, need_index)?;
12377 let entry = catalog
12378 .tables
12379 .iter()
12380 .find(|entry| {
12381 entry.table_id == table_id
12382 && matches!(entry.state, TableState::Live | TableState::Building { .. })
12383 })
12384 .ok_or_else(|| {
12385 MongrelError::NotFound(format!(
12386 "live table {table_id} not found during write validation"
12387 ))
12388 })?;
12389 if matches!(entry.state, TableState::Building { .. }) {
12390 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
12391 continue;
12392 }
12393 if need.insert {
12394 Self::require_columns_for_principal(
12395 &entry.name,
12396 &entry.schema,
12397 crate::auth::ColumnOperation::Insert,
12398 &need.insert_columns,
12399 principal,
12400 )?;
12401 }
12402 if need.update {
12403 Self::require_columns_for_principal(
12404 &entry.name,
12405 &entry.schema,
12406 crate::auth::ColumnOperation::Update,
12407 &need.update_columns,
12408 principal,
12409 )?;
12410 }
12411 if need.delete {
12412 self.require_for(
12413 Some(principal),
12414 &crate::auth::Permission::Delete {
12415 table: entry.name.clone(),
12416 },
12417 )?;
12418 }
12419 }
12420 Ok(())
12421 }
12422
12423 fn validate_security_writes(
12424 &self,
12425 staging: &[(u64, crate::txn::Staged)],
12426 read_epoch: Epoch,
12427 explicit_principal: Option<&crate::auth::Principal>,
12428 control: Option<&crate::ExecutionControl>,
12429 ) -> Result<()> {
12430 commit_prepare_checkpoint(control, 0)?;
12431 use crate::security::PolicyCommand;
12432 use crate::txn::Staged;
12433
12434 let catalog = self.catalog.read();
12435 if catalog.security.rls_tables.is_empty() {
12436 return Ok(());
12437 }
12438 let security = catalog.security.clone();
12439 let table_names = catalog
12440 .tables
12441 .iter()
12442 .filter(|entry| matches!(entry.state, TableState::Live))
12443 .map(|entry| (entry.table_id, entry.name.clone()))
12444 .collect::<HashMap<_, _>>();
12445 drop(catalog);
12446 if !staging.iter().any(|(table_id, _)| {
12447 table_names
12448 .get(table_id)
12449 .is_some_and(|table| security.rls_enabled(table))
12450 }) {
12451 return Ok(());
12452 }
12453 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
12454
12455 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
12456 commit_prepare_checkpoint(control, operation_index)?;
12457 let Some(table) = table_names.get(table_id) else {
12458 continue;
12459 };
12460 if !security.rls_enabled(table) || principal.is_admin {
12461 continue;
12462 }
12463 let denied = |command| MongrelError::PermissionDenied {
12464 required: match command {
12465 PolicyCommand::Insert => crate::auth::Permission::Insert {
12466 table: table.clone(),
12467 },
12468 PolicyCommand::Update => crate::auth::Permission::Update {
12469 table: table.clone(),
12470 },
12471 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
12472 crate::auth::Permission::Delete {
12473 table: table.clone(),
12474 }
12475 }
12476 },
12477 principal: principal.username.clone(),
12478 };
12479 match operation {
12480 Staged::Put(cells) => {
12481 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
12482 row.columns.extend(cells.iter().cloned());
12483 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
12484 return Err(denied(PolicyCommand::Insert));
12485 }
12486 }
12487 Staged::Update {
12488 row_id,
12489 new_row: cells,
12490 ..
12491 } => {
12492 let old = self
12493 .table_by_id(*table_id)?
12494 .lock()
12495 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12496 .ok_or_else(|| {
12497 MongrelError::NotFound(format!("row {} not found", row_id.0))
12498 })?;
12499 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
12500 return Err(denied(PolicyCommand::Update));
12501 }
12502 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
12503 new.columns.extend(cells.iter().cloned());
12504 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
12505 return Err(denied(PolicyCommand::Update));
12506 }
12507 }
12508 Staged::Delete(row_id) => {
12509 let old = self
12510 .table_by_id(*table_id)?
12511 .lock()
12512 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12513 .ok_or_else(|| {
12514 MongrelError::NotFound(format!("row {} not found", row_id.0))
12515 })?;
12516 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
12517 return Err(denied(PolicyCommand::Delete));
12518 }
12519 }
12520 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
12521 }
12522 }
12523 Ok(())
12524 }
12525
12526 #[allow(clippy::too_many_arguments)]
12533 pub(crate) fn commit_transaction_with_external_states(
12534 &self,
12535 txn_id: u64,
12536 read_epoch: Epoch,
12537 staging: Vec<(u64, crate::txn::Staged)>,
12538 external_states: Vec<(String, Vec<u8>)>,
12539 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12540 security_principal: Option<crate::auth::Principal>,
12541 principal_catalog_bound: bool,
12542 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12543 context: crate::txn::TxnCommitContext,
12544 ) -> Result<(Epoch, Vec<RowId>)> {
12545 self.commit_transaction_with_external_states_inner(
12546 txn_id,
12547 read_epoch,
12548 staging,
12549 external_states,
12550 materialized_view_updates,
12551 security_principal,
12552 principal_catalog_bound,
12553 external_trigger_bridge,
12554 context,
12555 None,
12556 None,
12557 )
12558 }
12559
12560 #[allow(clippy::too_many_arguments)]
12561 pub(crate) fn commit_transaction_with_external_states_controlled(
12562 &self,
12563 txn_id: u64,
12564 read_epoch: Epoch,
12565 staging: Vec<(u64, crate::txn::Staged)>,
12566 external_states: Vec<(String, Vec<u8>)>,
12567 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12568 security_principal: Option<crate::auth::Principal>,
12569 principal_catalog_bound: bool,
12570 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12571 context: crate::txn::TxnCommitContext,
12572 control: &crate::ExecutionControl,
12573 before_commit: &mut dyn FnMut() -> Result<()>,
12574 ) -> Result<(Epoch, Vec<RowId>)> {
12575 self.commit_transaction_with_external_states_inner(
12576 txn_id,
12577 read_epoch,
12578 staging,
12579 external_states,
12580 materialized_view_updates,
12581 security_principal,
12582 principal_catalog_bound,
12583 external_trigger_bridge,
12584 context,
12585 Some(control),
12586 Some(before_commit),
12587 )
12588 }
12589
12590 #[allow(clippy::too_many_arguments)]
12591 fn commit_transaction_with_external_states_inner(
12592 &self,
12593 txn_id: u64,
12594 read_epoch: Epoch,
12595 mut staging: Vec<(u64, crate::txn::Staged)>,
12596 external_states: Vec<(String, Vec<u8>)>,
12597 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12598 mut security_principal: Option<crate::auth::Principal>,
12599 principal_catalog_bound: bool,
12600 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12601 context: crate::txn::TxnCommitContext,
12602 control: Option<&crate::ExecutionControl>,
12603 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
12604 ) -> Result<(Epoch, Vec<RowId>)> {
12605 use crate::memtable::Row;
12606 use crate::txn::{Staged, StagedOp, WriteKey};
12607 use crate::wal::Op;
12608 use std::collections::hash_map::DefaultHasher;
12609 use std::hash::{Hash, Hasher};
12610 use std::sync::atomic::Ordering;
12611
12612 if txn_id == crate::wal::SYSTEM_TXN_ID {
12613 return Err(MongrelError::Full(
12614 "per-open transaction id namespace exhausted; reopen the database".into(),
12615 ));
12616 }
12617 if self.read_only {
12618 return Err(MongrelError::ReadOnlyReplica);
12619 }
12620 commit_prepare_checkpoint(control, 0)?;
12621 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
12622 self.refresh_security_catalog_if_stale(observed_security_version)?;
12623 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
12624 if self.auth_state.require_auth() && security_principal.is_none() {
12625 return Err(MongrelError::AuthRequired);
12626 }
12627 {
12628 let catalog = self.catalog.read();
12629 if principal_catalog_bound
12630 || security_principal
12631 .as_ref()
12632 .is_some_and(|principal| principal.user_id != 0)
12633 {
12634 let principal = security_principal
12635 .as_ref()
12636 .ok_or(MongrelError::AuthRequired)?;
12637 security_principal =
12638 Self::resolve_bound_principal_from_catalog(&catalog, principal);
12639 if security_principal.is_none() {
12640 return Err(MongrelError::AuthRequired);
12641 }
12642 }
12643 }
12644 let _replication_guard = self.replication_barrier.read();
12645 if self.poisoned.load(Ordering::Relaxed) {
12646 return Err(MongrelError::Other(
12647 "database poisoned by fsync error".into(),
12648 ));
12649 }
12650 let _operation = self.admit_operation()?;
12654
12655 if context.state.is_some() {
12664 self.acquire_txn_lock(
12665 txn_id,
12666 crate::locks::LockKey::schema_barrier(),
12667 crate::locks::LockMode::Shared,
12668 control,
12669 )?;
12670 }
12671 let _txn_lock_guard = TxnLockGuard {
12672 locks: &self.lock_manager,
12673 txn_id,
12674 };
12675
12676 let idempotency_request = match &context.idempotency {
12682 Some(request) => match self.idempotency.check_and_reserve(request)? {
12683 crate::txn::IdempotencyCheck::Replay(receipt) => {
12684 let epoch = Epoch(receipt.log_position.index);
12685 if let Some(state) = &context.state {
12686 state.committed(receipt);
12687 }
12688 return Ok((epoch, Vec::new()));
12689 }
12690 crate::txn::IdempotencyCheck::Reserved => Some(request.clone()),
12691 },
12692 None => None,
12693 };
12694 let mut idempotency_guard = idempotency_request.as_ref().map(|request| {
12696 crate::txn::IdempotencyReservationGuard::new(&self.idempotency, request.clone())
12697 });
12698 let mut external_states = dedup_external_states(external_states);
12699 if !external_states.is_empty() {
12700 let cat = self.catalog.read();
12701 for (name, _) in &external_states {
12702 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
12703 return Err(MongrelError::NotFound(format!(
12704 "external table {name:?} not found"
12705 )));
12706 }
12707 }
12708 }
12709 let prepared_materialized_views = {
12710 let mut deduplicated = HashMap::new();
12711 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
12712 {
12713 commit_prepare_checkpoint(control, definition_index)?;
12714 if definition.name.is_empty() || definition.query.trim().is_empty() {
12715 return Err(MongrelError::InvalidArgument(
12716 "materialized view name and query must not be empty".into(),
12717 ));
12718 }
12719 deduplicated.insert(definition.name.clone(), definition);
12720 }
12721 let catalog = self.catalog.read();
12722 let mut prepared = Vec::with_capacity(deduplicated.len());
12723 for (definition_index, definition) in deduplicated.into_values().enumerate() {
12724 commit_prepare_checkpoint(control, definition_index)?;
12725 let table_id = catalog
12726 .live(&definition.name)
12727 .ok_or_else(|| {
12728 MongrelError::NotFound(format!(
12729 "materialized view table {:?} not found",
12730 definition.name
12731 ))
12732 })?
12733 .table_id;
12734 prepared.push((table_id, definition));
12735 }
12736 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
12737 prepared
12738 };
12739
12740 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12743 self.expand_table_triggers(
12744 txn_id,
12745 &mut staging,
12746 read_epoch,
12747 external_trigger_bridge,
12748 &mut external_states,
12749 control,
12750 )?;
12751 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12752 external_states = dedup_external_states(external_states);
12753 let expected_external_generations = {
12754 let catalog = self.catalog.read();
12755 let mut generations = HashMap::with_capacity(external_states.len());
12756 for (name, _) in &external_states {
12757 let entry = catalog
12758 .external_tables
12759 .iter()
12760 .find(|entry| entry.name == *name)
12761 .ok_or_else(|| {
12762 MongrelError::NotFound(format!("external table {name:?} not found"))
12763 })?;
12764 generations.insert(name.clone(), entry.created_epoch);
12765 }
12766 generations
12767 };
12768
12769 self.acquire_unique_key_claims(txn_id, &staging, control)?;
12775 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
12779 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
12780 self.validate_constraints(
12786 txn_id,
12787 &mut staging,
12788 read_epoch,
12789 security_principal.as_ref(),
12790 control,
12791 )?;
12792 let mut normalized = Vec::with_capacity(staging.len() * 2);
12793 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
12794 commit_prepare_checkpoint(control, staged_index)?;
12795 match op {
12796 crate::txn::Staged::Update {
12797 row_id,
12798 new_row: cells,
12799 ..
12800 } => {
12801 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
12802 normalized.push((table_id, crate::txn::Staged::Put(cells)));
12803 }
12804 op => normalized.push((table_id, op)),
12805 }
12806 }
12807 staging = normalized;
12808 let has_changes = !staging.is_empty()
12809 || !external_states.is_empty()
12810 || !prepared_materialized_views.is_empty();
12811 let truncated_tables: HashSet<u64> = staging
12812 .iter()
12813 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
12814 .collect();
12815
12816 let write_keys = {
12817 let cat = self.catalog.read();
12818 let mut keys: Vec<WriteKey> = Vec::new();
12819 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12820 commit_prepare_checkpoint(control, staged_index)?;
12821 match staged {
12822 Staged::Put(cells) => {
12823 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
12824 for col in &entry.schema.columns {
12825 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
12826 if let Some((_, val)) =
12827 cells.iter().find(|(id, _)| *id == col.id)
12828 {
12829 let mut h = DefaultHasher::new();
12830 val.encode_key().hash(&mut h);
12831 keys.push(WriteKey::Unique {
12832 table_id: *table_id,
12833 index_id: 0,
12834 key_hash: h.finish(),
12835 });
12836 }
12837 }
12838 }
12839 for uc in &entry.schema.constraints.uniques {
12846 if let Some(key_bytes) = crate::constraint::encode_composite_key(
12847 &uc.columns,
12848 &cells.iter().cloned().collect(),
12849 ) {
12850 let mut h = DefaultHasher::new();
12851 key_bytes.hash(&mut h);
12852 keys.push(WriteKey::Unique {
12853 table_id: *table_id,
12854 index_id: uc.id | 0x8000,
12855 key_hash: h.finish(),
12856 });
12857 }
12858 }
12859 }
12860 }
12861 Staged::Delete(rid) => keys.push(WriteKey::Row {
12862 table_id: *table_id,
12863 row_id: rid.0,
12864 }),
12865 Staged::Truncate => keys.push(WriteKey::Table {
12866 table_id: *table_id,
12867 }),
12868 Staged::Update { .. } => {
12869 return Err(MongrelError::Other(
12870 "transaction contains an unnormalized update during preparation".into(),
12871 ));
12872 }
12873 }
12874 }
12875 for (external_index, (name, _)) in external_states.iter().enumerate() {
12876 commit_prepare_checkpoint(control, external_index)?;
12877 let mut h = DefaultHasher::new();
12878 name.hash(&mut h);
12879 keys.push(WriteKey::Unique {
12880 table_id: EXTERNAL_TABLE_ID,
12881 index_id: 0,
12882 key_hash: h.finish(),
12883 });
12884 }
12885 keys
12886 };
12887
12888 let min_active = self.active_txns.min_read_epoch();
12890 if min_active < u64::MAX {
12891 self.conflicts.prune_below(Epoch(min_active));
12892 }
12893
12894 let ssi_keys = match context.isolation.canonical() {
12901 crate::txn::IsolationLevel::Serializable => {
12902 crate::txn::ssi_validation_keys(&context.read_set, &context.predicate_set)
12903 }
12904 _ => Vec::new(),
12905 };
12906
12907 if self.conflicts.conflicts(&write_keys, read_epoch) {
12911 return Err(MongrelError::Conflict(
12912 "write-write conflict (pre-validate, first-committer-wins)".into(),
12913 ));
12914 }
12915 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
12916 return Err(MongrelError::SerializationFailure {
12917 message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
12918 .into(),
12919 });
12920 }
12921 let pre_validate_version = self.conflicts.version();
12922
12923 let mut spilled: Vec<SpilledRun> = Vec::new();
12927 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
12928 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
12932 {
12933 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
12934 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
12935 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12936 commit_prepare_checkpoint(control, staged_index)?;
12937 if let Staged::Put(cells) = staged {
12938 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
12939 bytes.saturating_add(value.estimated_bytes())
12940 });
12941 let table_bytes = table_bytes.entry(*table_id).or_default();
12942 *table_bytes = table_bytes.saturating_add(bytes);
12943 put_indexes.entry(*table_id).or_default().push(staged_index);
12944 }
12945 }
12946 let tables = self.tables.read();
12947 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
12948 commit_prepare_checkpoint(control, table_index)?;
12949 if bytes
12950 <= self
12951 .spill_threshold
12952 .load(std::sync::atomic::Ordering::Relaxed)
12953 {
12954 continue;
12955 }
12956 let Some(handle) = tables.get(&table_id) else {
12957 continue;
12958 };
12959 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
12960 let mut t = handle.lock();
12961 let tdir = t.table_dir().to_path_buf();
12962 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
12963 std::fs::create_dir_all(&txn_dir)?;
12964 let run_id = t.alloc_run_id()? as u128;
12965 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
12966 let final_path = t.run_path(run_id as u64);
12967
12968 let mut rows: Vec<Row> = Vec::new();
12969 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
12970 commit_prepare_checkpoint(control, put_index)?;
12971 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
12972 return Err(MongrelError::Other(
12973 "transaction put index no longer references a put".into(),
12974 ));
12975 };
12976 t.validate_cells_not_null(cells)?;
12977 let row_id = t.alloc_row_id()?;
12978 let mut row = Row::new(row_id, Epoch(0));
12979 row.columns.extend(std::mem::take(cells));
12980 rows.push(row);
12981 }
12982 let schema = t.schema_ref().clone();
12983 let kek = t.kek_ref().cloned();
12984 let specs = t.indexable_column_specs();
12985 drop(t);
12986
12987 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
12988 .uniform_epoch(true);
12989 if let Some(ref kek) = kek {
12990 writer = writer.with_encryption(kek.as_ref(), specs);
12991 }
12992 commit_prepare_checkpoint(control, 0)?;
12993 let header = writer.write(&pending_path, &rows)?;
12994 commit_prepare_checkpoint(control, 0)?;
12995 let row_count = header.row_count;
12996 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
12997 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
12998
12999 spilled.push(SpilledRun {
13000 table_id,
13001 run_id,
13002 pending_path,
13003 final_path,
13004 rows,
13005 row_count,
13006 min_rid,
13007 max_rid,
13008 content_hash: header.content_hash,
13009 });
13010 spilled_tables.insert(table_id);
13011 }
13012 }
13013
13014 if spill_guard.is_some() {
13016 if let Some(hook) = self.spill_hook.lock().as_ref() {
13017 hook();
13018 }
13019 }
13020
13021 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13032 .take(staging.len())
13033 .collect();
13034 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13035 .take(staging.len())
13036 .collect();
13037 {
13038 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
13039 for (index, (table_id, staged)) in staging.iter().enumerate() {
13040 commit_prepare_checkpoint(control, index)?;
13041 if matches!(staged, Staged::Delete(_))
13042 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
13043 {
13044 indexes_by_table.entry(*table_id).or_default().push(index);
13045 }
13046 }
13047 let tables = self.tables.read();
13048 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
13049 commit_prepare_checkpoint(control, table_index)?;
13050 let handle = tables.get(&table_id).ok_or_else(|| {
13051 MongrelError::NotFound(format!("table {table_id} not mounted"))
13052 })?;
13053 #[cfg(test)]
13054 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13055 let mut t = handle.lock();
13056 for (prepare_index, index) in indexes.into_iter().enumerate() {
13057 commit_prepare_checkpoint(control, prepare_index)?;
13058 match &staging[index].1 {
13059 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
13060 t.validate_cells_not_null(cells)?;
13061 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
13062 for (column, value) in cells {
13063 row.columns.insert(*column, value.clone());
13064 }
13065 prebuilt[index] = Some(row);
13066 }
13067 Staged::Delete(row_id) => {
13068 delete_images[index] =
13069 t.get(*row_id, self.snapshot_for_epoch(read_epoch));
13070 }
13071 Staged::Put(_) | Staged::Truncate => {}
13072 Staged::Update { .. } => {
13073 return Err(MongrelError::Other(
13074 "transaction contains an unnormalized update during row preparation"
13075 .into(),
13076 ));
13077 }
13078 }
13079 }
13080 }
13081 }
13082
13083 let prepared_table_handles = {
13087 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
13088 let put_table_ids: HashSet<u64> = staging
13089 .iter()
13090 .filter_map(|(table_id, staged)| {
13091 matches!(staged, Staged::Put(_)).then_some(*table_id)
13092 })
13093 .collect();
13094 let tables = self.tables.read();
13095 let mut handles = HashMap::with_capacity(table_ids.len());
13096 for (table_index, table_id) in table_ids.into_iter().enumerate() {
13097 commit_prepare_checkpoint(control, table_index)?;
13098 let handle = tables.get(&table_id).ok_or_else(|| {
13099 MongrelError::NotFound(format!("table {table_id} not mounted"))
13100 })?;
13101 if put_table_ids.contains(&table_id) {
13102 match control {
13103 Some(control) => {
13104 handle.lock().prepare_durable_publish_controlled(control)?
13105 }
13106 None => handle.lock().prepare_durable_publish()?,
13107 }
13108 }
13109 handles.insert(table_id, handle.clone());
13110 }
13111 handles
13112 };
13113
13114 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
13118
13119 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
13120 .iter()
13121 .map(|run| {
13122 (
13123 run.table_id,
13124 run.rows.iter().map(|row| row.row_id).collect(),
13125 )
13126 })
13127 .collect();
13128 let committed_row_ids = staging
13129 .iter()
13130 .enumerate()
13131 .filter_map(|(index, (table_id, staged))| {
13132 if !matches!(staged, Staged::Put(_)) {
13133 return None;
13134 }
13135 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
13136 spilled_row_ids
13137 .get_mut(table_id)
13138 .and_then(VecDeque::pop_front)
13139 })
13140 })
13141 .collect();
13142
13143 let mut prepared_external = Vec::with_capacity(external_states.len());
13144 for (external_index, (name, state)) in external_states.iter().enumerate() {
13145 commit_prepare_checkpoint(control, external_index)?;
13146 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
13147 prepared_external.push((name.clone(), state.clone(), pending));
13148 }
13149
13150 if context.state.is_some() {
13160 self.lock_manager
13161 .release(txn_id, &crate::locks::LockKey::schema_barrier());
13162 }
13163 let added_runs: Vec<crate::wal::AddedRun> = spilled
13164 .iter()
13165 .map(|s| crate::wal::AddedRun {
13166 table_id: s.table_id,
13167 run_id: s.run_id,
13168 row_count: s.row_count,
13169 level: 0,
13170 min_row_id: s.min_rid,
13171 max_row_id: s.max_rid,
13172 content_hash: s.content_hash,
13173 })
13174 .collect();
13175 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
13176 hook();
13177 }
13178 let security_guard = self.security_coordinator.gate.read();
13182 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
13183 return Err(MongrelError::Conflict(
13184 "security policy changed during write".into(),
13185 ));
13186 }
13187 if spill_guard.is_some() {
13188 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
13189 hook();
13190 }
13191 }
13192 let commit_guard = self.commit_lock.lock();
13193 let catalog_generation_result = (|| {
13194 {
13195 let catalog = self.catalog.read();
13196 for table_id in prepared_table_handles.keys() {
13197 let is_current = catalog.tables.iter().any(|entry| {
13198 entry.table_id == *table_id
13199 && matches!(entry.state, TableState::Live | TableState::Building { .. })
13200 });
13201 if !is_current {
13202 return Err(MongrelError::Conflict(format!(
13203 "table {table_id} changed during transaction preparation"
13204 )));
13205 }
13206 }
13207 for (name, created_epoch) in &expected_external_generations {
13208 let current = catalog
13209 .external_tables
13210 .iter()
13211 .find(|entry| entry.name == *name)
13212 .map(|entry| entry.created_epoch);
13213 if current != Some(*created_epoch) {
13214 return Err(MongrelError::Conflict(format!(
13215 "external table {name:?} changed during transaction preparation"
13216 )));
13217 }
13218 }
13219 for (table_id, definition) in &prepared_materialized_views {
13220 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
13221 if current != Some(*table_id) {
13222 return Err(MongrelError::Conflict(format!(
13223 "materialized view {:?} changed during transaction preparation",
13224 definition.name
13225 )));
13226 }
13227 }
13228 if trigger_catalog_binding(&catalog) != trigger_binding {
13229 return Err(MongrelError::Conflict(
13230 "trigger or referenced table generation changed during transaction preparation"
13231 .into(),
13232 ));
13233 }
13234 }
13235 let tables = self.tables.read();
13236 for (table_id, prepared) in &prepared_table_handles {
13237 if !tables
13238 .get(table_id)
13239 .is_some_and(|current| current.ptr_eq(prepared))
13240 {
13241 return Err(MongrelError::Conflict(format!(
13242 "table {table_id} mount changed during transaction preparation"
13243 )));
13244 }
13245 }
13246 Ok(())
13247 })();
13248 if let Err(error) = catalog_generation_result {
13249 drop(commit_guard);
13250 for (_, _, pending) in &prepared_external {
13251 let _ = std::fs::remove_file(pending);
13252 }
13253 return Err(error);
13254 }
13255 let new_epoch = self.epoch.assigned().next();
13259 let commit_ts = match context.read_ts {
13265 Some(read_ts) => self.hlc.commit_timestamp([read_ts]),
13266 None => self.hlc.now().map_err(|skew| {
13267 MongrelError::Other(format!(
13268 "clock skew rejected commit timestamp allocation: {skew}"
13269 ))
13270 })?,
13271 };
13272 let mut spilled_wal_bytes = 0;
13273 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
13274 let spill_prepare = (|| {
13275 for run in &mut spilled {
13276 for row in &mut run.rows {
13277 row.committed_epoch = new_epoch;
13278 row.commit_ts = Some(commit_ts);
13279 }
13280 for rows in encode_spilled_row_chunks(
13281 &run.rows,
13282 &mut spilled_wal_bytes,
13283 SPILLED_WAL_TOTAL_MAX_BYTES,
13284 control,
13285 )? {
13286 spilled_wal_records.push((
13287 run.table_id,
13288 Op::SpilledRows {
13289 table_id: run.table_id,
13290 rows,
13291 },
13292 ));
13293 }
13294 }
13295 Result::<()>::Ok(())
13296 })();
13297 if let Err(error) = spill_prepare {
13298 for (_, _, pending) in &prepared_external {
13299 let _ = std::fs::remove_file(pending);
13300 }
13301 return Err(error);
13302 }
13303 let (
13304 new_epoch,
13305 mut _epoch_guard,
13306 applies,
13307 committed_materialized_views,
13308 commit_seq,
13309 commit_ts,
13310 ) = {
13311 let mut wal = self.shared_wal.lock();
13312
13313 if self.conflicts.version() != pre_validate_version {
13318 if self.conflicts.conflicts(&write_keys, read_epoch) {
13319 drop(wal);
13322 for (_, _, pending) in &prepared_external {
13323 let _ = std::fs::remove_file(pending);
13324 }
13325 return Err(MongrelError::Conflict(
13326 "write-write conflict (sequencer delta re-check)".into(),
13327 ));
13328 }
13329 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
13330 drop(wal);
13334 for (_, _, pending) in &prepared_external {
13335 let _ = std::fs::remove_file(pending);
13336 }
13337 return Err(MongrelError::SerializationFailure {
13338 message:
13339 "a concurrent commit invalidated this transaction's reads (sequencer re-check)"
13340 .into(),
13341 });
13342 }
13343 }
13344
13345 if let Some(control) = control {
13346 if let Err(error) = control.checkpoint() {
13347 drop(wal);
13348 for (_, _, pending) in &prepared_external {
13349 let _ = std::fs::remove_file(pending);
13350 }
13351 return Err(error);
13352 }
13353 }
13354 let mut applies = Vec::<TableApplyBatch>::new();
13355 let mut apply_indexes = HashMap::<u64, usize>::new();
13356 let mut committed_materialized_views = Vec::new();
13357 let mut wal_records = spilled_wal_records;
13358
13359 let mut index = 0;
13360 while index < staging.len() {
13361 let table_id = staging[index].0;
13362 let handle = prepared_table_handles
13363 .get(&table_id)
13364 .cloned()
13365 .ok_or_else(|| {
13366 MongrelError::NotFound(format!("table {table_id} not prepared"))
13367 })?;
13368 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
13369 let index = applies.len();
13370 applies.push(TableApplyBatch {
13371 table_id,
13372 handle,
13373 ops: Vec::new(),
13374 });
13375 index
13376 });
13377
13378 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
13381 {
13382 index += 1;
13383 continue;
13384 }
13385
13386 match &staging[index].1 {
13387 Staged::Put(_) => {
13388 let mut rows = Vec::new();
13389 while index < staging.len()
13390 && staging[index].0 == table_id
13391 && matches!(&staging[index].1, Staged::Put(_))
13392 {
13393 let mut row = prebuilt[index].take().ok_or_else(|| {
13394 MongrelError::Other(
13395 "transaction prepare lost a prebuilt put row".into(),
13396 )
13397 })?;
13398 row.committed_epoch = new_epoch;
13399 row.commit_ts = Some(commit_ts);
13400 rows.push(row);
13401 index += 1;
13402 }
13403 let payload = bincode::serialize(&rows)
13404 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
13405 wal_records.push((
13406 table_id,
13407 Op::Put {
13408 table_id,
13409 rows: payload,
13410 },
13411 ));
13412 applies[batch_index].ops.push(StagedOp::Put(rows));
13413 }
13414 Staged::Delete(_) => {
13415 let mut row_ids = Vec::new();
13416 while index < staging.len()
13417 && staging[index].0 == table_id
13418 && matches!(&staging[index].1, Staged::Delete(_))
13419 {
13420 let Staged::Delete(row_id) = &staging[index].1 else {
13421 return Err(MongrelError::Other(
13422 "transaction delete batch changed during WAL preparation"
13423 .into(),
13424 ));
13425 };
13426 if let Some(before) = &delete_images[index] {
13427 wal_records.push((
13428 table_id,
13429 Op::BeforeImage {
13430 table_id,
13431 row_id: *row_id,
13432 row: bincode::serialize(before).map_err(|error| {
13433 MongrelError::Other(format!(
13434 "before-image serialize: {error}"
13435 ))
13436 })?,
13437 },
13438 ));
13439 }
13440 row_ids.push(*row_id);
13441 index += 1;
13442 }
13443 wal_records.push((
13444 table_id,
13445 Op::Delete {
13446 table_id,
13447 row_ids: row_ids.clone(),
13448 },
13449 ));
13450 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
13451 }
13452 Staged::Truncate => {
13453 wal_records.push((table_id, Op::TruncateTable { table_id }));
13454 applies[batch_index].ops.push(StagedOp::Truncate);
13455 index += 1;
13456 }
13457 Staged::Update { .. } => {
13458 return Err(MongrelError::Other(
13459 "transaction contains an unnormalized update at the sequencer".into(),
13460 ));
13461 }
13462 }
13463 }
13464
13465 for (name, state, _) in &prepared_external {
13466 wal_records.push((
13467 EXTERNAL_TABLE_ID,
13468 Op::ExternalTableState {
13469 name: name.clone(),
13470 state: state.clone(),
13471 },
13472 ));
13473 }
13474
13475 for (table_id, definition) in &prepared_materialized_views {
13476 let mut definition = definition.clone();
13477 definition.last_refresh_epoch = new_epoch.0;
13478 wal_records.push((
13479 *table_id,
13480 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
13481 name: definition.name.clone(),
13482 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
13483 }),
13484 ));
13485 committed_materialized_views.push(definition);
13486 }
13487 if !committed_materialized_views.is_empty() {
13488 let mut next_catalog = self.catalog.read().clone();
13489 for definition in &committed_materialized_views {
13490 if let Some(existing) = next_catalog
13491 .materialized_views
13492 .iter_mut()
13493 .find(|existing| existing.name == definition.name)
13494 {
13495 *existing = definition.clone();
13496 } else {
13497 next_catalog.materialized_views.push(definition.clone());
13498 }
13499 }
13500 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13501 wal_records.push((
13502 WAL_TABLE_ID,
13503 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
13504 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
13505 }),
13506 ));
13507 }
13508
13509 if let Some(control) = control {
13510 if let Err(error) = control.checkpoint() {
13511 drop(wal);
13512 for (_, _, pending) in &prepared_external {
13513 let _ = std::fs::remove_file(pending);
13514 }
13515 return Err(error);
13516 }
13517 }
13518 if let Some(before_commit) = before_commit.as_mut() {
13519 if let Err(error) = before_commit() {
13520 drop(wal);
13521 for (_, _, pending) in &prepared_external {
13522 let _ = std::fs::remove_file(pending);
13523 }
13524 return Err(error);
13525 }
13526 }
13527
13528 let assigned_epoch = self.epoch.bump_assigned();
13529 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
13530 if assigned_epoch != new_epoch {
13531 for (_, _, pending) in &prepared_external {
13532 let _ = std::fs::remove_file(pending);
13533 }
13534 return Err(MongrelError::Conflict(
13535 "commit epoch changed while sequencer lock was held".into(),
13536 ));
13537 }
13538
13539 if let Err(fault) = mongreldb_fault::inject("txn.decision.before") {
13546 for (_, _, pending) in &prepared_external {
13547 let _ = std::fs::remove_file(pending);
13548 }
13549 return Err(crate::commit_log::fault_as_io(fault));
13550 }
13551 if let Some(state) = &context.state {
13552 state.enter_commit_critical();
13553 }
13554
13555 prepared_run_links.disarm();
13559
13560 let commit_seq = self
13564 .standalone_commit_log
13565 .append_transaction(
13566 &mut wal,
13567 txn_id,
13568 new_epoch,
13569 commit_ts,
13570 wal_records,
13571 &added_runs,
13572 )
13573 .map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
13574
13575 self.conflicts.record(&write_keys, new_epoch);
13580 (
13581 new_epoch,
13582 _epoch_guard,
13583 applies,
13584 committed_materialized_views,
13585 commit_seq,
13586 commit_ts,
13587 )
13588 };
13589 drop(commit_guard);
13590
13591 let receipt =
13593 self.await_durable_commit_with_ts(txn_id, commit_seq, new_epoch, commit_ts)?;
13594 drop(security_guard);
13595
13596 if let Some(request) = &idempotency_request {
13600 if let Err(error) =
13601 self.idempotency
13602 .complete(request, txn_id, new_epoch, receipt.commit_ts)
13603 {
13604 return Err(MongrelError::DurableCommit {
13605 epoch: new_epoch.0,
13606 message: format!("idempotency record was not durable: {error}"),
13607 });
13608 }
13609 if let Some(guard) = idempotency_guard.as_mut() {
13610 guard.disarm();
13611 }
13612 }
13613
13614 let publish_result: Result<()> = {
13616 let mut first_error = None;
13617 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
13618 for run in &spilled {
13619 spilled_by_table.entry(run.table_id).or_default().push(run);
13620 }
13621 let mut modified_tables = Vec::with_capacity(applies.len());
13622 for batch in applies {
13625 #[cfg(test)]
13626 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13627 let mut t = batch.handle.lock();
13628 for op in batch.ops {
13629 match op {
13630 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
13631 StagedOp::Delete(row_ids) => {
13632 for row_id in row_ids {
13633 t.apply_delete_at(row_id, new_epoch, Some(commit_ts));
13634 }
13635 }
13636 StagedOp::Truncate => t.apply_truncate(new_epoch),
13637 }
13638 }
13639 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
13640 for run in runs {
13641 t.link_run(crate::manifest::RunRef {
13642 run_id: run.run_id,
13643 level: 0,
13644 epoch_created: new_epoch.0,
13645 row_count: run.row_count,
13646 });
13647 t.apply_run_metadata_prepared(&run.rows)?;
13648 if truncated_tables.contains(&batch.table_id) {
13649 t.set_flushed_epoch(new_epoch);
13654 }
13655 }
13656 }
13657 t.invalidate_pending_cache();
13658 drop(t);
13659 modified_tables.push(batch.handle);
13660 }
13661
13662 for handle in modified_tables {
13666 #[cfg(test)]
13667 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
13668 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
13669 first_error.get_or_insert(error);
13670 }
13671 }
13672 for (name, _, pending) in &prepared_external {
13673 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
13674 first_error.get_or_insert(error);
13675 }
13676 }
13677 if !committed_materialized_views.is_empty() {
13678 let mut next_catalog = self.catalog.read().clone();
13679 for definition in committed_materialized_views {
13680 if let Some(existing) = next_catalog
13681 .materialized_views
13682 .iter_mut()
13683 .find(|existing| existing.name == definition.name)
13684 {
13685 *existing = definition;
13686 } else {
13687 next_catalog.materialized_views.push(definition);
13688 }
13689 }
13690 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13691 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
13692 first_error.get_or_insert(error);
13693 }
13694 }
13695 match first_error {
13696 Some(error) => Err(error),
13697 None => Ok(()),
13698 }
13699 };
13700
13701 if has_changes {
13702 let _ = self.change_wake.send(());
13703 }
13704 self.finish_durable_publish(new_epoch, &mut _epoch_guard, &receipt, publish_result)?;
13705 if let Some(state) = &context.state {
13709 state.committed(receipt.clone());
13710 }
13711 mongreldb_fault::inject("txn.decision.after").map_err(|fault| {
13715 MongrelError::DurableCommit {
13716 epoch: new_epoch.0,
13717 message: fault.to_string(),
13718 }
13719 })?;
13720 Ok((new_epoch, committed_row_ids))
13721 }
13722
13723 pub fn visible_snapshot(&self) -> Snapshot {
13727 self.snapshot_for_epoch(self.epoch.visible())
13728 }
13729
13730 pub fn snapshot_for_epoch(&self, epoch: Epoch) -> Snapshot {
13735 let commit_ts = self
13736 .commit_ts_for_epoch(epoch)
13737 .filter(|ts| *ts != mongreldb_types::hlc::HlcTimestamp::ZERO)
13738 .or_else(|| self.hlc.now().ok())
13739 .unwrap_or(mongreldb_types::hlc::HlcTimestamp::MAX);
13740 Snapshot::at_hlc(epoch, commit_ts)
13741 }
13742
13743 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
13746 let snap = self.visible_snapshot();
13747 let g = self.snapshots.register(snap.epoch);
13748 (snap, g)
13749 }
13750
13751 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
13754 let snap = self.visible_snapshot();
13755 let g = self.snapshots.register_owned(snap.epoch);
13756 (snap, g)
13757 }
13758
13759 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
13764 let _guard = self.ddl_lock.lock();
13765 let current = self.epoch.visible();
13766 let (old_epochs, old_start) = self.snapshots.history_config();
13767 let earliest_already_guaranteed = if old_epochs == 0 {
13768 current
13769 } else {
13770 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
13771 };
13772 let start = if epochs == 0 {
13773 current
13774 } else {
13775 earliest_already_guaranteed
13776 };
13777 let published = std::cell::Cell::new(false);
13778 let result = write_history_retention(&self.root, epochs, start, || {
13779 self.snapshots.configure_history(epochs, start);
13780 published.set(true);
13781 });
13782 match result {
13783 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
13784 epoch: current.0,
13785 message: format!("history-retention publication was not durable: {error}"),
13786 }),
13787 result => result,
13788 }
13789 }
13790
13791 pub fn history_retention_epochs(&self) -> u64 {
13792 self.snapshots.history_config().0
13793 }
13794
13795 pub fn earliest_retained_epoch(&self) -> Epoch {
13796 let current = self.epoch.visible();
13797 self.snapshots.history_floor(current).unwrap_or(current)
13798 }
13799
13800 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
13805 let current = self.epoch.visible();
13806 if epoch > current {
13807 return Err(MongrelError::InvalidArgument(format!(
13808 "epoch {} is in the future; current epoch is {}",
13809 epoch.0, current.0
13810 )));
13811 }
13812 let earliest = self.earliest_retained_epoch();
13813 if epoch < earliest {
13814 return Err(MongrelError::InvalidArgument(format!(
13815 "epoch {} is no longer retained; earliest available epoch is {}",
13816 epoch.0, earliest.0
13817 )));
13818 }
13819 let guard = self.snapshots.register_owned(epoch);
13820 let snap = match self.commit_ts_for_epoch(epoch) {
13821 Some(commit_ts) => Snapshot::at_hlc(epoch, commit_ts),
13822 None => Snapshot::at(epoch),
13823 };
13824 Ok((snap, guard))
13825 }
13826
13827 pub fn table_names(&self) -> Vec<String> {
13829 self.catalog
13830 .read()
13831 .tables
13832 .iter()
13833 .filter(|t| matches!(t.state, TableState::Live))
13834 .map(|t| t.name.clone())
13835 .collect()
13836 }
13837
13838 pub fn close(&self) -> Result<()> {
13844 for name in self.table_names() {
13845 if let Ok(handle) = self.table(&name) {
13846 if let Err(e) = handle.lock().close() {
13847 eprintln!("[close] flush failed for {name}: {e}");
13848 }
13849 }
13850 }
13851 Ok(())
13852 }
13853
13854 pub fn compact(&self) -> Result<(usize, usize)> {
13861 self.require(&crate::auth::Permission::Ddl)?;
13862 let _operation = self.admit_operation()?;
13864 let mut compacted = 0;
13865 let mut skipped = 0;
13866 for name in self.table_names() {
13867 let Ok(handle) = self.table(&name) else {
13868 continue;
13869 };
13870 {
13871 let mut t = handle.lock();
13872 let before = t.run_count();
13873 if before < 2 && !t.should_compact() {
13874 skipped += 1;
13875 continue;
13876 }
13877 match t.compact() {
13878 Ok(()) => {
13879 let after = t.run_count();
13880 compacted += 1;
13881 eprintln!("[compact] {name}: {before} -> {after} runs");
13882 }
13883 Err(e) => {
13884 eprintln!("[compact] {name}: compaction failed: {e}");
13885 skipped += 1;
13886 }
13887 }
13888 }
13889 }
13890 Ok((compacted, skipped))
13891 }
13892
13893 pub fn compact_table(&self, name: &str) -> Result<bool> {
13896 self.require(&crate::auth::Permission::Ddl)?;
13897 let handle = self.table(name)?;
13898 let mut t = handle.lock();
13899 let before = t.run_count();
13900 if before < 2 {
13901 return Ok(false);
13902 }
13903 t.compact()?;
13904 Ok(t.run_count() < before)
13905 }
13906
13907 pub fn table(&self, name: &str) -> Result<TableHandle> {
13909 self.ensure_owner_process()?;
13910 let cat = self.catalog.read();
13911 let entry = cat
13912 .live(name)
13913 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13914 let id = entry.table_id;
13915 drop(cat);
13916 self.tables
13917 .read()
13918 .get(&id)
13919 .cloned()
13920 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
13921 }
13922
13923 pub fn has_ttl_tables(&self) -> bool {
13926 self.tables
13927 .read()
13928 .values()
13929 .any(|table| table.lock().ttl().is_some())
13930 }
13931
13932 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
13935 self.tables
13936 .read()
13937 .get(&id)
13938 .cloned()
13939 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
13940 }
13941
13942 pub(crate) fn table_auto_inc_would_allocate(
13948 &self,
13949 table_id: u64,
13950 cells: &[(u16, Value)],
13951 ) -> bool {
13952 let catalog = self.catalog.read();
13953 let Some(entry) = catalog
13954 .tables
13955 .iter()
13956 .find(|entry| entry.table_id == table_id)
13957 else {
13958 return false;
13959 };
13960 let Some(column) = entry.schema.auto_increment_column() else {
13961 return false;
13962 };
13963 matches!(
13964 cells.iter().find(|(id, _)| *id == column.id),
13965 None | Some((_, Value::Null))
13966 )
13967 }
13968
13969 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
13975 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
13976 return Err(MongrelError::InvalidArgument(format!(
13977 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
13978 )));
13979 }
13980 self.create_table_with_state(name, schema, TableState::Live)
13981 }
13982
13983 #[doc(hidden)]
13985 pub fn create_building_table(
13986 &self,
13987 build_name: &str,
13988 intended_name: &str,
13989 query_id: &str,
13990 schema: Schema,
13991 ) -> Result<u64> {
13992 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13993 || intended_name.is_empty()
13994 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13995 || query_id.is_empty()
13996 {
13997 return Err(MongrelError::InvalidArgument(
13998 "invalid CTAS building-table identity".into(),
13999 ));
14000 }
14001 self.create_table_with_state(
14002 build_name,
14003 schema,
14004 TableState::Building {
14005 intended_name: intended_name.to_string(),
14006 query_id: query_id.to_string(),
14007 created_at_unix_nanos: current_unix_nanos(),
14008 replaces_table_id: None,
14009 },
14010 )
14011 }
14012
14013 #[doc(hidden)]
14016 pub fn create_rebuilding_table(
14017 &self,
14018 build_name: &str,
14019 intended_name: &str,
14020 query_id: &str,
14021 schema: Schema,
14022 ) -> Result<u64> {
14023 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14024 || intended_name.is_empty()
14025 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14026 || query_id.is_empty()
14027 {
14028 return Err(MongrelError::InvalidArgument(
14029 "invalid rebuilding-table identity".into(),
14030 ));
14031 }
14032 let replaces_table_id = self
14033 .catalog
14034 .read()
14035 .live(intended_name)
14036 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
14037 .table_id;
14038 self.create_table_with_state(
14039 build_name,
14040 schema,
14041 TableState::Building {
14042 intended_name: intended_name.to_string(),
14043 query_id: query_id.to_string(),
14044 created_at_unix_nanos: current_unix_nanos(),
14045 replaces_table_id: Some(replaces_table_id),
14046 },
14047 )
14048 }
14049
14050 fn create_table_with_state(
14051 &self,
14052 name: &str,
14053 schema: Schema,
14054 state: TableState,
14055 ) -> Result<u64> {
14056 use crate::wal::DdlOp;
14057 use std::sync::atomic::Ordering;
14058
14059 self.require(&crate::auth::Permission::Ddl)?;
14060 if self.poisoned.load(Ordering::Relaxed) {
14061 return Err(MongrelError::Other(
14062 "database poisoned by fsync error".into(),
14063 ));
14064 }
14065 let _operation = self.admit_operation()?;
14067 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14068 let _g = self.ddl_lock.lock();
14069 let _security_write = self.security_write()?;
14070 self.require(&crate::auth::Permission::Ddl)?;
14071 {
14072 let cat = self.catalog.read();
14073 match &state {
14074 TableState::Live => {
14075 if cat.live(name).is_some() || cat.building_for(name).is_some() {
14076 return Err(MongrelError::InvalidArgument(format!(
14077 "table {name:?} already exists or is being built"
14078 )));
14079 }
14080 }
14081 TableState::Building {
14082 intended_name,
14083 replaces_table_id,
14084 ..
14085 } => {
14086 let target_matches = match replaces_table_id {
14087 Some(table_id) => cat
14088 .live(intended_name)
14089 .is_some_and(|entry| entry.table_id == *table_id),
14090 None => cat.live(intended_name).is_none(),
14091 };
14092 if !target_matches || cat.building_for(intended_name).is_some() {
14093 return Err(MongrelError::InvalidArgument(format!(
14094 "table {intended_name:?} changed or is already being built"
14095 )));
14096 }
14097 if cat.building(name).is_some() {
14098 return Err(MongrelError::InvalidArgument(format!(
14099 "building table {name:?} already exists"
14100 )));
14101 }
14102 }
14103 TableState::Dropped { .. } => {
14104 return Err(MongrelError::InvalidArgument(
14105 "cannot create a dropped table".into(),
14106 ));
14107 }
14108 }
14109 }
14110
14111 let commit_lock = Arc::clone(&self.commit_lock);
14116 let _c = commit_lock.lock();
14117 let live_create = matches!(state, TableState::Live);
14118 let legacy_table_id = if live_create {
14119 None
14120 } else {
14121 let mut cat = self.catalog.write();
14122 let id = cat.next_table_id;
14123 cat.next_table_id = id
14124 .checked_add(1)
14125 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
14126 Some(id)
14127 };
14128 let epoch = self.epoch.bump_assigned();
14129 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14130 let txn_id = self.alloc_txn_id()?;
14131
14132 let mut schema = schema;
14133 if let Some(table_id) = legacy_table_id {
14134 schema.schema_id = table_id;
14138 schema.validate_auto_increment()?;
14146 schema.validate_defaults()?;
14147 schema.validate_ai()?;
14148 for index in &schema.indexes {
14149 index.validate_options()?;
14150 }
14151 for constraint in &schema.constraints.checks {
14152 constraint.expr.validate()?;
14153 }
14154 }
14155
14156 let mut next_catalog = self.catalog.read().clone();
14157 let (table_id, schema) = match legacy_table_id {
14158 Some(table_id) => {
14159 next_catalog.tables.push(CatalogEntry {
14160 table_id,
14161 name: name.to_string(),
14162 schema: schema.clone(),
14163 state: state.clone(),
14164 created_epoch: epoch.0,
14165 });
14166 (table_id, schema)
14167 }
14168 None => {
14169 let command = crate::catalog_cmds::CatalogCommand::CreateTable {
14173 name: name.to_string(),
14174 schema,
14175 created_epoch: epoch.0,
14176 };
14177 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
14178 let crate::catalog_cmds::CatalogDelta::TableCreated { entry } = delta else {
14179 return Err(MongrelError::Other(
14180 "CreateTable resolved to an unexpected catalog delta".into(),
14181 ));
14182 };
14183 (entry.table_id, entry.schema)
14184 }
14185 };
14186 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14187
14188 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
14191 let canonical_tdir = self.root.join(&table_relative);
14192 let table_root = Arc::new(
14193 self.durable_root
14194 .create_directory_all_pinned(&table_relative)?,
14195 );
14196 let tdir = table_root.io_path()?;
14197 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
14198 let ctx = SharedCtx {
14199 root_guard: Some(table_root),
14200 epoch: Arc::clone(&self.epoch),
14201 page_cache: Arc::clone(&self.page_cache),
14202 decoded_cache: Arc::clone(&self.decoded_cache),
14203 snapshots: Arc::clone(&self.snapshots),
14204 kek: self.kek.clone(),
14205 commit_lock: Arc::clone(&self.commit_lock),
14206 shared: Some(crate::engine::SharedWalCtx {
14207 wal: Arc::clone(&self.shared_wal),
14208 group: Arc::clone(&self.group),
14209 poisoned: Arc::clone(&self.poisoned),
14210 txn_ids: Arc::clone(&self.next_txn_id),
14211 change_wake: self.change_wake.clone(),
14212 lifecycle: Arc::clone(&self.lifecycle),
14213 hlc: Arc::clone(&self.hlc),
14214 }),
14215 table_name: Some(name.to_string()),
14216 auth: self.table_auth_checker(),
14217 read_only: self.read_only,
14218 };
14219 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
14220
14221 let schema_json = DdlOp::encode_schema(&schema)?;
14224 let ddl = match &state {
14225 TableState::Live => DdlOp::CreateTable {
14226 table_id,
14227 name: name.to_string(),
14228 schema_json,
14229 },
14230 TableState::Building {
14231 intended_name,
14232 query_id,
14233 created_at_unix_nanos,
14234 replaces_table_id,
14235 } => match replaces_table_id {
14236 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
14237 table_id,
14238 build_name: name.to_string(),
14239 intended_name: intended_name.clone(),
14240 query_id: query_id.clone(),
14241 created_at_unix_nanos: *created_at_unix_nanos,
14242 replaces_table_id: *replaces_table_id,
14243 schema_json,
14244 },
14245 None => DdlOp::CreateBuildingTable {
14246 table_id,
14247 build_name: name.to_string(),
14248 intended_name: intended_name.clone(),
14249 query_id: query_id.clone(),
14250 created_at_unix_nanos: *created_at_unix_nanos,
14251 schema_json,
14252 },
14253 },
14254 TableState::Dropped { .. } => {
14255 return Err(MongrelError::InvalidArgument(
14256 "cannot create a table in dropped state".into(),
14257 ));
14258 }
14259 };
14260 let commit_seq = {
14261 let mut wal = self.shared_wal.lock();
14262 let append: Result<u64> = (|| {
14263 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
14264 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14265 wal.append_commit(txn_id, epoch, &[])
14266 })();
14267 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14268 };
14269 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14270 pending_table_dir.disarm();
14271
14272 self.tables
14275 .write()
14276 .insert(table_id, TableHandle::new(table));
14277 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14278 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14279 Ok(table_id)
14280 }
14281
14282 pub fn drop_table(&self, name: &str) -> Result<()> {
14284 self.drop_table_with_epoch(name).map(|_| ())
14285 }
14286
14287 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
14289 self.drop_table_with_state(name, false, None)
14290 }
14291
14292 pub fn drop_table_with_epoch_controlled<F>(
14293 &self,
14294 name: &str,
14295 mut before_commit: F,
14296 ) -> Result<Epoch>
14297 where
14298 F: FnMut() -> Result<()>,
14299 {
14300 self.drop_table_with_state(name, false, Some(&mut before_commit))
14301 }
14302
14303 #[doc(hidden)]
14305 pub fn discard_building_table(&self, name: &str) -> Result<()> {
14306 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
14307 return Err(MongrelError::InvalidArgument(
14308 "not a CTAS building table".into(),
14309 ));
14310 }
14311 self.drop_table_with_state(name, true, None).map(|_| ())
14312 }
14313
14314 fn drop_table_with_state(
14315 &self,
14316 name: &str,
14317 building: bool,
14318 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14319 ) -> Result<Epoch> {
14320 use crate::wal::DdlOp;
14321 use std::sync::atomic::Ordering;
14322
14323 self.require(&crate::auth::Permission::Ddl)?;
14324 if self.poisoned.load(Ordering::Relaxed) {
14325 return Err(MongrelError::Other(
14326 "database poisoned by fsync error".into(),
14327 ));
14328 }
14329 let _operation = self.admit_operation()?;
14331 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14332 let _g = self.ddl_lock.lock();
14333 let _security_write = self.security_write()?;
14334 self.require(&crate::auth::Permission::Ddl)?;
14335 let table_id = {
14336 let cat = self.catalog.read();
14337 if building {
14338 cat.building(name)
14339 } else {
14340 cat.live(name)
14341 }
14342 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
14343 .table_id
14344 };
14345
14346 let commit_lock = Arc::clone(&self.commit_lock);
14347 let _c = commit_lock.lock();
14348 let epoch = self.epoch.bump_assigned();
14349 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14350 let txn_id = self.alloc_txn_id()?;
14351 let mut next_catalog = self.catalog.read().clone();
14352 if building {
14353 let entry = next_catalog
14356 .tables
14357 .iter_mut()
14358 .find(|t| t.table_id == table_id)
14359 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14360 entry.state = TableState::Dropped { at_epoch: epoch.0 };
14361 next_catalog.triggers.retain(|trigger| {
14362 !matches!(
14363 &trigger.trigger.target,
14364 TriggerTarget::Table(target) if target == name
14365 )
14366 });
14367 next_catalog
14368 .materialized_views
14369 .retain(|definition| definition.name != name);
14370 next_catalog
14371 .security
14372 .rls_tables
14373 .retain(|table| table != name);
14374 next_catalog
14375 .security
14376 .policies
14377 .retain(|policy| policy.table != name);
14378 next_catalog
14379 .security
14380 .masks
14381 .retain(|mask| mask.table != name);
14382 for role in &mut next_catalog.roles {
14383 role.permissions
14384 .retain(|permission| permission_table(permission) != Some(name));
14385 }
14386 advance_security_version(&mut next_catalog)?;
14387 } else {
14388 let command = crate::catalog_cmds::CatalogCommand::DropTable {
14392 name: name.to_string(),
14393 at_epoch: epoch.0,
14394 };
14395 self.apply_catalog_command_to(&mut next_catalog, command)?;
14396 }
14397 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14398 let commit_seq = {
14399 let mut wal = self.shared_wal.lock();
14400 if let Some(before_commit) = before_commit {
14401 before_commit()?;
14402 }
14403 let append: Result<u64> = (|| {
14404 wal.append(
14405 txn_id,
14406 table_id,
14407 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
14408 )?;
14409 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14410 wal.append_commit(txn_id, epoch, &[])
14411 })();
14412 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14413 };
14414 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14415
14416 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14417 self.tables.write().remove(&table_id);
14418 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14419 Ok(epoch)
14420 }
14421
14422 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
14431 self.rename_table_with_epoch(name, new_name).map(|_| ())
14432 }
14433
14434 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
14436 self.rename_table_with_epoch_inner(name, new_name, None)
14437 }
14438
14439 pub fn rename_table_with_epoch_controlled<F>(
14440 &self,
14441 name: &str,
14442 new_name: &str,
14443 mut before_commit: F,
14444 ) -> Result<Epoch>
14445 where
14446 F: FnMut() -> Result<()>,
14447 {
14448 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
14449 }
14450
14451 fn rename_table_with_epoch_inner(
14452 &self,
14453 name: &str,
14454 new_name: &str,
14455 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14456 ) -> Result<Epoch> {
14457 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14458 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14459 {
14460 return Err(MongrelError::InvalidArgument(
14461 "the CTAS building-table namespace is reserved".into(),
14462 ));
14463 }
14464 self.rename_table_with_state(name, new_name, false, None, before_commit)
14465 }
14466
14467 #[doc(hidden)]
14469 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14470 self.publish_building_table_inner(build_name, new_name, None)
14471 }
14472
14473 #[doc(hidden)]
14474 pub fn publish_building_table_controlled<F>(
14475 &self,
14476 build_name: &str,
14477 new_name: &str,
14478 mut before_commit: F,
14479 ) -> Result<Epoch>
14480 where
14481 F: FnMut() -> Result<()>,
14482 {
14483 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
14484 }
14485
14486 fn publish_building_table_inner(
14487 &self,
14488 build_name: &str,
14489 new_name: &str,
14490 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14491 ) -> Result<Epoch> {
14492 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14493 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14494 {
14495 return Err(MongrelError::InvalidArgument(
14496 "invalid CTAS publish identity".into(),
14497 ));
14498 }
14499 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
14500 }
14501
14502 #[doc(hidden)]
14504 pub fn publish_materialized_building_table(
14505 &self,
14506 build_name: &str,
14507 new_name: &str,
14508 definition: crate::catalog::MaterializedViewEntry,
14509 ) -> Result<Epoch> {
14510 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
14511 }
14512
14513 #[doc(hidden)]
14514 pub fn publish_materialized_building_table_controlled<F>(
14515 &self,
14516 build_name: &str,
14517 new_name: &str,
14518 definition: crate::catalog::MaterializedViewEntry,
14519 mut before_commit: F,
14520 ) -> Result<Epoch>
14521 where
14522 F: FnMut() -> Result<()>,
14523 {
14524 self.publish_materialized_building_table_inner(
14525 build_name,
14526 new_name,
14527 definition,
14528 Some(&mut before_commit),
14529 )
14530 }
14531
14532 fn publish_materialized_building_table_inner(
14533 &self,
14534 build_name: &str,
14535 new_name: &str,
14536 definition: crate::catalog::MaterializedViewEntry,
14537 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14538 ) -> Result<Epoch> {
14539 if definition.name != new_name || definition.query.trim().is_empty() {
14540 return Err(MongrelError::InvalidArgument(
14541 "invalid materialized-view publication".into(),
14542 ));
14543 }
14544 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
14545 }
14546
14547 #[doc(hidden)]
14549 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14550 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
14551 }
14552
14553 #[doc(hidden)]
14554 pub fn publish_rebuilding_table_controlled<F>(
14555 &self,
14556 build_name: &str,
14557 new_name: &str,
14558 mut before_commit: F,
14559 ) -> Result<Epoch>
14560 where
14561 F: FnMut() -> Result<()>,
14562 {
14563 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
14564 }
14565
14566 #[doc(hidden)]
14568 pub fn publish_materialized_rebuilding_table_controlled<F>(
14569 &self,
14570 build_name: &str,
14571 new_name: &str,
14572 definition: crate::catalog::MaterializedViewEntry,
14573 mut before_commit: F,
14574 ) -> Result<Epoch>
14575 where
14576 F: FnMut() -> Result<()>,
14577 {
14578 self.publish_rebuilding_table_inner(
14579 build_name,
14580 new_name,
14581 Some(definition),
14582 Some(&mut before_commit),
14583 )
14584 }
14585
14586 fn publish_rebuilding_table_inner(
14587 &self,
14588 build_name: &str,
14589 new_name: &str,
14590 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14591 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14592 ) -> Result<Epoch> {
14593 use crate::wal::DdlOp;
14594
14595 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14596 || new_name.is_empty()
14597 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14598 {
14599 return Err(MongrelError::InvalidArgument(
14600 "invalid rebuilding-table publish identity".into(),
14601 ));
14602 }
14603 if materialized_view.as_ref().is_some_and(|definition| {
14604 definition.name != new_name || definition.query.trim().is_empty()
14605 }) {
14606 return Err(MongrelError::InvalidArgument(
14607 "invalid materialized-view replacement".into(),
14608 ));
14609 }
14610 self.require(&crate::auth::Permission::Ddl)?;
14611 if self.poisoned.load(Ordering::Relaxed) {
14612 return Err(MongrelError::Other(
14613 "database poisoned by fsync error".into(),
14614 ));
14615 }
14616 let _operation = self.admit_operation()?;
14618 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14619 let _ddl = self.ddl_lock.lock();
14620 let _security_write = self.security_write()?;
14621 let (table_id, replaced_table_id) = {
14622 let catalog = self.catalog.read();
14623 let build = catalog.building(build_name).ok_or_else(|| {
14624 MongrelError::NotFound(format!("building table {build_name:?} not found"))
14625 })?;
14626 let replaced_table_id = match &build.state {
14627 TableState::Building {
14628 intended_name,
14629 replaces_table_id: Some(replaced_table_id),
14630 ..
14631 } if intended_name == new_name => *replaced_table_id,
14632 _ => {
14633 return Err(MongrelError::InvalidArgument(format!(
14634 "building table {build_name:?} is not a replacement for {new_name:?}"
14635 )))
14636 }
14637 };
14638 if catalog
14639 .live(new_name)
14640 .is_none_or(|entry| entry.table_id != replaced_table_id)
14641 {
14642 return Err(MongrelError::Conflict(format!(
14643 "table {new_name:?} changed while its replacement was built"
14644 )));
14645 }
14646 (build.table_id, replaced_table_id)
14647 };
14648
14649 let _commit = self.commit_lock.lock();
14650 let epoch = self.epoch.assigned().next();
14651 let txn_id = self.alloc_txn_id()?;
14652 let mut next_catalog = self.catalog.read().clone();
14653 apply_rebuilding_publish(
14654 &mut next_catalog,
14655 table_id,
14656 replaced_table_id,
14657 new_name,
14658 epoch.0,
14659 )?;
14660 if let Some(definition) = materialized_view.as_mut() {
14661 definition.last_refresh_epoch = epoch.0;
14662 }
14663 let materialized_view_json = materialized_view
14664 .as_ref()
14665 .map(DdlOp::encode_materialized_view)
14666 .transpose()?;
14667 if let Some(definition) = materialized_view {
14668 if let Some(existing) = next_catalog
14669 .materialized_views
14670 .iter_mut()
14671 .find(|existing| existing.name == definition.name)
14672 {
14673 *existing = definition;
14674 } else {
14675 next_catalog.materialized_views.push(definition);
14676 }
14677 }
14678 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14679 if let Some(before_commit) = before_commit {
14680 before_commit()?;
14681 }
14682 let assigned_epoch = self.epoch.bump_assigned();
14683 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
14684 if assigned_epoch != epoch {
14685 return Err(MongrelError::Conflict(
14686 "commit epoch changed while sequencer lock was held".into(),
14687 ));
14688 }
14689 let commit_seq = {
14690 let mut wal = self.shared_wal.lock();
14691 let append: Result<u64> = (|| {
14692 wal.append(
14693 txn_id,
14694 table_id,
14695 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
14696 table_id,
14697 replaced_table_id,
14698 new_name: new_name.to_string(),
14699 }),
14700 )?;
14701 if let Some(definition_json) = materialized_view_json {
14702 wal.append(
14703 txn_id,
14704 table_id,
14705 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14706 name: new_name.to_string(),
14707 definition_json,
14708 }),
14709 )?;
14710 }
14711 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14712 wal.append_commit(txn_id, epoch, &[])
14713 })();
14714 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14715 };
14716 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14717
14718 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14719 self.tables.write().remove(&replaced_table_id);
14720 if let Some(table) = self.tables.read().get(&table_id) {
14721 table.lock().set_catalog_name(new_name.to_string());
14722 }
14723 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
14724 Ok(epoch)
14725 }
14726
14727 fn rename_table_with_state(
14728 &self,
14729 name: &str,
14730 new_name: &str,
14731 building: bool,
14732 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14733 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14734 ) -> Result<Epoch> {
14735 use crate::wal::DdlOp;
14736 use std::sync::atomic::Ordering;
14737
14738 self.require(&crate::auth::Permission::Ddl)?;
14739 if self.poisoned.load(Ordering::Relaxed) {
14740 return Err(MongrelError::Other(
14741 "database poisoned by fsync error".into(),
14742 ));
14743 }
14744
14745 if name == new_name {
14748 return Ok(self.visible_epoch());
14749 }
14750 if new_name.is_empty() {
14751 return Err(MongrelError::InvalidArgument(
14752 "rename_table: new name must not be empty".into(),
14753 ));
14754 }
14755 let _operation = self.admit_operation()?;
14757 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14758 let _g = self.ddl_lock.lock();
14759 let _security_write = self.security_write()?;
14760 self.require(&crate::auth::Permission::Ddl)?;
14761 let table_id = {
14762 let cat = self.catalog.read();
14763 let src = if building {
14764 cat.building(name)
14765 } else {
14766 cat.live(name)
14767 }
14768 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14769 if building
14770 && !matches!(
14771 &src.state,
14772 TableState::Building { intended_name, .. } if intended_name == new_name
14773 )
14774 {
14775 return Err(MongrelError::InvalidArgument(format!(
14776 "building table {name:?} is not reserved for {new_name:?}"
14777 )));
14778 }
14779 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
14783 return Err(MongrelError::InvalidArgument(format!(
14784 "rename_table: a table named {new_name:?} already exists"
14785 )));
14786 }
14787 src.table_id
14788 };
14789
14790 let commit_lock = Arc::clone(&self.commit_lock);
14791 let _c = commit_lock.lock();
14792 let epoch = self.epoch.bump_assigned();
14793 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14794 let txn_id = self.alloc_txn_id()?;
14795 if let Some(definition) = materialized_view.as_mut() {
14796 definition.last_refresh_epoch = epoch.0;
14797 }
14798 let materialized_view_json = materialized_view
14799 .as_ref()
14800 .map(DdlOp::encode_materialized_view)
14801 .transpose()?;
14802 let mut next_catalog = self.catalog.read().clone();
14803 if building {
14804 let entry = next_catalog
14807 .tables
14808 .iter_mut()
14809 .find(|t| t.table_id == table_id)
14810 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14811 entry.name = new_name.to_string();
14812 entry.state = TableState::Live;
14813 for trigger in &mut next_catalog.triggers {
14814 if matches!(
14815 &trigger.trigger.target,
14816 TriggerTarget::Table(target) if target == name
14817 ) {
14818 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
14819 }
14820 }
14821 if let Some(definition) = next_catalog
14822 .materialized_views
14823 .iter_mut()
14824 .find(|definition| definition.name == name)
14825 {
14826 definition.name = new_name.to_string();
14827 }
14828 if let Some(definition) = materialized_view.take() {
14829 next_catalog.materialized_views.push(definition);
14830 }
14831 for table in &mut next_catalog.security.rls_tables {
14832 if table == name {
14833 *table = new_name.to_string();
14834 }
14835 }
14836 for policy in &mut next_catalog.security.policies {
14837 if policy.table == name {
14838 policy.table = new_name.to_string();
14839 }
14840 }
14841 for mask in &mut next_catalog.security.masks {
14842 if mask.table == name {
14843 mask.table = new_name.to_string();
14844 }
14845 }
14846 for role in &mut next_catalog.roles {
14847 for permission in &mut role.permissions {
14848 rename_permission_table(permission, name, new_name);
14849 }
14850 }
14851 advance_security_version(&mut next_catalog)?;
14852 } else {
14853 let command = crate::catalog_cmds::CatalogCommand::RenameTable {
14857 name: name.to_string(),
14858 new_name: new_name.to_string(),
14859 at_epoch: epoch.0,
14860 };
14861 self.apply_catalog_command_to(&mut next_catalog, command)?;
14862 }
14863 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14864 let ddl = if building {
14865 DdlOp::PublishBuildingTable {
14866 table_id,
14867 new_name: new_name.to_string(),
14868 }
14869 } else {
14870 DdlOp::RenameTable {
14871 table_id,
14872 new_name: new_name.to_string(),
14873 }
14874 };
14875 let commit_seq = {
14876 let mut wal = self.shared_wal.lock();
14877 if let Some(before_commit) = before_commit {
14878 before_commit()?;
14879 }
14880 let append: Result<u64> = (|| {
14881 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
14882 if let Some(definition_json) = materialized_view_json {
14883 wal.append(
14884 txn_id,
14885 table_id,
14886 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14887 name: new_name.to_string(),
14888 definition_json,
14889 }),
14890 )?;
14891 }
14892 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14893 wal.append_commit(txn_id, epoch, &[])
14894 })();
14895 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14896 };
14897 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14898
14899 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14900 if let Some(table) = self.tables.read().get(&table_id) {
14903 table.lock().set_catalog_name(new_name.to_string());
14904 }
14905 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14906 Ok(epoch)
14907 }
14908
14909 pub fn add_column(
14915 &self,
14916 table_name: &str,
14917 name: &str,
14918 ty: TypeId,
14919 flags: ColumnFlags,
14920 default_value: Option<crate::schema::DefaultExpr>,
14921 ) -> Result<ColumnDef> {
14922 self.add_column_with_id(table_name, name, ty, flags, default_value, None)
14923 }
14924
14925 pub fn add_column_with_id(
14927 &self,
14928 table_name: &str,
14929 name: &str,
14930 ty: TypeId,
14931 flags: ColumnFlags,
14932 default_value: Option<crate::schema::DefaultExpr>,
14933 requested_id: Option<u16>,
14934 ) -> Result<ColumnDef> {
14935 use crate::wal::DdlOp;
14936 use std::sync::atomic::Ordering;
14937
14938 self.require(&crate::auth::Permission::Ddl)?;
14939 if self.poisoned.load(Ordering::Relaxed) {
14940 return Err(MongrelError::Other(
14941 "database poisoned by fsync error".into(),
14942 ));
14943 }
14944 let _operation = self.admit_operation()?;
14945 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14946 let _ddl = self.ddl_lock.lock();
14947 let _security_write = self.security_write()?;
14948 self.require(&crate::auth::Permission::Ddl)?;
14949 let table_id = {
14950 let catalog = self.catalog.read();
14951 catalog
14952 .live(table_name)
14953 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
14954 .table_id
14955 };
14956 let handle =
14957 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
14958 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
14959 })?;
14960 let durable_epoch = std::cell::Cell::new(None);
14961 let result: Result<ColumnDef> = (|| {
14962 let mut table = handle.lock();
14963 let (column, prepared_schema) =
14964 table.prepare_add_column(name, ty, flags, default_value, requested_id)?;
14965 let command = crate::catalog_cmds::CatalogCommand::AddColumn {
14966 table: table_name.to_string(),
14967 column: column.clone(),
14968 };
14969 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
14970
14971 let commit_lock = Arc::clone(&self.commit_lock);
14972 let _commit = commit_lock.lock();
14973 let epoch = self.epoch.bump_assigned();
14974 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14975 let txn_id = self.alloc_txn_id()?;
14976 let column_json = DdlOp::encode_column(&column)?;
14977 let mut next_catalog = self.catalog.read().clone();
14978 let catalog_entry_index = next_catalog
14979 .tables
14980 .iter()
14981 .position(|entry| entry.table_id == table_id)
14982 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
14983 self.apply_catalog_command_to(&mut next_catalog, command)?;
14984 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
14985 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14986 let commit_seq = {
14987 let mut wal = self.shared_wal.lock();
14988 let append: Result<u64> = (|| {
14989 wal.append(
14990 txn_id,
14991 table_id,
14992 crate::wal::Op::Ddl(DdlOp::AlterTable {
14993 table_id,
14994 column_json,
14995 }),
14996 )?;
14997 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14998 wal.append_commit(txn_id, epoch, &[])
14999 })();
15000 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15001 };
15002 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15003 durable_epoch.set(Some(epoch));
15004
15005 table.apply_altered_schema_prepared(prepared_schema);
15006 let schema = table.schema().clone();
15007 let table_checkpoint = table.checkpoint_altered_schema();
15008 drop(table);
15009 next_catalog.tables[catalog_entry_index].schema = schema;
15010 let catalog_result =
15011 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15012 *self.catalog.write() = next_catalog;
15013 self.publish_committed(&receipt, epoch)?;
15014 epoch_guard.disarm();
15015 if let Err(error) = table_checkpoint.and(catalog_result) {
15016 self.poisoned.store(true, Ordering::Relaxed);
15017 self.lifecycle.poison();
15018 return Err(MongrelError::DurableCommit {
15019 epoch: epoch.0,
15020 message: error.to_string(),
15021 });
15022 }
15023 Ok(column)
15024 })();
15025 result.map_err(|error| match (durable_epoch.get(), error) {
15026 (_, error @ MongrelError::DurableCommit { .. }) => error,
15027 (Some(epoch), error) => MongrelError::DurableCommit {
15028 epoch: epoch.0,
15029 message: error.to_string(),
15030 },
15031 (None, error) => error,
15032 })
15033 }
15034
15035 pub fn alter_column(
15036 &self,
15037 table_name: &str,
15038 column_name: &str,
15039 change: AlterColumn,
15040 ) -> Result<ColumnDef> {
15041 self.alter_column_with_epoch(table_name, column_name, change)
15042 .map(|(column, _)| column)
15043 }
15044
15045 pub fn alter_column_with_epoch(
15046 &self,
15047 table_name: &str,
15048 column_name: &str,
15049 change: AlterColumn,
15050 ) -> Result<(ColumnDef, Option<Epoch>)> {
15051 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
15052 }
15053
15054 pub fn alter_column_with_epoch_controlled<B, A>(
15059 &self,
15060 table_name: &str,
15061 column_name: &str,
15062 change: AlterColumn,
15063 control: &crate::ExecutionControl,
15064 mut before_commit: B,
15065 mut after_commit: A,
15066 ) -> Result<(ColumnDef, Option<Epoch>)>
15067 where
15068 B: FnMut() -> Result<()>,
15069 A: FnMut(Option<Epoch>) -> Result<()>,
15070 {
15071 self.alter_column_with_epoch_inner(
15072 table_name,
15073 column_name,
15074 change,
15075 Some(control),
15076 Some(&mut before_commit),
15077 Some(&mut after_commit),
15078 )
15079 }
15080
15081 #[allow(clippy::too_many_arguments)]
15082 fn alter_column_with_epoch_inner(
15083 &self,
15084 table_name: &str,
15085 column_name: &str,
15086 change: AlterColumn,
15087 control: Option<&crate::ExecutionControl>,
15088 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
15089 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
15090 ) -> Result<(ColumnDef, Option<Epoch>)> {
15091 use crate::wal::DdlOp;
15092 use std::sync::atomic::Ordering;
15093
15094 self.require(&crate::auth::Permission::Ddl)?;
15095 commit_prepare_checkpoint(control, 0)?;
15096 if self.poisoned.load(Ordering::Relaxed) {
15097 return Err(MongrelError::Other(
15098 "database poisoned by fsync error".into(),
15099 ));
15100 }
15101 let _operation = self.admit_operation()?;
15103 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
15104 let _g = self.ddl_lock.lock();
15105 let table_id = {
15106 let cat = self.catalog.read();
15107 cat.live(table_name)
15108 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15109 .table_id
15110 };
15111 let handle =
15112 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15113 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15114 })?;
15115
15116 let backfill = {
15122 let table = handle.lock();
15123 let old = table
15124 .schema()
15125 .column(column_name)
15126 .cloned()
15127 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
15128 let next_flags = change.flags.unwrap_or(old.flags);
15129 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
15130 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
15131 && old.default_value.is_some()
15132 {
15133 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
15134 let mut updates = Vec::new();
15135 let rows = match control {
15136 Some(control) => table.visible_rows_controlled(snapshot, control)?,
15137 None => table.visible_rows(snapshot)?,
15138 };
15139 for (row_index, row) in rows.into_iter().enumerate() {
15140 commit_prepare_checkpoint(control, row_index)?;
15141 if row
15142 .columns
15143 .get(&old.id)
15144 .is_some_and(|value| !matches!(value, Value::Null))
15145 {
15146 continue;
15147 }
15148 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
15149 table.apply_defaults(&mut cells)?;
15150 updates.push((
15151 table_id,
15152 crate::txn::Staged::Update {
15153 row_id: row.row_id,
15154 new_row: cells,
15155 changed_columns: vec![old.id],
15156 },
15157 ));
15158 }
15159 updates
15160 } else {
15161 Vec::new()
15162 }
15163 };
15164 let durable_epoch = std::cell::Cell::new(None);
15165 let backfill_epoch = if backfill.is_empty() {
15166 None
15167 } else {
15168 let (principal, catalog_bound) = self.transaction_principal_snapshot();
15169 let txn_id = self.alloc_txn_id()?;
15170 let mut entered_fence = false;
15171 let commit_result = match (control, before_commit.as_deref_mut()) {
15172 (Some(control), Some(before_commit)) => self
15173 .commit_transaction_with_external_states_controlled(
15174 txn_id,
15175 self.epoch.visible(),
15176 backfill,
15177 Vec::new(),
15178 Vec::new(),
15179 principal.clone(),
15180 catalog_bound,
15181 None,
15182 crate::txn::TxnCommitContext::internal(),
15183 control,
15184 &mut || {
15185 before_commit()?;
15186 entered_fence = true;
15187 Ok(())
15188 },
15189 )
15190 .map(|(epoch, _)| epoch),
15191 _ => self
15192 .commit_transaction_with_external_states(
15193 txn_id,
15194 self.epoch.visible(),
15195 backfill,
15196 Vec::new(),
15197 Vec::new(),
15198 principal,
15199 catalog_bound,
15200 None,
15201 crate::txn::TxnCommitContext::internal(),
15202 )
15203 .map(|(epoch, _)| epoch),
15204 };
15205 let commit_result = if entered_fence {
15206 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15207 } else {
15208 commit_result
15209 };
15210 match &commit_result {
15211 Ok(epoch) => durable_epoch.set(Some(*epoch)),
15212 Err(MongrelError::DurableCommit { epoch, .. }) => {
15213 durable_epoch.set(Some(Epoch(*epoch)));
15214 }
15215 Err(_) => {}
15216 }
15217 Some(commit_result?)
15218 };
15219 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
15220 let _security_write = self.security_write()?;
15221 self.require(&crate::auth::Permission::Ddl)?;
15222 if self
15223 .catalog
15224 .read()
15225 .live(table_name)
15226 .is_none_or(|entry| entry.table_id != table_id)
15227 {
15228 return Err(MongrelError::Conflict(format!(
15229 "table {table_name:?} changed during ALTER"
15230 )));
15231 }
15232 let mut table = handle.lock();
15233 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
15234 let renamed_column = (column.name != column_name).then(|| column.name.clone());
15235 let Some(prepared_schema) = prepared_schema else {
15236 return Ok((column, backfill_epoch));
15237 };
15238 let command = crate::catalog_cmds::CatalogCommand::AlterColumn {
15243 table: table_name.to_string(),
15244 column: column.clone(),
15245 };
15246 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
15247
15248 let commit_lock = Arc::clone(&self.commit_lock);
15249 let _c = commit_lock.lock();
15250 let epoch = self.epoch.bump_assigned();
15251 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15252 let txn_id = self.alloc_txn_id()?;
15253 let column_json = DdlOp::encode_column(&column)?;
15254 let mut next_catalog = self.catalog.read().clone();
15255 let catalog_entry_index = next_catalog
15256 .tables
15257 .iter()
15258 .position(|entry| entry.table_id == table_id)
15259 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
15260 if let Some(new_column_name) = &renamed_column {
15261 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
15262 commit_prepare_checkpoint(control, trigger_index)?;
15263 if matches!(
15264 &trigger.trigger.target,
15265 TriggerTarget::Table(target) if target == table_name
15266 ) {
15267 trigger.trigger = trigger.trigger.renamed_update_column(
15268 column_name,
15269 new_column_name.clone(),
15270 epoch.0,
15271 )?;
15272 }
15273 }
15274 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
15275 commit_prepare_checkpoint(control, role_index)?;
15276 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
15277 commit_prepare_checkpoint(control, permission_index)?;
15278 rename_permission_column(
15279 permission,
15280 table_name,
15281 column_name,
15282 new_column_name,
15283 );
15284 }
15285 }
15286 advance_security_version(&mut next_catalog)?;
15287 }
15288 self.apply_catalog_command_to(&mut next_catalog, command)?;
15294 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
15295 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15296 commit_prepare_checkpoint(control, 0)?;
15297 let mut entered_fence = false;
15298 if let Some(before_commit) = before_commit.as_deref_mut() {
15299 before_commit()?;
15300 entered_fence = true;
15301 }
15302 let commit_result: Result<Epoch> = (|| {
15303 let commit_seq = {
15304 let mut wal = self.shared_wal.lock();
15305 let append: Result<u64> = (|| {
15306 wal.append(
15307 txn_id,
15308 table_id,
15309 crate::wal::Op::Ddl(DdlOp::AlterTable {
15310 table_id,
15311 column_json,
15312 }),
15313 )?;
15314 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15315 wal.append_commit(txn_id, epoch, &[])
15316 })();
15317 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15318 };
15319 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15320 durable_epoch.set(Some(epoch));
15321
15322 table.apply_altered_schema_prepared(prepared_schema);
15323 let schema = table.schema().clone();
15324 let table_checkpoint = table.checkpoint_altered_schema();
15325 drop(table);
15326 next_catalog.tables[catalog_entry_index].schema = schema;
15327 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15328 let catalog_result =
15329 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15330 let security_version = next_catalog.security_version;
15331 *self.catalog.write() = next_catalog;
15332 if renamed_column.is_some() {
15333 self.security_coordinator
15334 .version
15335 .store(security_version, Ordering::Release);
15336 }
15337 self.publish_committed(&receipt, epoch)?;
15338 _epoch_guard.disarm();
15339 if let Err(error) = table_checkpoint.and(catalog_result) {
15340 self.poisoned.store(true, Ordering::Relaxed);
15341 self.lifecycle.poison();
15342 return Err(MongrelError::DurableCommit {
15343 epoch: epoch.0,
15344 message: error.to_string(),
15345 });
15346 }
15347 Ok(epoch)
15348 })();
15349 let commit_result = if entered_fence {
15350 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15351 } else {
15352 commit_result
15353 };
15354 let epoch = commit_result?;
15355 Ok((column, Some(epoch)))
15356 })();
15357 result.map_err(|error| match (durable_epoch.get(), error) {
15358 (_, error @ MongrelError::DurableCommit { .. }) => error,
15359 (Some(epoch), error) => MongrelError::DurableCommit {
15360 epoch: epoch.0,
15361 message: error.to_string(),
15362 },
15363 (None, error) => error,
15364 })
15365 }
15366
15367 pub fn set_table_ttl(
15370 &self,
15371 table_name: &str,
15372 column_name: &str,
15373 duration_nanos: u64,
15374 ) -> Result<crate::manifest::TtlPolicy> {
15375 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
15376 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
15377 }
15378
15379 #[doc(hidden)]
15381 pub fn set_building_table_ttl(
15382 &self,
15383 table_name: &str,
15384 column_name: &str,
15385 duration_nanos: u64,
15386 ) -> Result<crate::manifest::TtlPolicy> {
15387 let policy = self.replace_table_ttl_with_state(
15388 table_name,
15389 Some((column_name, duration_nanos)),
15390 true,
15391 )?;
15392 policy
15393 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
15394 }
15395
15396 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
15397 self.replace_table_ttl(table_name, None)?;
15398 Ok(())
15399 }
15400
15401 fn replace_table_ttl(
15402 &self,
15403 table_name: &str,
15404 requested: Option<(&str, u64)>,
15405 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15406 self.replace_table_ttl_with_state(table_name, requested, false)
15407 }
15408
15409 fn replace_table_ttl_with_state(
15410 &self,
15411 table_name: &str,
15412 requested: Option<(&str, u64)>,
15413 building: bool,
15414 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15415 use crate::wal::DdlOp;
15416 use std::sync::atomic::Ordering;
15417
15418 self.require(&crate::auth::Permission::Ddl)?;
15419 if self.poisoned.load(Ordering::Relaxed) {
15420 return Err(MongrelError::Other(
15421 "database poisoned by fsync error".into(),
15422 ));
15423 }
15424
15425 let _g = self.ddl_lock.lock();
15426 let _security_write = self.security_write()?;
15427 self.require(&crate::auth::Permission::Ddl)?;
15428 let table_id = {
15429 let cat = self.catalog.read();
15430 if building {
15431 cat.building(table_name)
15432 } else {
15433 cat.live(table_name)
15434 }
15435 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15436 .table_id
15437 };
15438 let handle =
15439 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15440 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15441 })?;
15442 let mut table = handle.lock();
15443 let policy = match requested {
15444 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
15445 None => None,
15446 };
15447 if table.ttl() == policy {
15448 return Ok(policy);
15449 }
15450
15451 let commit_lock = Arc::clone(&self.commit_lock);
15452 let _c = commit_lock.lock();
15453 let epoch = self.epoch.bump_assigned();
15454 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15455 let txn_id = self.alloc_txn_id()?;
15456 let policy_json = DdlOp::encode_ttl(policy)?;
15457 let mut next_catalog = self.catalog.read().clone();
15458 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15459 let commit_seq = {
15460 let mut wal = self.shared_wal.lock();
15461 let append: Result<u64> = (|| {
15462 wal.append(
15463 txn_id,
15464 table_id,
15465 crate::wal::Op::Ddl(DdlOp::SetTtl {
15466 table_id,
15467 policy_json,
15468 }),
15469 )?;
15470 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15471 wal.append_commit(txn_id, epoch, &[])
15472 })();
15473 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15474 };
15475 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15476
15477 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
15478 drop(table);
15479 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
15480 publish_error.get_or_insert(error);
15481 }
15482 self.finish_durable_publish(
15483 epoch,
15484 &mut _epoch_guard,
15485 &receipt,
15486 publish_error.map_or(Ok(()), Err),
15487 )?;
15488 Ok(policy)
15489 }
15490
15491 pub fn gc(&self) -> Result<usize> {
15497 let control = crate::ExecutionControl::new(None);
15498 self.gc_controlled(&control, || true)
15499 }
15500
15501 #[doc(hidden)]
15504 pub fn gc_controlled<F>(
15505 &self,
15506 control: &crate::ExecutionControl,
15507 before_publish: F,
15508 ) -> Result<usize>
15509 where
15510 F: FnOnce() -> bool,
15511 {
15512 self.gc_controlled_with_receipt(control, before_publish)
15513 .map(|(reclaimed, _)| reclaimed)
15514 }
15515
15516 #[doc(hidden)]
15519 pub fn gc_controlled_with_receipt<F>(
15520 &self,
15521 control: &crate::ExecutionControl,
15522 before_publish: F,
15523 ) -> Result<(usize, Option<MaintenanceReceipt>)>
15524 where
15525 F: FnOnce() -> bool,
15526 {
15527 enum Candidate {
15528 Directory(PathBuf),
15529 File(PathBuf),
15530 }
15531
15532 self.require(&crate::auth::Permission::Ddl)?;
15533 let _operation = self.admit_operation()?;
15535 let _ddl = self.ddl_lock.lock();
15536 self.require(&crate::auth::Permission::Ddl)?;
15537 control.checkpoint()?;
15538 let maintenance_epoch = self.epoch.visible();
15539 let min_active = self.snapshots.min_active(maintenance_epoch).0;
15540 let mut candidates = Vec::new();
15541
15542 let cat = self.catalog.read();
15544 for (entry_index, entry) in cat.tables.iter().enumerate() {
15545 if entry_index % 256 == 0 {
15546 control.checkpoint()?;
15547 }
15548 if let TableState::Dropped { at_epoch } = entry.state {
15549 if at_epoch <= min_active {
15550 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
15551 if tdir.exists() {
15552 candidates.push(Candidate::Directory(tdir));
15553 }
15554 }
15555 }
15556 }
15557 drop(cat);
15558
15559 let cat = self.catalog.read();
15564 for (entry_index, entry) in cat.tables.iter().enumerate() {
15565 if entry_index % 256 == 0 {
15566 control.checkpoint()?;
15567 }
15568 if !matches!(entry.state, TableState::Live) {
15569 continue;
15570 }
15571 let txn_dir = self
15572 .root
15573 .join(TABLES_DIR)
15574 .join(entry.table_id.to_string())
15575 .join("_txn");
15576 if !txn_dir.exists() {
15577 continue;
15578 }
15579 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
15580 if sub_index % 256 == 0 {
15581 control.checkpoint()?;
15582 }
15583 let sub = sub?;
15584 let name = sub.file_name();
15585 let Some(name) = name.to_str() else { continue };
15586 let is_active = name
15588 .parse::<u64>()
15589 .map(|id| self.active_spills.is_active(id))
15590 .unwrap_or(false);
15591 if is_active {
15592 continue;
15593 }
15594 candidates.push(Candidate::Directory(sub.path()));
15595 }
15596 }
15597 drop(cat);
15598
15599 let external_names = {
15600 let cat = self.catalog.read();
15601 cat.external_tables
15602 .iter()
15603 .map(|entry| entry.name.clone())
15604 .collect::<std::collections::HashSet<_>>()
15605 };
15606 let vtab_dir = self.root.join(VTAB_DIR);
15607 if vtab_dir.exists() {
15608 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
15609 if entry_index % 256 == 0 {
15610 control.checkpoint()?;
15611 }
15612 let entry = entry?;
15613 let name = entry.file_name();
15614 let Some(name) = name.to_str() else { continue };
15615 if external_names.contains(name) {
15616 continue;
15617 }
15618 let path = entry.path();
15619 if path.is_dir() {
15620 candidates.push(Candidate::Directory(path));
15621 } else {
15622 candidates.push(Candidate::File(path));
15623 }
15624 }
15625 }
15626
15627 let tables = self
15631 .tables
15632 .read()
15633 .iter()
15634 .map(|(table_id, handle)| (*table_id, handle.clone()))
15635 .collect::<Vec<_>>();
15636 let mut retiring = Vec::new();
15637 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
15638 if table_index % 256 == 0 {
15639 control.checkpoint()?;
15640 }
15641 let backup_pinned: HashSet<u128> = self
15642 .backup_pins
15643 .lock()
15644 .keys()
15645 .filter_map(|(pinned_table, run_id)| {
15646 (*pinned_table == *table_id).then_some(*run_id)
15647 })
15648 .collect();
15649 if handle
15650 .lock()
15651 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
15652 {
15653 retiring.push((handle.clone(), backup_pinned));
15654 }
15655 }
15656
15657 let all_durable = self.active_spills.is_idle()
15665 && tables.iter().all(|(_, handle)| {
15666 let g = handle.lock();
15667 g.memtable_len() == 0 && g.mutable_run_len() == 0
15668 });
15669 let retain = self
15670 .replication_wal_retention_segments
15671 .load(std::sync::atomic::Ordering::Relaxed);
15672 let reap_wal = all_durable
15673 && self
15674 .shared_wal
15675 .lock()
15676 .has_gc_segments_retain_recent(retain)?;
15677
15678 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
15679 return Ok((0, None));
15680 }
15681 control.checkpoint()?;
15682 if !before_publish() {
15683 return Err(MongrelError::Cancelled);
15684 }
15685
15686 let mut reclaimed = 0;
15687 for candidate in candidates {
15688 match candidate {
15689 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
15690 Candidate::File(path) => std::fs::remove_file(path)?,
15691 }
15692 reclaimed += 1;
15693 }
15694 for (handle, backup_pinned) in retiring {
15695 reclaimed += handle
15696 .lock()
15697 .reap_retiring(Epoch(min_active), &backup_pinned)?;
15698 }
15699 if reap_wal {
15700 reclaimed += self
15701 .shared_wal
15702 .lock()
15703 .gc_segments_retain_recent(u64::MAX, retain)?;
15704 }
15705
15706 Ok((
15707 reclaimed,
15708 Some(MaintenanceReceipt {
15709 epoch: maintenance_epoch,
15710 }),
15711 ))
15712 }
15713
15714 pub fn checkpoint(&self) -> Result<()> {
15732 self.checkpoint_controlled(|| Ok(()))
15733 }
15734
15735 #[doc(hidden)]
15738 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
15739 where
15740 F: FnOnce() -> Result<()>,
15741 {
15742 self.require(&crate::auth::Permission::Ddl)?;
15743 let _operation = self.admit_operation()?;
15745 let _replication = self.replication_barrier.write();
15749 let _ddl = self.ddl_lock.lock();
15750 let _security = self.security_coordinator.gate.read();
15751 self.require(&crate::auth::Permission::Ddl)?;
15752
15753 let mut handles = self
15754 .tables
15755 .read()
15756 .iter()
15757 .map(|(table_id, handle)| (*table_id, handle.clone()))
15758 .collect::<Vec<_>>();
15759 handles.sort_by_key(|(table_id, _)| *table_id);
15760 let mut tables = handles
15761 .iter()
15762 .map(|(table_id, handle)| (*table_id, handle.lock()))
15763 .collect::<Vec<_>>();
15764
15765 for (_, table) in &mut tables {
15767 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
15768 {
15769 table.force_flush()?;
15770 }
15771 }
15772
15773 for (_, table) in &mut tables {
15776 if table.run_count() >= 2 || table.should_compact() {
15777 table.compact()?;
15778 }
15779 }
15780
15781 before_wal_reset()?;
15782
15783 let maintenance_epoch = self.epoch.visible();
15785 let min_active = self.snapshots.min_active(maintenance_epoch);
15786 for (table_id, table) in &mut tables {
15787 let backup_pinned: HashSet<u128> = self
15788 .backup_pins
15789 .lock()
15790 .keys()
15791 .filter_map(|(pinned_table, run_id)| {
15792 (*pinned_table == *table_id).then_some(*run_id)
15793 })
15794 .collect();
15795 table.reap_retiring(min_active, &backup_pinned)?;
15796 }
15797
15798 self.shared_wal.lock().reset_after_checkpoint()?;
15801
15802 let catalog_snapshot = self.catalog.read().clone();
15804 for entry in &catalog_snapshot.tables {
15805 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
15806 crate::durable_file::remove_directory_all(
15807 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
15808 )?;
15809 }
15810 if !matches!(entry.state, TableState::Live) {
15811 continue;
15812 }
15813 let transaction_dir = self
15814 .root
15815 .join(TABLES_DIR)
15816 .join(entry.table_id.to_string())
15817 .join("_txn");
15818 if transaction_dir.is_dir() {
15819 for child in std::fs::read_dir(&transaction_dir)? {
15820 let child = child?;
15821 let active = child
15822 .file_name()
15823 .to_str()
15824 .and_then(|name| name.parse::<u64>().ok())
15825 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
15826 if !active {
15827 crate::durable_file::remove_directory_all(&child.path())?;
15828 }
15829 }
15830 }
15831 }
15832 let external_names = catalog_snapshot
15833 .external_tables
15834 .iter()
15835 .map(|entry| entry.name.as_str())
15836 .collect::<HashSet<_>>();
15837 let external_root = self.root.join(VTAB_DIR);
15838 if external_root.is_dir() {
15839 for entry in std::fs::read_dir(&external_root)? {
15840 let entry = entry?;
15841 let name = entry.file_name();
15842 if name
15843 .to_str()
15844 .is_some_and(|name| external_names.contains(name))
15845 {
15846 continue;
15847 }
15848 if entry.file_type()?.is_dir() {
15849 crate::durable_file::remove_directory_all(&entry.path())?;
15850 } else {
15851 std::fs::remove_file(entry.path())?;
15852 crate::durable_file::sync_directory(&external_root)?;
15853 }
15854 }
15855 }
15856
15857 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
15860 let visible = self.epoch.visible();
15861 for (_, table) in &tables {
15862 table.persist_manifest(visible)?;
15863 }
15864
15865 Ok(())
15866 }
15867 fn alloc_txn_id(&self) -> Result<u64> {
15868 self.ensure_owner_process()?;
15869 crate::txn::allocate_txn_id(&self.next_txn_id)
15870 }
15871
15872 pub fn allocate_lock_txn_id(&self) -> Result<u64> {
15876 self.alloc_txn_id()
15877 }
15878
15879 pub fn set_spill_threshold(&self, bytes: u64) {
15883 self.spill_threshold
15884 .store(bytes, std::sync::atomic::Ordering::Relaxed);
15885 }
15886
15887 #[doc(hidden)]
15891 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15892 *self.spill_hook.lock() = Some(Box::new(f));
15893 }
15894
15895 #[doc(hidden)]
15898 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15899 *self.security_commit_hook.lock() = Some(Box::new(f));
15900 }
15901
15902 #[doc(hidden)]
15905 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15906 *self.catalog_commit_hook.lock() = Some(Box::new(f));
15907 }
15908
15909 #[doc(hidden)]
15912 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15913 *self.backup_hook.lock() = Some(Box::new(f));
15914 }
15915
15916 #[doc(hidden)]
15920 pub fn __set_fk_lock_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15921 *self.fk_lock_hook.lock() = Some(Arc::new(f));
15922 }
15923
15924 #[doc(hidden)]
15926 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15927 *self.replication_hook.lock() = Some(Box::new(f));
15928 }
15929
15930 #[doc(hidden)]
15934 pub fn __wal_group_sync_count(&self) -> u64 {
15935 self.shared_wal.lock().group_sync_count()
15936 }
15937
15938 #[doc(hidden)]
15941 pub fn __poison(&self) {
15942 self.poisoned
15943 .store(true, std::sync::atomic::Ordering::Relaxed);
15944 }
15945
15946 pub fn check(&self) -> Vec<CheckIssue> {
15959 match self.check_inner(None) {
15960 Ok(issues) => issues,
15961 Err(error) => vec![CheckIssue {
15962 table_id: WAL_TABLE_ID,
15963 table_name: "shared WAL".into(),
15964 severity: "error".into(),
15965 description: error.to_string(),
15966 }],
15967 }
15968 }
15969
15970 #[doc(hidden)]
15972 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
15973 self.check_inner(Some(control))
15974 }
15975
15976 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
15977 let mut issues = Vec::new();
15978 let io_root = self.durable_root.io_path()?;
15979 let cat = self.catalog.read();
15980 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
15981 for (table_index, entry) in cat.tables.iter().enumerate() {
15982 if table_index % 256 == 0 {
15983 if let Some(control) = control {
15984 control.checkpoint()?;
15985 }
15986 }
15987 if !matches!(entry.state, TableState::Live) {
15988 continue;
15989 }
15990 let tdir = io_root.join(TABLES_DIR).join(entry.table_id.to_string());
15991 let mut err = |sev: &str, desc: String| {
15992 issues.push(CheckIssue {
15993 table_id: entry.table_id,
15994 table_name: entry.name.clone(),
15995 severity: sev.into(),
15996 description: desc,
15997 });
15998 };
15999 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
16000 Ok(m) => m,
16001 Err(e) => {
16002 err("error", format!("manifest read failed: {e}"));
16003 continue;
16004 }
16005 };
16006 if m.flushed_epoch > m.current_epoch {
16007 err(
16008 "error",
16009 format!(
16010 "flushed_epoch {} exceeds current_epoch {} (impossible)",
16011 m.flushed_epoch, m.current_epoch
16012 ),
16013 );
16014 }
16015
16016 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
16017 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
16018 for (run_index, rr) in m.runs.iter().enumerate() {
16019 if run_index % 256 == 0 {
16020 if let Some(control) = control {
16021 control.checkpoint()?;
16022 }
16023 }
16024 referenced.insert(rr.run_id);
16025 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
16026 if !run_path.exists() {
16027 err("error", format!("missing run file: r-{}.sr", rr.run_id));
16028 continue;
16029 }
16030 match crate::sorted_run::RunReader::open(
16031 &run_path,
16032 entry.schema.clone(),
16033 self.kek.clone(),
16034 ) {
16035 Ok(reader) => {
16036 if reader.row_count() as u64 != rr.row_count {
16037 err(
16038 "error",
16039 format!(
16040 "run r-{} row count mismatch: manifest {} vs run {}",
16041 rr.run_id,
16042 rr.row_count,
16043 reader.row_count()
16044 ),
16045 );
16046 }
16047 }
16048 Err(e) => {
16049 err(
16050 "error",
16051 format!("run r-{} integrity check failed: {e}", rr.run_id),
16052 );
16053 }
16054 }
16055 }
16056
16057 for r in &m.retiring {
16061 referenced.insert(r.run_id);
16062 }
16063
16064 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
16066 for (entry_index, ent) in rd.flatten().enumerate() {
16067 if entry_index % 256 == 0 {
16068 if let Some(control) = control {
16069 control.checkpoint()?;
16070 }
16071 }
16072 let p = ent.path();
16073 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
16074 continue;
16075 }
16076 let run_id = p
16077 .file_stem()
16078 .and_then(|s| s.to_str())
16079 .and_then(|s| s.strip_prefix("r-"))
16080 .and_then(|s| s.parse::<u128>().ok());
16081 if let Some(id) = run_id {
16082 if !referenced.contains(&id) {
16083 err(
16084 "warning",
16085 format!("orphan run file r-{id}.sr not referenced by the manifest"),
16086 );
16087 }
16088 }
16089 }
16090 }
16091 }
16092
16093 let external_names = cat
16094 .external_tables
16095 .iter()
16096 .map(|entry| entry.name.clone())
16097 .collect::<std::collections::HashSet<_>>();
16098 let vtab_dir = io_root.join(VTAB_DIR);
16099 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
16100 for (entry_index, entry) in entries.flatten().enumerate() {
16101 if entry_index % 256 == 0 {
16102 if let Some(control) = control {
16103 control.checkpoint()?;
16104 }
16105 }
16106 let name = entry.file_name();
16107 let Some(name) = name.to_str() else { continue };
16108 if !external_names.contains(name) {
16109 issues.push(CheckIssue {
16110 table_id: EXTERNAL_TABLE_ID,
16111 table_name: name.to_string(),
16112 severity: "warning".into(),
16113 description: format!(
16114 "orphan external table state entry {:?} not referenced by the catalog",
16115 entry.path()
16116 ),
16117 });
16118 }
16119 }
16120 }
16121
16122 if let Some(control) = control {
16129 control.checkpoint()?;
16130 }
16131 for (seg, msg) in self.shared_wal.lock().verify_segments() {
16132 issues.push(CheckIssue {
16133 table_id: WAL_TABLE_ID,
16134 table_name: "<wal>".into(),
16135 severity: "error".into(),
16136 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
16137 });
16138 }
16139 Ok(issues)
16140 }
16141
16142 pub fn doctor(&self) -> Result<Vec<u64>> {
16146 let control = crate::ExecutionControl::new(None);
16147 self.doctor_controlled(&control, || true)
16148 }
16149
16150 #[doc(hidden)]
16154 pub fn doctor_controlled<F>(
16155 &self,
16156 control: &crate::ExecutionControl,
16157 before_publish: F,
16158 ) -> Result<Vec<u64>>
16159 where
16160 F: FnOnce() -> bool,
16161 {
16162 self.doctor_controlled_with_receipt(control, before_publish)
16163 .map(|(quarantined, _)| quarantined)
16164 }
16165
16166 #[doc(hidden)]
16169 pub fn doctor_controlled_with_receipt<F>(
16170 &self,
16171 control: &crate::ExecutionControl,
16172 before_publish: F,
16173 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
16174 where
16175 F: FnOnce() -> bool,
16176 {
16177 let _ddl = self.ddl_lock.lock();
16180 let _security_write = self.security_write()?;
16181 let issues = self.check_inner(Some(control))?;
16182 let bad_tables: std::collections::HashSet<u64> = issues
16187 .iter()
16188 .filter(|i| {
16189 i.severity == "error"
16190 && i.table_id != WAL_TABLE_ID
16191 && i.table_id != EXTERNAL_TABLE_ID
16192 })
16193 .map(|i| i.table_id)
16194 .collect();
16195 if bad_tables.is_empty() {
16196 return Ok((Vec::new(), None));
16197 }
16198 let _commit = self.commit_lock.lock();
16199 control.checkpoint()?;
16200 if !before_publish() {
16201 return Err(MongrelError::Cancelled);
16202 }
16203 let maintenance_epoch = self.epoch.bump_assigned();
16204 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
16205
16206 let qdir = self.root.join("_quarantine");
16207 crate::durable_file::create_directory(&qdir)?;
16208 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
16209 bad_tables.sort_unstable();
16210
16211 let mut handles = self
16215 .tables
16216 .read()
16217 .iter()
16218 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
16219 .map(|(table_id, handle)| (*table_id, handle.clone()))
16220 .collect::<Vec<_>>();
16221 handles.sort_by_key(|(table_id, _)| *table_id);
16222 let mut table_guards = handles
16223 .iter()
16224 .map(|(table_id, handle)| (*table_id, handle.lock()))
16225 .collect::<Vec<_>>();
16226
16227 let mut next_catalog = self.catalog.read().clone();
16228 for table_id in &bad_tables {
16229 if let Some(entry) = next_catalog
16230 .tables
16231 .iter_mut()
16232 .find(|entry| entry.table_id == *table_id)
16233 {
16234 entry.state = TableState::Dropped {
16235 at_epoch: maintenance_epoch.0,
16236 };
16237 }
16238 }
16239 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
16240
16241 let txn_id = self.alloc_txn_id()?;
16242 let commit_seq = {
16243 let mut wal = self.shared_wal.lock();
16244 let append: Result<u64> = (|| {
16245 for table_id in &bad_tables {
16246 wal.append(
16247 txn_id,
16248 *table_id,
16249 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
16250 table_id: *table_id,
16251 }),
16252 )?;
16253 }
16254 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
16255 wal.append_commit(txn_id, maintenance_epoch, &[])
16256 })();
16257 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
16258 };
16259 let receipt = self.await_durable_commit(txn_id, commit_seq, maintenance_epoch)?;
16260 for (_, table) in &mut table_guards {
16261 table.mark_unavailable_after_quarantine();
16262 }
16263 {
16264 let mut live_tables = self.tables.write();
16265 for table_id in &bad_tables {
16266 live_tables.remove(table_id);
16267 }
16268 }
16269 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
16270 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, &receipt, checkpoint)?;
16271
16272 drop(table_guards);
16276 drop(handles);
16277
16278 for table_id in &bad_tables {
16282 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
16283 if source.exists() {
16284 let destination = qdir.join(table_id.to_string());
16285 if let Err(error) = crate::durable_file::rename(&source, &destination) {
16286 return Err(MongrelError::DurableCommit {
16287 epoch: maintenance_epoch.0,
16288 message: format!(
16289 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
16290 ),
16291 });
16292 }
16293 }
16294 }
16295 Ok((
16296 bad_tables,
16297 Some(MaintenanceReceipt {
16298 epoch: maintenance_epoch,
16299 }),
16300 ))
16301 }
16302
16303 #[allow(dead_code)]
16305 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
16306 self.kek.as_ref()
16307 }
16308
16309 #[allow(dead_code)]
16311 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
16312 &self.epoch
16313 }
16314
16315 #[allow(dead_code)]
16317 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
16318 &self.snapshots
16319 }
16320}
16321
16322fn external_state_dir(root: &Path, name: &str) -> PathBuf {
16323 root.join(VTAB_DIR).join(name)
16324}
16325
16326fn append_catalog_snapshot(
16327 wal: &mut crate::wal::SharedWal,
16328 txn_id: u64,
16329 catalog: &Catalog,
16330) -> Result<()> {
16331 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
16332 wal.append(
16333 txn_id,
16334 WAL_TABLE_ID,
16335 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
16336 )?;
16337 Ok(())
16338}
16339
16340fn filter_ignored_staging(
16341 staging: Vec<(u64, crate::txn::Staged)>,
16342 ignored_indices: &std::collections::BTreeSet<usize>,
16343) -> Vec<(u64, crate::txn::Staged)> {
16344 if ignored_indices.is_empty() {
16345 return staging;
16346 }
16347 staging
16348 .into_iter()
16349 .enumerate()
16350 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
16351 .collect()
16352}
16353
16354fn external_state_file(root: &Path, name: &str) -> PathBuf {
16355 external_state_dir(root, name).join("state.json")
16356}
16357
16358fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
16359 let path = external_state_file(root, name);
16360 match std::fs::read(path) {
16361 Ok(bytes) => Ok(bytes),
16362 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
16363 Err(e) => Err(e.into()),
16364 }
16365}
16366
16367fn current_external_state_bytes(
16368 root: &Path,
16369 external_states: &[(String, Vec<u8>)],
16370 name: &str,
16371) -> Result<Vec<u8>> {
16372 for (table, state) in external_states.iter().rev() {
16373 if table == name {
16374 return Ok(state.clone());
16375 }
16376 }
16377 read_external_state_file(root, name)
16378}
16379
16380fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
16381 let mut out = external_states;
16382 dedup_external_states_in_place(&mut out);
16383 out
16384}
16385
16386fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
16387 let mut seen = std::collections::HashSet::new();
16388 let mut out = Vec::with_capacity(external_states.len());
16389 for (name, state) in std::mem::take(external_states).into_iter().rev() {
16390 if seen.insert(name.clone()) {
16391 out.push((name, state));
16392 }
16393 }
16394 out.reverse();
16395 *external_states = out;
16396}
16397
16398fn prepare_external_state_file(
16399 root: &Path,
16400 name: &str,
16401 state: &[u8],
16402 txn_id: u64,
16403) -> Result<PathBuf> {
16404 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
16405 let dir = external_state_dir(root, name);
16406 crate::durable_file::create_directory(&dir)?;
16407 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
16408 {
16409 let mut file = std::fs::OpenOptions::new()
16410 .create_new(true)
16411 .write(true)
16412 .open(&pending)?;
16413 file.write_all(state)?;
16414 file.sync_all()?;
16415 }
16416 Ok(pending)
16417}
16418
16419fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
16420 let path = external_state_file(root, name);
16421 crate::durable_file::replace(pending, &path)?;
16422 Ok(())
16423}
16424
16425fn write_external_state_file(
16426 durable: &crate::durable_file::DurableRoot,
16427 name: &str,
16428 state: &[u8],
16429) -> Result<()> {
16430 let directory = Path::new(VTAB_DIR).join(name);
16431 durable.create_directory_all(&directory)?;
16432 durable.write_atomic(directory.join("state.json"), state)?;
16433 Ok(())
16434}
16435
16436fn validate_recovered_data_table(
16437 catalog: &Catalog,
16438 tables: &HashMap<u64, TableHandle>,
16439 table_id: u64,
16440 commit_epoch: u64,
16441 offset: u64,
16442) -> Result<bool> {
16443 let entry = catalog
16444 .tables
16445 .iter()
16446 .find(|entry| entry.table_id == table_id)
16447 .ok_or_else(|| MongrelError::CorruptWal {
16448 offset,
16449 reason: format!("committed record references unknown table {table_id}"),
16450 })?;
16451 if commit_epoch < entry.created_epoch {
16452 return Err(MongrelError::CorruptWal {
16453 offset,
16454 reason: format!(
16455 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16456 entry.created_epoch
16457 ),
16458 });
16459 }
16460 match entry.state {
16461 TableState::Dropped { at_epoch } => {
16462 let abandoned_build_boundary =
16467 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16468 if commit_epoch >= at_epoch && !abandoned_build_boundary {
16469 Err(MongrelError::CorruptWal {
16470 offset,
16471 reason: format!(
16472 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16473 ),
16474 })
16475 } else {
16476 Ok(false)
16477 }
16478 }
16479 TableState::Live | TableState::Building { .. } => {
16480 if tables.contains_key(&table_id) {
16481 Ok(true)
16482 } else {
16483 Err(MongrelError::CorruptWal {
16484 offset,
16485 reason: format!("live table {table_id} has no mounted recovery handle"),
16486 })
16487 }
16488 }
16489 }
16490}
16491
16492type RecoveryTableStage = (
16493 Vec<crate::memtable::Row>,
16494 Vec<(crate::rowid::RowId, Epoch)>,
16495 Option<Epoch>,
16496 Epoch,
16497);
16498
16499#[derive(Clone)]
16500struct RecoveryValidationTable {
16501 schema: Schema,
16502 flushed_epoch: u64,
16503}
16504
16505fn validate_shared_wal_recovery_plan(
16506 durable_root: &crate::durable_file::DurableRoot,
16507 catalog: &Catalog,
16508 recovered_table_ids: &HashSet<u64>,
16509 reconciled_table_ids: &HashSet<u64>,
16510 meta_dek: Option<&[u8; META_DEK_LEN]>,
16511 kek: Option<Arc<crate::encryption::Kek>>,
16512 records: &[crate::wal::Record],
16513) -> Result<()> {
16514 use crate::wal::{DdlOp, Op};
16515
16516 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
16517 for entry in &catalog.tables {
16518 if !matches!(entry.state, TableState::Live) {
16519 continue;
16520 }
16521 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
16522 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
16523 Ok(manifest) => Some(manifest),
16524 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
16525 Err(error) => return Err(error),
16526 };
16527 let flushed_epoch = if let Some(manifest) = manifest {
16528 if manifest.table_id != entry.table_id {
16529 return Err(MongrelError::Conflict(format!(
16530 "catalog table {} storage identity mismatch",
16531 entry.table_id
16532 )));
16533 }
16534 if (manifest.schema_id != entry.schema.schema_id
16535 && !reconciled_table_ids.contains(&entry.table_id))
16536 || manifest.flushed_epoch > manifest.current_epoch
16537 || manifest.global_idx_epoch > manifest.current_epoch
16538 || manifest.next_row_id == u64::MAX
16539 || manifest.auto_inc_next < 0
16540 || manifest.auto_inc_next == i64::MAX
16541 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
16542 {
16543 return Err(MongrelError::InvalidArgument(format!(
16544 "table {} manifest counters or schema identity are invalid",
16545 entry.table_id
16546 )));
16547 }
16548 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
16549 crate::global_idx::read_durable_for(
16550 durable_root,
16551 &relative_dir,
16552 entry.table_id,
16553 &entry.schema,
16554 idx_dek.as_deref(),
16555 )?;
16556 let mut run_ids = HashSet::new();
16557 let mut maximum_row_id = None::<u64>;
16558 for run in &manifest.runs {
16559 if run.run_id >= u64::MAX as u128
16560 || run.epoch_created > manifest.current_epoch
16561 || !run_ids.insert(run.run_id)
16562 {
16563 return Err(MongrelError::InvalidArgument(format!(
16564 "table {} manifest contains an invalid or duplicate run id",
16565 entry.table_id
16566 )));
16567 }
16568 let relative = relative_dir
16569 .join(crate::engine::RUNS_DIR)
16570 .join(format!("r-{}.sr", run.run_id as u64));
16571 let file = durable_root.open_regular(&relative)?;
16572 let mut reader = crate::sorted_run::RunReader::open_file(
16573 file,
16574 entry.schema.clone(),
16575 kek.clone(),
16576 )?;
16577 let header = reader.header();
16578 if header.run_id != run.run_id
16579 || header.level != run.level
16580 || header.row_count != run.row_count
16581 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
16582 || header.is_uniform_epoch() && header.epoch_created != 0
16583 || header.schema_id > entry.schema.schema_id
16584 {
16585 return Err(MongrelError::InvalidArgument(format!(
16586 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
16587 entry.table_id,
16588 run.run_id,
16589 header.run_id,
16590 header.level,
16591 header.row_count,
16592 header.epoch_created,
16593 header.schema_id,
16594 run.run_id,
16595 run.level,
16596 run.row_count,
16597 run.epoch_created,
16598 entry.schema.schema_id,
16599 )));
16600 }
16601 if header.row_count != 0 {
16602 maximum_row_id = Some(
16603 maximum_row_id
16604 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
16605 );
16606 }
16607 reader.validate_all_pages()?;
16608 }
16609 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
16610 return Err(MongrelError::InvalidArgument(format!(
16611 "table {} next_row_id does not advance beyond persisted rows",
16612 entry.table_id
16613 )));
16614 }
16615 for run in &manifest.retiring {
16616 if run.run_id >= u64::MAX as u128
16617 || run.retire_epoch > manifest.current_epoch
16618 || !run_ids.insert(run.run_id)
16619 {
16620 return Err(MongrelError::InvalidArgument(format!(
16621 "table {} manifest contains an invalid or aliased retired run",
16622 entry.table_id
16623 )));
16624 }
16625 }
16626 manifest.flushed_epoch
16627 } else {
16628 if !recovered_table_ids.contains(&entry.table_id) {
16629 return Err(MongrelError::NotFound(format!(
16630 "live table {} manifest is missing",
16631 entry.table_id
16632 )));
16633 }
16634 0
16635 };
16636 tables.insert(
16637 entry.table_id,
16638 RecoveryValidationTable {
16639 schema: entry.schema.clone(),
16640 flushed_epoch,
16641 },
16642 );
16643 }
16644
16645 let committed = records
16646 .iter()
16647 .filter_map(|record| match record.op {
16648 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
16649 _ => None,
16650 })
16651 .collect::<HashMap<_, _>>();
16652 let mut run_ids = HashSet::new();
16653 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
16654 for record in records {
16655 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
16656 continue;
16657 };
16658 match &record.op {
16659 Op::Put { table_id, rows } => {
16660 let table = validate_recovery_data_table_plan(
16661 catalog,
16662 &tables,
16663 *table_id,
16664 commit_epoch,
16665 record.seq.0,
16666 )?;
16667 let decoded: Vec<crate::memtable::Row> =
16668 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
16669 offset: record.seq.0,
16670 reason: format!(
16671 "committed Put payload for transaction {} could not be decoded: {error}",
16672 record.txn_id
16673 ),
16674 })?;
16675 if let Some(table) = table {
16676 for row in &decoded {
16677 if !recovered_row_ids
16678 .entry(*table_id)
16679 .or_default()
16680 .insert(row.row_id.0)
16681 {
16682 return Err(MongrelError::CorruptWal {
16683 offset: record.seq.0,
16684 reason: format!(
16685 "committed WAL repeats recovered row id {} for table {table_id}",
16686 row.row_id.0
16687 ),
16688 });
16689 }
16690 validate_recovered_row(&table.schema, row)?;
16691 }
16692 }
16693 }
16694 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
16695 validate_recovery_data_table_plan(
16696 catalog,
16697 &tables,
16698 *table_id,
16699 commit_epoch,
16700 record.seq.0,
16701 )?;
16702 }
16703 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
16704 Op::Ddl(DdlOp::ResetExternalTableState {
16705 name,
16706 generation_epoch,
16707 }) => {
16708 if *generation_epoch != commit_epoch {
16709 return Err(MongrelError::CorruptWal {
16710 offset: record.seq.0,
16711 reason: format!(
16712 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
16713 ),
16714 });
16715 }
16716 validate_recovered_external_name(name)?;
16717 }
16718 Op::TxnCommit { added_runs, .. } => {
16719 for added in added_runs {
16720 let Some(table) = validate_recovery_data_table_plan(
16721 catalog,
16722 &tables,
16723 added.table_id,
16724 commit_epoch,
16725 record.seq.0,
16726 )?
16727 else {
16728 continue;
16729 };
16730 if added.run_id >= u64::MAX as u128
16731 || !run_ids.insert((added.table_id, added.run_id))
16732 {
16733 return Err(MongrelError::CorruptWal {
16734 offset: record.seq.0,
16735 reason: format!(
16736 "duplicate or invalid recovered run {} for table {}",
16737 added.run_id, added.table_id
16738 ),
16739 });
16740 }
16741 if commit_epoch <= table.flushed_epoch {
16742 continue;
16743 }
16744 validate_planned_spilled_run(
16745 durable_root,
16746 record.txn_id,
16747 commit_epoch,
16748 added,
16749 &table.schema,
16750 kek.clone(),
16751 )?;
16752 }
16753 }
16754 _ => {}
16755 }
16756 }
16757 Ok(())
16758}
16759
16760fn validate_recovery_data_table_plan<'a>(
16761 catalog: &Catalog,
16762 tables: &'a HashMap<u64, RecoveryValidationTable>,
16763 table_id: u64,
16764 commit_epoch: u64,
16765 offset: u64,
16766) -> Result<Option<&'a RecoveryValidationTable>> {
16767 let entry = catalog
16768 .tables
16769 .iter()
16770 .find(|entry| entry.table_id == table_id)
16771 .ok_or_else(|| MongrelError::CorruptWal {
16772 offset,
16773 reason: format!("committed record references unknown table {table_id}"),
16774 })?;
16775 if commit_epoch < entry.created_epoch {
16776 return Err(MongrelError::CorruptWal {
16777 offset,
16778 reason: format!(
16779 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16780 entry.created_epoch
16781 ),
16782 });
16783 }
16784 match entry.state {
16785 TableState::Dropped { at_epoch } => {
16786 let abandoned =
16787 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16788 if commit_epoch >= at_epoch && !abandoned {
16789 return Err(MongrelError::CorruptWal {
16790 offset,
16791 reason: format!(
16792 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16793 ),
16794 });
16795 }
16796 Ok(None)
16797 }
16798 TableState::Live => {
16799 tables
16800 .get(&table_id)
16801 .map(Some)
16802 .ok_or_else(|| MongrelError::CorruptWal {
16803 offset,
16804 reason: format!("live table {table_id} has no recovery plan"),
16805 })
16806 }
16807 TableState::Building { .. } => Err(MongrelError::CorruptWal {
16808 offset,
16809 reason: format!("building table {table_id} was not normalized before recovery"),
16810 }),
16811 }
16812}
16813
16814fn validate_planned_spilled_run(
16815 root: &crate::durable_file::DurableRoot,
16816 txn_id: u64,
16817 commit_epoch: u64,
16818 added: &crate::wal::AddedRun,
16819 schema: &Schema,
16820 kek: Option<Arc<crate::encryption::Kek>>,
16821) -> Result<()> {
16822 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
16823 let destination = table
16824 .join(crate::engine::RUNS_DIR)
16825 .join(format!("r-{}.sr", added.run_id as u64));
16826 let pending = table
16827 .join("_txn")
16828 .join(txn_id.to_string())
16829 .join(format!("r-{}.sr", added.run_id as u64));
16830 let file = match root.open_regular(&destination) {
16831 Ok(file) => file,
16832 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
16833 root.open_regular(&pending).map_err(|pending_error| {
16834 if pending_error.kind() == std::io::ErrorKind::NotFound {
16835 MongrelError::CorruptWal {
16836 offset: commit_epoch,
16837 reason: format!(
16838 "committed spilled run {} for transaction {txn_id} is missing",
16839 added.run_id
16840 ),
16841 }
16842 } else {
16843 pending_error.into()
16844 }
16845 })?
16846 }
16847 Err(error) => return Err(error.into()),
16848 };
16849 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
16850 let header = reader.header();
16851 if header.run_id != added.run_id
16852 || header.content_hash != added.content_hash
16853 || header.row_count != added.row_count
16854 || header.level != added.level
16855 || header.min_row_id != added.min_row_id
16856 || header.max_row_id != added.max_row_id
16857 || header.schema_id != schema.schema_id
16858 || !header.is_uniform_epoch()
16859 || header.epoch_created != 0
16860 {
16861 return Err(MongrelError::CorruptWal {
16862 offset: commit_epoch,
16863 reason: format!(
16864 "committed spilled run {} metadata differs from WAL",
16865 added.run_id
16866 ),
16867 });
16868 }
16869 reader.validate_all_pages()?;
16870 Ok(())
16871}
16872
16873#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16893pub enum StagedTxnWrite {
16894 Put {
16899 table_id: u64,
16901 rows: Vec<u8>,
16903 },
16904 Delete {
16906 table_id: u64,
16908 row_ids: Vec<u64>,
16910 },
16911}
16912
16913impl StagedTxnWrite {
16914 pub fn encode(&self) -> Result<Vec<u8>> {
16916 Ok(bincode::serialize(self)?)
16917 }
16918
16919 pub fn decode(bytes: &[u8]) -> Result<Self> {
16921 Ok(bincode::deserialize(bytes)?)
16922 }
16923}
16924
16925pub fn translate_records_for_replication(
16962 records: &[crate::wal::Record],
16963) -> Result<Vec<crate::wal::Record>> {
16964 use crate::wal::Op;
16965
16966 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
16969 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
16970 })?;
16971 if records.iter().any(|record| record.txn_id != txn_id) {
16972 return Err(MongrelError::InvalidArgument(
16973 "replicated transaction payload mixes transaction ids".into(),
16974 ));
16975 }
16976 let commits = records
16977 .iter()
16978 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
16979 .count();
16980 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
16981 return Err(MongrelError::InvalidArgument(
16982 "replicated transaction payload must end in exactly one commit marker".into(),
16983 ));
16984 }
16985
16986 let mut spilled_rows: HashMap<u64, Vec<crate::memtable::Row>> = HashMap::new();
16990 for record in records {
16991 if let Op::SpilledRows { table_id, rows } = &record.op {
16992 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(rows).map_err(|error| {
16993 MongrelError::InvalidArgument(format!(
16994 "spilled row payload for table {table_id} cannot decode for replication: \
16995 {error}"
16996 ))
16997 })?;
16998 spilled_rows.entry(*table_id).or_default().extend(chunk);
16999 }
17000 }
17001
17002 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
17006 unreachable!("one trailing commit marker validated above");
17007 };
17008 for run in added_runs {
17009 let rows = spilled_rows.get(&run.table_id).ok_or_else(|| {
17010 MongrelError::InvalidArgument(format!(
17011 "commit links spilled run {} for table {} but carries no logical row records \
17012 for it",
17013 run.run_id, run.table_id
17014 ))
17015 })?;
17016 let covered = rows
17017 .iter()
17018 .filter(|row| row.row_id.0 >= run.min_row_id && row.row_id.0 <= run.max_row_id)
17019 .count() as u64;
17020 if covered != run.row_count {
17021 return Err(MongrelError::InvalidArgument(format!(
17022 "commit links spilled run {} for table {} ({} rows in [{}, {}]) but the logical \
17023 row records cover {} rows",
17024 run.run_id, run.table_id, run.row_count, run.min_row_id, run.max_row_id, covered
17025 )));
17026 }
17027 }
17028
17029 let translated = records
17032 .iter()
17033 .map(|record| {
17034 let op = match &record.op {
17035 Op::SpilledRows { table_id, rows } => Op::Put {
17036 table_id: *table_id,
17037 rows: rows.clone(),
17038 },
17039 Op::TxnCommit { epoch, .. } => Op::TxnCommit {
17040 epoch: *epoch,
17041 added_runs: Vec::new(),
17042 },
17043 op => op.clone(),
17044 };
17045 crate::wal::Record::new(record.seq, record.txn_id, op)
17046 })
17047 .collect();
17048 Ok(translated)
17049}
17050
17051fn recover_shared_wal(
17052 durable_root: &crate::durable_file::DurableRoot,
17053 tables: &HashMap<u64, TableHandle>,
17054 catalog: &Catalog,
17055 epoch: &EpochAuthority,
17056 records: &[crate::wal::Record],
17057) -> Result<()> {
17058 use crate::memtable::Row;
17059 use crate::wal::{DdlOp, Op};
17060
17061 let mut committed: HashMap<u64, u64> = HashMap::new();
17063 let mut commit_ts_by_txn: HashMap<u64, mongreldb_types::hlc::HlcTimestamp> = HashMap::new();
17065 let mut spilled_to_link: Vec<(
17066 u64, u64, Vec<crate::wal::AddedRun>,
17069 )> = Vec::new();
17070 for r in records {
17071 match &r.op {
17072 Op::CommitTimestamp { unix_nanos } => {
17073 commit_ts_by_txn.insert(
17074 r.txn_id,
17075 mongreldb_types::hlc::HlcTimestamp {
17076 physical_micros: unix_nanos / 1_000,
17077 logical: 0,
17078 node_tiebreaker: 0,
17079 },
17080 );
17081 }
17082 Op::TxnCommit {
17083 epoch: ce,
17084 ref added_runs,
17085 } => {
17086 committed.insert(r.txn_id, *ce);
17087 if !added_runs.is_empty() {
17088 spilled_to_link.push((r.txn_id, *ce, added_runs.clone()));
17089 }
17090 }
17091 _ => {}
17092 }
17093 }
17094 for record in records {
17095 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
17096 continue;
17097 };
17098 match &record.op {
17099 Op::Put { table_id, .. }
17100 | Op::Delete { table_id, .. }
17101 | Op::TruncateTable { table_id } => {
17102 validate_recovered_data_table(
17103 catalog,
17104 tables,
17105 *table_id,
17106 commit_epoch,
17107 record.seq.0,
17108 )?;
17109 }
17110 Op::TxnCommit { added_runs, .. } => {
17111 for run in added_runs {
17112 validate_recovered_data_table(
17113 catalog,
17114 tables,
17115 run.table_id,
17116 commit_epoch,
17117 record.seq.0,
17118 )?;
17119 }
17120 }
17121 _ => {}
17122 }
17123 }
17124 let truncated_transactions: HashSet<(u64, u64)> = records
17125 .iter()
17126 .filter_map(|record| {
17127 committed.get(&record.txn_id)?;
17128 match record.op {
17129 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
17130 _ => None,
17131 }
17132 })
17133 .collect();
17134
17135 enum ExternalRecoveryAction {
17137 Write { name: String, state: Vec<u8> },
17138 Reset { name: String },
17139 }
17140 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
17141 let mut external_actions = Vec::new();
17142 let mut max_epoch = epoch.visible().0;
17143 for r in records.iter().cloned() {
17144 let Some(&ce) = committed.get(&r.txn_id) else {
17145 continue; };
17147 let commit_epoch = Epoch(ce);
17148 max_epoch = max_epoch.max(ce);
17149 match r.op {
17150 Op::Put { table_id, rows } => {
17151 let skip = tables
17153 .get(&table_id)
17154 .map(|h| h.lock().flushed_epoch() >= ce)
17155 .unwrap_or(true);
17156 if skip {
17157 continue;
17158 }
17159 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
17160 MongrelError::CorruptWal {
17161 offset: r.seq.0,
17162 reason: format!(
17163 "committed Put payload for transaction {} could not be decoded: {error}",
17164 r.txn_id
17165 ),
17166 }
17167 })?;
17168 let txn_commit_ts = commit_ts_by_txn.get(&r.txn_id).copied();
17173 let rows: Vec<Row> = rows
17174 .into_iter()
17175 .map(|mut row| {
17176 row.committed_epoch = commit_epoch;
17177 if row.commit_ts.is_none() {
17178 row.commit_ts = txn_commit_ts;
17179 }
17180 row
17181 })
17182 .collect();
17183 let entry = stage
17184 .entry(table_id)
17185 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17186 entry.0.extend(rows);
17187 entry.3 = commit_epoch;
17188 }
17189 Op::Delete { table_id, row_ids } => {
17190 let skip = tables
17191 .get(&table_id)
17192 .map(|h| h.lock().flushed_epoch() >= ce)
17193 .unwrap_or(true);
17194 if skip {
17195 continue;
17196 }
17197 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
17198 let entry = stage
17199 .entry(table_id)
17200 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17201 entry.1.extend(dels);
17202 entry.3 = commit_epoch;
17203 }
17204 Op::TruncateTable { table_id } => {
17205 let skip = tables
17206 .get(&table_id)
17207 .map(|h| h.lock().flushed_epoch() >= ce)
17208 .unwrap_or(true);
17209 if skip {
17210 continue;
17211 }
17212 stage.insert(
17213 table_id,
17214 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
17215 );
17216 }
17217 Op::ExternalTableState { name, state } => {
17218 let current_generation = catalog
17219 .external_tables
17220 .iter()
17221 .find(|entry| entry.name == name)
17222 .map(|entry| entry.created_epoch);
17223 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
17224 validate_recovered_external_name(&name)?;
17225 external_actions.push(ExternalRecoveryAction::Write { name, state });
17226 }
17227 }
17228 Op::Ddl(DdlOp::ResetExternalTableState {
17229 name,
17230 generation_epoch,
17231 }) => {
17232 if generation_epoch != ce {
17233 return Err(MongrelError::CorruptWal {
17234 offset: r.seq.0,
17235 reason: format!(
17236 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
17237 ),
17238 });
17239 }
17240 validate_recovered_external_name(&name)?;
17241 external_actions.push(ExternalRecoveryAction::Reset { name });
17242 }
17243 Op::Flush { .. }
17244 | Op::TxnCommit { .. }
17245 | Op::TxnAbort
17246 | Op::Ddl(_)
17247 | Op::BeforeImage { .. }
17248 | Op::CommitTimestamp { .. }
17249 | Op::SpilledRows { .. } => {}
17250 }
17251 }
17252 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
17253 added_runs.retain(|added| {
17254 tables
17255 .get(&added.table_id)
17256 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
17257 });
17258 }
17259 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
17260 validate_recovery_table_stages(tables, &stage)?;
17261 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
17262
17263 for action in external_actions {
17267 match action {
17268 ExternalRecoveryAction::Write { name, state } => {
17269 write_external_state_file(durable_root, &name, &state)?;
17270 }
17271 ExternalRecoveryAction::Reset { name } => {
17272 durable_root.create_directory_all(VTAB_DIR)?;
17273 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
17274 }
17275 }
17276 }
17277 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
17278 let Some(handle) = tables.get(&table_id) else {
17279 continue;
17280 };
17281 let mut t = handle.lock();
17282 if let Some(epoch) = truncate_epoch {
17283 t.apply_truncate(epoch);
17284 }
17285 t.recover_apply(rows, deletes)?;
17286 let rows = t.visible_rows(Snapshot::unbounded())?;
17290 t.live_count = rows.len() as u64;
17291 t.persist_manifest(table_epoch.max(epoch.visible()))?;
17295 }
17296
17297 for (txn_id, ce, added_runs) in &spilled_to_link {
17301 for ar in added_runs {
17302 let Some(handle) = tables.get(&ar.table_id) else {
17303 continue;
17304 };
17305 let mut t = handle.lock();
17306 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
17307 let destination = table_dir
17308 .join(crate::engine::RUNS_DIR)
17309 .join(format!("r-{}.sr", ar.run_id));
17310 match durable_root.open_regular(&destination) {
17311 Ok(_) => {}
17312 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
17313 let pending = table_dir
17314 .join("_txn")
17315 .join(txn_id.to_string())
17316 .join(format!("r-{}.sr", ar.run_id));
17317 durable_root.rename_file_new(&pending, &destination)?;
17318 }
17319 Err(error) => return Err(error.into()),
17320 }
17321 let linked = t.recover_spilled_run(crate::manifest::RunRef {
17327 run_id: ar.run_id,
17328 level: ar.level,
17329 epoch_created: *ce,
17330 row_count: ar.row_count,
17331 });
17332 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
17333 if replaced {
17334 t.set_flushed_epoch(Epoch(*ce));
17335 }
17336 if linked || replaced {
17337 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
17338 }
17339 }
17340 }
17341
17342 epoch.advance_recovered(Epoch(max_epoch));
17343 Ok(())
17344}
17345
17346fn reconcile_recovered_table_metadata(
17347 tables: &HashMap<u64, TableHandle>,
17348 epoch: Epoch,
17349) -> Result<()> {
17350 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
17351 table_ids.sort_unstable();
17352 let mut plans = Vec::with_capacity(table_ids.len());
17353 for table_id in &table_ids {
17354 let handle = tables.get(table_id).ok_or_else(|| {
17355 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17356 })?;
17357 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
17358 }
17359 for (table_id, plan) in plans {
17362 let handle = tables.get(&table_id).ok_or_else(|| {
17363 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17364 })?;
17365 handle.lock().apply_recovered_metadata(plan, epoch)?;
17366 }
17367 Ok(())
17368}
17369
17370fn validate_recovered_external_name(name: &str) -> Result<()> {
17371 if name.is_empty()
17372 || !name.chars().all(|character| {
17373 character.is_ascii_alphanumeric() || character == '_' || character == '-'
17374 })
17375 {
17376 return Err(MongrelError::CorruptWal {
17377 offset: 0,
17378 reason: format!("unsafe recovered external-table name {name:?}"),
17379 });
17380 }
17381 Ok(())
17382}
17383
17384fn validate_recovery_table_stages(
17385 tables: &HashMap<u64, TableHandle>,
17386 stages: &HashMap<u64, RecoveryTableStage>,
17387) -> Result<()> {
17388 for (table_id, (rows, _, _, _)) in stages {
17389 let handle = tables
17390 .get(table_id)
17391 .ok_or_else(|| MongrelError::CorruptWal {
17392 offset: *table_id,
17393 reason: format!("recovery stage references unmounted table {table_id}"),
17394 })?;
17395 let table = handle.lock();
17396 table.visible_rows(Snapshot::unbounded())?;
17399 for row in rows {
17400 validate_recovered_row(table.schema(), row)?;
17401 }
17402 }
17403 Ok(())
17404}
17405
17406fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
17407 if row.deleted || row.row_id.0 == u64::MAX {
17408 return Err(MongrelError::CorruptWal {
17409 offset: row.row_id.0,
17410 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
17411 });
17412 }
17413 let cells = row
17414 .columns
17415 .iter()
17416 .map(|(column, value)| (*column, value.clone()))
17417 .collect::<Vec<_>>();
17418 schema
17419 .validate_persisted_values(&cells)
17420 .map_err(|error| MongrelError::CorruptWal {
17421 offset: row.row_id.0,
17422 reason: format!("recovered row violates table schema: {error}"),
17423 })?;
17424 if schema.auto_increment_column().is_some_and(|column| {
17425 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
17426 }) {
17427 return Err(MongrelError::CorruptWal {
17428 offset: row.row_id.0,
17429 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
17430 });
17431 }
17432 Ok(())
17433}
17434
17435fn validate_recovery_spilled_runs(
17436 root: &crate::durable_file::DurableRoot,
17437 tables: &HashMap<u64, TableHandle>,
17438 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
17439) -> Result<()> {
17440 let mut identities = HashSet::new();
17441 for (txn_id, commit_epoch, added_runs) in spilled {
17442 for added in added_runs {
17443 if added.run_id >= u64::MAX as u128 {
17444 return Err(MongrelError::CorruptWal {
17445 offset: *commit_epoch,
17446 reason: format!(
17447 "recovered run id {} exceeds the on-disk namespace",
17448 added.run_id
17449 ),
17450 });
17451 }
17452 let Some(handle) = tables.get(&added.table_id) else {
17453 continue;
17454 };
17455 if !identities.insert((added.table_id, added.run_id)) {
17456 return Err(MongrelError::CorruptWal {
17457 offset: *commit_epoch,
17458 reason: format!(
17459 "duplicate recovered run {} for table {}",
17460 added.run_id, added.table_id
17461 ),
17462 });
17463 }
17464 let table = handle.lock();
17465 validate_planned_spilled_run(
17466 root,
17467 *txn_id,
17468 *commit_epoch,
17469 added,
17470 table.schema(),
17471 table.kek(),
17472 )?;
17473 }
17474 }
17475 Ok(())
17476}
17477
17478fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
17479 match condition {
17480 ProcedureCondition::Pk { .. } => {
17481 if schema.primary_key().is_none() {
17482 return Err(MongrelError::InvalidArgument(
17483 "procedure condition Pk references a table without a primary key".into(),
17484 ));
17485 }
17486 }
17487 ProcedureCondition::BitmapEq { column_id, .. }
17488 | ProcedureCondition::BitmapIn { column_id, .. }
17489 | ProcedureCondition::Range { column_id, .. }
17490 | ProcedureCondition::RangeF64 { column_id, .. }
17491 | ProcedureCondition::IsNull { column_id }
17492 | ProcedureCondition::IsNotNull { column_id }
17493 | ProcedureCondition::FmContains { column_id, .. } => {
17494 validate_column_id(*column_id, schema)?;
17495 }
17496 }
17497 Ok(())
17498}
17499
17500fn bind_procedure_args(
17501 procedure: &StoredProcedure,
17502 mut args: HashMap<String, crate::Value>,
17503) -> Result<HashMap<String, crate::Value>> {
17504 let mut out = HashMap::new();
17505 for param in &procedure.params {
17506 let value = match args.remove(¶m.name) {
17507 Some(value) => value,
17508 None => param.default.clone().ok_or_else(|| {
17509 MongrelError::InvalidArgument(format!(
17510 "missing required procedure parameter {:?}",
17511 param.name
17512 ))
17513 })?,
17514 };
17515 if !param.nullable && matches!(value, crate::Value::Null) {
17516 return Err(MongrelError::InvalidArgument(format!(
17517 "procedure parameter {:?} must not be NULL",
17518 param.name
17519 )));
17520 }
17521 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
17522 return Err(MongrelError::InvalidArgument(format!(
17523 "procedure parameter {:?} has wrong type",
17524 param.name
17525 )));
17526 }
17527 out.insert(param.name.clone(), value);
17528 }
17529 if let Some(extra) = args.keys().next() {
17530 return Err(MongrelError::InvalidArgument(format!(
17531 "unknown procedure parameter {extra:?}"
17532 )));
17533 }
17534 Ok(out)
17535}
17536
17537fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
17538 matches!(
17539 (value, ty),
17540 (crate::Value::Bool(_), crate::TypeId::Bool)
17541 | (crate::Value::Int64(_), crate::TypeId::Int8)
17542 | (crate::Value::Int64(_), crate::TypeId::Int16)
17543 | (crate::Value::Int64(_), crate::TypeId::Int32)
17544 | (crate::Value::Int64(_), crate::TypeId::Int64)
17545 | (crate::Value::Int64(_), crate::TypeId::UInt8)
17546 | (crate::Value::Int64(_), crate::TypeId::UInt16)
17547 | (crate::Value::Int64(_), crate::TypeId::UInt32)
17548 | (crate::Value::Int64(_), crate::TypeId::UInt64)
17549 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
17550 | (crate::Value::Int64(_), crate::TypeId::Date32)
17551 | (crate::Value::Float64(_), crate::TypeId::Float32)
17552 | (crate::Value::Float64(_), crate::TypeId::Float64)
17553 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
17554 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
17555 | (
17556 crate::Value::GeneratedEmbedding(_),
17557 crate::TypeId::Embedding { .. }
17558 )
17559 )
17560}
17561
17562fn eval_cells(
17563 cells: &[crate::procedure::ProcedureCell],
17564 args: &HashMap<String, crate::Value>,
17565 outputs: &HashMap<String, ProcedureCallOutput>,
17566) -> Result<Vec<(u16, crate::Value)>> {
17567 cells
17568 .iter()
17569 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
17570 .collect()
17571}
17572
17573fn eval_condition(
17574 condition: &ProcedureCondition,
17575 args: &HashMap<String, crate::Value>,
17576 outputs: &HashMap<String, ProcedureCallOutput>,
17577) -> Result<crate::Condition> {
17578 Ok(match condition {
17579 ProcedureCondition::Pk { value } => {
17580 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
17581 }
17582 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
17583 column_id: *column_id,
17584 value: eval_value(value, args, outputs)?.encode_key(),
17585 },
17586 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
17587 column_id: *column_id,
17588 values: values
17589 .iter()
17590 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
17591 .collect::<Result<Vec<_>>>()?,
17592 },
17593 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
17594 column_id: *column_id,
17595 lo: expect_i64(eval_value(lo, args, outputs)?)?,
17596 hi: expect_i64(eval_value(hi, args, outputs)?)?,
17597 },
17598 ProcedureCondition::RangeF64 {
17599 column_id,
17600 lo,
17601 lo_inclusive,
17602 hi,
17603 hi_inclusive,
17604 } => crate::Condition::RangeF64 {
17605 column_id: *column_id,
17606 lo: expect_f64(eval_value(lo, args, outputs)?)?,
17607 lo_inclusive: *lo_inclusive,
17608 hi: expect_f64(eval_value(hi, args, outputs)?)?,
17609 hi_inclusive: *hi_inclusive,
17610 },
17611 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
17612 column_id: *column_id,
17613 },
17614 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
17615 column_id: *column_id,
17616 },
17617 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
17618 column_id: *column_id,
17619 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
17620 },
17621 })
17622}
17623
17624fn eval_value(
17625 value: &ProcedureValue,
17626 args: &HashMap<String, crate::Value>,
17627 outputs: &HashMap<String, ProcedureCallOutput>,
17628) -> Result<crate::Value> {
17629 match value {
17630 ProcedureValue::Literal(value) => Ok(value.clone()),
17631 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
17632 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17633 }),
17634 ProcedureValue::StepScalar(id) => match outputs.get(id) {
17635 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
17636 _ => Err(MongrelError::InvalidArgument(format!(
17637 "procedure step {id:?} did not return a scalar"
17638 ))),
17639 },
17640 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
17641 Err(MongrelError::InvalidArgument(
17642 "row-valued procedure reference cannot be used as a scalar".into(),
17643 ))
17644 }
17645 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
17646 "structured procedure value cannot be used as a scalar cell".into(),
17647 )),
17648 }
17649}
17650
17651fn eval_return_output(
17652 value: &ProcedureValue,
17653 args: &HashMap<String, crate::Value>,
17654 outputs: &HashMap<String, ProcedureCallOutput>,
17655) -> Result<ProcedureCallOutput> {
17656 match value {
17657 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
17658 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
17659 args.get(name).cloned().ok_or_else(|| {
17660 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17661 })?,
17662 )),
17663 ProcedureValue::StepRows(id)
17664 | ProcedureValue::StepRow(id)
17665 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
17666 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
17667 }),
17668 ProcedureValue::Object(fields) => {
17669 let mut out = Vec::with_capacity(fields.len());
17670 for (name, value) in fields {
17671 out.push((name.clone(), eval_return_output(value, args, outputs)?));
17672 }
17673 Ok(ProcedureCallOutput::Object(out))
17674 }
17675 ProcedureValue::Array(values) => {
17676 let mut out = Vec::with_capacity(values.len());
17677 for value in values {
17678 out.push(eval_return_output(value, args, outputs)?);
17679 }
17680 Ok(ProcedureCallOutput::Array(out))
17681 }
17682 }
17683}
17684
17685fn expect_i64(value: crate::Value) -> Result<i64> {
17686 match value {
17687 crate::Value::Int64(value) => Ok(value),
17688 _ => Err(MongrelError::InvalidArgument(
17689 "procedure value must be Int64".into(),
17690 )),
17691 }
17692}
17693
17694fn expect_f64(value: crate::Value) -> Result<f64> {
17695 match value {
17696 crate::Value::Float64(value) => Ok(value),
17697 _ => Err(MongrelError::InvalidArgument(
17698 "procedure value must be Float64".into(),
17699 )),
17700 }
17701}
17702
17703fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
17704 match value {
17705 crate::Value::Bytes(value) => Ok(value),
17706 _ => Err(MongrelError::InvalidArgument(
17707 "procedure value must be Bytes".into(),
17708 )),
17709 }
17710}
17711
17712fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
17713 if schema.columns.iter().any(|c| c.id == column_id) {
17714 Ok(())
17715 } else {
17716 Err(MongrelError::InvalidArgument(format!(
17717 "unknown column id {column_id}"
17718 )))
17719 }
17720}
17721
17722fn trigger_matches_event(
17723 trigger: &StoredTrigger,
17724 event: &WriteEvent,
17725 cat: &Catalog,
17726) -> Result<bool> {
17727 if trigger.event != event.kind {
17728 return Ok(false);
17729 }
17730 let TriggerTarget::Table(target) = &trigger.target else {
17731 return Ok(false);
17732 };
17733 if target != &event.table {
17734 return Ok(false);
17735 }
17736 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
17737 let schema = &cat
17738 .live(target)
17739 .ok_or_else(|| {
17740 MongrelError::InvalidArgument(format!(
17741 "trigger {:?} references unknown table {target:?}",
17742 trigger.name
17743 ))
17744 })?
17745 .schema;
17746 let mut watched = Vec::with_capacity(trigger.update_of.len());
17747 for name in &trigger.update_of {
17748 let col = schema.column(name).ok_or_else(|| {
17749 MongrelError::InvalidArgument(format!(
17750 "trigger {:?} references unknown UPDATE OF column {name:?}",
17751 trigger.name
17752 ))
17753 })?;
17754 watched.push(col.id);
17755 }
17756 if !event
17757 .changed_columns
17758 .iter()
17759 .any(|column_id| watched.contains(column_id))
17760 {
17761 return Ok(false);
17762 }
17763 }
17764 Ok(true)
17765}
17766
17767fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
17768 let mut ids = std::collections::BTreeSet::new();
17769 if let Some(old) = old {
17770 ids.extend(old.columns.keys().copied());
17771 }
17772 if let Some(new) = new {
17773 ids.extend(new.columns.keys().copied());
17774 }
17775 ids.into_iter()
17776 .filter(|id| {
17777 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
17778 })
17779 .collect()
17780}
17781
17782fn eval_trigger_cells(
17783 cells: &[crate::trigger::TriggerCell],
17784 event: &WriteEvent,
17785 selected: Option<&TriggerRowImage>,
17786) -> Result<Vec<(u16, Value)>> {
17787 cells
17788 .iter()
17789 .map(|cell| {
17790 Ok((
17791 cell.column_id,
17792 eval_trigger_value(&cell.value, event, selected)?,
17793 ))
17794 })
17795 .collect()
17796}
17797
17798fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
17799 match expr {
17800 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
17801 Value::Bool(value) => Ok(value),
17802 Value::Null => Ok(false),
17803 other => Err(MongrelError::InvalidArgument(format!(
17804 "trigger WHEN value must be boolean, got {other:?}"
17805 ))),
17806 },
17807 TriggerExpr::Eq { left, right } => Ok(values_equal(
17808 &eval_trigger_value(left, event, None)?,
17809 &eval_trigger_value(right, event, None)?,
17810 )),
17811 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
17812 &eval_trigger_value(left, event, None)?,
17813 &eval_trigger_value(right, event, None)?,
17814 )),
17815 TriggerExpr::Lt { left, right } => match value_order(
17816 &eval_trigger_value(left, event, None)?,
17817 &eval_trigger_value(right, event, None)?,
17818 ) {
17819 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
17820 None => Ok(false),
17821 },
17822 TriggerExpr::Lte { left, right } => match value_order(
17823 &eval_trigger_value(left, event, None)?,
17824 &eval_trigger_value(right, event, None)?,
17825 ) {
17826 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
17827 None => Ok(false),
17828 },
17829 TriggerExpr::Gt { left, right } => match value_order(
17830 &eval_trigger_value(left, event, None)?,
17831 &eval_trigger_value(right, event, None)?,
17832 ) {
17833 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
17834 None => Ok(false),
17835 },
17836 TriggerExpr::Gte { left, right } => match value_order(
17837 &eval_trigger_value(left, event, None)?,
17838 &eval_trigger_value(right, event, None)?,
17839 ) {
17840 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
17841 None => Ok(false),
17842 },
17843 TriggerExpr::IsNull(value) => Ok(matches!(
17844 eval_trigger_value(value, event, None)?,
17845 Value::Null
17846 )),
17847 TriggerExpr::IsNotNull(value) => Ok(!matches!(
17848 eval_trigger_value(value, event, None)?,
17849 Value::Null
17850 )),
17851 TriggerExpr::And { left, right } => {
17852 if !eval_trigger_expr(left, event)? {
17853 Ok(false)
17854 } else {
17855 Ok(eval_trigger_expr(right, event)?)
17856 }
17857 }
17858 TriggerExpr::Or { left, right } => {
17859 if eval_trigger_expr(left, event)? {
17860 Ok(true)
17861 } else {
17862 Ok(eval_trigger_expr(right, event)?)
17863 }
17864 }
17865 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
17866 }
17867}
17868
17869fn eval_trigger_condition(
17870 condition: &TriggerCondition,
17871 event: &WriteEvent,
17872 selected: &TriggerRowImage,
17873 schema: &Schema,
17874) -> Result<bool> {
17875 match condition {
17876 TriggerCondition::Pk { value } => {
17877 let pk = schema.primary_key().ok_or_else(|| {
17878 MongrelError::InvalidArgument(
17879 "trigger condition Pk references a table without a primary key".into(),
17880 )
17881 })?;
17882 let lhs = eval_trigger_value(value, event, Some(selected))?;
17883 Ok(values_equal(
17884 &lhs,
17885 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
17886 ))
17887 }
17888 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
17889 selected.columns.get(column_id).unwrap_or(&Value::Null),
17890 &eval_trigger_value(value, event, Some(selected))?,
17891 )),
17892 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
17893 selected.columns.get(column_id).unwrap_or(&Value::Null),
17894 &eval_trigger_value(value, event, Some(selected))?,
17895 )),
17896 TriggerCondition::Lt { column_id, value } => match value_order(
17897 selected.columns.get(column_id).unwrap_or(&Value::Null),
17898 &eval_trigger_value(value, event, Some(selected))?,
17899 ) {
17900 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
17901 None => Ok(false),
17902 },
17903 TriggerCondition::Lte { column_id, value } => match value_order(
17904 selected.columns.get(column_id).unwrap_or(&Value::Null),
17905 &eval_trigger_value(value, event, Some(selected))?,
17906 ) {
17907 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
17908 None => Ok(false),
17909 },
17910 TriggerCondition::Gt { column_id, value } => match value_order(
17911 selected.columns.get(column_id).unwrap_or(&Value::Null),
17912 &eval_trigger_value(value, event, Some(selected))?,
17913 ) {
17914 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
17915 None => Ok(false),
17916 },
17917 TriggerCondition::Gte { column_id, value } => match value_order(
17918 selected.columns.get(column_id).unwrap_or(&Value::Null),
17919 &eval_trigger_value(value, event, Some(selected))?,
17920 ) {
17921 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
17922 None => Ok(false),
17923 },
17924 TriggerCondition::IsNull { column_id } => Ok(matches!(
17925 selected.columns.get(column_id),
17926 None | Some(Value::Null)
17927 )),
17928 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
17929 selected.columns.get(column_id),
17930 None | Some(Value::Null)
17931 )),
17932 TriggerCondition::And { left, right } => {
17933 if !eval_trigger_condition(left, event, selected, schema)? {
17934 Ok(false)
17935 } else {
17936 Ok(eval_trigger_condition(right, event, selected, schema)?)
17937 }
17938 }
17939 TriggerCondition::Or { left, right } => {
17940 if eval_trigger_condition(left, event, selected, schema)? {
17941 Ok(true)
17942 } else {
17943 Ok(eval_trigger_condition(right, event, selected, schema)?)
17944 }
17945 }
17946 TriggerCondition::Not(condition) => {
17947 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
17948 }
17949 }
17950}
17951
17952fn eval_trigger_value(
17953 value: &TriggerValue,
17954 event: &WriteEvent,
17955 selected: Option<&TriggerRowImage>,
17956) -> Result<Value> {
17957 match value {
17958 TriggerValue::Literal(value) => Ok(value.clone()),
17959 TriggerValue::NewColumn(column_id) => event
17960 .new
17961 .as_ref()
17962 .and_then(|row| row.columns.get(column_id))
17963 .cloned()
17964 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
17965 TriggerValue::OldColumn(column_id) => event
17966 .old
17967 .as_ref()
17968 .and_then(|row| row.columns.get(column_id))
17969 .cloned()
17970 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
17971 TriggerValue::SelectedColumn(column_id) => selected
17972 .and_then(|row| row.columns.get(column_id))
17973 .cloned()
17974 .ok_or_else(|| {
17975 MongrelError::InvalidArgument("SELECTED column is not available".into())
17976 }),
17977 }
17978}
17979
17980fn values_equal(left: &Value, right: &Value) -> bool {
17981 match (left, right) {
17982 (Value::Null, Value::Null) => true,
17983 (Value::Bool(a), Value::Bool(b)) => a == b,
17984 (Value::Int64(a), Value::Int64(b)) => a == b,
17985 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
17986 (Value::Bytes(a), Value::Bytes(b)) => a == b,
17987 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => {
17988 let a = a.as_embedding().unwrap();
17989 let b = b.as_embedding().unwrap();
17990 a.len() == b.len()
17991 && a.iter()
17992 .zip(b.iter())
17993 .all(|(a, b)| a.to_bits() == b.to_bits())
17994 }
17995 _ => false,
17996 }
17997}
17998
17999fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
18000 match (left, right) {
18001 (Value::Null, _) | (_, Value::Null) => None,
18002 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
18003 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
18004 (Value::Int64(a), Value::Float64(b)) => {
18007 let af = *a as f64;
18008 Some(af.total_cmp(b))
18009 }
18010 (Value::Float64(a), Value::Int64(b)) => {
18013 let bf = *b as f64;
18014 Some(a.total_cmp(&bf))
18015 }
18016 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
18017 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
18018 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => None,
18019 _ => None,
18020 }
18021}
18022
18023fn trigger_message(value: Value) -> String {
18024 match value {
18025 Value::Null => "NULL".into(),
18026 Value::Bool(value) => value.to_string(),
18027 Value::Int64(value) => value.to_string(),
18028 Value::Float64(value) => value.to_string(),
18029 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
18030 Value::Embedding(value) => format!("{value:?}"),
18031 Value::GeneratedEmbedding(value) => format!("{:?}", value.vector),
18032 Value::Decimal(value) => value.to_string(),
18033 Value::Interval {
18034 months,
18035 days,
18036 nanos,
18037 } => format!("{months}m {days}d {nanos}ns"),
18038 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
18039 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
18040 }
18041}
18042
18043fn validate_trigger_step<'a>(
18044 step: &TriggerStep,
18045 cat: &'a Catalog,
18046 target_schema: &Schema,
18047 event: TriggerEvent,
18048 select_schemas: &mut HashMap<String, &'a Schema>,
18049) -> Result<()> {
18050 match step {
18051 TriggerStep::SetNew { cells } => {
18052 if event == TriggerEvent::Delete {
18053 return Err(MongrelError::InvalidArgument(
18054 "SetNew trigger step is not valid for DELETE triggers".into(),
18055 ));
18056 }
18057 for cell in cells {
18058 validate_column_id(cell.column_id, target_schema)?;
18059 validate_trigger_value(&cell.value, target_schema, event)?;
18060 }
18061 }
18062 TriggerStep::Insert { table, cells } => {
18063 let schema = trigger_write_schema(cat, table, "insert")?;
18064 for cell in cells {
18065 validate_column_id(cell.column_id, schema)?;
18066 validate_trigger_value(&cell.value, target_schema, event)?;
18067 }
18068 }
18069 TriggerStep::UpdateByPk { table, pk, cells } => {
18070 let schema = trigger_write_schema(cat, table, "update")?;
18071 if schema.primary_key().is_none() {
18072 return Err(MongrelError::InvalidArgument(format!(
18073 "trigger update_by_pk references table {table:?} without a primary key"
18074 )));
18075 }
18076 validate_trigger_value(pk, target_schema, event)?;
18077 for cell in cells {
18078 validate_column_id(cell.column_id, schema)?;
18079 validate_trigger_value(&cell.value, target_schema, event)?;
18080 }
18081 }
18082 TriggerStep::DeleteByPk { table, pk } => {
18083 let schema = trigger_write_schema(cat, table, "delete")?;
18084 if schema.primary_key().is_none() {
18085 return Err(MongrelError::InvalidArgument(format!(
18086 "trigger delete_by_pk references table {table:?} without a primary key"
18087 )));
18088 }
18089 validate_trigger_value(pk, target_schema, event)?;
18090 }
18091 TriggerStep::Select {
18092 id,
18093 table,
18094 conditions,
18095 } => {
18096 let schema = trigger_read_schema(cat, table)?;
18097 for condition in conditions {
18098 validate_trigger_condition(condition, schema, target_schema, event)?;
18099 }
18100 if select_schemas.contains_key(id) {
18101 return Err(MongrelError::InvalidArgument(format!(
18102 "duplicate select id {id:?} in trigger program"
18103 )));
18104 }
18105 select_schemas.insert(id.clone(), schema);
18106 }
18107 TriggerStep::Foreach { id, steps } => {
18108 if !select_schemas.contains_key(id) {
18109 return Err(MongrelError::InvalidArgument(format!(
18110 "foreach references unknown select id {id:?}"
18111 )));
18112 }
18113 let mut inner_select_schemas = select_schemas.clone();
18114 for step in steps {
18115 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
18116 }
18117 }
18118 TriggerStep::DeleteWhere { table, conditions } => {
18119 let schema = trigger_write_schema(cat, table, "delete")?;
18120 for condition in conditions {
18121 validate_trigger_condition(condition, schema, target_schema, event)?;
18122 }
18123 }
18124 TriggerStep::UpdateWhere {
18125 table,
18126 conditions,
18127 cells,
18128 } => {
18129 let schema = trigger_write_schema(cat, table, "update")?;
18130 for condition in conditions {
18131 validate_trigger_condition(condition, schema, target_schema, event)?;
18132 }
18133 for cell in cells {
18134 validate_column_id(cell.column_id, schema)?;
18135 validate_trigger_value(&cell.value, target_schema, event)?;
18136 }
18137 }
18138 TriggerStep::Raise { message, .. } => {
18139 validate_trigger_value(message, target_schema, event)?
18140 }
18141 }
18142 Ok(())
18143}
18144
18145fn trigger_validation_error(error: MongrelError) -> MongrelError {
18146 match error {
18147 MongrelError::TriggerValidation(_) => error,
18148 MongrelError::InvalidArgument(message)
18149 | MongrelError::Conflict(message)
18150 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
18151 error => error,
18152 }
18153}
18154
18155fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
18156 if let Some(entry) = cat.live(table) {
18157 return Ok(&entry.schema);
18158 }
18159 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18160 let allowed = match op {
18161 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
18162 "update" | "delete" => entry.capabilities.writable,
18163 _ => false,
18164 };
18165 if !allowed {
18166 return Err(MongrelError::InvalidArgument(format!(
18167 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
18168 entry.module
18169 )));
18170 }
18171 if !entry.capabilities.transaction_safe {
18172 return Err(MongrelError::InvalidArgument(format!(
18173 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
18174 entry.module
18175 )));
18176 }
18177 return Ok(&entry.declared_schema);
18178 }
18179 Err(MongrelError::InvalidArgument(format!(
18180 "trigger references unknown table {table:?}"
18181 )))
18182}
18183
18184fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
18185 if let Some(entry) = cat.live(table) {
18186 return Ok(&entry.schema);
18187 }
18188 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18189 if entry.capabilities.trigger_safe {
18190 return Ok(&entry.declared_schema);
18191 }
18192 return Err(MongrelError::InvalidArgument(format!(
18193 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
18194 entry.module
18195 )));
18196 }
18197 Err(MongrelError::InvalidArgument(format!(
18198 "trigger references unknown table {table:?}"
18199 )))
18200}
18201
18202fn validate_trigger_condition(
18203 condition: &TriggerCondition,
18204 schema: &Schema,
18205 target_schema: &Schema,
18206 event: TriggerEvent,
18207) -> Result<()> {
18208 match condition {
18209 TriggerCondition::Pk { value } => {
18210 if schema.primary_key().is_none() {
18211 return Err(MongrelError::InvalidArgument(
18212 "trigger condition Pk references a table without a primary key".into(),
18213 ));
18214 }
18215 validate_trigger_value(value, target_schema, event)
18216 }
18217 TriggerCondition::Eq { column_id, value }
18218 | TriggerCondition::NotEq { column_id, value }
18219 | TriggerCondition::Lt { column_id, value }
18220 | TriggerCondition::Lte { column_id, value }
18221 | TriggerCondition::Gt { column_id, value }
18222 | TriggerCondition::Gte { column_id, value } => {
18223 validate_column_id(*column_id, schema)?;
18224 validate_trigger_value(value, target_schema, event)
18225 }
18226 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
18227 validate_column_id(*column_id, schema)
18228 }
18229 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
18230 validate_trigger_condition(left, schema, target_schema, event)?;
18231 validate_trigger_condition(right, schema, target_schema, event)
18232 }
18233 TriggerCondition::Not(condition) => {
18234 validate_trigger_condition(condition, schema, target_schema, event)
18235 }
18236 }
18237}
18238
18239fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
18240 match expr {
18241 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
18242 validate_trigger_value(value, schema, event)
18243 }
18244 TriggerExpr::Eq { left, right }
18245 | TriggerExpr::NotEq { left, right }
18246 | TriggerExpr::Lt { left, right }
18247 | TriggerExpr::Lte { left, right }
18248 | TriggerExpr::Gt { left, right }
18249 | TriggerExpr::Gte { left, right } => {
18250 validate_trigger_value(left, schema, event)?;
18251 validate_trigger_value(right, schema, event)
18252 }
18253 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
18254 validate_trigger_expr(left, schema, event)?;
18255 validate_trigger_expr(right, schema, event)
18256 }
18257 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
18258 }
18259}
18260
18261fn validate_trigger_value(
18262 value: &TriggerValue,
18263 schema: &Schema,
18264 event: TriggerEvent,
18265) -> Result<()> {
18266 match value {
18267 TriggerValue::Literal(_) => Ok(()),
18268 TriggerValue::NewColumn(id) => {
18269 if event == TriggerEvent::Delete {
18270 return Err(MongrelError::InvalidArgument(
18271 "DELETE triggers cannot reference NEW".into(),
18272 ));
18273 }
18274 validate_column_id(*id, schema)
18275 }
18276 TriggerValue::OldColumn(id) => {
18277 if event == TriggerEvent::Insert {
18278 return Err(MongrelError::InvalidArgument(
18279 "INSERT triggers cannot reference OLD".into(),
18280 ));
18281 }
18282 validate_column_id(*id, schema)
18283 }
18284 TriggerValue::SelectedColumn(_) => Ok(()),
18288 }
18289}
18290
18291const COMMIT_TS_LEDGER_CAP: usize = 10_000;
18296
18297fn commit_ts_ledger_from_recovery(
18305 records: &[crate::wal::Record],
18306) -> std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp> {
18307 use crate::wal::Op;
18308 let mut commits = HashMap::new();
18309 let mut timestamps = HashMap::new();
18310 for record in records {
18311 match &record.op {
18312 Op::TxnCommit { epoch, .. } => {
18313 commits.insert(record.txn_id, *epoch);
18314 }
18315 Op::CommitTimestamp { unix_nanos } => {
18316 timestamps.insert(record.txn_id, *unix_nanos);
18317 }
18318 _ => {}
18319 }
18320 }
18321 let mut ledger = std::collections::BTreeMap::new();
18322 for (txn_id, epoch) in commits {
18323 let Some(unix_nanos) = timestamps.get(&txn_id) else {
18324 continue;
18325 };
18326 ledger.insert(
18327 epoch,
18328 mongreldb_types::hlc::HlcTimestamp {
18329 physical_micros: unix_nanos / 1_000,
18330 logical: 0,
18331 node_tiebreaker: 0,
18332 },
18333 );
18334 }
18335 while ledger.len() > COMMIT_TS_LEDGER_CAP {
18336 ledger.pop_first();
18337 }
18338 ledger
18339}
18340
18341fn recover_ddl_from_wal(
18347 root: &Path,
18348 durable_root: Option<&crate::durable_file::DurableRoot>,
18349 target_catalog: &mut Catalog,
18350 meta_dek: Option<&[u8; META_DEK_LEN]>,
18351 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
18352 apply: bool,
18353 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18354) -> Result<()> {
18355 use crate::wal::SharedWal;
18356 let records = match durable_root {
18357 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
18358 None => SharedWal::replay_with_dek(root, wal_dek)?,
18359 };
18360 recover_ddl_from_records(
18361 root,
18362 durable_root,
18363 target_catalog,
18364 meta_dek,
18365 apply,
18366 table_roots,
18367 &records,
18368 )
18369}
18370
18371fn recover_ddl_from_records(
18372 root: &Path,
18373 durable_root: Option<&crate::durable_file::DurableRoot>,
18374 target_catalog: &mut Catalog,
18375 meta_dek: Option<&[u8; META_DEK_LEN]>,
18376 apply: bool,
18377 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18378 records: &[crate::wal::Record],
18379) -> Result<()> {
18380 use crate::wal::{DdlOp, Op};
18381
18382 let original_catalog = target_catalog.clone();
18383 let mut recovered_catalog = original_catalog.clone();
18384 let cat = &mut recovered_catalog;
18385 let mut created_table_ids = HashSet::<u64>::new();
18386 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
18387
18388 let mut committed: HashMap<u64, u64> = HashMap::new();
18389 for r in records {
18390 if let Op::TxnCommit { epoch: ce, .. } = r.op {
18391 committed.insert(r.txn_id, ce);
18392 }
18393 }
18394 let catalog_snapshot_txns = records
18395 .iter()
18396 .filter_map(|record| {
18397 (committed.contains_key(&record.txn_id)
18398 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
18399 .then_some(record.txn_id)
18400 })
18401 .collect::<HashSet<_>>();
18402
18403 let mut changed = false;
18404 let mut applied_catalog_epoch = cat.db_epoch;
18405 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
18406 for r in records.iter().cloned() {
18407 let Some(&ce) = committed.get(&r.txn_id) else {
18408 continue;
18409 };
18410 let txn_id = r.txn_id;
18411 match r.op {
18412 Op::Ddl(DdlOp::CreateTable {
18413 table_id,
18414 ref name,
18415 ref schema_json,
18416 }) => {
18417 if cat.tables.iter().any(|t| t.table_id == table_id) {
18418 continue;
18419 }
18420 let schema = DdlOp::decode_schema(schema_json)?;
18421 validate_recovered_schema(&schema)?;
18422 created_table_ids.insert(table_id);
18423 cat.tables.push(CatalogEntry {
18424 table_id,
18425 name: name.clone(),
18426 schema,
18427 state: TableState::Live,
18428 created_epoch: ce,
18429 });
18430 cat.next_table_id =
18431 cat.next_table_id
18432 .max(table_id.checked_add(1).ok_or_else(|| {
18433 MongrelError::Full("table id namespace exhausted".into())
18434 })?);
18435 changed = true;
18436 }
18437 Op::Ddl(DdlOp::CreateBuildingTable {
18438 table_id,
18439 ref build_name,
18440 ref intended_name,
18441 ref query_id,
18442 created_at_unix_nanos,
18443 ref schema_json,
18444 }) => {
18445 if cat.tables.iter().any(|table| table.table_id == table_id) {
18446 continue;
18447 }
18448 let schema = DdlOp::decode_schema(schema_json)?;
18449 validate_recovered_schema(&schema)?;
18450 created_table_ids.insert(table_id);
18451 cat.tables.push(CatalogEntry {
18452 table_id,
18453 name: build_name.clone(),
18454 schema,
18455 state: TableState::Building {
18456 intended_name: intended_name.clone(),
18457 query_id: query_id.clone(),
18458 created_at_unix_nanos,
18459 replaces_table_id: None,
18460 },
18461 created_epoch: ce,
18462 });
18463 cat.next_table_id =
18464 cat.next_table_id
18465 .max(table_id.checked_add(1).ok_or_else(|| {
18466 MongrelError::Full("table id namespace exhausted".into())
18467 })?);
18468 changed = true;
18469 }
18470 Op::Ddl(DdlOp::CreateRebuildingTable {
18471 table_id,
18472 ref build_name,
18473 ref intended_name,
18474 ref query_id,
18475 created_at_unix_nanos,
18476 replaces_table_id,
18477 ref schema_json,
18478 }) => {
18479 if cat.tables.iter().any(|table| table.table_id == table_id) {
18480 continue;
18481 }
18482 let schema = DdlOp::decode_schema(schema_json)?;
18483 validate_recovered_schema(&schema)?;
18484 created_table_ids.insert(table_id);
18485 cat.tables.push(CatalogEntry {
18486 table_id,
18487 name: build_name.clone(),
18488 schema,
18489 state: TableState::Building {
18490 intended_name: intended_name.clone(),
18491 query_id: query_id.clone(),
18492 created_at_unix_nanos,
18493 replaces_table_id: Some(replaces_table_id),
18494 },
18495 created_epoch: ce,
18496 });
18497 cat.next_table_id =
18498 cat.next_table_id
18499 .max(table_id.checked_add(1).ok_or_else(|| {
18500 MongrelError::Full("table id namespace exhausted".into())
18501 })?);
18502 changed = true;
18503 }
18504 Op::Ddl(DdlOp::DropTable { table_id }) => {
18505 let mut dropped_name = None;
18506 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18507 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18508 dropped_name = Some(entry.name.clone());
18509 entry.state = TableState::Dropped { at_epoch: ce };
18510 changed = true;
18511 }
18512 }
18513 if let Some(name) = dropped_name {
18514 let before = cat.materialized_views.len();
18515 cat.materialized_views
18516 .retain(|definition| definition.name != name);
18517 changed |= before != cat.materialized_views.len();
18518 cat.security.rls_tables.retain(|table| table != &name);
18519 cat.security.policies.retain(|policy| policy.table != name);
18520 cat.security.masks.retain(|mask| mask.table != name);
18521 for role in &mut cat.roles {
18522 role.permissions
18523 .retain(|permission| permission_table(permission) != Some(&name));
18524 }
18525 if !catalog_snapshot_txns.contains(&txn_id) {
18526 advance_security_version(cat)?;
18527 }
18528 }
18529 }
18530 Op::Ddl(DdlOp::PublishBuildingTable {
18531 table_id,
18532 ref new_name,
18533 }) => {
18534 if let Some(entry) = cat
18535 .tables
18536 .iter_mut()
18537 .find(|table| table.table_id == table_id)
18538 {
18539 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
18540 entry.name = new_name.clone();
18541 entry.state = TableState::Live;
18542 changed = true;
18543 }
18544 }
18545 }
18546 Op::Ddl(DdlOp::ReplaceBuildingTable {
18547 table_id,
18548 replaced_table_id,
18549 ref new_name,
18550 }) => {
18551 changed |=
18552 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
18553 }
18554 Op::Ddl(DdlOp::RenameTable {
18555 table_id,
18556 ref new_name,
18557 }) => {
18558 let mut old_name = None;
18559 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18560 if entry.name != *new_name {
18561 old_name = Some(entry.name.clone());
18562 entry.name = new_name.clone();
18563 changed = true;
18564 }
18565 }
18566 if let Some(old_name) = old_name {
18567 if let Some(definition) = cat
18568 .materialized_views
18569 .iter_mut()
18570 .find(|definition| definition.name == old_name)
18571 {
18572 definition.name = new_name.clone();
18573 }
18574 for table in &mut cat.security.rls_tables {
18575 if *table == old_name {
18576 *table = new_name.clone();
18577 }
18578 }
18579 for policy in &mut cat.security.policies {
18580 if policy.table == old_name {
18581 policy.table = new_name.clone();
18582 }
18583 }
18584 for mask in &mut cat.security.masks {
18585 if mask.table == old_name {
18586 mask.table = new_name.clone();
18587 }
18588 }
18589 for role in &mut cat.roles {
18590 for permission in &mut role.permissions {
18591 rename_permission_table(permission, &old_name, new_name);
18592 }
18593 }
18594 if !catalog_snapshot_txns.contains(&txn_id) {
18595 advance_security_version(cat)?;
18596 }
18597 }
18598 }
18602 Op::Ddl(DdlOp::AlterTable {
18603 table_id,
18604 ref column_json,
18605 }) => {
18606 let column = DdlOp::decode_column(column_json)?;
18607 let mut renamed = None;
18608 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18609 renamed = entry
18610 .schema
18611 .columns
18612 .iter()
18613 .find(|existing| existing.id == column.id && existing.name != column.name)
18614 .map(|existing| {
18615 (
18616 entry.name.clone(),
18617 existing.name.clone(),
18618 column.name.clone(),
18619 )
18620 });
18621 if apply_recovered_column_def(&mut entry.schema, column)? {
18622 validate_recovered_schema(&entry.schema)?;
18623 changed = true;
18624 }
18625 }
18626 if let Some((table, old_name, new_name)) = renamed {
18627 for role in &mut cat.roles {
18628 for permission in &mut role.permissions {
18629 rename_permission_column(permission, &table, &old_name, &new_name);
18630 }
18631 }
18632 if !catalog_snapshot_txns.contains(&txn_id) {
18633 advance_security_version(cat)?;
18634 }
18635 }
18636 }
18637 Op::Ddl(DdlOp::SetTtl {
18638 table_id,
18639 ref policy_json,
18640 }) => {
18641 let policy = DdlOp::decode_ttl(policy_json)?;
18642 let entry = cat
18643 .tables
18644 .iter()
18645 .find(|entry| entry.table_id == table_id)
18646 .ok_or_else(|| {
18647 MongrelError::Schema(format!(
18648 "recovered TTL references unknown table id {table_id}"
18649 ))
18650 })?;
18651 if let Some(policy) = policy {
18652 let valid = entry
18653 .schema
18654 .columns
18655 .iter()
18656 .find(|column| column.id == policy.column_id)
18657 .is_some_and(|column| {
18658 column.ty == TypeId::TimestampNanos
18659 && policy.duration_nanos > 0
18660 && policy.duration_nanos <= i64::MAX as u64
18661 });
18662 if !valid {
18663 return Err(MongrelError::Schema(format!(
18664 "invalid recovered TTL policy for table id {table_id}"
18665 )));
18666 }
18667 }
18668 ttl_updates.insert(table_id, (policy, ce));
18669 }
18670 Op::Ddl(DdlOp::SetMaterializedView {
18671 ref name,
18672 ref definition_json,
18673 }) => {
18674 let definition = DdlOp::decode_materialized_view(definition_json)?;
18675 if definition.name != *name {
18676 return Err(MongrelError::Schema(format!(
18677 "materialized view WAL name mismatch: {name:?}"
18678 )));
18679 }
18680 if cat.live(name).is_some() {
18681 if let Some(existing) = cat
18682 .materialized_views
18683 .iter_mut()
18684 .find(|existing| existing.name == *name)
18685 {
18686 if *existing != definition {
18687 *existing = definition;
18688 changed = true;
18689 }
18690 } else {
18691 cat.materialized_views.push(definition);
18692 changed = true;
18693 }
18694 }
18695 }
18696 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
18697 let security = DdlOp::decode_security(security_json)?;
18698 validate_security_catalog(cat, &security)?;
18699 if cat.security != security {
18700 cat.security = security;
18701 if !catalog_snapshot_txns.contains(&txn_id) {
18702 advance_security_version(cat)?;
18703 }
18704 changed = true;
18705 }
18706 }
18707 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
18708 let target = match key.as_str() {
18709 "user_version" => &mut cat.user_version,
18710 "application_id" => &mut cat.application_id,
18711 _ => {
18712 return Err(MongrelError::InvalidArgument(format!(
18713 "unsupported recovered SQL pragma {key:?}"
18714 )))
18715 }
18716 };
18717 if *target != Some(value) {
18718 *target = Some(value);
18719 cat.db_epoch = cat.db_epoch.max(ce);
18720 changed = true;
18721 }
18722 }
18723 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
18724 if ce <= applied_catalog_epoch {
18725 continue;
18726 }
18727 let snapshot = DdlOp::decode_catalog(catalog_json)?;
18728 if snapshot.db_epoch != ce {
18729 return Err(MongrelError::Schema(format!(
18730 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
18731 snapshot.db_epoch
18732 )));
18733 }
18734 validate_recovered_catalog(&snapshot)?;
18735 validate_catalog_transition(cat, &snapshot)?;
18736 *cat = snapshot;
18737 applied_catalog_epoch = ce;
18738 changed = true;
18739 }
18740 _ => {}
18741 }
18742 }
18743
18744 if cat.db_epoch < max_committed_epoch {
18745 cat.db_epoch = max_committed_epoch;
18746 changed = true;
18747 }
18748 changed |= repair_catalog_allocator_counters(cat)?;
18749
18750 validate_recovered_catalog(cat)?;
18751 let storage_reconciliation = validate_recovered_storage_plan(
18752 root,
18753 durable_root,
18754 cat,
18755 &created_table_ids,
18756 &ttl_updates,
18757 meta_dek,
18758 )?;
18759
18760 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
18761 if apply && (changed || needs_storage_apply) {
18762 for table_id in storage_reconciliation {
18763 let entry = cat
18764 .tables
18765 .iter()
18766 .find(|entry| entry.table_id == table_id)
18767 .ok_or_else(|| MongrelError::CorruptWal {
18768 offset: table_id,
18769 reason: "recovery storage plan lost its catalog table".into(),
18770 })?;
18771 ensure_recovered_table_storage(
18772 table_roots
18773 .and_then(|roots| roots.get(&table_id))
18774 .map(Arc::as_ref),
18775 durable_root,
18776 &root.join(TABLES_DIR).join(table_id.to_string()),
18777 table_id,
18778 &entry.schema,
18779 meta_dek,
18780 )?;
18781 }
18782 for (table_id, (policy, ttl_epoch)) in ttl_updates {
18783 let Some(entry) = cat.tables.iter().find(|entry| {
18784 entry.table_id == table_id
18785 && matches!(entry.state, TableState::Live | TableState::Building { .. })
18786 }) else {
18787 continue;
18788 };
18789 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
18790 {
18791 root.try_clone()?
18792 } else if let Some(root) = durable_root {
18793 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
18794 } else {
18795 crate::durable_file::DurableRoot::open(
18796 root.join(TABLES_DIR).join(table_id.to_string()),
18797 )?
18798 };
18799 let table_dir = table_root.io_path()?;
18800 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
18801 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
18802 manifest.ttl = policy;
18803 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
18804 manifest.schema_id = entry.schema.schema_id;
18805 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18806 }
18807 }
18808 if changed {
18809 match durable_root {
18810 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
18811 None => catalog::write_atomic(root, cat, meta_dek)?,
18812 }
18813 }
18814 }
18815 *target_catalog = recovered_catalog;
18816 Ok(())
18817}
18818
18819fn ensure_recovered_table_storage(
18820 pinned_table: Option<&crate::durable_file::DurableRoot>,
18821 durable_root: Option<&crate::durable_file::DurableRoot>,
18822 fallback_table_dir: &Path,
18823 table_id: u64,
18824 schema: &Schema,
18825 meta_dek: Option<&[u8; META_DEK_LEN]>,
18826) -> Result<()> {
18827 let table_root = if let Some(root) = pinned_table {
18828 root.try_clone()?
18829 } else if let Some(root) = durable_root {
18830 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
18831 match root.open_directory(&relative) {
18832 Ok(table) => table,
18833 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
18834 root.create_directory_all_pinned(relative)?
18835 }
18836 Err(error) => return Err(error.into()),
18837 }
18838 } else {
18839 crate::durable_file::create_directory_all(fallback_table_dir)?;
18840 crate::durable_file::DurableRoot::open(fallback_table_dir)?
18841 };
18842 let table_dir = table_root.io_path()?;
18843 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
18844 Ok(manifest) => {
18845 if manifest.table_id != table_id {
18846 return Err(MongrelError::Conflict(format!(
18847 "recovered table directory id mismatch: expected {table_id}, found {}",
18848 manifest.table_id
18849 )));
18850 }
18851 Some(manifest)
18852 }
18853 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
18854 Err(error) => return Err(error),
18855 };
18856
18857 table_root.create_directory_all(crate::engine::WAL_DIR)?;
18858 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
18859 crate::engine::write_schema(&table_dir, schema)?;
18860
18861 if let Some(mut manifest) = existing_manifest.take() {
18862 if manifest.schema_id != schema.schema_id {
18863 manifest.schema_id = schema.schema_id;
18864 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18865 }
18866 } else {
18867 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
18869 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18870 }
18871 Ok(())
18872}
18873
18874fn validate_recovered_schema(schema: &Schema) -> Result<()> {
18875 schema.validate_auto_increment()?;
18876 schema.validate_defaults()?;
18877 schema.validate_ai()?;
18878 let mut column_ids = HashSet::new();
18879 let mut column_names = HashSet::new();
18880 for column in &schema.columns {
18881 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
18882 return Err(MongrelError::Schema(
18883 "recovered schema contains duplicate columns".into(),
18884 ));
18885 }
18886 match &column.ty {
18887 TypeId::Decimal128 { precision, scale }
18888 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
18889 {
18890 return Err(MongrelError::Schema(format!(
18891 "column {:?} has invalid decimal precision or scale",
18892 column.name
18893 )));
18894 }
18895 TypeId::Enum { variants }
18896 if variants.is_empty()
18897 || variants.iter().any(String::is_empty)
18898 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
18899 {
18900 return Err(MongrelError::Schema(format!(
18901 "column {:?} has invalid enum variants",
18902 column.name
18903 )));
18904 }
18905 _ => {}
18906 }
18907 }
18908 let mut index_names = HashSet::new();
18909 for index in &schema.indexes {
18910 index.validate_options()?;
18911 if index.name.is_empty()
18912 || !index_names.insert(index.name.as_str())
18913 || schema
18914 .columns
18915 .iter()
18916 .all(|column| column.id != index.column_id)
18917 {
18918 return Err(MongrelError::Schema(format!(
18919 "recovered index {:?} references missing column {}",
18920 index.name, index.column_id
18921 )));
18922 }
18923 }
18924 let mut colocated = HashSet::new();
18925 for group in &schema.colocation {
18926 if group.is_empty()
18927 || group.iter().any(|id| !column_ids.contains(id))
18928 || group.iter().any(|id| !colocated.insert(*id))
18929 {
18930 return Err(MongrelError::Schema(
18931 "recovered schema contains invalid column co-location groups".into(),
18932 ));
18933 }
18934 }
18935
18936 let mut constraint_ids = HashSet::new();
18937 let mut constraint_names = HashSet::<String>::new();
18938 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
18939 if name.is_empty()
18940 || !constraint_ids.insert(id)
18941 || !constraint_names.insert(name.to_owned())
18942 {
18943 return Err(MongrelError::Schema(
18944 "recovered schema contains duplicate or empty constraint identities".into(),
18945 ));
18946 }
18947 Ok(())
18948 };
18949 for unique in &schema.constraints.uniques {
18950 validate_constraint_identity(unique.id, &unique.name)?;
18951 if unique.columns.is_empty()
18952 || unique.columns.iter().any(|id| !column_ids.contains(id))
18953 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
18954 {
18955 return Err(MongrelError::Schema(format!(
18956 "unique constraint {:?} has invalid columns",
18957 unique.name
18958 )));
18959 }
18960 }
18961 for foreign_key in &schema.constraints.foreign_keys {
18962 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
18963 if foreign_key.ref_table.is_empty()
18964 || foreign_key.columns.is_empty()
18965 || foreign_key.columns.len() != foreign_key.ref_columns.len()
18966 || foreign_key
18967 .columns
18968 .iter()
18969 .any(|id| !column_ids.contains(id))
18970 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
18971 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
18972 != foreign_key.ref_columns.len()
18973 {
18974 return Err(MongrelError::Schema(format!(
18975 "foreign key {:?} has invalid columns",
18976 foreign_key.name
18977 )));
18978 }
18979 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
18980 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
18981 && foreign_key.columns.iter().any(|id| {
18982 schema
18983 .columns
18984 .iter()
18985 .find(|column| column.id == *id)
18986 .is_none_or(|column| {
18987 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
18988 })
18989 })
18990 {
18991 return Err(MongrelError::Schema(format!(
18992 "foreign key {:?} uses SET NULL on a non-nullable column",
18993 foreign_key.name
18994 )));
18995 }
18996 }
18997 for check in &schema.constraints.checks {
18998 validate_constraint_identity(check.id, &check.name)?;
18999 check.expr.validate()?;
19000 validate_check_columns(&check.expr, &column_ids)?;
19001 }
19002 Ok(())
19003}
19004
19005fn validate_check_columns(
19006 expression: &crate::constraint::CheckExpr,
19007 column_ids: &HashSet<u16>,
19008) -> Result<()> {
19009 use crate::constraint::CheckExpr;
19010 match expression {
19011 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
19012 if column_ids.contains(id) {
19013 Ok(())
19014 } else {
19015 Err(MongrelError::Schema(format!(
19016 "check constraint references unknown column {id}"
19017 )))
19018 }
19019 }
19020 CheckExpr::Regex { col, .. } => {
19021 if column_ids.contains(col) {
19022 Ok(())
19023 } else {
19024 Err(MongrelError::Schema(format!(
19025 "check constraint references unknown column {col}"
19026 )))
19027 }
19028 }
19029 CheckExpr::Add(left, right)
19030 | CheckExpr::Sub(left, right)
19031 | CheckExpr::Mul(left, right)
19032 | CheckExpr::Div(left, right)
19033 | CheckExpr::Mod(left, right)
19034 | CheckExpr::Eq(left, right)
19035 | CheckExpr::Ne(left, right)
19036 | CheckExpr::Lt(left, right)
19037 | CheckExpr::Le(left, right)
19038 | CheckExpr::Gt(left, right)
19039 | CheckExpr::Ge(left, right)
19040 | CheckExpr::And(left, right)
19041 | CheckExpr::Or(left, right) => {
19042 validate_check_columns(left, column_ids)?;
19043 validate_check_columns(right, column_ids)
19044 }
19045 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
19046 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
19047 }
19048}
19049
19050fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
19051 for (name, prior, candidate) in [
19052 ("db_epoch", current.db_epoch, next.db_epoch),
19053 ("next_table_id", current.next_table_id, next.next_table_id),
19054 (
19055 "next_segment_no",
19056 current.next_segment_no,
19057 next.next_segment_no,
19058 ),
19059 ("next_user_id", current.next_user_id, next.next_user_id),
19060 (
19061 "security_version",
19062 current.security_version,
19063 next.security_version,
19064 ),
19065 ] {
19066 if candidate < prior {
19067 return Err(MongrelError::Schema(format!(
19068 "catalog snapshot rolls back {name} from {prior} to {candidate}"
19069 )));
19070 }
19071 }
19072 for prior in ¤t.tables {
19073 let Some(candidate) = next
19074 .tables
19075 .iter()
19076 .find(|entry| entry.table_id == prior.table_id)
19077 else {
19078 return Err(MongrelError::Schema(format!(
19079 "catalog snapshot removes table identity {}",
19080 prior.table_id
19081 )));
19082 };
19083 if candidate.created_epoch != prior.created_epoch
19084 || candidate.schema.schema_id < prior.schema.schema_id
19085 || matches!(prior.state, TableState::Dropped { .. })
19086 && !matches!(candidate.state, TableState::Dropped { .. })
19087 {
19088 return Err(MongrelError::Schema(format!(
19089 "catalog snapshot rolls back table identity {}",
19090 prior.table_id
19091 )));
19092 }
19093 }
19094 for prior in ¤t.users {
19095 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
19096 if candidate.username != prior.username
19097 || candidate.created_epoch != prior.created_epoch
19098 {
19099 return Err(MongrelError::Schema(format!(
19100 "catalog snapshot reuses user identity {}",
19101 prior.id
19102 )));
19103 }
19104 }
19105 }
19106 Ok(())
19107}
19108
19109fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
19110 let mut table_ids = HashSet::new();
19111 let mut active_names = HashSet::new();
19112 let mut max_table_id = None::<u64>;
19113 for entry in &catalog.tables {
19114 if !table_ids.insert(entry.table_id) {
19115 return Err(MongrelError::Schema(format!(
19116 "catalog contains duplicate table id {}",
19117 entry.table_id
19118 )));
19119 }
19120 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
19121 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
19122 return Err(MongrelError::Schema(format!(
19123 "catalog table {} has invalid name or creation epoch",
19124 entry.table_id
19125 )));
19126 }
19127 validate_recovered_schema(&entry.schema)?;
19128 match &entry.state {
19129 TableState::Live => {
19130 if !active_names.insert(entry.name.as_str()) {
19131 return Err(MongrelError::Schema(format!(
19132 "catalog contains duplicate active table name {:?}",
19133 entry.name
19134 )));
19135 }
19136 }
19137 TableState::Dropped { at_epoch } => {
19138 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
19139 return Err(MongrelError::Schema(format!(
19140 "catalog table {} has invalid drop epoch {at_epoch}",
19141 entry.table_id
19142 )));
19143 }
19144 }
19145 TableState::Building {
19146 intended_name,
19147 query_id,
19148 replaces_table_id,
19149 ..
19150 } => {
19151 if intended_name.is_empty() || query_id.is_empty() {
19152 return Err(MongrelError::Schema(format!(
19153 "building table {} has empty identity fields",
19154 entry.table_id
19155 )));
19156 }
19157 if !active_names.insert(entry.name.as_str()) {
19158 return Err(MongrelError::Schema(format!(
19159 "catalog contains duplicate active/building table name {:?}",
19160 entry.name
19161 )));
19162 }
19163 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
19164 return Err(MongrelError::Schema(
19165 "building table cannot replace itself".into(),
19166 ));
19167 }
19168 }
19169 }
19170 }
19171 if let Some(maximum) = max_table_id {
19172 let required = maximum
19173 .checked_add(1)
19174 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19175 if catalog.next_table_id < required {
19176 return Err(MongrelError::Schema(format!(
19177 "catalog next_table_id {} precedes required {required}",
19178 catalog.next_table_id
19179 )));
19180 }
19181 }
19182 for entry in &catalog.tables {
19183 if let TableState::Building {
19184 replaces_table_id: Some(replaced),
19185 ..
19186 } = entry.state
19187 {
19188 if !table_ids.contains(&replaced) {
19189 return Err(MongrelError::Schema(format!(
19190 "building table {} replaces unknown table {replaced}",
19191 entry.table_id
19192 )));
19193 }
19194 }
19195 }
19196 for entry in &catalog.tables {
19197 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19198 validate_foreign_key_targets(catalog, &entry.schema)?;
19199 }
19200 }
19201
19202 let mut external_names = HashSet::new();
19203 for entry in &catalog.external_tables {
19204 entry.validate()?;
19205 validate_recovered_schema(&entry.declared_schema)?;
19206 if !entry.declared_schema.constraints.is_empty() {
19207 return Err(MongrelError::Schema(format!(
19208 "external table {:?} cannot carry engine-enforced constraints",
19209 entry.name
19210 )));
19211 }
19212 if entry.created_epoch > catalog.db_epoch
19213 || !external_names.insert(entry.name.as_str())
19214 || active_names.contains(entry.name.as_str())
19215 {
19216 return Err(MongrelError::Schema(format!(
19217 "invalid or duplicate external table {:?}",
19218 entry.name
19219 )));
19220 }
19221 }
19222
19223 let mut procedure_names = HashSet::new();
19224 for entry in &catalog.procedures {
19225 entry.procedure.validate()?;
19226 if entry.procedure.created_epoch > entry.procedure.updated_epoch
19227 || entry.procedure.updated_epoch > catalog.db_epoch
19228 || !procedure_names.insert(entry.procedure.name.as_str())
19229 {
19230 return Err(MongrelError::Schema(format!(
19231 "invalid or duplicate procedure {:?}",
19232 entry.procedure.name
19233 )));
19234 }
19235 validate_recovered_procedure_references(catalog, &entry.procedure)?;
19236 }
19237
19238 let mut trigger_names = HashSet::new();
19239 for entry in &catalog.triggers {
19240 entry.trigger.validate()?;
19241 if entry.trigger.created_epoch > entry.trigger.updated_epoch
19242 || entry.trigger.updated_epoch > catalog.db_epoch
19243 || !trigger_names.insert(entry.trigger.name.as_str())
19244 {
19245 return Err(MongrelError::Schema(format!(
19246 "invalid or duplicate trigger {:?}",
19247 entry.trigger.name
19248 )));
19249 }
19250 validate_recovered_trigger_references(catalog, &entry.trigger)?;
19251 }
19252
19253 let mut views = HashSet::new();
19254 for view in &catalog.materialized_views {
19255 let target = catalog.live(&view.name).ok_or_else(|| {
19256 MongrelError::Schema(format!(
19257 "materialized view {:?} has no live table",
19258 view.name
19259 ))
19260 })?;
19261 if view.name.is_empty()
19262 || view.query.trim().is_empty()
19263 || view.last_refresh_epoch > catalog.db_epoch
19264 || !views.insert(view.name.as_str())
19265 {
19266 return Err(MongrelError::Schema(format!(
19267 "materialized view {:?} has no unique live table",
19268 view.name
19269 )));
19270 }
19271 if let Some(incremental) = &view.incremental {
19272 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
19273 MongrelError::Schema(format!(
19274 "materialized view {:?} references missing source {:?}",
19275 view.name, incremental.source_table
19276 ))
19277 })?;
19278 if source.table_id != incremental.source_table_id
19279 || source
19280 .schema
19281 .columns
19282 .iter()
19283 .all(|column| column.id != incremental.group_column)
19284 {
19285 return Err(MongrelError::Schema(format!(
19286 "materialized view {:?} has invalid incremental source",
19287 view.name
19288 )));
19289 }
19290 let target_ids = target
19291 .schema
19292 .columns
19293 .iter()
19294 .map(|column| column.id)
19295 .collect::<HashSet<_>>();
19296 let mut output_ids = HashSet::new();
19297 let count_outputs = incremental
19298 .outputs
19299 .iter()
19300 .filter(|output| {
19301 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19302 })
19303 .count();
19304 if incremental.checkpoint_event_id.is_empty()
19305 || !target_ids.contains(&incremental.group_output_column)
19306 || !target_ids.contains(&incremental.count_output_column)
19307 || incremental.outputs.is_empty()
19308 || count_outputs != 1
19309 || incremental.outputs.iter().any(|output| {
19310 !target_ids.contains(&output.output_column)
19311 || output.output_column == incremental.group_output_column
19312 || !output_ids.insert(output.output_column)
19313 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19314 && output.output_column != incremental.count_output_column
19315 || match output.kind {
19316 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
19317 source
19318 .schema
19319 .columns
19320 .iter()
19321 .all(|column| column.id != source_column)
19322 }
19323 crate::catalog::IncrementalAggregateKind::Count => false,
19324 }
19325 })
19326 {
19327 return Err(MongrelError::Schema(format!(
19328 "materialized view {:?} has invalid incremental outputs",
19329 view.name
19330 )));
19331 }
19332 }
19333 }
19334
19335 validate_security_catalog(catalog, &catalog.security)?;
19336 validate_recovered_auth_catalog(catalog)?;
19337 Ok(())
19338}
19339
19340fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
19341 let mut changed = false;
19342 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
19343 let required = maximum
19344 .checked_add(1)
19345 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19346 if catalog.next_table_id < required {
19347 catalog.next_table_id = required;
19348 changed = true;
19349 }
19350 }
19351 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
19352 let required = maximum
19353 .checked_add(1)
19354 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
19355 if catalog.next_user_id < required {
19356 catalog.next_user_id = required;
19357 changed = true;
19358 }
19359 }
19360 Ok(changed)
19361}
19362
19363fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
19364 for foreign_key in &schema.constraints.foreign_keys {
19365 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
19366 MongrelError::Schema(format!(
19367 "foreign key {:?} references unknown live table {:?}",
19368 foreign_key.name, foreign_key.ref_table
19369 ))
19370 })?;
19371 let referenced_unique = parent
19372 .schema
19373 .constraints
19374 .uniques
19375 .iter()
19376 .any(|unique| unique.columns == foreign_key.ref_columns)
19377 || foreign_key.ref_columns.len() == 1
19378 && parent
19379 .schema
19380 .primary_key()
19381 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
19382 if !referenced_unique {
19383 return Err(MongrelError::Schema(format!(
19384 "foreign key {:?} does not reference a unique key",
19385 foreign_key.name
19386 )));
19387 }
19388 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
19389 let local = schema.columns.iter().find(|column| column.id == *local_id);
19390 let referenced = parent
19391 .schema
19392 .columns
19393 .iter()
19394 .find(|column| column.id == *parent_id);
19395 if local
19396 .zip(referenced)
19397 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
19398 {
19399 return Err(MongrelError::Schema(format!(
19400 "foreign key {:?} has missing or incompatible columns",
19401 foreign_key.name
19402 )));
19403 }
19404 }
19405 }
19406 Ok(())
19407}
19408
19409fn validate_recovered_procedure_references(
19410 catalog: &Catalog,
19411 procedure: &StoredProcedure,
19412) -> Result<()> {
19413 for step in &procedure.body.steps {
19414 let Some(table_name) = step.table() else {
19415 continue;
19416 };
19417 let schema = &catalog
19418 .live(table_name)
19419 .ok_or_else(|| {
19420 MongrelError::Schema(format!(
19421 "procedure {:?} references unknown table {table_name:?}",
19422 procedure.name
19423 ))
19424 })?
19425 .schema;
19426 match step {
19427 ProcedureStep::NativeQuery {
19428 conditions,
19429 projection,
19430 ..
19431 } => {
19432 for condition in conditions {
19433 validate_condition_columns(condition, schema)?;
19434 }
19435 for id in projection.iter().flatten() {
19436 validate_column_id(*id, schema)?;
19437 }
19438 }
19439 ProcedureStep::Put { cells, .. } => {
19440 for cell in cells {
19441 validate_column_id(cell.column_id, schema)?;
19442 }
19443 }
19444 ProcedureStep::Upsert {
19445 cells,
19446 update_cells,
19447 ..
19448 } => {
19449 for cell in cells.iter().chain(update_cells.iter().flatten()) {
19450 validate_column_id(cell.column_id, schema)?;
19451 }
19452 }
19453 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
19454 return Err(MongrelError::Schema(format!(
19455 "procedure {:?} deletes by primary key on table without one",
19456 procedure.name
19457 )));
19458 }
19459 ProcedureStep::DeleteByPk { .. }
19460 | ProcedureStep::DeleteRows { .. }
19461 | ProcedureStep::SqlQuery { .. } => {}
19462 }
19463 }
19464 Ok(())
19465}
19466
19467fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
19468 let target_schema = match &trigger.target {
19469 TriggerTarget::Table(name) => catalog
19470 .live(name)
19471 .ok_or_else(|| {
19472 MongrelError::Schema(format!(
19473 "trigger {:?} references unknown table {name:?}",
19474 trigger.name
19475 ))
19476 })?
19477 .schema
19478 .clone(),
19479 TriggerTarget::View(_) => Schema {
19480 columns: trigger.target_columns.clone(),
19481 ..Schema::default()
19482 },
19483 };
19484 for column in &trigger.update_of {
19485 if target_schema.column(column).is_none() {
19486 return Err(MongrelError::Schema(format!(
19487 "trigger {:?} references unknown UPDATE OF column {column:?}",
19488 trigger.name
19489 )));
19490 }
19491 }
19492 if let Some(expr) = &trigger.when {
19493 validate_trigger_expr(expr, &target_schema, trigger.event)?;
19494 }
19495 let mut selects = HashMap::new();
19496 for step in &trigger.program.steps {
19497 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
19498 return Err(MongrelError::Schema(
19499 "SetNew is only valid in BEFORE triggers".into(),
19500 ));
19501 }
19502 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
19503 }
19504 Ok(())
19505}
19506
19507fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
19508 let mut role_names = HashSet::new();
19509 for role in &catalog.roles {
19510 if role.name.is_empty()
19511 || role.created_epoch > catalog.db_epoch
19512 || !role_names.insert(role.name.as_str())
19513 {
19514 return Err(MongrelError::Schema(format!(
19515 "invalid or duplicate role {:?}",
19516 role.name
19517 )));
19518 }
19519 for permission in &role.permissions {
19520 if let Some(table) = permission_table(permission) {
19521 let schema = catalog
19522 .live(table)
19523 .map(|entry| &entry.schema)
19524 .or_else(|| {
19525 catalog
19526 .external_tables
19527 .iter()
19528 .find(|entry| entry.name == table)
19529 .map(|entry| &entry.declared_schema)
19530 })
19531 .ok_or_else(|| {
19532 MongrelError::Schema(format!(
19533 "role {:?} references unknown table {table:?}",
19534 role.name
19535 ))
19536 })?;
19537 let columns = match permission {
19538 crate::auth::Permission::SelectColumns { columns, .. }
19539 | crate::auth::Permission::InsertColumns { columns, .. }
19540 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
19541 _ => None,
19542 };
19543 if columns.is_some_and(|columns| {
19544 columns.is_empty()
19545 || columns.iter().any(|column| schema.column(column).is_none())
19546 }) {
19547 return Err(MongrelError::Schema(format!(
19548 "role {:?} contains invalid column permissions",
19549 role.name
19550 )));
19551 }
19552 }
19553 }
19554 }
19555 let mut user_ids = HashSet::new();
19556 let mut usernames = HashSet::new();
19557 let mut maximum_user_id = 0;
19558 for user in &catalog.users {
19559 maximum_user_id = maximum_user_id.max(user.id);
19560 if user.id == 0
19561 || user.username.is_empty()
19562 || user.password_hash.is_empty()
19563 || user.created_epoch > catalog.db_epoch
19564 || !user_ids.insert(user.id)
19565 || !usernames.insert(user.username.as_str())
19566 || user
19567 .roles
19568 .iter()
19569 .any(|role| !role_names.contains(role.as_str()))
19570 {
19571 return Err(MongrelError::Schema(format!(
19572 "invalid or duplicate user {:?}",
19573 user.username
19574 )));
19575 }
19576 }
19577 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
19578 return Err(MongrelError::Schema(
19579 "catalog next_user_id does not advance beyond existing user ids".into(),
19580 ));
19581 }
19582 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
19583 return Err(MongrelError::Schema(
19584 "authenticated catalog has no administrator".into(),
19585 ));
19586 }
19587 Ok(())
19588}
19589
19590fn validate_recovered_storage_plan(
19591 root: &Path,
19592 durable_root: Option<&crate::durable_file::DurableRoot>,
19593 catalog: &Catalog,
19594 created_table_ids: &HashSet<u64>,
19595 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
19596 meta_dek: Option<&[u8; META_DEK_LEN]>,
19597) -> Result<Vec<u64>> {
19598 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
19599 let mut reconcile = Vec::new();
19600 for entry in &catalog.tables {
19601 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19602 continue;
19603 }
19604 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19605 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
19606 let table_exists = match durable_root {
19607 Some(root) => match root.open_directory(&relative_dir) {
19608 Ok(_) => true,
19609 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19610 Err(error) => return Err(error.into()),
19611 },
19612 None => table_dir.is_dir(),
19613 };
19614 if !table_exists {
19615 if created_table_ids.contains(&entry.table_id) {
19616 reconcile.push(entry.table_id);
19617 continue;
19618 }
19619 return Err(MongrelError::NotFound(format!(
19620 "catalog table {} storage is missing",
19621 entry.table_id
19622 )));
19623 }
19624 let manifest_result = match durable_root {
19625 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
19626 None => crate::manifest::read(&table_dir, meta_dek),
19627 };
19628 let manifest = match manifest_result {
19629 Ok(manifest) => manifest,
19630 Err(MongrelError::Io(error))
19631 if created_table_ids.contains(&entry.table_id)
19632 && error.kind() == std::io::ErrorKind::NotFound =>
19633 {
19634 reconcile.push(entry.table_id);
19635 continue;
19636 }
19637 Err(error) => return Err(error),
19638 };
19639 if manifest.table_id != entry.table_id {
19640 return Err(MongrelError::Conflict(format!(
19641 "catalog table {} storage identity mismatch",
19642 entry.table_id
19643 )));
19644 }
19645 let schema_result = match durable_root {
19646 Some(root) => root
19647 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
19648 .map_err(MongrelError::from),
19649 None => crate::durable_file::open_regular_nofollow(
19650 &table_dir.join(crate::engine::SCHEMA_FILENAME),
19651 ),
19652 };
19653 let file = match schema_result {
19654 Ok(file) => file,
19655 Err(MongrelError::Io(error))
19656 if created_table_ids.contains(&entry.table_id)
19657 && error.kind() == std::io::ErrorKind::NotFound =>
19658 {
19659 reconcile.push(entry.table_id);
19660 continue;
19661 }
19662 Err(error) => return Err(error),
19663 };
19664 let length = file.metadata()?.len();
19665 if length > MAX_SCHEMA_BYTES {
19666 return Err(MongrelError::ResourceLimitExceeded {
19667 resource: "recovered schema bytes",
19668 requested: usize::try_from(length).unwrap_or(usize::MAX),
19669 limit: MAX_SCHEMA_BYTES as usize,
19670 });
19671 }
19672 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
19673 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
19674 if manifest.schema_id != entry.schema.schema_id
19675 || crate::wal::DdlOp::encode_schema(&disk_schema)?
19676 != crate::wal::DdlOp::encode_schema(&entry.schema)?
19677 {
19678 reconcile.push(entry.table_id);
19679 }
19680 }
19681 for table_id in ttl_updates.keys() {
19682 if !catalog.tables.iter().any(|entry| {
19683 entry.table_id == *table_id
19684 && matches!(entry.state, TableState::Live | TableState::Building { .. })
19685 }) {
19686 continue;
19687 }
19688 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
19689 let table_exists = match durable_root {
19690 Some(root) => match root.open_directory(&relative_dir) {
19691 Ok(_) => true,
19692 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19693 Err(error) => return Err(error.into()),
19694 },
19695 None => root.join(&relative_dir).is_dir(),
19696 };
19697 if !table_exists && !created_table_ids.contains(table_id) {
19698 return Err(MongrelError::NotFound(format!(
19699 "TTL recovery table {table_id} storage is missing"
19700 )));
19701 }
19702 }
19703 reconcile.sort_unstable();
19704 reconcile.dedup();
19705 Ok(reconcile)
19706}
19707
19708fn validate_catalog_table_storage(
19709 root: &crate::durable_file::DurableRoot,
19710 catalog: &Catalog,
19711 meta_dek: Option<&[u8; META_DEK_LEN]>,
19712) -> Result<()> {
19713 for entry in &catalog.tables {
19714 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19715 continue;
19716 }
19717 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19718 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
19719 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
19720 return Err(MongrelError::Conflict(format!(
19721 "catalog table {} storage identity mismatch",
19722 entry.table_id
19723 )));
19724 }
19725 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
19726 }
19727 Ok(())
19728}
19729
19730fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
19731 match schema.columns.iter_mut().find(|c| c.id == column.id) {
19732 Some(existing) if *existing == column => Ok(false),
19733 Some(existing) => {
19734 *existing = column;
19735 schema.schema_id = schema
19736 .schema_id
19737 .checked_add(1)
19738 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19739 Ok(true)
19740 }
19741 None => {
19742 schema.columns.push(column);
19743 schema.schema_id = schema
19744 .schema_id
19745 .checked_add(1)
19746 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19747 Ok(true)
19748 }
19749 }
19750}
19751
19752fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
19753 use crate::auth::Permission;
19754 match permission {
19755 Permission::Select { table }
19756 | Permission::Insert { table }
19757 | Permission::Update { table }
19758 | Permission::Delete { table }
19759 | Permission::SelectColumns { table, .. }
19760 | Permission::InsertColumns { table, .. }
19761 | Permission::UpdateColumns { table, .. } => Some(table),
19762 Permission::All | Permission::Ddl | Permission::Admin => None,
19763 }
19764}
19765
19766fn apply_rebuilding_publish(
19767 catalog: &mut Catalog,
19768 table_id: u64,
19769 replaced_table_id: u64,
19770 new_name: &str,
19771 epoch: u64,
19772) -> Result<bool> {
19773 let already_published = catalog.tables.iter().any(|entry| {
19774 entry.table_id == table_id
19775 && entry.name == new_name
19776 && matches!(entry.state, TableState::Live)
19777 }) && catalog.tables.iter().any(|entry| {
19778 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
19779 });
19780 if already_published {
19781 return Ok(false);
19782 }
19783 let schema = catalog
19784 .tables
19785 .iter()
19786 .find(|entry| entry.table_id == table_id)
19787 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
19788 .schema
19789 .clone();
19790 let replaced = catalog
19791 .tables
19792 .iter_mut()
19793 .find(|entry| entry.table_id == replaced_table_id)
19794 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
19795 replaced.state = TableState::Dropped { at_epoch: epoch };
19796 let replacement = catalog
19797 .tables
19798 .iter_mut()
19799 .find(|entry| entry.table_id == table_id)
19800 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
19801 replacement.name = new_name.to_string();
19802 replacement.state = TableState::Live;
19803
19804 for role in &mut catalog.roles {
19805 role.permissions.retain_mut(|permission| {
19806 retain_rebuilt_permission_columns(permission, new_name, &schema)
19807 });
19808 }
19809 for definition in &mut catalog.materialized_views {
19810 if let Some(incremental) = definition.incremental.as_mut() {
19811 if incremental.source_table == new_name
19812 && incremental.source_table_id == replaced_table_id
19813 {
19814 incremental.source_table_id = table_id;
19815 }
19816 }
19817 }
19818 advance_security_version(catalog)?;
19819 Ok(true)
19820}
19821
19822fn retain_rebuilt_permission_columns(
19823 permission: &mut crate::auth::Permission,
19824 target_table: &str,
19825 schema: &Schema,
19826) -> bool {
19827 use crate::auth::Permission;
19828 let columns = match permission {
19829 Permission::SelectColumns { table, columns }
19830 | Permission::InsertColumns { table, columns }
19831 | Permission::UpdateColumns { table, columns }
19832 if table == target_table =>
19833 {
19834 Some(columns)
19835 }
19836 _ => None,
19837 };
19838 if let Some(columns) = columns {
19839 columns.retain(|column| schema.column(column).is_some());
19840 !columns.is_empty()
19841 } else {
19842 true
19843 }
19844}
19845
19846fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
19847 use crate::auth::Permission;
19848 let table = match permission {
19849 Permission::Select { table }
19850 | Permission::Insert { table }
19851 | Permission::Update { table }
19852 | Permission::Delete { table }
19853 | Permission::SelectColumns { table, .. }
19854 | Permission::InsertColumns { table, .. }
19855 | Permission::UpdateColumns { table, .. } => Some(table),
19856 Permission::All | Permission::Ddl | Permission::Admin => None,
19857 };
19858 if let Some(table) = table.filter(|table| table.as_str() == old) {
19859 *table = new.to_string();
19860 }
19861}
19862
19863fn rename_permission_column(
19864 permission: &mut crate::auth::Permission,
19865 target_table: &str,
19866 old: &str,
19867 new: &str,
19868) {
19869 use crate::auth::Permission;
19870 let columns = match permission {
19871 Permission::SelectColumns { table, columns }
19872 | Permission::InsertColumns { table, columns }
19873 | Permission::UpdateColumns { table, columns }
19874 if table == target_table =>
19875 {
19876 Some(columns)
19877 }
19878 _ => None,
19879 };
19880 if let Some(column) = columns
19881 .into_iter()
19882 .flatten()
19883 .find(|column| column.as_str() == old)
19884 {
19885 *column = new.to_string();
19886 }
19887}
19888
19889pub(crate) fn validate_security_catalog(
19890 catalog: &Catalog,
19891 security: &crate::security::SecurityCatalog,
19892) -> Result<()> {
19893 let mut policy_names = HashSet::new();
19894 for table in &security.rls_tables {
19895 if catalog.live(table).is_none() {
19896 return Err(MongrelError::NotFound(format!(
19897 "RLS table {table:?} not found"
19898 )));
19899 }
19900 }
19901 for policy in &security.policies {
19902 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
19903 return Err(MongrelError::InvalidArgument(format!(
19904 "duplicate policy {:?} on {:?}",
19905 policy.name, policy.table
19906 )));
19907 }
19908 let schema = &catalog
19909 .live(&policy.table)
19910 .ok_or_else(|| {
19911 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
19912 })?
19913 .schema;
19914 if let Some(expression) = &policy.using {
19915 validate_security_expression(expression, schema)?;
19916 }
19917 if let Some(expression) = &policy.with_check {
19918 validate_security_expression(expression, schema)?;
19919 }
19920 }
19921 let mut mask_names = HashSet::new();
19922 for mask in &security.masks {
19923 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
19924 return Err(MongrelError::InvalidArgument(format!(
19925 "duplicate mask {:?} on {:?}",
19926 mask.name, mask.table
19927 )));
19928 }
19929 let column = catalog
19930 .live(&mask.table)
19931 .and_then(|entry| {
19932 entry
19933 .schema
19934 .columns
19935 .iter()
19936 .find(|column| column.id == mask.column)
19937 })
19938 .ok_or_else(|| {
19939 MongrelError::NotFound(format!(
19940 "mask column {} on {:?} not found",
19941 mask.column, mask.table
19942 ))
19943 })?;
19944 if matches!(
19945 mask.strategy,
19946 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
19947 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
19948 {
19949 return Err(MongrelError::InvalidArgument(format!(
19950 "mask {:?} requires a string/bytes column",
19951 mask.name
19952 )));
19953 }
19954 }
19955 Ok(())
19956}
19957
19958fn validate_security_expression(
19959 expression: &crate::security::SecurityExpr,
19960 schema: &Schema,
19961) -> Result<()> {
19962 use crate::security::SecurityExpr;
19963 match expression {
19964 SecurityExpr::True => Ok(()),
19965 SecurityExpr::ColumnEqCurrentUser { column }
19966 | SecurityExpr::ColumnEqValue { column, .. } => {
19967 if schema
19968 .columns
19969 .iter()
19970 .any(|candidate| candidate.id == *column)
19971 {
19972 Ok(())
19973 } else {
19974 Err(MongrelError::InvalidArgument(format!(
19975 "security expression references unknown column id {column}"
19976 )))
19977 }
19978 }
19979 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
19980 validate_security_expression(left, schema)?;
19981 validate_security_expression(right, schema)
19982 }
19983 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
19984 }
19985}
19986
19987fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
19989 let referenced = cat
19990 .tables
19991 .iter()
19992 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
19993 .map(|entry| entry.table_id)
19994 .collect::<HashSet<_>>();
19995 let tables_dir = root.join(TABLES_DIR);
19996 let entries = match std::fs::read_dir(&tables_dir) {
19997 Ok(entries) => entries,
19998 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
19999 Err(error) => return Err(error.into()),
20000 };
20001 for entry in entries {
20002 let entry = entry?;
20003 if !entry.file_type()?.is_dir() {
20004 continue;
20005 }
20006 let file_name = entry.file_name();
20007 let Some(name) = file_name.to_str() else {
20008 continue;
20009 };
20010 let Ok(table_id) = name.parse::<u64>() else {
20011 continue;
20012 };
20013 if name != table_id.to_string() {
20014 continue;
20015 }
20016 if !referenced.contains(&table_id) {
20017 crate::durable_file::remove_directory_all(&entry.path())?;
20018 }
20019 }
20020 Ok(())
20021}
20022
20023fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
20028 for entry in &cat.tables {
20029 let txn_dir = root
20030 .join(TABLES_DIR)
20031 .join(entry.table_id.to_string())
20032 .join("_txn");
20033 if txn_dir.exists() {
20034 let _ = std::fs::remove_dir_all(&txn_dir);
20035 }
20036 }
20037}
20038
20039#[cfg(test)]
20040mod write_permission_tests {
20041 use super::*;
20042 use crate::txn::Staged;
20043
20044 struct NoopExternalBridge;
20045
20046 impl ExternalTriggerBridge for NoopExternalBridge {
20047 fn apply_trigger_external_write(
20048 &self,
20049 _entry: &ExternalTableEntry,
20050 base_state: Vec<u8>,
20051 _op: ExternalTriggerWrite,
20052 ) -> Result<ExternalTriggerWriteResult> {
20053 Ok(ExternalTriggerWriteResult::new(base_state))
20054 }
20055 }
20056
20057 fn assert_txn_namespace_full<T>(result: Result<T>) {
20058 assert!(matches!(result, Err(MongrelError::Full(_))));
20059 }
20060
20061 #[test]
20062 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
20063 let directory = tempfile::tempdir().unwrap();
20064 let database = Database::create(directory.path()).unwrap();
20065 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
20066 *database.next_txn_id.lock() = generation << 32;
20067 let before = crate::wal::SharedWal::replay(directory.path())
20068 .unwrap()
20069 .len();
20070 let bridge = NoopExternalBridge;
20071
20072 assert_txn_namespace_full(database.begin().commit());
20073 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
20074 assert_txn_namespace_full(
20075 database
20076 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
20077 .commit(),
20078 );
20079 assert_txn_namespace_full(
20080 database
20081 .begin_with_external_trigger_bridge(&bridge)
20082 .commit(),
20083 );
20084 assert_txn_namespace_full(
20085 database
20086 .begin_with_external_trigger_bridge_as(&bridge, None)
20087 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
20088 );
20089
20090 assert_eq!(
20091 crate::wal::SharedWal::replay(directory.path())
20092 .unwrap()
20093 .len(),
20094 before
20095 );
20096 drop(database);
20097 Database::open(directory.path()).unwrap();
20098 }
20099
20100 #[test]
20101 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
20102 let directory = tempfile::tempdir().unwrap();
20103 let table_dir = directory.path().join("7");
20104 crate::durable_file::create_directory_all(&table_dir).unwrap();
20105 let original_schema = test_schema();
20106 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
20107 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
20108 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
20109 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
20110 let original_bytes = std::fs::read(&schema_path).unwrap();
20111
20112 let mut replacement_schema = original_schema;
20113 replacement_schema.schema_id += 1;
20114 assert!(matches!(
20115 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
20116 Err(MongrelError::Conflict(_))
20117 ));
20118
20119 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
20120 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
20121 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
20122 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
20123 }
20124
20125 #[test]
20126 fn catalog_table_missing_storage_fails_without_recreating_it() {
20127 let directory = tempfile::tempdir().unwrap();
20128 let table_dir = {
20129 let database = Database::create(directory.path()).unwrap();
20130 database.create_table("docs", test_schema()).unwrap();
20131 directory
20132 .path()
20133 .join(TABLES_DIR)
20134 .join(database.table_id("docs").unwrap().to_string())
20135 };
20136 std::fs::remove_dir_all(&table_dir).unwrap();
20137
20138 assert!(matches!(
20139 Database::open(directory.path()),
20140 Err(MongrelError::NotFound(_))
20141 ));
20142 assert!(!table_dir.exists());
20143 }
20144
20145 #[test]
20146 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
20147 let directory = tempfile::tempdir().unwrap();
20148 let database = std::sync::Arc::new(
20149 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
20150 );
20151 database.create_user("alice", "old-password").unwrap();
20152 let old_identity = database.user_identity("alice").unwrap();
20153 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
20154 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
20155 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
20156 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
20157
20158 std::thread::scope(|scope| {
20159 let authenticate = {
20160 let database = std::sync::Arc::clone(&database);
20161 scope.spawn(move || {
20162 database.authenticate_principal_inner("alice", "old-password", || {
20163 verified_tx.send(()).unwrap();
20164 resume_rx.recv().unwrap();
20165 })
20166 })
20167 };
20168 verified_rx.recv().unwrap();
20169 let mutate = {
20170 let database = std::sync::Arc::clone(&database);
20171 scope.spawn(move || {
20172 mutation_started_tx.send(()).unwrap();
20173 database.drop_user("alice").unwrap();
20174 database.create_user("alice", "new-password").unwrap();
20175 mutation_done_tx.send(()).unwrap();
20176 })
20177 };
20178 mutation_started_rx.recv().unwrap();
20179 assert!(mutation_done_rx
20180 .recv_timeout(std::time::Duration::from_millis(50))
20181 .is_err());
20182 resume_tx.send(()).unwrap();
20183 let principal = authenticate.join().unwrap().unwrap().unwrap();
20184 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
20185 mutate.join().unwrap();
20186 });
20187
20188 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
20189 assert!(database
20190 .authenticate_principal("alice", "old-password")
20191 .unwrap()
20192 .is_none());
20193 assert!(database
20194 .authenticate_principal("alice", "new-password")
20195 .unwrap()
20196 .is_some());
20197 }
20198
20199 #[test]
20200 fn homogeneous_batch_summarizes_to_one_permission_decision() {
20201 let staging = (0..10_050)
20202 .map(|_| {
20203 (
20204 7,
20205 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
20206 )
20207 })
20208 .collect::<Vec<_>>();
20209
20210 let needs = summarize_write_permissions(&staging);
20211 let table = needs.get(&7).unwrap();
20212 assert_eq!(needs.len(), 1);
20213 assert!(table.insert);
20214 assert_eq!(table.insert_columns, [1, 2]);
20215 assert!(!table.update);
20216 assert!(!table.delete);
20217 assert!(!table.truncate);
20218 }
20219
20220 #[test]
20221 fn mixed_writes_union_columns_and_preserve_empty_operations() {
20222 let staging = vec![
20223 (7, Staged::Put(vec![(2, Value::Int64(2))])),
20224 (7, Staged::Put(vec![(1, Value::Int64(1))])),
20225 (
20226 7,
20227 Staged::Update {
20228 row_id: RowId(1),
20229 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
20230 changed_columns: vec![2],
20231 },
20232 ),
20233 (7, Staged::Delete(RowId(2))),
20234 (8, Staged::Truncate),
20235 ];
20236
20237 let needs = summarize_write_permissions(&staging);
20238 let table = needs.get(&7).unwrap();
20239 assert_eq!(table.insert_columns, [1, 2]);
20240 assert!(table.update);
20241 assert_eq!(table.update_columns, [2]);
20242 assert!(table.delete);
20243 assert!(needs.get(&8).unwrap().truncate);
20244 }
20245
20246 #[test]
20247 fn final_permission_decisions_do_not_scale_with_rows() {
20248 let credentialless_dir = tempfile::tempdir().unwrap();
20249 let credentialless = Database::create(credentialless_dir.path()).unwrap();
20250 credentialless.create_table("docs", test_schema()).unwrap();
20251 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20252 credentialless
20253 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
20254 .unwrap();
20255 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
20256
20257 let authenticated_dir = tempfile::tempdir().unwrap();
20258 let authenticated =
20259 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
20260 .unwrap();
20261 authenticated.create_table("docs", test_schema()).unwrap();
20262 let admin = authenticated.resolve_principal("admin").unwrap();
20263 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20264 authenticated
20265 .validate_write_permissions(
20266 &puts(authenticated.table_id("docs").unwrap()),
20267 Some(&admin),
20268 None,
20269 )
20270 .unwrap();
20271 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20272 }
20273
20274 #[test]
20275 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
20276 let dir = tempfile::tempdir().unwrap();
20277 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20278 db.create_table("docs", test_schema()).unwrap();
20279 let admin = db.resolve_principal("admin").unwrap();
20280 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20281
20282 let mut transaction = db.begin_as(Some(admin));
20283 transaction
20284 .delete_batch("docs", (0..100).map(RowId).collect())
20285 .unwrap();
20286 transaction.commit().unwrap();
20287
20288 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
20289 }
20290
20291 #[test]
20292 fn truncate_validation_checks_admin_once_for_all_tables() {
20293 let dir = tempfile::tempdir().unwrap();
20294 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20295 db.create_table("first", test_schema()).unwrap();
20296 db.create_table("second", test_schema()).unwrap();
20297 let admin = db.resolve_principal("admin").unwrap();
20298 let staging = vec![
20299 (db.table_id("first").unwrap(), Staged::Truncate),
20300 (db.table_id("second").unwrap(), Staged::Truncate),
20301 ];
20302
20303 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20304 db.validate_write_permissions(&staging, Some(&admin), None)
20305 .unwrap();
20306 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20307 }
20308
20309 #[test]
20310 fn one_table_commit_batches_structural_work() {
20311 let dir = tempfile::tempdir().unwrap();
20312 let db = Database::create(dir.path()).unwrap();
20313 db.create_table("docs", test_schema()).unwrap();
20314 let table_id = db.table_id("docs").unwrap();
20315
20316 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
20317 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20318 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20319 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20320 db.transaction(|transaction| {
20321 for id in 0..100 {
20322 transaction.put("docs", vec![(1, Value::Int64(id))])?;
20323 }
20324 Ok(())
20325 })
20326 .unwrap();
20327
20328 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
20329 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20330 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20331 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20332
20333 let puts = crate::wal::SharedWal::replay(dir.path())
20334 .unwrap()
20335 .into_iter()
20336 .filter_map(|record| match record.op {
20337 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
20338 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
20339 .unwrap()
20340 .len(),
20341 ),
20342 _ => None,
20343 })
20344 .collect::<Vec<_>>();
20345 assert_eq!(puts, [100]);
20346
20347 let row_ids = db
20348 .table("docs")
20349 .unwrap()
20350 .lock()
20351 .visible_rows(db.snapshot().0)
20352 .unwrap()
20353 .into_iter()
20354 .take(2)
20355 .map(|row| row.row_id)
20356 .collect::<Vec<_>>();
20357 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20358 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20359 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20360 db.transaction(|transaction| {
20361 for row_id in row_ids {
20362 transaction.delete("docs", row_id)?;
20363 }
20364 Ok(())
20365 })
20366 .unwrap();
20367 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20368 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20369 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20370
20371 let deletes = crate::wal::SharedWal::replay(dir.path())
20372 .unwrap()
20373 .into_iter()
20374 .filter_map(|record| match record.op {
20375 crate::wal::Op::Delete {
20376 table_id: id,
20377 row_ids,
20378 } if id == table_id => Some(row_ids.len()),
20379 _ => None,
20380 })
20381 .collect::<Vec<_>>();
20382 assert_eq!(deletes, [2]);
20383 }
20384
20385 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
20386 (0..10_050)
20387 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
20388 .collect()
20389 }
20390
20391 fn test_schema() -> Schema {
20392 Schema {
20393 columns: vec![ColumnDef {
20394 id: 1,
20395 name: "id".into(),
20396 ty: TypeId::Int64,
20397 flags: crate::schema::ColumnFlags::empty()
20398 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20399 default_value: None,
20400 embedding_source: None,
20401 }],
20402 ..Schema::default()
20403 }
20404 }
20405}
20406
20407#[cfg(test)]
20408mod cdc_bounds_tests {
20409 use super::*;
20410
20411 #[test]
20412 fn retained_byte_limit_rejects_without_allocating_payload() {
20413 let mut retained = 0;
20414 let error = charge_cdc_bytes(
20415 &mut retained,
20416 CDC_MAX_RETAINED_BYTES.saturating_add(1),
20417 "CDC retained bytes",
20418 )
20419 .unwrap_err();
20420 assert!(matches!(
20421 error,
20422 MongrelError::ResourceLimitExceeded {
20423 resource: "CDC retained bytes",
20424 ..
20425 }
20426 ));
20427 }
20428
20429 #[test]
20430 fn row_json_estimate_accounts_for_byte_array_expansion() {
20431 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
20432 .with_column(1, Value::Bytes(vec![0; 1024]));
20433 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
20434 }
20435}
20436
20437#[cfg(test)]
20438mod generation_metrics_tests {
20439 use super::*;
20440 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20441
20442 #[test]
20443 fn legacy_cow_fallback_is_measured() {
20444 let dir = tempfile::tempdir().unwrap();
20445 let table = Table::create(
20446 dir.path(),
20447 Schema {
20448 columns: vec![ColumnDef {
20449 id: 1,
20450 name: "id".into(),
20451 ty: TypeId::Int64,
20452 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20453 default_value: None,
20454 embedding_source: None,
20455 }],
20456 ..Schema::default()
20457 },
20458 1,
20459 )
20460 .unwrap();
20461 let handle = TableHandle::from_table(table);
20462 let held = match &handle.inner {
20463 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
20464 TableHandleInner::Direct(_) => unreachable!(),
20465 };
20466
20467 handle.lock().set_sync_byte_threshold(1);
20468
20469 let stats = handle.generation_stats();
20470 assert_eq!(stats.cow_clone_count, 1);
20471 assert!(stats.estimated_cow_clone_bytes > 0);
20472 drop(held);
20473 }
20474}
20475
20476#[cfg(test)]
20477mod trigger_engine_tests {
20478 use super::*;
20479
20480 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
20481 WriteEvent {
20482 table: "test".into(),
20483 kind: TriggerEvent::Insert,
20484 new: Some(TriggerRowImage {
20485 columns: new_cells.iter().cloned().collect(),
20486 }),
20487 old: Some(TriggerRowImage {
20488 columns: old_cells.iter().cloned().collect(),
20489 }),
20490 changed_columns: Vec::new(),
20491 op_indices: Vec::new(),
20492 put_idx: None,
20493 trigger_stack: Vec::new(),
20494 }
20495 }
20496
20497 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
20498 WriteEvent {
20499 table: "test".into(),
20500 kind: TriggerEvent::Insert,
20501 new: Some(TriggerRowImage {
20502 columns: new_cells.iter().cloned().collect(),
20503 }),
20504 old: None,
20505 changed_columns: Vec::new(),
20506 op_indices: Vec::new(),
20507 put_idx: None,
20508 trigger_stack: Vec::new(),
20509 }
20510 }
20511
20512 #[test]
20513 fn value_order_int64_vs_float64() {
20514 assert_eq!(
20515 value_order(&Value::Int64(5), &Value::Float64(5.0)),
20516 Some(std::cmp::Ordering::Equal)
20517 );
20518 assert_eq!(
20519 value_order(&Value::Int64(5), &Value::Float64(3.0)),
20520 Some(std::cmp::Ordering::Greater)
20521 );
20522 assert_eq!(
20523 value_order(&Value::Int64(2), &Value::Float64(3.0)),
20524 Some(std::cmp::Ordering::Less)
20525 );
20526 }
20527
20528 #[test]
20529 fn value_order_null_returns_none() {
20530 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
20531 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
20532 assert_eq!(value_order(&Value::Null, &Value::Null), None);
20533 }
20534
20535 #[test]
20536 fn value_order_cross_group_returns_none() {
20537 assert_eq!(
20538 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
20539 None
20540 );
20541 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
20542 assert_eq!(
20543 value_order(
20544 &Value::Embedding(vec![1.0, 2.0]),
20545 &Value::Embedding(vec![1.0, 2.0])
20546 ),
20547 None
20548 );
20549 }
20550
20551 #[test]
20552 fn eval_trigger_expr_ranges_and_booleans() {
20553 let expr = TriggerExpr::And {
20554 left: Box::new(TriggerExpr::Gt {
20555 left: TriggerValue::NewColumn(1),
20556 right: TriggerValue::Literal(Value::Int64(0)),
20557 }),
20558 right: Box::new(TriggerExpr::Lte {
20559 left: TriggerValue::NewColumn(1),
20560 right: TriggerValue::Literal(Value::Int64(100)),
20561 }),
20562 };
20563 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
20564 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
20565 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
20566
20567 let or_expr = TriggerExpr::Or {
20568 left: Box::new(TriggerExpr::Lt {
20569 left: TriggerValue::NewColumn(1),
20570 right: TriggerValue::Literal(Value::Int64(0)),
20571 }),
20572 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
20573 TriggerValue::OldColumn(2),
20574 )))),
20575 };
20576 assert!(eval_trigger_expr(
20577 &or_expr,
20578 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
20579 )
20580 .unwrap());
20581 assert!(!eval_trigger_expr(
20582 &or_expr,
20583 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
20584 )
20585 .unwrap());
20586
20587 assert!(eval_trigger_expr(
20588 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
20589 &event_insert(&[])
20590 )
20591 .unwrap());
20592 assert!(!eval_trigger_expr(
20593 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
20594 &event_insert(&[])
20595 )
20596 .unwrap());
20597 assert!(!eval_trigger_expr(
20598 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
20599 &event_insert(&[])
20600 )
20601 .unwrap());
20602 }
20603}
20604
20605#[cfg(test)]
20606mod core_resource_tests {
20607 use super::*;
20608
20609 fn int_pk_schema() -> Schema {
20610 Schema {
20611 columns: vec![ColumnDef {
20612 id: 1,
20613 name: "id".into(),
20614 ty: TypeId::Int64,
20615 flags: crate::schema::ColumnFlags::empty()
20616 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20617 default_value: None,
20618 embedding_source: None,
20619 }],
20620 ..Schema::default()
20621 }
20622 }
20623
20624 #[test]
20625 fn open_constructs_governor_spill_and_jobs_with_documented_defaults() {
20626 let dir = tempfile::tempdir().unwrap();
20627 let db = Database::create(dir.path()).unwrap();
20628 assert_eq!(
20629 db.memory_governor().max_bytes(),
20630 DEFAULT_MEMORY_BUDGET_BYTES
20631 );
20632 assert_eq!(
20633 db.spill_manager().config().global_bytes,
20634 DEFAULT_TEMP_DISK_BUDGET_BYTES
20635 );
20636 assert!(db.job_registry().list().is_empty());
20637 assert_eq!(
20639 db.resource_groups().len(),
20640 crate::resource::WorkloadClass::ALL.len()
20641 );
20642 assert!(db.resource_groups().get("control").is_some());
20643 assert!(db.embedding_providers().list_ids().is_empty());
20644 let err = db
20646 .embedding_providers()
20647 .embed(
20648 &crate::embedding::EmbeddingSource::SuppliedByApplication,
20649 &["text"],
20650 4,
20651 )
20652 .unwrap_err();
20653 assert!(matches!(
20654 err,
20655 crate::embedding::EmbeddingError::SuppliedByApplication
20656 ));
20657 }
20658
20659 #[test]
20660 fn lock_rows_for_update_acquires_exclusive_row_locks() {
20661 use crate::locks::LockKey;
20662 use crate::rowid::RowId;
20663
20664 let dir = tempfile::tempdir().unwrap();
20665 let db = Database::create(dir.path()).unwrap();
20666 let txn_id = db.allocate_lock_txn_id().unwrap();
20667 let rid = RowId(42);
20668 db.lock_rows_for_update(txn_id, 7, &[rid], None).unwrap();
20669 assert!(db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20670 db.release_txn_locks(txn_id);
20671 assert!(!db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20672 }
20673
20674 #[test]
20675 fn open_with_options_sizes_the_core_budgets() {
20676 let dir = tempfile::tempdir().unwrap();
20677 let db = Database::create(dir.path()).unwrap();
20678 drop(db);
20679 let db = Database::open_with_options(
20680 dir.path(),
20681 OpenOptions::default()
20682 .with_memory_budget_bytes(256 * 1024 * 1024)
20683 .with_temp_disk_budget_bytes(16 * 1024 * 1024),
20684 )
20685 .unwrap();
20686 assert_eq!(db.memory_governor().max_bytes(), 256 * 1024 * 1024);
20687 assert_eq!(db.spill_manager().config().global_bytes, 16 * 1024 * 1024);
20688 }
20689
20690 #[test]
20691 fn zero_budgets_are_rejected() {
20692 let dir = tempfile::tempdir().unwrap();
20693 let db = Database::create(dir.path()).unwrap();
20694 drop(db);
20695 let result = Database::open_with_options(
20696 dir.path(),
20697 OpenOptions::default().with_memory_budget_bytes(0),
20698 );
20699 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20700 let result = Database::open_with_options(
20701 dir.path(),
20702 OpenOptions::default().with_temp_disk_budget_bytes(0),
20703 );
20704 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20705 }
20706
20707 #[test]
20708 fn page_caches_reserve_under_the_governor() {
20709 let dir = tempfile::tempdir().unwrap();
20710 let db = Database::create(dir.path()).unwrap();
20711 db.create_table("t", int_pk_schema()).unwrap();
20712 let mut txn = db.begin();
20713 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20714 txn.commit().unwrap();
20715 let stats = db.memory_governor().stats();
20719 assert_eq!(
20720 stats.usage_for(crate::memory::MemoryClass::PageCache),
20721 db.page_cache.used_bytes()
20722 );
20723 assert_eq!(
20724 stats.usage_for(crate::memory::MemoryClass::DecodedCache),
20725 db.decoded_cache.used_bytes()
20726 );
20727 assert_eq!(
20728 db.memory_governor().reclaimable_bytes(),
20729 db.page_cache.used_bytes() + db.decoded_cache.used_bytes()
20730 );
20731 let _ = db.memory_governor().evict_reclaimable(1024 * 1024);
20733 }
20734
20735 #[test]
20736 fn job_registry_persists_across_reopen() {
20737 let dir = tempfile::tempdir().unwrap();
20738 let db = Database::create(dir.path()).unwrap();
20739 db.create_table("t", int_pk_schema()).unwrap();
20740 let job_id = db
20741 .job_registry()
20742 .submit(
20743 crate::jobs::JobKind::IndexBuild,
20744 crate::jobs::JobTarget {
20745 table: "t".to_string(),
20746 index: Some("idx".to_string()),
20747 },
20748 )
20749 .unwrap();
20750 drop(db);
20751 let db = Database::open(dir.path()).unwrap();
20752 let record = db.job_registry().get(job_id).expect("job survives reopen");
20753 assert_eq!(record.state, crate::jobs::JobState::Pending);
20754 }
20755
20756 #[test]
20757 fn spill_manager_open_sweeps_stale_temp_tree() {
20758 let dir = tempfile::tempdir().unwrap();
20759 let db = Database::create(dir.path()).unwrap();
20760 let stale = dir.path().join("temp").join("spill").join("q-deadbeef");
20761 std::fs::create_dir_all(&stale).unwrap();
20762 std::fs::write(stale.join("chunk-0"), b"stale").unwrap();
20763 drop(db);
20764 let db = Database::open(dir.path()).unwrap();
20765 assert!(
20766 !stale.exists(),
20767 "the startup sweep removes stale spill files (S1E-004)"
20768 );
20769 let session = db
20771 .spill_manager()
20772 .begin_query(
20773 mongreldb_types::ids::QueryId::from_bytes([7u8; 16]),
20774 1024 * 1024,
20775 )
20776 .unwrap();
20777 assert_eq!(session.used(), 0);
20778 }
20779}
20780
20781#[cfg(test)]
20782mod version_pin_tests {
20783 use super::*;
20784
20785 fn int_pk_schema() -> Schema {
20786 Schema {
20787 columns: vec![ColumnDef {
20788 id: 1,
20789 name: "id".into(),
20790 ty: TypeId::Int64,
20791 flags: crate::schema::ColumnFlags::empty()
20792 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20793 default_value: None,
20794 embedding_source: None,
20795 }],
20796 ..Schema::default()
20797 }
20798 }
20799
20800 fn pins_for(
20801 report: &[TablePinsReport],
20802 table: &str,
20803 source: crate::retention::PinSource,
20804 ) -> Option<crate::retention::PinInfo> {
20805 report
20806 .iter()
20807 .find(|entry| entry.table == table)
20808 .and_then(|entry| entry.pins.get(source).cloned())
20809 }
20810
20811 #[test]
20812 fn backup_boundary_registers_backup_pitr_pin() {
20813 let source = tempfile::tempdir().unwrap();
20814 let destination_parent = tempfile::tempdir().unwrap();
20815 let destination = destination_parent.path().join("backup");
20816 let db = Arc::new(Database::create(source.path()).unwrap());
20817 db.create_table("t", int_pk_schema()).unwrap();
20818 let mut txn = db.begin();
20819 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
20820 let boundary_epoch = txn.commit().unwrap();
20821
20822 let hold = Arc::new(std::sync::Barrier::new(2));
20823 let resume = Arc::new(std::sync::Barrier::new(2));
20824 db.__set_backup_hook({
20825 let hold = Arc::clone(&hold);
20826 let resume = Arc::clone(&resume);
20827 move || {
20828 hold.wait();
20829 resume.wait();
20830 }
20831 });
20832
20833 let backup = {
20834 let db = Arc::clone(&db);
20835 let destination = destination.clone();
20836 std::thread::spawn(move || db.hot_backup(destination))
20837 };
20838 hold.wait();
20839 let report = db.version_pins_report();
20842 let pin = pins_for(&report, "t", crate::retention::PinSource::BackupPitr)
20843 .expect("backup boundary must register a BackupPitr pin");
20844 assert_eq!(pin.oldest_epoch, boundary_epoch);
20845 assert!(pin.pin_count >= 1);
20846 resume.wait();
20847 backup.join().unwrap().unwrap();
20848
20849 let report = db.version_pins_report();
20850 assert!(
20851 pins_for(&report, "t", crate::retention::PinSource::BackupPitr).is_none(),
20852 "the BackupPitr pin releases when the backup finishes"
20853 );
20854 }
20855
20856 #[test]
20857 fn snapshot_and_read_generation_pins_surface_in_report() {
20858 let dir = tempfile::tempdir().unwrap();
20859 let db = Database::create(dir.path()).unwrap();
20860 db.create_table("t", int_pk_schema()).unwrap();
20861 let mut txn = db.begin();
20862 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20863 txn.commit().unwrap();
20864
20865 let (_snapshot, guard) = db.snapshot();
20866 let report = db.version_pins_report();
20867 assert!(
20868 pins_for(
20869 &report,
20870 "t",
20871 crate::retention::PinSource::TransactionSnapshot
20872 )
20873 .is_some(),
20874 "a database snapshot projects the TransactionSnapshot source"
20875 );
20876 drop(guard);
20877
20878 let handle = db.table("t").unwrap();
20879 let (generation, _snapshot) = handle.read_generation_with_context(None).unwrap();
20880 let report = db.version_pins_report();
20881 assert!(
20882 pins_for(&report, "t", crate::retention::PinSource::ReadGeneration).is_some(),
20883 "a cloned read generation registers a ReadGeneration pin"
20884 );
20885 drop(generation);
20886
20887 let report = db.version_pins_report();
20888 let entry = report.iter().find(|entry| entry.table == "t").unwrap();
20889 assert!(
20890 entry
20891 .pins
20892 .get(crate::retention::PinSource::BackupPitr)
20893 .is_none()
20894 && entry
20895 .pins
20896 .get(crate::retention::PinSource::Replication)
20897 .is_none()
20898 && entry
20899 .pins
20900 .get(crate::retention::PinSource::OnlineIndexBuild)
20901 .is_none(),
20902 "untaken sources stay absent from the report"
20903 );
20904 }
20905}
20906
20907#[cfg(test)]
20908mod lock_manager_tests {
20909 use super::*;
20910 use crate::locks::LockKey;
20911
20912 fn col(id: u16, name: &str, ty: TypeId, flags: crate::schema::ColumnFlags) -> ColumnDef {
20913 ColumnDef {
20914 id,
20915 name: name.into(),
20916 ty,
20917 flags,
20918 default_value: None,
20919 embedding_source: None,
20920 }
20921 }
20922
20923 fn unique_schema() -> Schema {
20924 let mut constraints = crate::constraint::TableConstraints::default();
20925 constraints
20926 .uniques
20927 .push(crate::constraint::UniqueConstraint {
20928 id: 1,
20929 name: "users_email_unique".into(),
20930 columns: vec![1],
20931 });
20932 Schema {
20933 columns: vec![
20934 col(
20935 0,
20936 "id",
20937 TypeId::Int64,
20938 crate::schema::ColumnFlags::empty()
20939 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20940 ),
20941 col(
20942 1,
20943 "email",
20944 TypeId::Bytes,
20945 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
20946 ),
20947 ],
20948 constraints,
20949 ..Schema::default()
20950 }
20951 }
20952
20953 fn parent_schema() -> Schema {
20954 Schema {
20955 columns: vec![col(
20956 0,
20957 "id",
20958 TypeId::Int64,
20959 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::PRIMARY_KEY),
20960 )],
20961 ..Schema::default()
20962 }
20963 }
20964
20965 fn child_schema() -> Schema {
20966 let mut constraints = crate::constraint::TableConstraints::default();
20967 constraints
20968 .foreign_keys
20969 .push(crate::constraint::ForeignKey {
20970 id: 1,
20971 name: "child_parent_fk".into(),
20972 columns: vec![1],
20973 ref_table: "parent".into(),
20974 ref_columns: vec![0],
20975 on_delete: crate::constraint::FkAction::Restrict,
20976 on_update: crate::constraint::FkAction::Restrict,
20977 });
20978 Schema {
20979 columns: vec![
20980 col(
20981 0,
20982 "id",
20983 TypeId::Int64,
20984 crate::schema::ColumnFlags::empty()
20985 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20986 ),
20987 col(
20988 1,
20989 "pid",
20990 TypeId::Int64,
20991 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
20992 ),
20993 ],
20994 constraints,
20995 ..Schema::default()
20996 }
20997 }
20998
20999 fn auto_inc_schema() -> Schema {
21000 Schema {
21001 columns: vec![col(
21002 0,
21003 "id",
21004 TypeId::Int64,
21005 crate::schema::ColumnFlags::empty()
21006 .with(crate::schema::ColumnFlags::PRIMARY_KEY)
21007 .with(crate::schema::ColumnFlags::AUTO_INCREMENT),
21008 )],
21009 ..Schema::default()
21010 }
21011 }
21012
21013 fn pk_lock_key(table_id: u64, value: i64) -> LockKey {
21014 let mut key = b"pk:".to_vec();
21015 key.extend_from_slice(&Value::Int64(value).encode_key());
21016 LockKey::key(table_id, key)
21017 }
21018
21019 #[test]
21020 fn unique_claims_serialize_concurrent_commits() {
21021 let dir = tempfile::tempdir().unwrap();
21022 let db = Arc::new(Database::create(dir.path()).unwrap());
21023 let table_id = db.create_table("users", unique_schema()).unwrap();
21024 let pk_key = pk_lock_key(table_id, 1);
21025 let entered = Arc::new(std::sync::Barrier::new(2));
21026 let resume = Arc::new(std::sync::Barrier::new(2));
21027 let parked = Arc::new(AtomicBool::new(false));
21028 db.__set_catalog_commit_hook({
21029 let entered = Arc::clone(&entered);
21030 let resume = Arc::clone(&resume);
21031 let parked = Arc::clone(&parked);
21032 move || {
21033 if !parked.swap(true, Ordering::SeqCst) {
21036 entered.wait();
21037 resume.wait();
21038 }
21039 }
21040 });
21041
21042 let mut txn_a = db.begin();
21043 txn_a
21044 .put(
21045 "users",
21046 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21047 )
21048 .unwrap();
21049 let a_id = txn_a.txn_id();
21050 let (a_tx, a_rx) = std::sync::mpsc::channel();
21051 let (b_tx, b_rx) = std::sync::mpsc::channel();
21052 std::thread::scope(|scope| {
21053 scope.spawn(|| {
21054 a_tx.send(txn_a.commit()).unwrap();
21055 });
21056 entered.wait();
21057 assert!(
21059 db.lock_manager().holds(a_id, &pk_key),
21060 "primary-key claim must be held until the commit ends"
21061 );
21062 let mut uq_key = format!("uq{}:", 1).into_bytes();
21063 let cells_map: HashMap<u16, Value> = [(1u16, Value::Bytes(b"a@x".to_vec()))]
21064 .into_iter()
21065 .collect();
21066 uq_key.extend_from_slice(
21067 &crate::constraint::encode_composite_key(&[1], &cells_map).unwrap(),
21068 );
21069 assert!(
21070 db.lock_manager()
21071 .holds(a_id, &LockKey::key(table_id, uq_key)),
21072 "declared-unique claim must be held until the commit ends"
21073 );
21074
21075 let mut txn_b = db.begin();
21076 txn_b
21077 .put(
21078 "users",
21079 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"b@x".to_vec()))],
21080 )
21081 .unwrap();
21082 scope.spawn(|| {
21083 b_tx.send(txn_b.commit()).unwrap();
21084 });
21085 std::thread::sleep(std::time::Duration::from_millis(100));
21086 assert!(
21087 b_rx.try_recv().is_err(),
21088 "the concurrent claim must block until A ends its transaction"
21089 );
21090 resume.wait();
21091 assert!(a_rx.recv().unwrap().is_ok());
21092 let b_result = b_rx.recv().unwrap();
21093 assert!(
21094 matches!(b_result, Err(MongrelError::Conflict(_))),
21095 "the loser surfaces a conflict after serializing: {b_result:?}"
21096 );
21097 });
21098 assert!(
21099 !db.lock_manager().holds(a_id, &pk_key),
21100 "no phantom holds remain after the commit"
21101 );
21102 }
21103
21104 #[test]
21105 fn ddl_waits_for_inflight_dml_commit_on_schema_barrier() {
21106 let dir = tempfile::tempdir().unwrap();
21107 let db = Arc::new(Database::create(dir.path()).unwrap());
21108 db.create_table("parent", parent_schema()).unwrap();
21109 db.create_table("child", child_schema()).unwrap();
21110 let mut seed = db.begin();
21111 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21112 seed.commit().unwrap();
21113
21114 let entered = Arc::new(std::sync::Barrier::new(2));
21115 let resume = Arc::new(std::sync::Barrier::new(2));
21116 db.__set_fk_lock_hook({
21117 let entered = Arc::clone(&entered);
21118 let resume = Arc::clone(&resume);
21119 move || {
21120 entered.wait();
21121 resume.wait();
21122 }
21123 });
21124
21125 let mut txn_a = db.begin();
21126 txn_a
21127 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(1))])
21128 .unwrap();
21129 let a_id = txn_a.txn_id();
21130 let (a_tx, a_rx) = std::sync::mpsc::channel();
21131 let (ddl_tx, ddl_rx) = std::sync::mpsc::channel();
21132 std::thread::scope(|scope| {
21133 scope.spawn(|| {
21134 a_tx.send(txn_a.commit()).unwrap();
21135 });
21136 entered.wait();
21137 assert!(
21139 db.lock_manager().holds(a_id, &LockKey::schema_barrier()),
21140 "DML holds the schema barrier Shared for its commit"
21141 );
21142 let db = Arc::clone(&db);
21143 scope.spawn(move || {
21144 ddl_tx.send(db.drop_table("parent")).unwrap();
21145 });
21146 std::thread::sleep(std::time::Duration::from_millis(100));
21147 assert!(
21148 ddl_rx.try_recv().is_err(),
21149 "DDL must wait on the Exclusive schema barrier while DML is in flight"
21150 );
21151 resume.wait();
21152 let a_result = a_rx.recv().unwrap();
21158 match &a_result {
21159 Ok(_) => {}
21160 Err(MongrelError::Conflict(message)) => {
21161 assert!(
21162 message.contains("security policy changed during write"),
21163 "unexpected commit conflict: {message}"
21164 );
21165 }
21166 other => panic!("unexpected commit outcome: {other:?}"),
21167 }
21168 assert!(ddl_rx.recv().unwrap().is_ok());
21169 });
21170 assert!(!db.lock_manager().holds(a_id, &LockKey::schema_barrier()));
21171 }
21172
21173 #[test]
21174 fn auto_increment_sequence_barrier_held_until_commit() {
21175 let dir = tempfile::tempdir().unwrap();
21176 let db = Database::create(dir.path()).unwrap();
21177 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
21178 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
21179 let entered = Arc::new(std::sync::Barrier::new(2));
21180 let resume = Arc::new(std::sync::Barrier::new(2));
21181 let parked = Arc::new(AtomicBool::new(false));
21182 db.__set_catalog_commit_hook({
21183 let entered = Arc::clone(&entered);
21184 let resume = Arc::clone(&resume);
21185 let parked = Arc::clone(&parked);
21186 move || {
21187 if !parked.swap(true, Ordering::SeqCst) {
21188 entered.wait();
21189 resume.wait();
21190 }
21191 }
21192 });
21193
21194 let mut txn_a = db.begin();
21195 txn_a.put("seq_t", vec![(0, Value::Null)]).unwrap();
21196 let a_id = txn_a.txn_id();
21197 assert!(
21199 db.lock_manager().holds(a_id, &barrier_key),
21200 "sequence allocation takes the barrier at stage time"
21201 );
21202 let (a_tx, a_rx) = std::sync::mpsc::channel();
21203 std::thread::scope(|scope| {
21204 scope.spawn(|| {
21205 a_tx.send(txn_a.commit()).unwrap();
21206 });
21207 entered.wait();
21208 assert!(
21209 db.lock_manager().holds(a_id, &barrier_key),
21210 "the barrier is held through the commit"
21211 );
21212 resume.wait();
21213 assert!(a_rx.recv().unwrap().is_ok());
21214 });
21215 assert!(
21216 !db.lock_manager().holds(a_id, &barrier_key),
21217 "the barrier releases when the commit ends"
21218 );
21219 }
21220
21221 #[test]
21222 fn fk_wait_for_cycle_surfaces_deadlock_victim() {
21223 let dir = tempfile::tempdir().unwrap();
21224 let db = Database::create(dir.path()).unwrap();
21225 db.create_table("parent", parent_schema()).unwrap();
21226 db.create_table("child", child_schema()).unwrap();
21227 let mut seed = db.begin();
21228 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21229 seed.put("parent", vec![(0, Value::Int64(2))]).unwrap();
21230 seed.commit().unwrap();
21231 let (rid1, rid2) = {
21232 let handle = db.table("parent").unwrap();
21233 let table = handle.lock();
21234 let rid = |pk: i64| {
21235 table
21236 .lookup_pk(&Value::Int64(pk).encode_key())
21237 .expect("seeded parent row")
21238 };
21239 (rid(1), rid(2))
21240 };
21241
21242 let rendezvous = Arc::new(std::sync::Barrier::new(2));
21247 let calls = Arc::new(AtomicUsize::new(0));
21248 db.__set_fk_lock_hook({
21249 let rendezvous = Arc::clone(&rendezvous);
21250 let calls = Arc::clone(&calls);
21251 move || {
21252 if calls.fetch_add(1, Ordering::SeqCst) < 2 {
21253 rendezvous.wait();
21254 }
21255 }
21256 });
21257
21258 let mut txn_a = db.begin();
21261 txn_a.delete("parent", rid1).unwrap();
21262 txn_a
21263 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(2))])
21264 .unwrap();
21265 let mut txn_b = db.begin();
21266 txn_b.delete("parent", rid2).unwrap();
21267 txn_b
21268 .put("child", vec![(0, Value::Int64(101)), (1, Value::Int64(1))])
21269 .unwrap();
21270 let b_id = txn_b.txn_id();
21271
21272 let (a_tx, a_rx) = std::sync::mpsc::channel();
21273 let (b_tx, b_rx) = std::sync::mpsc::channel();
21274 std::thread::scope(|scope| {
21275 scope.spawn(|| {
21276 a_tx.send(txn_a.commit()).unwrap();
21277 });
21278 scope.spawn(|| {
21279 b_tx.send(txn_b.commit()).unwrap();
21280 });
21281 let a_result = a_rx.recv().unwrap();
21282 let b_result = b_rx.recv().unwrap();
21283 assert!(
21284 a_result.is_ok(),
21285 "the survivor commits once the victim releases: {a_result:?}"
21286 );
21287 match b_result {
21288 Err(MongrelError::Deadlock { victim, .. }) => {
21289 assert_eq!(victim, b_id, "the youngest transaction is the victim");
21290 }
21291 other => panic!("the victim must surface a deadlock, got {other:?}"),
21292 }
21293 });
21294 let fk_key = |table: &str, pk: i64| {
21296 let table_id = db.table_id(table).unwrap();
21297 let mut key = b"fk:".to_vec();
21298 key.extend_from_slice(&Value::Int64(pk).encode_key());
21299 LockKey::key(table_id, key)
21300 };
21301 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 2)));
21302 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 1)));
21303 }
21304
21305 #[test]
21306 fn locks_release_after_commit_rollback_and_failed_commit() {
21307 let dir = tempfile::tempdir().unwrap();
21308 let db = Database::create(dir.path()).unwrap();
21309 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
21310 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
21311
21312 let mut txn = db.begin();
21314 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21315 let committed_id = txn.txn_id();
21316 txn.commit().unwrap();
21317 assert!(!db.lock_manager().holds(committed_id, &barrier_key));
21318
21319 let mut txn = db.begin();
21321 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21322 let rolled_back_id = txn.txn_id();
21323 assert!(db.lock_manager().holds(rolled_back_id, &barrier_key));
21324 txn.rollback();
21325 assert!(
21326 !db.lock_manager().holds(rolled_back_id, &barrier_key),
21327 "rollback must not leave phantom holds"
21328 );
21329
21330 db.create_table("users", unique_schema()).unwrap();
21333 let users_id = db.table_id("users").unwrap();
21334 let mut seed = db.begin();
21335 seed.put(
21336 "users",
21337 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21338 )
21339 .unwrap();
21340 seed.commit().unwrap();
21341 let mut txn = db.begin();
21342 txn.put(
21345 "users",
21346 vec![(0, Value::Int64(2)), (1, Value::Bytes(b"a@x".to_vec()))],
21347 )
21348 .unwrap();
21349 let failed_id = txn.txn_id();
21350 let result = txn.commit();
21351 assert!(matches!(result, Err(MongrelError::Conflict(_))));
21352 assert!(
21353 !db.lock_manager()
21354 .holds(failed_id, &pk_lock_key(users_id, 2)),
21355 "a failed commit must not leave phantom holds"
21356 );
21357 }
21358}
21359
21360#[cfg(test)]
21361mod lifecycle_tests {
21362 use super::*;
21363
21364 fn int_pk_schema() -> Schema {
21365 Schema {
21366 columns: vec![ColumnDef {
21367 id: 1,
21368 name: "id".into(),
21369 ty: TypeId::Int64,
21370 flags: crate::schema::ColumnFlags::empty()
21371 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21372 default_value: None,
21373 embedding_source: None,
21374 }],
21375 ..Schema::default()
21376 }
21377 }
21378
21379 #[test]
21380 fn poisoned_core_rejects_operations_with_typed_errors() {
21381 let dir = tempfile::tempdir().unwrap();
21382 let db = Database::create(dir.path()).unwrap();
21383 db.create_table("t", int_pk_schema()).unwrap();
21384 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Open);
21385
21386 db.poisoned.store(true, Ordering::Relaxed);
21391 db.lifecycle.poison();
21392 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Poisoned);
21393
21394 let error = db.gc().unwrap_err();
21397 assert!(
21398 matches!(error, MongrelError::Conflict(_)),
21399 "gc must reject on a poisoned core: {error:?}"
21400 );
21401 let error = db.compact().unwrap_err();
21402 assert!(
21403 matches!(error, MongrelError::Conflict(_)),
21404 "compact must reject on a poisoned core: {error:?}"
21405 );
21406 assert!(db.operation_guard().is_err());
21407 let error = db.create_table("t2", int_pk_schema()).unwrap_err();
21410 assert!(
21411 error.to_string().contains("database poisoned"),
21412 "the legacy poison error still wins where it existed: {error:?}"
21413 );
21414 let mut txn = db.begin();
21415 txn.put("t", vec![(1, Value::Int64(2))]).unwrap();
21416 assert!(txn
21417 .commit()
21418 .unwrap_err()
21419 .to_string()
21420 .contains("database poisoned"));
21421 }
21422
21423 #[test]
21424 fn shutdown_waits_for_operation_guards_to_drain() {
21425 let dir = tempfile::tempdir().unwrap();
21426 let db = Arc::new(Database::create(dir.path()).unwrap());
21427 db.create_table("t", int_pk_schema()).unwrap();
21428 let guard = db.operation_guard().unwrap();
21431 let (started_tx, started_rx) = std::sync::mpsc::channel();
21432 let (done_tx, done_rx) = std::sync::mpsc::channel();
21433 let shutdown_thread = std::thread::spawn(move || {
21434 started_tx.send(()).unwrap();
21435 let result = db.shutdown();
21436 let _ = done_tx.send(result);
21437 });
21438 started_rx.recv().unwrap();
21439 std::thread::sleep(std::time::Duration::from_millis(100));
21440 assert!(
21441 done_rx.try_recv().is_err(),
21442 "shutdown must wait for the outstanding guard to drain"
21443 );
21444 drop(guard);
21445 shutdown_thread.join().unwrap();
21446 assert!(
21447 done_rx.recv().unwrap().is_ok(),
21448 "shutdown completes once the guard drops"
21449 );
21450 }
21451}
21452
21453#[cfg(test)]
21454mod commit_ts_ledger_tests {
21455 use super::*;
21456 use crate::memtable::Row;
21457
21458 fn int_pk_schema() -> Schema {
21459 Schema {
21460 columns: vec![ColumnDef {
21461 id: 1,
21462 name: "id".into(),
21463 ty: TypeId::Int64,
21464 flags: crate::schema::ColumnFlags::empty()
21465 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21466 default_value: None,
21467 embedding_source: None,
21468 }],
21469 ..Schema::default()
21470 }
21471 }
21472
21473 fn commit_one(db: &Database) -> (Epoch, mongreldb_types::hlc::HlcTimestamp) {
21474 let mut txn = db.begin();
21475 let handle = txn.state_handle();
21476 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
21477 let epoch = txn.commit().unwrap();
21478 let crate::txn::TransactionState::Committed(receipt) = handle.state() else {
21479 panic!("expected Committed, got {:?}", handle.state());
21480 };
21481 (epoch, receipt.commit_ts)
21482 }
21483
21484 #[test]
21485 fn commit_ts_for_epoch_returns_the_exact_receipt_within_one_open() {
21486 let dir = tempfile::tempdir().unwrap();
21487 let db = Database::create(dir.path()).unwrap();
21488 db.create_table("t", int_pk_schema()).unwrap();
21489
21490 let (epoch, commit_ts) = commit_one(&db);
21491 assert_eq!(db.commit_ts_for_epoch(epoch), Some(commit_ts));
21492 assert_eq!(db.commit_ts_for_epoch(Epoch(epoch.0 + 100)), None);
21494 }
21495
21496 #[test]
21497 fn commit_ts_for_epoch_survives_reopen_with_the_physical_component() {
21498 let dir = tempfile::tempdir().unwrap();
21499 let (epoch, commit_ts) = {
21500 let db = Database::create(dir.path()).unwrap();
21501 db.create_table("t", int_pk_schema()).unwrap();
21502 commit_one(&db)
21503 };
21504
21505 let db = Database::open(dir.path()).unwrap();
21506 let reconstructed = db
21507 .commit_ts_for_epoch(epoch)
21508 .expect("the durable WAL CommitTimestamp ledger reconstructs the epoch");
21509 assert_eq!(reconstructed.physical_micros, commit_ts.physical_micros);
21510 assert_eq!(reconstructed.logical, 0);
21513 assert_eq!(reconstructed.node_tiebreaker, 0);
21514 }
21515
21516 #[test]
21517 fn recovery_ledger_keeps_only_newest_epochs_and_ignores_aborted_txns() {
21518 use crate::wal::Op;
21519 let records = vec![
21520 crate::wal::Record::new(Epoch(1), 7, Op::CommitTimestamp { unix_nanos: 1_000 }),
21521 crate::wal::Record::new(
21522 Epoch(2),
21523 7,
21524 Op::TxnCommit {
21525 epoch: 41,
21526 added_runs: vec![],
21527 },
21528 ),
21529 crate::wal::Record::new(
21531 Epoch(3),
21532 8,
21533 Op::TxnCommit {
21534 epoch: 42,
21535 added_runs: vec![],
21536 },
21537 ),
21538 crate::wal::Record::new(Epoch(4), 9, Op::CommitTimestamp { unix_nanos: 9_000 }),
21540 ];
21541 let ledger = commit_ts_ledger_from_recovery(&records);
21542 assert_eq!(ledger.len(), 1);
21543 assert_eq!(
21544 ledger.get(&41),
21545 Some(&mongreldb_types::hlc::HlcTimestamp {
21546 physical_micros: 1,
21547 logical: 0,
21548 node_tiebreaker: 0,
21549 })
21550 );
21551 }
21552
21553 #[test]
21554 fn new_writes_always_have_some_commit_ts() {
21555 let dir = tempfile::tempdir().unwrap();
21556 let db = Database::create(dir.path()).unwrap();
21557 db.create_table("t", int_pk_schema()).unwrap();
21558 let mut txn = db.begin();
21559 let state = txn.state_handle();
21560 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
21561 let epoch = txn.commit().unwrap();
21562 let crate::txn::TransactionState::Committed(receipt) = state.state() else {
21563 panic!("expected Committed receipt");
21564 };
21565 let handle = db.table("t").unwrap();
21566 let table = handle.lock();
21567 let (snap, _g) = db.snapshot();
21570 let rows = table.visible_rows(snap).expect("visible rows");
21571 assert_eq!(
21572 rows.len(),
21573 1,
21574 "committed put must be visible under HLC snapshot"
21575 );
21576 assert_eq!(rows[0].commit_ts, Some(receipt.commit_ts));
21577 assert_eq!(db.commit_ts_for_epoch(epoch), Some(receipt.commit_ts));
21578 }
21579
21580 #[test]
21581 fn same_transaction_identical_hlc_on_apply() {
21582 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21583
21584 let dir = tempfile::tempdir().unwrap();
21585 let db = Database::create_cluster_replica(
21586 dir.path(),
21587 ClusterId::from_bytes([1; 16]),
21588 NodeId::from_bytes([2; 16]),
21589 DatabaseId::from_bytes([3; 16]),
21590 )
21591 .unwrap();
21592 db.apply_replicated_catalog_command(&crate::catalog_cmds::CatalogCommandRecord {
21593 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21594 catalog_version: 1,
21595 command: crate::catalog_cmds::CatalogCommand::CreateTable {
21596 name: "t".into(),
21597 schema: int_pk_schema(),
21598 created_epoch: 1,
21599 },
21600 })
21601 .unwrap();
21602 let table_id = db.table_id("t").unwrap();
21603 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
21604 physical_micros: 42_000,
21605 logical: 3,
21606 node_tiebreaker: 9,
21607 };
21608 let staged = vec![StagedTxnWrite::Put {
21611 table_id,
21612 rows: bincode::serialize(&vec![
21613 Row::new(crate::RowId(1), Epoch(0)).with_column(1, Value::Int64(1))
21614 ])
21615 .unwrap(),
21616 }
21617 .encode()
21618 .unwrap()];
21619 assert!(db
21620 .apply_committed_transaction(1 << 63, commit_ts, &staged)
21621 .unwrap());
21622 let staged2 = vec![StagedTxnWrite::Put {
21623 table_id,
21624 rows: bincode::serialize(&vec![
21625 Row::new(crate::RowId(2), Epoch(0)).with_column(1, Value::Int64(2))
21626 ])
21627 .unwrap(),
21628 }
21629 .encode()
21630 .unwrap()];
21631 assert!(db
21632 .apply_committed_transaction((1 << 63) + 1, commit_ts, &staged2)
21633 .unwrap());
21634 let handle = db.table("t").unwrap();
21635 let table = handle.lock();
21636 let row1 = table
21637 .get(crate::RowId(1), Snapshot::unbounded())
21638 .expect("applied row 1");
21639 let row2 = table
21640 .get(crate::RowId(2), Snapshot::unbounded())
21641 .expect("applied row 2");
21642 assert_eq!(row1.commit_ts, Some(commit_ts));
21643 assert_eq!(row2.commit_ts, Some(commit_ts));
21644 assert_eq!(row1.commit_ts, row2.commit_ts);
21645 drop(table);
21646 let epoch = db.visible_epoch();
21649 assert_eq!(
21650 db.commit_ts_for_epoch(epoch).map(|ts| ts.physical_micros),
21651 Some(commit_ts.physical_micros)
21652 );
21653 }
21654
21655 #[test]
21656 fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
21657 use mongreldb_types::hlc::HlcTimestamp;
21658 let early = HlcTimestamp {
21659 physical_micros: 100,
21660 logical: 0,
21661 node_tiebreaker: 1,
21662 };
21663 let late = HlcTimestamp {
21664 physical_micros: 200,
21665 logical: 0,
21666 node_tiebreaker: 1,
21667 };
21668 let snap = Snapshot::at_hlc(Epoch(99), early);
21670 assert!(!snap.observes_version(Epoch(1), Some(late)));
21671 assert!(snap.observes_version(Epoch(1), Some(early)));
21672 let dir = tempfile::tempdir().unwrap();
21674 let db = Database::create(dir.path()).unwrap();
21675 let (snap, _g) = db.snapshot();
21676 assert_ne!(
21677 snap.commit_ts,
21678 HlcTimestamp::ZERO,
21679 "Database::snapshot must pin live HLC via at_hlc"
21680 );
21681 }
21682
21683 #[test]
21684 fn hlc_stamped_row_visible_at_hlc_snapshot_not_epoch_only() {
21685 let dir = tempfile::tempdir().unwrap();
21686 let db = Database::create(dir.path()).unwrap();
21687 db.create_table("t", int_pk_schema()).unwrap();
21688 let (_epoch, commit_ts) = commit_one(&db);
21689 assert_ne!(commit_ts, mongreldb_types::hlc::HlcTimestamp::ZERO);
21690
21691 let handle = db.table("t").unwrap();
21692 let table = handle.lock();
21693 let hlc_snap = Snapshot::at_hlc(Epoch(u64::MAX), commit_ts);
21695 let rows = table.visible_rows(hlc_snap).expect("visible");
21696 assert_eq!(rows.len(), 1);
21697 assert_eq!(rows[0].commit_ts, Some(commit_ts));
21698 assert!(table.get(rows[0].row_id, hlc_snap).is_some());
21699
21700 let legacy = Snapshot::at(Epoch(u64::MAX));
21702 assert!(!legacy.uses_hlc_authority());
21703 assert_eq!(
21704 table.visible_rows(legacy).expect("visible").len(),
21705 1,
21706 "epoch pin sees HLC-stamped rows by epoch during dual-model migration"
21707 );
21708 assert!(table.get(rows[0].row_id, legacy).is_some());
21709 }
21710
21711 #[test]
21712 fn hlc_gc_floor_reports_named_sources() {
21713 let dir = tempfile::tempdir().unwrap();
21714 let db = Database::create(dir.path()).unwrap();
21715 db.create_table("t", int_pk_schema()).unwrap();
21716 let (epoch, commit_ts) = commit_one(&db);
21717
21718 let empty = db.hlc_gc_floor();
21720 assert_eq!(empty.floor(), mongreldb_types::hlc::HlcTimestamp::ZERO);
21721 assert_eq!(empty.sources().len(), 6);
21722
21723 let (_snap, guard) = db.snapshot();
21725 let with_pin = db.hlc_gc_floor();
21726 let projected = db.commit_ts_for_epoch(epoch);
21728 if let Some(ts) = projected {
21729 assert_eq!(with_pin.transaction_snapshot, ts);
21731 assert_eq!(with_pin.floor(), ts);
21732 assert_eq!(ts.physical_micros, commit_ts.physical_micros);
21733 } else {
21734 assert_eq!(
21735 with_pin.transaction_snapshot,
21736 mongreldb_types::hlc::HlcTimestamp::ZERO
21737 );
21738 }
21739 drop(guard);
21740 }
21741}
21742
21743#[cfg(test)]
21744mod stage2e_storage_mode_tests {
21745 use super::*;
21746 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21747 use crate::storage_mode::{StorageMode, STORAGE_MODE_FILENAME};
21748 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21749
21750 fn identity(seed: u8) -> (ClusterId, NodeId, DatabaseId) {
21751 (
21752 ClusterId::from_bytes([seed; 16]),
21753 NodeId::from_bytes([seed + 1; 16]),
21754 DatabaseId::from_bytes([seed + 2; 16]),
21755 )
21756 }
21757
21758 fn marker(root: &Path) -> Option<StorageMode> {
21759 let durable = crate::durable_file::DurableRoot::open(root).unwrap();
21760 crate::storage_mode::read(&durable).unwrap()
21761 }
21762
21763 fn simple_schema() -> Schema {
21764 Schema {
21765 columns: vec![ColumnDef {
21766 id: 1,
21767 name: "id".into(),
21768 ty: TypeId::Int64,
21769 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21770 default_value: None,
21771 embedding_source: None,
21772 }],
21773 ..Schema::default()
21774 }
21775 }
21776
21777 #[test]
21778 fn standalone_create_writes_marker_and_reopens() {
21779 let dir = tempfile::tempdir().unwrap();
21780 let root = dir.path().join("db");
21781 let db = Database::create(&root).unwrap();
21782 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21783 assert_eq!(db.storage_mode().unwrap(), Some(StorageMode::Standalone));
21784 drop(db);
21785 let db = Database::open(&root).unwrap();
21786 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21787 drop(db);
21788 }
21789
21790 #[test]
21791 fn legacy_database_without_marker_opens_and_gains_marker() {
21792 let dir = tempfile::tempdir().unwrap();
21793 let root = dir.path().join("db");
21794 let db = Database::create(&root).unwrap();
21795 drop(db);
21796 std::fs::remove_file(root.join(META_DIR).join(STORAGE_MODE_FILENAME)).unwrap();
21798 assert_eq!(marker(&root), None);
21799 let db = Database::open(&root).unwrap();
21800 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21801 drop(db);
21802 }
21803
21804 #[test]
21805 fn server_owned_standalone_opens_embedded() {
21806 let dir = tempfile::tempdir().unwrap();
21807 let root = dir.path().join("db");
21808 let db = Database::create(&root).unwrap();
21809 drop(db);
21810 let durable = crate::durable_file::DurableRoot::open(&root).unwrap();
21811 crate::storage_mode::rewrite(&durable, &StorageMode::ServerOwnedStandalone).unwrap();
21812 let db = Database::open(&root).unwrap();
21813 assert_eq!(marker(&root), Some(StorageMode::ServerOwnedStandalone));
21814 drop(db);
21815 }
21816
21817 #[test]
21818 fn cluster_replica_is_rejected_by_normal_opens() {
21819 let dir = tempfile::tempdir().unwrap();
21820 let root = dir.path().join("db");
21821 let (cluster_id, node_id, database_id) = identity(10);
21822 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21823 assert_eq!(
21824 marker(&root),
21825 Some(StorageMode::ClusterReplica {
21826 cluster_id,
21827 node_id,
21828 database_id,
21829 })
21830 );
21831 drop(db);
21832
21833 let error = Database::open(&root).unwrap_err();
21834 let message = error.to_string();
21835 assert!(
21836 matches!(error, MongrelError::InvalidArgument(_)),
21837 "unexpected error: {message}"
21838 );
21839 assert!(message.contains("cluster node runtime"), "{message}");
21840 assert!(message.contains(&cluster_id.to_hex()), "{message}");
21841 assert!(message.contains(&node_id.to_hex()), "{message}");
21842 assert!(message.contains(&database_id.to_hex()), "{message}");
21843
21844 let error = Database::open_with_options(&root, OpenOptions::default()).unwrap_err();
21845 assert!(error.to_string().contains("cluster node runtime"));
21846 assert_eq!(
21848 marker(&root),
21849 Some(StorageMode::ClusterReplica {
21850 cluster_id,
21851 node_id,
21852 database_id,
21853 })
21854 );
21855 }
21856
21857 #[test]
21858 fn offline_validation_opens_cluster_replica_read_only() {
21859 let dir = tempfile::tempdir().unwrap();
21860 let root = dir.path().join("db");
21861 let (cluster_id, node_id, database_id) = identity(20);
21862 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21863 drop(db);
21864
21865 let options = OpenOptions::default().with_offline_validation(true);
21866 let db = Database::open_with_options(&root, options).unwrap();
21867 assert!(db.is_read_only_replica());
21868 let error = db.create_table("t", simple_schema()).unwrap_err();
21869 assert!(matches!(error, MongrelError::ReadOnlyReplica));
21870 drop(db);
21871 assert_eq!(
21873 marker(&root),
21874 Some(StorageMode::ClusterReplica {
21875 cluster_id,
21876 node_id,
21877 database_id,
21878 })
21879 );
21880 }
21881
21882 #[test]
21883 fn cluster_runtime_open_requires_exact_identity() {
21884 let dir = tempfile::tempdir().unwrap();
21885 let root = dir.path().join("db");
21886 let (cluster_id, node_id, database_id) = identity(30);
21887 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21888 drop(db);
21889
21890 let error = Database::open_cluster_replica(&root, &StorageMode::Standalone).unwrap_err();
21892 assert!(matches!(error, MongrelError::InvalidArgument(_)));
21893 let wrong = StorageMode::ClusterReplica {
21895 cluster_id,
21896 node_id,
21897 database_id: DatabaseId::from_bytes([99; 16]),
21898 };
21899 let error = Database::open_cluster_replica(&root, &wrong).unwrap_err();
21900 assert!(error.to_string().contains("identity mismatch"), "{error}");
21901 let legacy = dir.path().join("legacy");
21903 let legacy_db = Database::create(&legacy).unwrap();
21904 drop(legacy_db);
21905 let expected = StorageMode::ClusterReplica {
21906 cluster_id,
21907 node_id,
21908 database_id,
21909 };
21910 let error = Database::open_cluster_replica(&legacy, &expected).unwrap_err();
21911 assert!(error.to_string().contains("identity mismatch"), "{error}");
21912
21913 let db = Database::open_cluster_replica(&root, &expected).unwrap();
21916 assert!(db.is_read_only_replica());
21917 let error = db.create_table("t", simple_schema()).unwrap_err();
21918 assert!(matches!(error, MongrelError::ReadOnlyReplica));
21919 drop(db);
21920 }
21921}
21922
21923#[cfg(test)]
21924mod stage2e_replicated_apply_tests {
21925 use super::*;
21926 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord, CatalogDelta};
21927 use crate::memtable::{Row, Value};
21928 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21929 use crate::wal::{Op, Record};
21930 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21931 use std::sync::Arc;
21932
21933 fn ids() -> (ClusterId, NodeId, DatabaseId) {
21934 (
21935 ClusterId::from_bytes([1; 16]),
21936 NodeId::from_bytes([2; 16]),
21937 DatabaseId::from_bytes([3; 16]),
21938 )
21939 }
21940
21941 fn expected_mode() -> crate::storage_mode::StorageMode {
21942 let (cluster_id, node_id, database_id) = ids();
21943 crate::storage_mode::StorageMode::ClusterReplica {
21944 cluster_id,
21945 node_id,
21946 database_id,
21947 }
21948 }
21949
21950 fn simple_schema() -> Schema {
21951 Schema {
21952 columns: vec![ColumnDef {
21953 id: 1,
21954 name: "id".into(),
21955 ty: TypeId::Int64,
21956 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21957 default_value: None,
21958 embedding_source: None,
21959 }],
21960 ..Schema::default()
21961 }
21962 }
21963
21964 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
21965 CatalogCommandRecord {
21966 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21967 catalog_version,
21968 command: CatalogCommand::CreateTable {
21969 name: name.to_string(),
21970 schema: simple_schema(),
21971 created_epoch: 1,
21972 },
21973 }
21974 }
21975
21976 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
21977 let rows: Vec<Row> = values
21978 .iter()
21979 .map(|value| {
21980 Row::new(crate::RowId(*value as u64), Epoch(epoch))
21983 .with_column(1, Value::Int64(*value))
21984 })
21985 .collect();
21986 vec![
21987 Record::new(
21988 Epoch(0),
21989 txn_id,
21990 Op::Put {
21991 table_id,
21992 rows: bincode::serialize(&rows).unwrap(),
21993 },
21994 ),
21995 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
21996 Record::new(
21997 Epoch(0),
21998 txn_id,
21999 Op::TxnCommit {
22000 epoch,
22001 added_runs: Vec::new(),
22002 },
22003 ),
22004 ]
22005 }
22006
22007 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22008 let handle = db.table(table).unwrap();
22009 let snap = db.visible_snapshot();
22011 let rows = handle.lock().visible_rows(snap).unwrap();
22012 let mut values: Vec<i64> = rows
22013 .iter()
22014 .map(|row| match row.columns.get(&1) {
22015 Some(Value::Int64(value)) => *value,
22016 other => panic!("unexpected column: {other:?}"),
22017 })
22018 .collect();
22019 values.sort_unstable();
22020 values
22021 }
22022
22023 #[test]
22024 fn catalog_command_mounts_table_and_replays_as_noop() {
22025 let dir = tempfile::tempdir().unwrap();
22026 let (cluster_id, node_id, database_id) = ids();
22027 let db =
22028 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22029
22030 let record = create_table_record("items", 1);
22031 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22032 assert!(matches!(delta, CatalogDelta::TableCreated { .. }));
22033 assert_eq!(db.table_names(), vec!["items".to_string()]);
22034 assert_eq!(db.catalog_version(), 1);
22035
22036 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22038 assert!(matches!(delta, CatalogDelta::NoOp));
22039 assert_eq!(db.table_names().len(), 1);
22040 drop(db);
22041
22042 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22044 assert_eq!(db.table_names(), vec!["items".to_string()]);
22045 assert_eq!(db.catalog_version(), 1);
22046 }
22047
22048 #[test]
22049 fn records_apply_rows_and_skip_replays_across_restart() {
22050 let dir = tempfile::tempdir().unwrap();
22051 let (cluster_id, node_id, database_id) = ids();
22052 let db =
22053 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22054 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22055 .unwrap();
22056
22057 let records = put_records(1, 0, 2, &[10, 20, 30]);
22058 assert!(db.apply_replicated_records(&records).unwrap());
22059 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22060 assert_eq!(db.visible_epoch(), Epoch(2));
22061
22062 assert!(!db.apply_replicated_records(&records).unwrap());
22065 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22066
22067 let later = put_records(2, 0, 3, &[40]);
22069 assert!(db.apply_replicated_records(&later).unwrap());
22070 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22071 let db = Arc::new(db);
22072 db.shutdown().unwrap();
22073
22074 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22077 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22078 assert!(!db.apply_replicated_records(&later).unwrap());
22079 assert!(!db.apply_replicated_records(&records).unwrap());
22080 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22081 }
22082
22083 #[test]
22084 fn spilled_run_commits_fail_closed_this_wave() {
22085 let dir = tempfile::tempdir().unwrap();
22086 let (cluster_id, node_id, database_id) = ids();
22087 let db =
22088 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22089 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22090 .unwrap();
22091 let mut records = put_records(1, 0, 2, &[10]);
22092 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22093 panic!("put_records ends in TxnCommit");
22094 };
22095 added_runs.push(crate::wal::AddedRun {
22096 table_id: 0,
22097 run_id: 7,
22098 row_count: 1,
22099 level: 0,
22100 min_row_id: 1,
22101 max_row_id: 1,
22102 content_hash: [0; 32],
22103 });
22104 let error = db.apply_replicated_records(&records).unwrap_err();
22105 assert!(
22106 error.to_string().contains("spilled-run"),
22107 "unexpected error: {error}"
22108 );
22109 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22110 }
22111
22112 #[test]
22113 fn records_without_commit_marker_fail_closed() {
22114 let dir = tempfile::tempdir().unwrap();
22115 let (cluster_id, node_id, database_id) = ids();
22116 let db =
22117 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22118 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22119 .unwrap();
22120 let mut records = put_records(1, 0, 2, &[10]);
22121 records.pop(); let error = db.apply_replicated_records(&records).unwrap_err();
22123 assert!(matches!(error, MongrelError::InvalidArgument(_)));
22124 assert!(db.apply_replicated_records(&[]).is_err());
22125 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22126 }
22127}
22128
22129#[cfg(test)]
22130mod stage2c_spill_translation_tests {
22131 use super::*;
22132 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord};
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
22138 fn simple_schema() -> Schema {
22139 Schema {
22140 columns: vec![ColumnDef {
22141 id: 1,
22142 name: "id".into(),
22143 ty: TypeId::Int64,
22144 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
22145 default_value: None,
22146 embedding_source: None,
22147 }],
22148 ..Schema::default()
22149 }
22150 }
22151
22152 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
22153 CatalogCommandRecord {
22154 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
22155 catalog_version,
22156 command: CatalogCommand::CreateTable {
22157 name: name.to_string(),
22158 schema: simple_schema(),
22159 created_epoch: 1,
22160 },
22161 }
22162 }
22163
22164 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22165 let handle = db.table(table).unwrap();
22166 let snap = db.visible_snapshot();
22168 let rows = handle.lock().visible_rows(snap).unwrap();
22169 let mut values: Vec<i64> = rows
22170 .iter()
22171 .map(|row| match row.columns.get(&1) {
22172 Some(Value::Int64(value)) => *value,
22173 other => panic!("unexpected column: {other:?}"),
22174 })
22175 .collect();
22176 values.sort_unstable();
22177 values
22178 }
22179
22180 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
22181 let rows: Vec<Row> = values
22182 .iter()
22183 .map(|value| {
22184 Row::new(crate::RowId(*value as u64), Epoch(epoch))
22185 .with_column(1, Value::Int64(*value))
22186 })
22187 .collect();
22188 vec![
22189 Record::new(
22190 Epoch(0),
22191 txn_id,
22192 Op::Put {
22193 table_id,
22194 rows: bincode::serialize(&rows).unwrap(),
22195 },
22196 ),
22197 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
22198 Record::new(
22199 Epoch(0),
22200 txn_id,
22201 Op::TxnCommit {
22202 epoch,
22203 added_runs: Vec::new(),
22204 },
22205 ),
22206 ]
22207 }
22208
22209 fn added_run(
22210 table_id: u64,
22211 row_count: u64,
22212 min_row_id: u64,
22213 max_row_id: u64,
22214 ) -> crate::wal::AddedRun {
22215 crate::wal::AddedRun {
22216 table_id,
22217 run_id: 7,
22218 row_count,
22219 level: 0,
22220 min_row_id,
22221 max_row_id,
22222 content_hash: [0; 32],
22223 }
22224 }
22225
22226 fn spilled_commit_records(db: &Database) -> Vec<Record> {
22229 let records = crate::wal::SharedWal::replay_with_dek(&db.root, None).unwrap();
22230 let txn_id = records
22231 .iter()
22232 .find_map(|record| match &record.op {
22233 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty() => Some(record.txn_id),
22234 _ => None,
22235 })
22236 .expect("a spilled commit is present in the WAL");
22237 records
22238 .into_iter()
22239 .filter(|record| record.txn_id == txn_id)
22240 .collect()
22241 }
22242
22243 #[test]
22244 fn non_spilled_records_translate_byte_identical() {
22245 let records = put_records(1, 0, 2, &[10, 20, 30]);
22246 let translated = translate_records_for_replication(&records).unwrap();
22247 assert_eq!(
22248 bincode::serialize(&translated).unwrap(),
22249 bincode::serialize(&records).unwrap(),
22250 "a commit without spill links must pass through byte-identical"
22251 );
22252 }
22253
22254 #[test]
22255 fn translation_rejects_uncovered_or_malformed_spills() {
22256 let mut records = put_records(1, 0, 2, &[10]);
22258 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22259 panic!("put_records ends in TxnCommit");
22260 };
22261 added_runs.push(added_run(0, 1, 10, 10));
22262 let error = translate_records_for_replication(&records).unwrap_err();
22263 assert!(
22264 error.to_string().contains("no logical row records"),
22265 "unexpected error: {error}"
22266 );
22267
22268 let mut records = put_records(1, 0, 2, &[10]);
22270 let spilled: Vec<Row> = (0..3_u64)
22271 .map(|value| {
22272 Row::new(crate::RowId(value), Epoch(2)).with_column(1, Value::Int64(value as i64))
22273 })
22274 .collect();
22275 records.insert(
22276 0,
22277 Record::new(
22278 Epoch(0),
22279 1,
22280 Op::SpilledRows {
22281 table_id: 0,
22282 rows: bincode::serialize(&spilled).unwrap(),
22283 },
22284 ),
22285 );
22286 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22287 panic!("put_records ends in TxnCommit");
22288 };
22289 added_runs.push(added_run(0, 4, 0, 3));
22290 let error = translate_records_for_replication(&records).unwrap_err();
22291 assert!(
22292 error.to_string().contains("cover 3 rows"),
22293 "unexpected error: {error}"
22294 );
22295
22296 let mut records = put_records(1, 0, 2, &[10]);
22298 records.insert(
22299 0,
22300 Record::new(
22301 Epoch(0),
22302 1,
22303 Op::SpilledRows {
22304 table_id: 0,
22305 rows: vec![0xFF, 0x01, 0x02],
22306 },
22307 ),
22308 );
22309 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22310 panic!("put_records ends in TxnCommit");
22311 };
22312 added_runs.push(added_run(0, 1, 0, 0));
22313 assert!(translate_records_for_replication(&records).is_err());
22314
22315 assert!(translate_records_for_replication(&[]).is_err());
22317 let mut mixed = put_records(1, 0, 2, &[10]);
22318 mixed[0].txn_id = 99;
22319 assert!(translate_records_for_replication(&mixed).is_err());
22320 let mut no_commit = put_records(1, 0, 2, &[10]);
22321 no_commit.pop();
22322 assert!(translate_records_for_replication(&no_commit).is_err());
22323 }
22324
22325 #[test]
22326 fn spilled_commit_translates_to_logical_rows_and_applies_on_replica() {
22327 let leader_dir = tempfile::tempdir().unwrap();
22329 let leader = Database::create(leader_dir.path()).unwrap();
22330 leader.create_table("t", simple_schema()).unwrap();
22331 leader.set_spill_threshold(1);
22332 let table_id = leader.table_id("t").unwrap();
22333 let values: Vec<i64> = (0..60).collect();
22334 leader
22335 .transaction(|txn| {
22336 for value in &values {
22337 txn.put("t", vec![(1, Value::Int64(*value))])?;
22338 }
22339 Ok(())
22340 })
22341 .unwrap();
22342 assert_eq!(visible_ids(&leader, "t"), values);
22343
22344 let records = spilled_commit_records(&leader);
22347 assert!(records
22348 .iter()
22349 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22350 let Some(Op::TxnCommit { added_runs, epoch }) = records.last().map(|r| &r.op) else {
22351 panic!("a commit sequence ends in TxnCommit");
22352 };
22353 assert!(!added_runs.is_empty());
22354 let commit_epoch = *epoch;
22355
22356 let translated = translate_records_for_replication(&records).unwrap();
22359 assert!(records
22360 .iter()
22361 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22362 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
22363 panic!("a commit sequence ends in TxnCommit");
22364 };
22365 assert!(!added_runs.is_empty(), "input records must be unchanged");
22366 assert_eq!(translated.len(), records.len());
22367 assert!(translated
22368 .iter()
22369 .all(|record| !matches!(record.op, Op::SpilledRows { .. })));
22370 assert!(translated
22371 .iter()
22372 .any(|record| matches!(record.op, Op::Put { .. })));
22373 let Some(Op::TxnCommit { added_runs, epoch }) = translated.last().map(|r| &r.op) else {
22374 panic!("a commit sequence ends in TxnCommit");
22375 };
22376 assert!(added_runs.is_empty(), "no added_runs may reach a replica");
22377 assert_eq!(*epoch, commit_epoch);
22378
22379 let replica_dir = tempfile::tempdir().unwrap();
22381 let replica = Database::create_cluster_replica(
22382 replica_dir.path(),
22383 ClusterId::from_bytes([1; 16]),
22384 NodeId::from_bytes([2; 16]),
22385 DatabaseId::from_bytes([3; 16]),
22386 )
22387 .unwrap();
22388 replica
22389 .apply_replicated_catalog_command(&create_table_record("t", 1))
22390 .unwrap();
22391 assert_eq!(replica.table_id("t").unwrap(), table_id);
22392 assert!(replica.apply_replicated_records(&translated).unwrap());
22393 assert_eq!(visible_ids(&replica, "t"), values);
22394 assert_eq!(visible_ids(&replica, "t"), visible_ids(&leader, "t"));
22395
22396 drop(leader);
22399 let leader = Database::open(leader_dir.path()).unwrap();
22400 assert_eq!(visible_ids(&leader, "t"), values);
22401 }
22402
22403 #[test]
22404 fn staged_txn_writes_validate_and_apply() {
22405 let dir = tempfile::tempdir().unwrap();
22406 let db = Database::create_cluster_replica(
22407 dir.path(),
22408 ClusterId::from_bytes([1; 16]),
22409 NodeId::from_bytes([2; 16]),
22410 DatabaseId::from_bytes([3; 16]),
22411 )
22412 .unwrap();
22413 db.apply_replicated_catalog_command(&create_table_record("t", 1))
22414 .unwrap();
22415 let table_id = db.table_id("t").unwrap();
22416
22417 assert!(db.validate_staged_txn_writes(&[vec![0xFF]]).is_err());
22419 let unknown_table = StagedTxnWrite::Put {
22420 table_id: 99,
22421 rows: bincode::serialize(&Vec::<Row>::new()).unwrap(),
22422 }
22423 .encode()
22424 .unwrap();
22425 assert!(db.validate_staged_txn_writes(&[unknown_table]).is_err());
22426 let good: Vec<Vec<u8>> = [10_i64, 20, 30]
22427 .iter()
22428 .map(|value| {
22429 let rows = vec![Row::new(crate::RowId(*value as u64), Epoch(0))
22430 .with_column(1, Value::Int64(*value))];
22431 StagedTxnWrite::Put {
22432 table_id,
22433 rows: bincode::serialize(&rows).unwrap(),
22434 }
22435 .encode()
22436 .unwrap()
22437 })
22438 .collect();
22439 db.validate_staged_txn_writes(&good).unwrap();
22440
22441 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
22444 physical_micros: 5_000,
22445 logical: 0,
22446 node_tiebreaker: 0,
22447 };
22448 assert!(db
22449 .apply_staged_txn_writes(1 << 63, &good, commit_ts)
22450 .unwrap());
22451 assert_eq!(visible_ids(&db, "t"), vec![10, 20, 30]);
22452 let delete = StagedTxnWrite::Delete {
22453 table_id,
22454 row_ids: vec![20],
22455 }
22456 .encode()
22457 .unwrap();
22458 assert!(db
22459 .apply_staged_txn_writes((1 << 63) + 1, &[delete], commit_ts)
22460 .unwrap());
22461 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22462
22463 let db = Arc::new(db);
22466 db.shutdown().unwrap();
22467 let expected = crate::storage_mode::StorageMode::ClusterReplica {
22468 cluster_id: ClusterId::from_bytes([1; 16]),
22469 node_id: NodeId::from_bytes([2; 16]),
22470 database_id: DatabaseId::from_bytes([3; 16]),
22471 };
22472 let db = Database::open_cluster_replica(dir.path(), &expected).unwrap();
22473 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22474 }
22475}