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 decoded: Vec<crate::memtable::Row> =
5063 bincode::deserialize(&rows).map_err(|e| {
5064 MongrelError::Other(format!(
5065 "staged txn put rows could not be decoded: {e}"
5066 ))
5067 })?;
5068 let stamped = bincode::serialize(&decoded).map_err(|e| {
5069 MongrelError::Other(format!("staged txn put rows serialize: {e}"))
5070 })?;
5071 Op::Put {
5072 table_id,
5073 rows: stamped,
5074 }
5075 }
5076 StagedTxnWrite::Delete { table_id, row_ids } => Op::Delete {
5077 table_id,
5078 row_ids: row_ids.into_iter().map(crate::RowId).collect(),
5079 },
5080 };
5081 records.push(crate::wal::Record::new(Epoch(0), txn_id, op));
5082 }
5083 let epoch = self.epoch.visible().0 + 1;
5084 let unix_nanos = commit_ts.physical_micros.saturating_mul(1_000);
5088 records.push(crate::wal::Record::new(
5089 Epoch(0),
5090 txn_id,
5091 Op::CommitTimestamp { unix_nanos },
5092 ));
5093 records.push(crate::wal::Record::new(
5094 Epoch(0),
5095 txn_id,
5096 Op::TxnCommit {
5097 epoch,
5098 added_runs: Vec::new(),
5099 },
5100 ));
5101 let applied = self.apply_replicated_records(&records)?;
5102 if applied && commit_ts != mongreldb_types::hlc::HlcTimestamp::ZERO {
5103 self.record_commit_ts(Epoch(epoch), commit_ts);
5106 }
5107 Ok(applied)
5108 }
5109
5110 pub fn apply_committed_transaction(
5117 &self,
5118 transaction_id: u64,
5119 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5120 operations: &[Vec<u8>],
5121 ) -> Result<bool> {
5122 self.apply_staged_txn_writes(transaction_id, operations, commit_ts)
5123 }
5124
5125 pub fn apply_replicated_catalog_command(
5134 &self,
5135 record: &crate::catalog_cmds::CatalogCommandRecord,
5136 ) -> Result<crate::catalog_cmds::CatalogDelta> {
5137 let _operation = self.admit_operation()?;
5138 let _g = self.ddl_lock.lock();
5139 let mut next_catalog = self.catalog.read().clone();
5140 let delta = next_catalog.apply_command(record)?;
5141 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
5142 return Ok(delta);
5143 }
5144 let referenced_epoch = match &delta {
5150 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => Some(entry.created_epoch),
5151 crate::catalog_cmds::CatalogDelta::TableDropped { at_epoch, .. }
5152 | crate::catalog_cmds::CatalogDelta::TableRenamed { at_epoch, .. } => Some(*at_epoch),
5153 _ => None,
5154 };
5155 if let Some(referenced) = referenced_epoch {
5156 self.epoch.advance_recovered(Epoch(referenced));
5157 next_catalog.db_epoch = next_catalog.db_epoch.max(referenced);
5158 }
5159 match &delta {
5160 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => {
5161 if !self.tables.read().contains_key(&entry.table_id) {
5166 self.mount_catalog_entry(entry)?;
5167 }
5168 }
5169 crate::catalog_cmds::CatalogDelta::TableDropped { table_id, .. } => {
5170 self.tables.write().remove(table_id);
5171 }
5172 _ => {}
5173 }
5174 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref())?;
5178 *self.catalog.write() = next_catalog;
5179 Ok(delta)
5180 }
5181
5182 fn mount_catalog_entry(&self, entry: &crate::catalog::CatalogEntry) -> Result<()> {
5187 let table_relative = Path::new(TABLES_DIR).join(entry.table_id.to_string());
5188 let table_root = Arc::new(
5189 self.durable_root
5190 .create_directory_all_pinned(&table_relative)?,
5191 );
5192 let tdir = table_root.io_path()?;
5193 let ctx = SharedCtx {
5194 root_guard: Some(table_root),
5195 epoch: Arc::clone(&self.epoch),
5196 page_cache: Arc::clone(&self.page_cache),
5197 decoded_cache: Arc::clone(&self.decoded_cache),
5198 snapshots: Arc::clone(&self.snapshots),
5199 kek: self.kek.clone(),
5200 commit_lock: Arc::clone(&self.commit_lock),
5201 shared: Some(crate::engine::SharedWalCtx {
5202 wal: Arc::clone(&self.shared_wal),
5203 group: Arc::clone(&self.group),
5204 poisoned: Arc::clone(&self.poisoned),
5205 txn_ids: Arc::clone(&self.next_txn_id),
5206 change_wake: self.change_wake.clone(),
5207 lifecycle: Arc::clone(&self.lifecycle),
5208 hlc: Arc::clone(&self.hlc),
5209 }),
5210 table_name: Some(entry.name.clone()),
5211 auth: self.table_auth_checker(),
5212 read_only: self.read_only,
5213 };
5214 let table = Table::create_in(&tdir, entry.schema.clone(), entry.table_id, ctx)?;
5215 self.tables
5216 .write()
5217 .insert(entry.table_id, TableHandle::new(table));
5218 Ok(())
5219 }
5220
5221 fn publish_catalog_candidate(
5222 &self,
5223 catalog: Catalog,
5224 epoch: Epoch,
5225 epoch_guard: &mut EpochGuard<'_>,
5226 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5227 ) -> Result<()> {
5228 self.publish_catalog_candidate_with_prelude(
5229 catalog,
5230 epoch,
5231 epoch_guard,
5232 before_publish,
5233 Vec::new(),
5234 )
5235 }
5236
5237 fn publish_catalog_candidate_with_prelude(
5238 &self,
5239 catalog: Catalog,
5240 epoch: Epoch,
5241 epoch_guard: &mut EpochGuard<'_>,
5242 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5243 prelude: Vec<(u64, crate::wal::Op)>,
5244 ) -> Result<()> {
5245 use crate::wal::DdlOp;
5246
5247 if self.read_only {
5248 return Err(MongrelError::ReadOnlyReplica);
5249 }
5250 if self.poisoned.load(Ordering::Relaxed) {
5251 return Err(MongrelError::Other(
5252 "database poisoned by fsync error".into(),
5253 ));
5254 }
5255 let _operation = self.admit_operation()?;
5257 if let Some(before_publish) = before_publish.as_mut() {
5258 (**before_publish)()?;
5259 }
5260 if catalog.db_epoch != epoch.0 {
5261 return Err(MongrelError::InvalidArgument(format!(
5262 "catalog epoch {} does not match commit epoch {}",
5263 catalog.db_epoch, epoch.0
5264 )));
5265 }
5266 {
5267 let current = self.catalog.read();
5268 validate_catalog_transition(¤t, &catalog)?;
5269 }
5270 validate_recovered_catalog(&catalog)?;
5271 let catalog_json = DdlOp::encode_catalog(&catalog)?;
5272 let txn_id = self.alloc_txn_id()?;
5273 let commit_seq = {
5274 let mut wal = self.shared_wal.lock();
5275 let append: Result<u64> = (|| {
5276 for (table_id, op) in prelude {
5277 wal.append(txn_id, table_id, op)?;
5278 }
5279 wal.append(
5280 txn_id,
5281 WAL_TABLE_ID,
5282 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
5283 )?;
5284 wal.append_commit(txn_id, epoch, &[])
5285 })();
5286 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5287 };
5288 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5289 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
5290 self.finish_durable_publish(epoch, epoch_guard, &receipt, checkpoint)
5291 }
5292
5293 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
5297 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
5298 let version = catalog.security_version;
5299 let principal = self.principal.read().clone();
5300 let principal = if catalog.require_auth
5301 && principal
5302 .as_ref()
5303 .is_some_and(|principal| principal.user_id != 0)
5304 {
5305 principal.as_ref().and_then(|principal| {
5306 Self::resolve_bound_principal_from_catalog(&catalog, principal)
5307 })
5308 } else {
5309 principal
5310 };
5311 *self.catalog.write() = catalog;
5312 self.security_coordinator
5313 .version
5314 .store(version, Ordering::Release);
5315 self.auth_state
5316 .set_require_auth(self.catalog.read().require_auth);
5317 *self.principal.write() = principal.clone();
5318 self.auth_state.set_principal(principal);
5319 checkpoint
5320 }
5321
5322 fn finish_durable_publish(
5323 &self,
5324 epoch: Epoch,
5325 epoch_guard: &mut EpochGuard<'_>,
5326 receipt: &mongreldb_log::CommitReceipt,
5327 post_step: Result<()>,
5328 ) -> Result<()> {
5329 if let Err(error) = self.publish_committed(receipt, epoch) {
5330 self.poisoned.store(true, Ordering::Relaxed);
5335 self.lifecycle.poison();
5336 return Err(MongrelError::DurableCommit {
5337 epoch: epoch.0,
5338 message: error.to_string(),
5339 });
5340 }
5341 epoch_guard.disarm();
5342 match post_step {
5343 Ok(()) => Ok(()),
5344 Err(error) => {
5345 self.poisoned.store(true, Ordering::Relaxed);
5346 self.lifecycle.poison();
5347 Err(MongrelError::DurableCommit {
5348 epoch: epoch.0,
5349 message: error.to_string(),
5350 })
5351 }
5352 }
5353 }
5354
5355 fn publish_committed(
5361 &self,
5362 receipt: &mongreldb_log::CommitReceipt,
5363 epoch: Epoch,
5364 ) -> Result<()> {
5365 debug_assert_eq!(
5366 receipt.log_position.index, epoch.0,
5367 "commit receipt position must match the published epoch"
5368 );
5369 mongreldb_fault::inject("commit.publish.before").map_err(crate::commit_log::fault_as_io)?;
5370 self.epoch.publish_in_order(epoch);
5371 mongreldb_fault::inject("commit.publish.after").map_err(crate::commit_log::fault_as_io)?;
5372 Ok(())
5373 }
5374
5375 fn await_durable_commit(
5384 &self,
5385 txn_id: u64,
5386 commit_seq: u64,
5387 epoch: Epoch,
5388 ) -> Result<mongreldb_log::CommitReceipt> {
5389 match self
5390 .standalone_commit_log
5391 .seal_transaction(txn_id, epoch, commit_seq, None)
5392 {
5393 Ok(receipt) => {
5394 self.record_commit_ts(epoch, receipt.commit_ts);
5395 Ok(receipt)
5396 }
5397 Err(error) => {
5398 self.poisoned.store(true, Ordering::Relaxed);
5399 self.lifecycle.poison();
5400 Err(MongrelError::CommitOutcomeUnknown {
5401 epoch: epoch.0,
5402 message: error.to_string(),
5403 })
5404 }
5405 }
5406 }
5407
5408 fn await_durable_commit_with_ts(
5413 &self,
5414 txn_id: u64,
5415 commit_seq: u64,
5416 epoch: Epoch,
5417 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5418 ) -> Result<mongreldb_log::CommitReceipt> {
5419 match self.standalone_commit_log.seal_transaction(
5420 txn_id,
5421 epoch,
5422 commit_seq,
5423 Some(commit_ts),
5424 ) {
5425 Ok(receipt) => {
5426 self.record_commit_ts(epoch, receipt.commit_ts);
5427 Ok(receipt)
5428 }
5429 Err(error) => {
5430 self.poisoned.store(true, Ordering::Relaxed);
5431 self.lifecycle.poison();
5432 Err(MongrelError::CommitOutcomeUnknown {
5433 epoch: epoch.0,
5434 message: error.to_string(),
5435 })
5436 }
5437 }
5438 }
5439
5440 fn record_commit_ts(&self, epoch: Epoch, commit_ts: mongreldb_types::hlc::HlcTimestamp) {
5445 let mut ledger = self.commit_ts_ledger.lock();
5446 ledger.insert(epoch.0, commit_ts);
5447 while ledger.len() > COMMIT_TS_LEDGER_CAP {
5448 ledger.pop_first();
5449 }
5450 }
5451
5452 pub fn commit_ts_for_epoch(&self, epoch: Epoch) -> Option<mongreldb_types::hlc::HlcTimestamp> {
5466 self.commit_ts_ledger.lock().get(&epoch.0).copied()
5467 }
5468
5469 pub fn epoch_at_or_before_commit_ts(
5475 &self,
5476 timestamp: mongreldb_types::hlc::HlcTimestamp,
5477 ) -> Option<Epoch> {
5478 self.commit_ts_ledger
5479 .lock()
5480 .iter()
5481 .rev()
5482 .find_map(|(epoch, commit_ts)| (*commit_ts <= timestamp).then_some(Epoch(*epoch)))
5483 }
5484
5485 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
5486 self.poisoned.store(true, Ordering::Relaxed);
5487 self.lifecycle.poison();
5488 MongrelError::CommitOutcomeUnknown {
5489 epoch: epoch.0,
5490 message: error.to_string(),
5491 }
5492 }
5493
5494 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
5496 self.set_security_catalog_as_with_epoch(security, None)
5497 .map(|_| ())
5498 }
5499
5500 pub fn set_security_catalog_as(
5502 &self,
5503 security: crate::security::SecurityCatalog,
5504 principal: Option<&crate::auth::Principal>,
5505 ) -> Result<()> {
5506 self.set_security_catalog_as_with_epoch(security, principal)
5507 .map(|_| ())
5508 }
5509
5510 pub fn set_security_catalog_as_with_epoch(
5512 &self,
5513 security: crate::security::SecurityCatalog,
5514 principal: Option<&crate::auth::Principal>,
5515 ) -> Result<Epoch> {
5516 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
5517 }
5518
5519 pub fn set_security_catalog_as_with_epoch_controlled<F>(
5522 &self,
5523 security: crate::security::SecurityCatalog,
5524 principal: Option<&crate::auth::Principal>,
5525 mut before_commit: F,
5526 ) -> Result<Epoch>
5527 where
5528 F: FnMut() -> Result<()>,
5529 {
5530 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
5531 }
5532
5533 fn set_security_catalog_as_with_epoch_inner(
5534 &self,
5535 security: crate::security::SecurityCatalog,
5536 principal: Option<&crate::auth::Principal>,
5537 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
5538 ) -> Result<Epoch> {
5539 use crate::wal::DdlOp;
5540 use std::sync::atomic::Ordering;
5541
5542 let command = crate::catalog_cmds::CatalogCommand::SetSecurityCatalog {
5546 security: security.clone(),
5547 };
5548 self.require_for(
5549 principal,
5550 &crate::catalog_cmds::required_permission(&command),
5551 )?;
5552 if self.poisoned.load(Ordering::Relaxed) {
5553 return Err(MongrelError::Other(
5554 "database poisoned by fsync error".into(),
5555 ));
5556 }
5557 let _operation = self.admit_operation()?;
5560 let _ddl = self.ddl_lock.lock();
5561 let _security_write = self.security_write()?;
5564 self.require_for(
5565 principal,
5566 &crate::catalog_cmds::required_permission(&command),
5567 )?;
5568 let mut next_catalog = self.catalog.read().clone();
5569 validate_security_catalog(&next_catalog, &security)?;
5570 let payload = DdlOp::encode_security(&security)?;
5571 let _commit = self.commit_lock.lock();
5572 let epoch = self.epoch.bump_assigned();
5573 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5574 let txn_id = self.alloc_txn_id()?;
5575 self.apply_catalog_command_to(&mut next_catalog, command)?;
5576 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
5577 let commit_seq = {
5578 let mut wal = self.shared_wal.lock();
5579 if let Some(before_commit) = before_commit {
5580 before_commit()?;
5581 }
5582 let append: Result<u64> = (|| {
5583 wal.append(
5584 txn_id,
5585 WAL_TABLE_ID,
5586 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
5587 security_json: payload,
5588 }),
5589 )?;
5590 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
5591 wal.append_commit(txn_id, epoch, &[])
5592 })();
5593 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5594 };
5595 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5596 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
5597 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
5598 Ok(epoch)
5599 }
5600
5601 pub fn require_for(
5602 &self,
5603 principal: Option<&crate::auth::Principal>,
5604 permission: &crate::auth::Permission,
5605 ) -> Result<()> {
5606 let Some(principal) = principal else {
5607 return self.require(permission);
5608 };
5609 let resolved;
5610 let principal = if principal.user_id != 0 {
5611 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5612 .ok_or(MongrelError::AuthRequired)?;
5613 &resolved
5614 } else {
5615 principal
5616 };
5617 #[cfg(test)]
5618 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5619 if principal.has_permission(permission) {
5620 Ok(())
5621 } else {
5622 Err(MongrelError::PermissionDenied {
5623 required: permission.clone(),
5624 principal: principal.username.clone(),
5625 })
5626 }
5627 }
5628
5629 fn require_exact_principal_current(
5633 &self,
5634 principal: Option<&crate::auth::Principal>,
5635 permission: &crate::auth::Permission,
5636 ) -> Result<()> {
5637 let catalog = self.catalog.read();
5638 if !catalog.require_auth {
5639 return Ok(());
5640 }
5641 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
5642 let current = if supplied.user_id == 0 {
5643 supplied.clone()
5644 } else {
5645 Self::resolve_bound_principal_from_catalog(&catalog, supplied)
5646 .ok_or(MongrelError::AuthRequired)?
5647 };
5648 if current.has_permission(permission) {
5649 Ok(())
5650 } else {
5651 Err(MongrelError::PermissionDenied {
5652 required: permission.clone(),
5653 principal: current.username,
5654 })
5655 }
5656 }
5657
5658 pub(crate) fn with_exact_principal_current<T, F>(
5659 &self,
5660 principal: Option<&crate::auth::Principal>,
5661 permission: &crate::auth::Permission,
5662 operation: F,
5663 ) -> Result<T>
5664 where
5665 F: FnOnce() -> Result<T>,
5666 {
5667 let _security = self.security_coordinator.gate.read();
5668 self.require_exact_principal_current(principal, permission)?;
5669 operation()
5670 }
5671
5672 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
5673 self.principal.read().clone()
5674 }
5675
5676 #[cfg(test)]
5677 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
5678 *self.principal.write() = principal.clone();
5679 self.auth_state.set_principal(principal);
5680 }
5681
5682 pub fn require_columns_for(
5683 &self,
5684 table: &str,
5685 operation: crate::auth::ColumnOperation,
5686 column_ids: &[u16],
5687 principal: Option<&crate::auth::Principal>,
5688 ) -> Result<()> {
5689 if principal.is_none() && !self.auth_state.require_auth() {
5690 return Ok(());
5691 }
5692 let cached = self.principal.read().clone();
5693 let principal = principal.or(cached.as_ref());
5694 let Some(principal) = principal else {
5695 let permission = match operation {
5696 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5697 table: table.to_string(),
5698 },
5699 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5700 table: table.to_string(),
5701 },
5702 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5703 table: table.to_string(),
5704 },
5705 };
5706 return self.require(&permission);
5707 };
5708 let catalog = self.catalog.read();
5709 let resolved;
5710 let principal = if principal.user_id != 0 {
5711 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
5712 .ok_or(MongrelError::AuthRequired)?;
5713 &resolved
5714 } else {
5715 principal
5716 };
5717 let schema = &catalog
5718 .live(table)
5719 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5720 .schema;
5721 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
5722 }
5723
5724 fn require_columns_for_principal(
5725 table: &str,
5726 schema: &Schema,
5727 operation: crate::auth::ColumnOperation,
5728 column_ids: &[u16],
5729 principal: &crate::auth::Principal,
5730 ) -> Result<()> {
5731 #[cfg(test)]
5732 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5733 match principal.column_access(table, operation) {
5734 crate::auth::ColumnAccess::All => Ok(()),
5735 crate::auth::ColumnAccess::Columns(allowed) => {
5736 let denied = column_ids.iter().find_map(|column_id| {
5737 schema
5738 .columns
5739 .iter()
5740 .find(|column| column.id == *column_id)
5741 .filter(|column| !allowed.contains(&column.name))
5742 });
5743 if denied.is_none() {
5744 Ok(())
5745 } else {
5746 Err(MongrelError::PermissionDenied {
5747 required: match operation {
5748 crate::auth::ColumnOperation::Select => {
5749 crate::auth::Permission::SelectColumns {
5750 table: table.to_string(),
5751 columns: denied
5752 .into_iter()
5753 .map(|column| column.name.clone())
5754 .collect(),
5755 }
5756 }
5757 crate::auth::ColumnOperation::Insert => {
5758 crate::auth::Permission::InsertColumns {
5759 table: table.to_string(),
5760 columns: denied
5761 .into_iter()
5762 .map(|column| column.name.clone())
5763 .collect(),
5764 }
5765 }
5766 crate::auth::ColumnOperation::Update => {
5767 crate::auth::Permission::UpdateColumns {
5768 table: table.to_string(),
5769 columns: denied
5770 .into_iter()
5771 .map(|column| column.name.clone())
5772 .collect(),
5773 }
5774 }
5775 },
5776 principal: principal.username.clone(),
5777 })
5778 }
5779 }
5780 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5781 required: match operation {
5782 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5783 table: table.to_string(),
5784 },
5785 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5786 table: table.to_string(),
5787 },
5788 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5789 table: table.to_string(),
5790 },
5791 },
5792 principal: principal.username.clone(),
5793 }),
5794 }
5795 }
5796
5797 pub fn select_column_ids_for(
5798 &self,
5799 table: &str,
5800 principal: Option<&crate::auth::Principal>,
5801 ) -> Result<Vec<u16>> {
5802 let catalog = self.catalog.read();
5803 let columns = catalog
5804 .live(table)
5805 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5806 .schema
5807 .columns
5808 .iter()
5809 .map(|column| (column.id, column.name.clone()))
5810 .collect::<Vec<_>>();
5811 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
5812 drop(catalog);
5813 let Some(principal) = principal.as_ref() else {
5814 self.require(&crate::auth::Permission::Select {
5815 table: table.to_string(),
5816 })?;
5817 return Ok(columns.iter().map(|(id, _)| *id).collect());
5818 };
5819 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
5820 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
5821 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
5822 .iter()
5823 .filter(|(_, name)| allowed.contains(name))
5824 .map(|(id, _)| *id)
5825 .collect()),
5826 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5827 required: crate::auth::Permission::Select {
5828 table: table.to_string(),
5829 },
5830 principal: principal.username.clone(),
5831 }),
5832 }
5833 }
5834
5835 pub fn secure_rows_for(
5836 &self,
5837 table: &str,
5838 rows: Vec<crate::memtable::Row>,
5839 principal: Option<&crate::auth::Principal>,
5840 ) -> Result<Vec<crate::memtable::Row>> {
5841 self.secure_rows_for_with_context(table, rows, principal, None)
5842 }
5843
5844 pub fn secure_rows_for_with_context(
5845 &self,
5846 table: &str,
5847 rows: Vec<crate::memtable::Row>,
5848 principal: Option<&crate::auth::Principal>,
5849 context: Option<&crate::query::AiExecutionContext>,
5850 ) -> Result<Vec<crate::memtable::Row>> {
5851 let (security, principal) = {
5852 let catalog = self.catalog.read();
5853 (
5854 catalog.security.clone(),
5855 self.principal_for_authorized_read(&catalog, principal, false)?,
5856 )
5857 };
5858 if !security.table_has_security(table) {
5859 return Ok(rows);
5860 }
5861 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5862 let mut output = Vec::new();
5863 for mut row in rows {
5864 if let Some(context) = context {
5865 context.consume(1)?;
5866 }
5867 if security.row_allowed(
5868 table,
5869 crate::security::PolicyCommand::Select,
5870 &row,
5871 principal,
5872 false,
5873 ) {
5874 security.apply_masks(table, &mut row, principal);
5875 output.push(row);
5876 }
5877 }
5878 Ok(output)
5879 }
5880
5881 pub fn mask_search_hits_for(
5884 &self,
5885 table: &str,
5886 hits: &mut [crate::query::SearchHit],
5887 principal: Option<&crate::auth::Principal>,
5888 ) -> Result<()> {
5889 let (security, principal) = {
5890 let catalog = self.catalog.read();
5891 (
5892 catalog.security.clone(),
5893 self.principal_for_authorized_read(&catalog, principal, false)?,
5894 )
5895 };
5896 if !security.table_has_security(table) {
5897 return Ok(());
5898 }
5899 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5900 for hit in hits {
5901 security.apply_masks_to_cells(table, &mut hit.cells, principal);
5902 }
5903 Ok(())
5904 }
5905
5906 pub fn mask_rows_for(
5908 &self,
5909 table: &str,
5910 rows: &mut [crate::memtable::Row],
5911 principal: Option<&crate::auth::Principal>,
5912 ) -> Result<()> {
5913 let (security, principal) = {
5914 let catalog = self.catalog.read();
5915 (
5916 catalog.security.clone(),
5917 self.principal_for_authorized_read(&catalog, principal, false)?,
5918 )
5919 };
5920 if !security.table_has_security(table) {
5921 return Ok(());
5922 }
5923 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5924 for row in rows {
5925 security.apply_masks(table, row, principal);
5926 }
5927 Ok(())
5928 }
5929
5930 pub fn authorized_candidate_ids_for(
5932 &self,
5933 table: &str,
5934 principal: Option<&crate::auth::Principal>,
5935 ) -> Result<Option<std::collections::HashSet<RowId>>> {
5936 Ok(self
5937 .authorized_read_snapshot(table, principal)?
5938 .allowed_row_ids)
5939 }
5940
5941 fn allowed_row_ids_locked(
5942 &self,
5943 table_name: &str,
5944 table: &Table,
5945 table_snapshot: Snapshot,
5946 security_state: (&crate::security::SecurityCatalog, u64),
5947 principal: Option<&crate::auth::Principal>,
5948 context: Option<&crate::query::AiExecutionContext>,
5949 ) -> Result<Option<Arc<HashSet<RowId>>>> {
5950 let (security, security_version) = security_state;
5951 if !security.rls_enabled(table_name) {
5952 return Ok(None);
5953 }
5954 let authorization_started = std::time::Instant::now();
5955 let principal = principal.ok_or(MongrelError::AuthRequired)?;
5956 let mut roles = principal.roles.clone();
5957 roles.sort_unstable();
5958 let principal_key = format!(
5959 "{}:{}:{}:{}:{roles:?}",
5960 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
5961 );
5962 let cache_key = (
5963 table_name.to_string(),
5964 table.data_generation(),
5965 security_version,
5966 principal_key,
5967 );
5968 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
5969 crate::trace::QueryTrace::record(|trace| {
5970 trace.rls_cache_hit = true;
5971 trace.authorization_nanos = trace
5972 .authorization_nanos
5973 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
5974 });
5975 return Ok(Some(allowed));
5976 }
5977 if let Some(context) = context {
5978 context.checkpoint()?;
5979 }
5980 let started = std::time::Instant::now();
5982 let rows = table.visible_rows(table_snapshot)?;
5983 let rows_evaluated = rows.len() as u64;
5984 let mut allowed = HashSet::new();
5985 for chunk in rows.chunks(256) {
5986 if let Some(context) = context {
5987 context.consume(chunk.len())?;
5988 }
5989 allowed.extend(chunk.iter().filter_map(|row| {
5990 security
5991 .row_allowed(
5992 table_name,
5993 crate::security::PolicyCommand::Select,
5994 row,
5995 principal,
5996 false,
5997 )
5998 .then_some(row.row_id)
5999 }));
6000 }
6001 let allowed = Arc::new(allowed);
6002 let mut cache = self.rls_cache.lock();
6003 cache.build_nanos = cache
6004 .build_nanos
6005 .saturating_add(started.elapsed().as_nanos() as u64);
6006 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
6007 cache.insert(cache_key, Arc::clone(&allowed));
6008 crate::trace::QueryTrace::record(|trace| {
6009 trace.rls_rows_evaluated = trace
6010 .rls_rows_evaluated
6011 .saturating_add(rows_evaluated as usize);
6012 trace.authorization_nanos = trace
6013 .authorization_nanos
6014 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
6015 });
6016 Ok(Some(allowed))
6017 }
6018
6019 fn principal_for_authorized_read(
6020 &self,
6021 catalog: &Catalog,
6022 principal: Option<&crate::auth::Principal>,
6023 catalog_bound: bool,
6024 ) -> Result<Option<crate::auth::Principal>> {
6025 let principal = principal.cloned().or_else(|| self.principal.read().clone());
6026 let Some(principal) = principal else {
6027 return Ok(None);
6028 };
6029 if catalog_bound || principal.user_id != 0 {
6030 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
6031 .map(Some)
6032 .ok_or(MongrelError::AuthRequired);
6033 }
6034 Ok(Some(principal))
6035 }
6036
6037 pub fn with_authorized_read<T, F>(
6041 &self,
6042 table_name: &str,
6043 principal: Option<&crate::auth::Principal>,
6044 catalog_bound: bool,
6045 read: F,
6046 ) -> Result<T>
6047 where
6048 F: FnMut(
6049 &mut Table,
6050 Snapshot,
6051 Option<&HashSet<RowId>>,
6052 Option<&crate::auth::Principal>,
6053 ) -> Result<T>,
6054 {
6055 self.with_authorized_read_context(
6056 table_name,
6057 principal,
6058 catalog_bound,
6059 None,
6060 None,
6061 None,
6062 read,
6063 )
6064 }
6065
6066 #[allow(clippy::too_many_arguments)]
6067 pub fn with_authorized_read_context<T, F>(
6068 &self,
6069 table_name: &str,
6070 principal: Option<&crate::auth::Principal>,
6071 catalog_bound: bool,
6072 authorization: Option<&ReadAuthorization>,
6073 context: Option<&crate::query::AiExecutionContext>,
6074 snapshot_override: Option<Snapshot>,
6075 read: F,
6076 ) -> Result<T>
6077 where
6078 F: FnMut(
6079 &mut Table,
6080 Snapshot,
6081 Option<&HashSet<RowId>>,
6082 Option<&crate::auth::Principal>,
6083 ) -> Result<T>,
6084 {
6085 self.with_authorized_read_context_stamped(
6086 table_name,
6087 principal,
6088 catalog_bound,
6089 authorization,
6090 context,
6091 snapshot_override,
6092 read,
6093 )
6094 .map(|(result, _)| result)
6095 }
6096
6097 #[allow(clippy::too_many_arguments)]
6098 pub fn with_authorized_read_context_stamped<T, F>(
6099 &self,
6100 table_name: &str,
6101 principal: Option<&crate::auth::Principal>,
6102 catalog_bound: bool,
6103 authorization: Option<&ReadAuthorization>,
6104 context: Option<&crate::query::AiExecutionContext>,
6105 snapshot_override: Option<Snapshot>,
6106 mut read: F,
6107 ) -> Result<(T, AuthorizedReadStamp)>
6108 where
6109 F: FnMut(
6110 &mut Table,
6111 Snapshot,
6112 Option<&HashSet<RowId>>,
6113 Option<&crate::auth::Principal>,
6114 ) -> Result<T>,
6115 {
6116 if principal.is_none() && self.principal.read().is_some() {
6117 self.refresh_principal()?;
6118 }
6119 const RETRIES: usize = 3;
6120 let handle = self.table(table_name)?;
6121 for attempt in 0..RETRIES {
6122 crate::trace::QueryTrace::record(|trace| {
6123 trace.authorization_retries = attempt;
6124 });
6125 let (security, security_version, effective_principal) = {
6126 let catalog = self.catalog.read();
6127 (
6128 catalog.security.clone(),
6129 catalog.security_version,
6130 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6131 )
6132 };
6133 if let Some(authorization) = authorization {
6134 for permission in &authorization.permissions {
6135 self.require_for(effective_principal.as_ref(), permission)?;
6136 }
6137 self.require_columns_for(
6138 table_name,
6139 authorization.operation,
6140 &authorization.columns,
6141 effective_principal.as_ref(),
6142 )?;
6143 }
6144 let result = {
6145 let mut table = lock_table_with_context(&handle, context)?;
6146 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
6147 let allowed = self.allowed_row_ids_locked(
6148 table_name,
6149 &table,
6150 snapshot,
6151 (&security, security_version),
6152 effective_principal.as_ref(),
6153 context,
6154 )?;
6155 let stamp = AuthorizedReadStamp {
6156 table_id: table.table_id(),
6157 schema_id: table.schema().schema_id,
6158 data_generation: table.data_generation(),
6159 security_version,
6160 snapshot,
6161 };
6162 let result = read(
6163 &mut table,
6164 snapshot,
6165 allowed.as_deref(),
6166 effective_principal.as_ref(),
6167 )?;
6168 (result, stamp)
6169 };
6170 if let Some(context) = context {
6171 context.checkpoint()?;
6172 }
6173 if self.catalog.read().security_version == security_version {
6174 return Ok(result);
6175 }
6176 if attempt + 1 == RETRIES {
6177 return Err(MongrelError::Conflict(
6178 "security policy changed during scored read".into(),
6179 ));
6180 }
6181 }
6182 Err(MongrelError::Conflict(
6183 "authorization retry loop exhausted".into(),
6184 ))
6185 }
6186
6187 fn with_authorized_aggregate_table<T, F>(
6188 &self,
6189 table_name: &str,
6190 columns: &[u16],
6191 principal: Option<&crate::auth::Principal>,
6192 catalog_bound: bool,
6193 allow_table_security: bool,
6194 mut aggregate: F,
6195 ) -> Result<T>
6196 where
6197 F: FnMut(
6198 &mut Table,
6199 Option<&crate::security::CandidateAuthorization<'_>>,
6200 Option<&crate::auth::Principal>,
6201 u64,
6202 ) -> Result<T>,
6203 {
6204 if principal.is_none() && self.principal.read().is_some() {
6205 self.refresh_principal()?;
6206 }
6207 const RETRIES: usize = 3;
6208 let handle = self.table(table_name)?;
6209 for attempt in 0..RETRIES {
6210 let (security, security_version, effective_principal) = {
6211 let catalog = self.catalog.read();
6212 (
6213 catalog.security.clone(),
6214 catalog.security_version,
6215 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6216 )
6217 };
6218 self.require_columns_for(
6219 table_name,
6220 crate::auth::ColumnOperation::Select,
6221 columns,
6222 effective_principal.as_ref(),
6223 )?;
6224 if !allow_table_security && security.table_has_security(table_name) {
6225 return Err(MongrelError::InvalidArgument(
6226 "incremental aggregate is unsupported while RLS or column masks are active"
6227 .into(),
6228 ));
6229 }
6230 let result = {
6231 let mut table = handle.lock();
6232 let authorization = if security.rls_enabled(table_name) {
6233 Some(crate::security::CandidateAuthorization {
6234 table: table_name,
6235 security: &security,
6236 principal: effective_principal
6237 .as_ref()
6238 .ok_or(MongrelError::AuthRequired)?,
6239 })
6240 } else {
6241 None
6242 };
6243 aggregate(
6244 &mut table,
6245 authorization.as_ref(),
6246 effective_principal.as_ref(),
6247 security_version,
6248 )?
6249 };
6250 if self.catalog.read().security_version == security_version {
6251 return Ok(result);
6252 }
6253 if attempt + 1 == RETRIES {
6254 return Err(MongrelError::Conflict(
6255 "security policy changed during aggregate read".into(),
6256 ));
6257 }
6258 }
6259 Err(MongrelError::Conflict(
6260 "aggregate authorization retry loop exhausted".into(),
6261 ))
6262 }
6263
6264 pub fn with_authorized_scored_read_context<T, F>(
6268 &self,
6269 table_name: &str,
6270 principal: Option<&crate::auth::Principal>,
6271 catalog_bound: bool,
6272 authorization: Option<&ReadAuthorization>,
6273 context: Option<&crate::query::AiExecutionContext>,
6274 mut read: F,
6275 ) -> Result<T>
6276 where
6277 F: FnMut(
6278 &mut Table,
6279 Snapshot,
6280 Option<&crate::security::CandidateAuthorization<'_>>,
6281 Option<&crate::auth::Principal>,
6282 ) -> Result<T>,
6283 {
6284 self.with_authorized_scored_read_context_at(
6285 table_name,
6286 principal,
6287 catalog_bound,
6288 authorization,
6289 context,
6290 None,
6291 |table, snapshot, authorization, principal| {
6292 let mut table = table.clone();
6293 read(&mut table, snapshot, authorization, principal)
6294 },
6295 )
6296 }
6297
6298 #[allow(clippy::too_many_arguments)]
6299 pub fn with_authorized_scored_read_context_at<T, F>(
6300 &self,
6301 table_name: &str,
6302 principal: Option<&crate::auth::Principal>,
6303 catalog_bound: bool,
6304 authorization: Option<&ReadAuthorization>,
6305 context: Option<&crate::query::AiExecutionContext>,
6306 snapshot_override: Option<Snapshot>,
6307 read: F,
6308 ) -> Result<T>
6309 where
6310 F: FnMut(
6311 &Table,
6312 Snapshot,
6313 Option<&crate::security::CandidateAuthorization<'_>>,
6314 Option<&crate::auth::Principal>,
6315 ) -> Result<T>,
6316 {
6317 self.with_authorized_scored_read_context_at_stamped(
6318 table_name,
6319 principal,
6320 catalog_bound,
6321 authorization,
6322 context,
6323 snapshot_override,
6324 read,
6325 )
6326 .map(|(result, _)| result)
6327 }
6328
6329 #[allow(clippy::too_many_arguments)]
6330 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
6331 &self,
6332 table_name: &str,
6333 principal: Option<&crate::auth::Principal>,
6334 catalog_bound: bool,
6335 authorization: Option<&ReadAuthorization>,
6336 context: Option<&crate::query::AiExecutionContext>,
6337 snapshot_override: Option<Snapshot>,
6338 mut read: F,
6339 ) -> Result<(T, AuthorizedReadStamp)>
6340 where
6341 F: FnMut(
6342 &Table,
6343 Snapshot,
6344 Option<&crate::security::CandidateAuthorization<'_>>,
6345 Option<&crate::auth::Principal>,
6346 ) -> Result<T>,
6347 {
6348 if principal.is_none() && self.principal.read().is_some() {
6349 self.refresh_principal()?;
6350 }
6351 const RETRIES: usize = 3;
6352 let handle = self.table(table_name)?;
6353 for attempt in 0..RETRIES {
6354 if let Some(context) = context {
6355 context.checkpoint()?;
6356 }
6357 crate::trace::QueryTrace::record(|trace| {
6358 trace.authorization_retries = attempt;
6359 });
6360 let (security, security_version, effective_principal) = {
6361 let catalog = self.catalog.read();
6362 (
6363 catalog.security.clone(),
6364 catalog.security_version,
6365 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6366 )
6367 };
6368 if let Some(authorization) = authorization {
6369 for permission in &authorization.permissions {
6370 self.require_for(effective_principal.as_ref(), permission)?;
6371 }
6372 self.require_columns_for(
6373 table_name,
6374 authorization.operation,
6375 &authorization.columns,
6376 effective_principal.as_ref(),
6377 )?;
6378 }
6379 let result = {
6380 let (table, snapshot, _snapshot_guard, _run_pins) =
6381 self.scored_read_generation(&handle, context, snapshot_override)?;
6382 let candidate_authorization = if security.rls_enabled(table_name) {
6383 Some(crate::security::CandidateAuthorization {
6384 table: table_name,
6385 security: &security,
6386 principal: effective_principal
6387 .as_ref()
6388 .ok_or(MongrelError::AuthRequired)?,
6389 })
6390 } else {
6391 None
6392 };
6393 let stamp = AuthorizedReadStamp {
6394 table_id: table.table_id(),
6395 schema_id: table.schema().schema_id,
6396 data_generation: table.data_generation(),
6397 security_version,
6398 snapshot,
6399 };
6400 let result = read(
6401 table.as_ref(),
6402 snapshot,
6403 candidate_authorization.as_ref(),
6404 effective_principal.as_ref(),
6405 )?;
6406 (result, stamp)
6407 };
6408 if let Some(context) = context {
6409 context.checkpoint()?;
6410 }
6411 if self.catalog.read().security_version == security_version {
6412 return Ok(result);
6413 }
6414 if attempt + 1 == RETRIES {
6415 return Err(MongrelError::Conflict(
6416 "security policy changed during scored read".into(),
6417 ));
6418 }
6419 }
6420 Err(MongrelError::Conflict(
6421 "scored-read authorization retry loop exhausted".into(),
6422 ))
6423 }
6424
6425 fn scored_read_generation(
6426 &self,
6427 handle: &TableHandle,
6428 context: Option<&crate::query::AiExecutionContext>,
6429 snapshot_override: Option<Snapshot>,
6430 ) -> Result<(
6431 Arc<TableReadGeneration>,
6432 Snapshot,
6433 crate::retention::OwnedSnapshotGuard,
6434 RunPins,
6435 )> {
6436 let mut table = if let Some(context) = context {
6437 loop {
6438 context.checkpoint()?;
6439 let wait = context
6440 .remaining_duration()
6441 .unwrap_or(std::time::Duration::from_millis(5))
6442 .min(std::time::Duration::from_millis(5));
6443 if let Some(table) = handle.try_lock_for(wait) {
6444 break table;
6445 }
6446 }
6447 } else {
6448 handle.lock()
6449 };
6450 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
6451 self.snapshot_at_owned(snapshot.epoch)?
6452 } else {
6453 let snapshot = table.snapshot();
6454 let guard = self.snapshots.register_owned(snapshot.epoch);
6455 (snapshot, guard)
6456 };
6457 let table_id = table.table_id();
6458 let run_keys: Vec<_> = table
6459 .active_run_ids()
6460 .map(|run_id| (table_id, run_id))
6461 .collect();
6462 let generation = handle
6463 .generation_metrics
6464 .activate(table.clone_read_generation()?);
6465 let run_pins = self.pin_runs(&run_keys);
6466 Ok((generation, snapshot, snapshot_guard, run_pins))
6467 }
6468
6469 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
6470 let mut pins = self.backup_pins.lock();
6471 for run in runs {
6472 *pins.entry(*run).or_insert(0) += 1;
6473 }
6474 drop(pins);
6475 RunPins {
6476 pins: Arc::clone(&self.backup_pins),
6477 runs: runs.to_vec(),
6478 }
6479 }
6480
6481 pub fn query_for_current_principal(
6485 &self,
6486 table_name: &str,
6487 query: &crate::query::Query,
6488 projection: Option<&[u16]>,
6489 ) -> Result<Vec<crate::memtable::Row>> {
6490 let condition_columns = crate::query::condition_columns(&query.conditions);
6491 let catalog_bound = self
6492 .principal
6493 .read()
6494 .as_ref()
6495 .is_some_and(|principal| principal.user_id != 0);
6496 self.with_authorized_read(
6497 table_name,
6498 None,
6499 catalog_bound,
6500 |table, snapshot, allowed, principal| {
6501 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6502 self.require_columns_for(
6503 table_name,
6504 crate::auth::ColumnOperation::Select,
6505 &condition_columns,
6506 principal,
6507 )?;
6508 if let Some(projection) = projection {
6509 self.require_columns_for(
6510 table_name,
6511 crate::auth::ColumnOperation::Select,
6512 projection,
6513 principal,
6514 )?;
6515 }
6516 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
6517 let projection =
6518 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6519 for row in &mut rows {
6520 row.columns.retain(|column, _| {
6521 allowed_columns.contains(column)
6522 && projection
6523 .as_ref()
6524 .is_none_or(|projection| projection.contains(column))
6525 });
6526 }
6527 self.secure_rows_for(table_name, rows, principal)
6528 },
6529 )
6530 }
6531
6532 pub fn query_for_current_principal_controlled(
6536 &self,
6537 table_name: &str,
6538 query: &crate::query::Query,
6539 projection: Option<&[u16]>,
6540 control: &crate::ExecutionControl,
6541 ) -> Result<Vec<crate::memtable::Row>> {
6542 let catalog_bound = self
6543 .principal
6544 .read()
6545 .as_ref()
6546 .is_some_and(|principal| principal.user_id != 0);
6547 self.query_for_principal_controlled(
6548 table_name,
6549 query,
6550 projection,
6551 None,
6552 catalog_bound,
6553 control,
6554 )
6555 }
6556
6557 pub fn query_as_principal_controlled(
6561 &self,
6562 table_name: &str,
6563 query: &crate::query::Query,
6564 projection: Option<&[u16]>,
6565 principal: Option<&crate::auth::Principal>,
6566 control: &crate::ExecutionControl,
6567 ) -> Result<Vec<crate::memtable::Row>> {
6568 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6569 self.query_for_principal_controlled(
6570 table_name,
6571 query,
6572 projection,
6573 principal,
6574 catalog_bound,
6575 control,
6576 )
6577 }
6578
6579 fn query_for_principal_controlled(
6580 &self,
6581 table_name: &str,
6582 query: &crate::query::Query,
6583 projection: Option<&[u16]>,
6584 principal: Option<&crate::auth::Principal>,
6585 catalog_bound: bool,
6586 control: &crate::ExecutionControl,
6587 ) -> Result<Vec<crate::memtable::Row>> {
6588 control.checkpoint()?;
6589 let context = crate::query::AiExecutionContext::with_control(
6590 control.clone(),
6591 usize::MAX,
6592 crate::query::MAX_FUSED_CANDIDATES,
6593 );
6594 let condition_columns = crate::query::condition_columns(&query.conditions);
6595 self.with_authorized_read_context(
6596 table_name,
6597 principal,
6598 catalog_bound,
6599 None,
6600 Some(&context),
6601 None,
6602 |table, snapshot, allowed, principal| {
6603 control.checkpoint()?;
6604 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6605 self.require_columns_for(
6606 table_name,
6607 crate::auth::ColumnOperation::Select,
6608 &condition_columns,
6609 principal,
6610 )?;
6611 if let Some(projection) = projection {
6612 self.require_columns_for(
6613 table_name,
6614 crate::auth::ColumnOperation::Select,
6615 projection,
6616 principal,
6617 )?;
6618 }
6619 let rows =
6620 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
6621 let projection =
6622 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6623 let mut projected = Vec::with_capacity(rows.len());
6624 for (index, mut row) in rows.into_iter().enumerate() {
6625 if index & 255 == 0 {
6626 control.checkpoint()?;
6627 }
6628 row.columns.retain(|column, _| {
6629 allowed_columns.contains(column)
6630 && projection
6631 .as_ref()
6632 .is_none_or(|projection| projection.contains(column))
6633 });
6634 projected.push(row);
6635 }
6636 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
6637 },
6638 )
6639 }
6640
6641 pub fn approx_aggregate_for_current_principal(
6644 &self,
6645 table_name: &str,
6646 conditions: &[crate::query::Condition],
6647 column: Option<u16>,
6648 agg: crate::engine::ApproxAgg,
6649 z: f64,
6650 ) -> Result<Option<crate::engine::ApproxResult>> {
6651 if !z.is_finite() || z <= 0.0 {
6652 return Err(MongrelError::InvalidArgument(
6653 "z must be finite and > 0".into(),
6654 ));
6655 }
6656 let mut columns = crate::query::condition_columns(conditions);
6657 columns.extend(column);
6658 columns.sort_unstable();
6659 columns.dedup();
6660 self.with_authorized_aggregate_table(
6661 table_name,
6662 &columns,
6663 None,
6664 true,
6665 true,
6666 |table, authorization, _, _| {
6667 table.approx_aggregate_with_candidate_authorization(
6668 conditions,
6669 column,
6670 agg,
6671 z,
6672 authorization,
6673 )
6674 },
6675 )
6676 }
6677
6678 pub fn incremental_aggregate_for_current_principal(
6682 &self,
6683 table_name: &str,
6684 conditions: &[crate::query::Condition],
6685 column: Option<u16>,
6686 agg: crate::engine::NativeAgg,
6687 ) -> Result<crate::engine::IncrementalAggResult> {
6688 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
6689 }
6690
6691 pub fn incremental_aggregate_for_principal(
6695 &self,
6696 table_name: &str,
6697 conditions: &[crate::query::Condition],
6698 column: Option<u16>,
6699 agg: crate::engine::NativeAgg,
6700 principal: Option<&crate::auth::Principal>,
6701 catalog_bound: bool,
6702 ) -> Result<crate::engine::IncrementalAggResult> {
6703 let mut columns = crate::query::condition_columns(conditions);
6704 columns.extend(column);
6705 columns.sort_unstable();
6706 columns.dedup();
6707 self.with_authorized_aggregate_table(
6708 table_name,
6709 &columns,
6710 principal,
6711 catalog_bound,
6712 false,
6713 |table, _, principal, security_version| {
6714 let cache_key = incremental_aggregate_cache_key(
6715 table_name,
6716 conditions,
6717 column,
6718 agg,
6719 principal,
6720 security_version,
6721 );
6722 table.aggregate_incremental(cache_key, conditions, column, agg)
6723 },
6724 )
6725 }
6726
6727 pub fn get_for_current_principal(
6730 &self,
6731 table_name: &str,
6732 row_id: RowId,
6733 ) -> Result<Option<crate::memtable::Row>> {
6734 self.with_authorized_read(
6735 table_name,
6736 None,
6737 true,
6738 |table, snapshot, allowed, principal| {
6739 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6740 let Some(row) = table.get(row_id, snapshot) else {
6741 return Ok(None);
6742 };
6743 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
6744 return Ok(None);
6745 }
6746 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6747 if let Some(row) = rows.first_mut() {
6748 row.columns
6749 .retain(|column, _| allowed_columns.contains(column));
6750 }
6751 Ok(rows.pop())
6752 },
6753 )
6754 }
6755
6756 pub fn ann_rerank_for_current_principal(
6759 &self,
6760 table_name: &str,
6761 request: &crate::query::AnnRerankRequest,
6762 ) -> Result<Vec<crate::query::AnnRerankHit>> {
6763 self.with_authorized_scored_read_context_at(
6764 table_name,
6765 None,
6766 true,
6767 Some(&ReadAuthorization {
6768 operation: crate::auth::ColumnOperation::Select,
6769 columns: vec![request.column_id],
6770 permissions: Vec::new(),
6771 }),
6772 None,
6773 None,
6774 |table, snapshot, authorization, principal| {
6775 self.require_columns_for(
6776 table_name,
6777 crate::auth::ColumnOperation::Select,
6778 &[request.column_id],
6779 principal,
6780 )?;
6781 table.ann_rerank_at_with_candidate_authorization_on_generation(
6782 request,
6783 snapshot,
6784 authorization,
6785 None,
6786 )
6787 },
6788 )
6789 }
6790
6791 pub fn search_for_current_principal(
6795 &self,
6796 table_name: &str,
6797 request: &crate::query::SearchRequest,
6798 ) -> Result<Vec<crate::query::SearchHit>> {
6799 let principal = self.principal_snapshot();
6800 self.search_for_principal_with_context(table_name, request, principal.as_ref(), None)
6801 }
6802
6803 pub fn search_for_principal_with_context(
6809 &self,
6810 table_name: &str,
6811 request: &crate::query::SearchRequest,
6812 principal: Option<&crate::auth::Principal>,
6813 context: Option<&crate::query::AiExecutionContext>,
6814 ) -> Result<Vec<crate::query::SearchHit>> {
6815 let mut columns = crate::query::condition_columns(&request.must);
6816 for named in &request.retrievers {
6817 columns.push(named.retriever.column_id());
6818 }
6819 if let Some(crate::query::Rerank::ExactVector {
6820 embedding_column, ..
6821 }) = &request.rerank
6822 {
6823 columns.push(*embedding_column);
6824 }
6825 if let Some(proj) = &request.projection {
6826 columns.extend(proj);
6827 }
6828 columns.sort_unstable();
6829 columns.dedup();
6830 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6831 self.with_authorized_scored_read_context_at(
6832 table_name,
6833 principal,
6834 catalog_bound,
6835 Some(&ReadAuthorization {
6836 operation: crate::auth::ColumnOperation::Select,
6837 columns: columns.clone(),
6838 permissions: Vec::new(),
6839 }),
6840 context,
6841 None,
6842 |table, snapshot, authorization, principal| {
6843 self.require_columns_for(
6844 table_name,
6845 crate::auth::ColumnOperation::Select,
6846 &columns,
6847 principal,
6848 )?;
6849 let hits = table.search_at_with_candidate_authorization_on_generation(
6850 request,
6851 snapshot,
6852 authorization,
6853 context,
6854 )?;
6855 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6856 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
6857 for mut hit in hits {
6858 let row = crate::memtable::Row {
6859 row_id: hit.row_id,
6860 committed_epoch: crate::Epoch(0),
6861 columns: hit
6862 .cells
6863 .iter()
6864 .cloned()
6865 .collect::<std::collections::HashMap<u16, crate::Value>>(),
6866 deleted: false,
6867 commit_ts: None,
6868 };
6869 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6870 if let Some(mut row) = rows.pop() {
6871 row.columns
6872 .retain(|column, _| allowed_columns.contains(column));
6873 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
6874 hit.cells.sort_by_key(|(column, _)| *column);
6875 secured.push(hit);
6876 }
6877 }
6878 Ok(secured)
6879 },
6880 )
6881 }
6882
6883 pub fn retrieve_text(
6886 &self,
6887 table: &str,
6888 embedding_column: u16,
6889 text: &str,
6890 search_options: crate::embedding::TextSearchOptions,
6891 ) -> Result<crate::embedding::TextRetrieveResult> {
6892 let principal = self.principal_snapshot();
6893 self.retrieve_text_for_principal(
6894 table,
6895 embedding_column,
6896 text,
6897 search_options,
6898 principal.as_ref(),
6899 )
6900 }
6901
6902 pub fn retrieve_text_for_principal(
6903 &self,
6904 table: &str,
6905 embedding_column: u16,
6906 text: &str,
6907 search_options: crate::embedding::TextSearchOptions,
6908 principal: Option<&crate::auth::Principal>,
6909 ) -> Result<crate::embedding::TextRetrieveResult> {
6910 if search_options.k == 0 {
6911 return Err(MongrelError::InvalidArgument(
6912 "retrieve_text k must be greater than zero".into(),
6913 ));
6914 }
6915 let catalog_bound = principal.is_some_and(|p| p.user_id != 0);
6916 let (semantic_identity, registry_generation, expected_dim) = {
6917 let handle = self.table(table)?;
6918 let mut g = handle.lock();
6919 g.ensure_indexes_complete()?;
6920 let schema = g.schema().clone();
6921 let column = schema
6922 .columns
6923 .iter()
6924 .find(|c| c.id == embedding_column)
6925 .ok_or_else(|| {
6926 MongrelError::ColumnNotFound(format!(
6927 "embedding column {embedding_column} not found on table {table}"
6928 ))
6929 })?;
6930 let dim = match column.ty {
6931 crate::schema::TypeId::Embedding { dim } => dim,
6932 _ => {
6933 return Err(MongrelError::InvalidArgument(format!(
6934 "column {embedding_column} is not an embedding column"
6935 )))
6936 }
6937 };
6938 if let Some(identity) = g
6939 .ann_index(embedding_column)
6940 .and_then(|a| a.semantic_identity().cloned())
6941 {
6942 let (generation, provider) = self
6943 .embedding_providers
6944 .resolve_by_semantic_identity(&identity)
6945 .map_err(embedding_error)?;
6946 if provider.dimension() != dim {
6947 return Err(embedding_error(
6948 crate::embedding::EmbeddingError::DimensionMismatch {
6949 expected: dim,
6950 got: provider.dimension(),
6951 },
6952 ));
6953 }
6954 (identity, generation, dim)
6955 } else {
6956 let source = column.embedding_source.clone().ok_or_else(|| {
6957 embedding_error(crate::embedding::EmbeddingError::NoActiveAnnIdentity(
6958 embedding_column,
6959 ))
6960 })?;
6961 let (generation, provider) = self
6962 .embedding_providers
6963 .resolve_with_generation(&source)
6964 .map_err(embedding_error)?;
6965 let identity = provider.semantic_identity();
6966 if identity.dimension != dim {
6967 return Err(embedding_error(
6968 crate::embedding::EmbeddingError::DimensionMismatch {
6969 expected: dim,
6970 got: identity.dimension,
6971 },
6972 ));
6973 }
6974 (identity, generation, dim)
6975 }
6976 };
6977 let (_g, provider) = self
6978 .embedding_providers
6979 .resolve_by_semantic_identity(&semantic_identity)
6980 .map_err(embedding_error)?;
6981 let control = crate::ExecutionControl::new(None);
6982 let response = provider
6983 .embed(crate::embedding::EmbeddingRequest {
6984 texts: &[text],
6985 control: &control,
6986 trace_id: "retrieve-text",
6987 })
6988 .map_err(embedding_error)?;
6989 crate::embedding::validate_response(
6990 provider.as_ref(),
6991 1,
6992 expected_dim,
6993 crate::embedding::EmbeddingLimits::default(),
6994 &response.vectors,
6995 )
6996 .map_err(embedding_error)?;
6997 let query = response.vectors.into_iter().next().ok_or_else(|| {
6998 MongrelError::Other("embedding provider returned no query vector".into())
6999 })?;
7000 if provider.semantic_identity() != semantic_identity {
7001 return Err(embedding_error(
7002 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7003 column_id: embedding_column,
7004 expected: semantic_identity.fingerprint_sha256(),
7005 got: provider.semantic_identity().fingerprint_sha256(),
7006 },
7007 ));
7008 }
7009 let retriever = crate::query::Retriever::Ann {
7010 column_id: embedding_column,
7011 query,
7012 k: search_options.k,
7013 };
7014 let hits = self.with_authorized_scored_read_context_at(
7015 table,
7016 principal,
7017 catalog_bound,
7018 Some(&ReadAuthorization {
7019 operation: crate::auth::ColumnOperation::Select,
7020 columns: vec![embedding_column],
7021 permissions: Vec::new(),
7022 }),
7023 None,
7024 None,
7025 |table_guard, snapshot, authorization, principal| {
7026 self.require_columns_for(
7027 table,
7028 crate::auth::ColumnOperation::Select,
7029 &[embedding_column],
7030 principal,
7031 )?;
7032 if let Some(ann) = table_guard.ann_index(embedding_column) {
7033 if let Some(active) = ann.semantic_identity() {
7034 if active != &semantic_identity {
7035 return Err(embedding_error(
7036 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7037 column_id: embedding_column,
7038 expected: semantic_identity.fingerprint_sha256(),
7039 got: active.fingerprint_sha256(),
7040 },
7041 ));
7042 }
7043 }
7044 }
7045 table_guard.retrieve_at_with_candidate_authorization_on_generation(
7046 &retriever,
7047 snapshot,
7048 authorization,
7049 None,
7050 )
7051 },
7052 )?;
7053 Ok(crate::embedding::TextRetrieveResult {
7054 hits,
7055 provenance: crate::embedding::TextRetrieveProvenance {
7056 semantic_identity,
7057 provider_registry_generation: registry_generation,
7058 query_source_fingerprint: Sha256::digest(text.as_bytes()).into(),
7059 embedding_column,
7060 },
7061 })
7062 }
7063
7064 pub fn authorized_read_snapshot(
7067 &self,
7068 table: &str,
7069 principal: Option<&crate::auth::Principal>,
7070 ) -> Result<AuthorizedReadSnapshot> {
7071 let (security, security_version, effective_principal) = {
7072 let catalog = self.catalog.read();
7073 (
7074 catalog.security.clone(),
7075 catalog.security_version,
7076 self.principal_for_authorized_read(&catalog, principal, false)?,
7077 )
7078 };
7079 let handle = self.table(table)?;
7080 let (table_snapshot, data_generation, allowed_row_ids) = {
7081 let table_handle = handle.lock();
7082 let table_snapshot = table_handle.snapshot();
7083 let data_generation = table_handle.data_generation();
7084 let allowed = self.allowed_row_ids_locked(
7085 table,
7086 &table_handle,
7087 table_snapshot,
7088 (&security, security_version),
7089 effective_principal.as_ref(),
7090 None,
7091 )?;
7092 (
7093 table_snapshot,
7094 data_generation,
7095 allowed.map(|allowed| (*allowed).clone()),
7096 )
7097 };
7098 Ok(AuthorizedReadSnapshot {
7099 table: table.to_string(),
7100 table_snapshot,
7101 data_generation,
7102 security_version,
7103 allowed_row_ids,
7104 })
7105 }
7106
7107 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
7108 if self.catalog.read().security_version != snapshot.security_version {
7109 return false;
7110 }
7111 self.table(&snapshot.table)
7112 .ok()
7113 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
7114 }
7115
7116 pub fn rls_cache_stats(&self) -> RlsCacheStats {
7117 self.rls_cache.lock().stats()
7118 }
7119
7120 pub fn rows_for(
7122 &self,
7123 table: &str,
7124 principal: Option<&crate::auth::Principal>,
7125 ) -> Result<Vec<crate::memtable::Row>> {
7126 if principal.is_none() && self.principal.read().is_some() {
7127 self.refresh_principal()?;
7128 }
7129 let allowed = self.select_column_ids_for(table, principal)?;
7130 let handle = self.table(table)?;
7131 let rows = {
7132 let table = handle.lock();
7133 table.visible_rows(table.snapshot())?
7134 };
7135 let mut rows = self.secure_rows_for(table, rows, principal)?;
7136 for row in &mut rows {
7137 row.columns.retain(|column, _| allowed.contains(column));
7138 }
7139 Ok(rows)
7140 }
7141
7142 pub fn rows_at_epoch_for_current_principal(
7145 &self,
7146 table_name: &str,
7147 snapshot: Snapshot,
7148 ) -> Result<Vec<crate::memtable::Row>> {
7149 self.with_authorized_read_context(
7150 table_name,
7151 None,
7152 true,
7153 Some(&ReadAuthorization {
7154 operation: crate::auth::ColumnOperation::Select,
7155 columns: Vec::new(),
7156 permissions: Vec::new(),
7157 }),
7158 None,
7159 Some(snapshot),
7160 |table, snapshot, allowed, principal| {
7161 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
7162 let mut rows = table.visible_rows(snapshot)?;
7163 if let Some(allowed) = allowed {
7164 rows.retain(|row| allowed.contains(&row.row_id));
7165 }
7166 rows = self.secure_rows_for(table_name, rows, principal)?;
7167 for row in &mut rows {
7168 row.columns
7169 .retain(|column, _| allowed_columns.contains(column));
7170 }
7171 Ok(rows)
7172 },
7173 )
7174 }
7175
7176 pub fn count_for(
7178 &self,
7179 table: &str,
7180 principal: Option<&crate::auth::Principal>,
7181 ) -> Result<u64> {
7182 if principal.is_none() && self.principal.read().is_some() {
7183 self.refresh_principal()?;
7184 }
7185 self.select_column_ids_for(table, principal)?;
7186 if self.security_active_for(table) {
7187 Ok(self.rows_for(table, principal)?.len() as u64)
7188 } else {
7189 Ok(self.table(table)?.lock().count())
7190 }
7191 }
7192
7193 pub fn put_for(
7195 &self,
7196 table: &str,
7197 mut cells: Vec<(u16, crate::memtable::Value)>,
7198 principal: Option<&crate::auth::Principal>,
7199 ) -> Result<RowId> {
7200 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
7201 self.require_columns_for(
7202 table,
7203 crate::auth::ColumnOperation::Insert,
7204 &columns,
7205 principal,
7206 )?;
7207 let handle = self.table(table)?;
7208 let mut table_handle = handle.lock();
7209 table_handle.fill_auto_inc(&mut cells)?;
7210 table_handle.apply_defaults(&mut cells)?;
7211 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
7212 row.columns.extend(cells.iter().cloned());
7213 self.check_row_policy_for(
7214 table,
7215 crate::security::PolicyCommand::Insert,
7216 &row,
7217 true,
7218 principal,
7219 )?;
7220 table_handle.put(cells)
7221 }
7222
7223 pub fn check_row_policy_for(
7224 &self,
7225 table: &str,
7226 command: crate::security::PolicyCommand,
7227 row: &crate::memtable::Row,
7228 check_new: bool,
7229 principal: Option<&crate::auth::Principal>,
7230 ) -> Result<()> {
7231 let security = self.catalog.read().security.clone();
7232 if !security.rls_enabled(table) {
7233 return Ok(());
7234 }
7235 let cached = self.principal.read().clone();
7236 let principal = principal
7237 .or(cached.as_ref())
7238 .ok_or(MongrelError::AuthRequired)?;
7239 if security.row_allowed(table, command, row, principal, check_new) {
7240 return Ok(());
7241 }
7242 let required = match command {
7243 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
7244 table: table.to_string(),
7245 },
7246 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
7247 table: table.to_string(),
7248 },
7249 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
7250 table: table.to_string(),
7251 },
7252 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
7253 crate::auth::Permission::Delete {
7254 table: table.to_string(),
7255 }
7256 }
7257 };
7258 Err(MongrelError::PermissionDenied {
7259 required,
7260 principal: principal.username.clone(),
7261 })
7262 }
7263
7264 pub fn set_materialized_view(
7267 &self,
7268 definition: crate::catalog::MaterializedViewEntry,
7269 ) -> Result<()> {
7270 self.set_materialized_view_with_epoch(definition)
7271 .map(|_| ())
7272 }
7273
7274 pub fn set_materialized_view_with_epoch(
7276 &self,
7277 definition: crate::catalog::MaterializedViewEntry,
7278 ) -> Result<Epoch> {
7279 use crate::wal::DdlOp;
7280 use std::sync::atomic::Ordering;
7281
7282 let command = crate::catalog_cmds::CatalogCommand::CreateMaterializedView {
7283 definition: definition.clone(),
7284 };
7285 self.require(&crate::catalog_cmds::required_permission(&command))?;
7286 if self.poisoned.load(Ordering::Relaxed) {
7287 return Err(MongrelError::Other(
7288 "database poisoned by fsync error".into(),
7289 ));
7290 }
7291 if definition.name.is_empty() || definition.query.trim().is_empty() {
7292 return Err(MongrelError::InvalidArgument(
7293 "materialized view name and query must not be empty".into(),
7294 ));
7295 }
7296 let _operation = self.admit_operation()?;
7299 let _ddl = self.ddl_lock.lock();
7300 let _security_write = self.security_write()?;
7301 self.require(&crate::catalog_cmds::required_permission(&command))?;
7302 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7306 let table_id = self
7307 .catalog
7308 .read()
7309 .live(&definition.name)
7310 .ok_or_else(|| {
7311 MongrelError::NotFound(format!(
7312 "materialized view table {:?} not found",
7313 definition.name
7314 ))
7315 })?
7316 .table_id;
7317 let definition_json = DdlOp::encode_materialized_view(&definition)?;
7318 let _commit = self.commit_lock.lock();
7319 let epoch = self.epoch.bump_assigned();
7320 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7321 let txn_id = self.alloc_txn_id()?;
7322 let mut next_catalog = self.catalog.read().clone();
7323 self.apply_catalog_command_to(&mut next_catalog, command)?;
7324 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
7325 let commit_seq = {
7326 let mut wal = self.shared_wal.lock();
7327 let append: Result<u64> = (|| {
7328 wal.append(
7329 txn_id,
7330 table_id,
7331 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
7332 name: definition.name.clone(),
7333 definition_json,
7334 }),
7335 )?;
7336 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
7337 wal.append_commit(txn_id, epoch, &[])
7338 })();
7339 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
7340 };
7341 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
7342
7343 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
7344 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
7345 Ok(epoch)
7346 }
7347
7348 pub fn root(&self) -> &Path {
7350 self.durable_root.canonical_path()
7351 }
7352
7353 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
7356 Arc::clone(&self.durable_root)
7357 }
7358
7359 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
7363 self.kek
7364 .as_deref()
7365 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
7366 }
7367
7368 pub fn is_read_only_replica(&self) -> bool {
7369 self.read_only
7370 }
7371
7372 pub fn ensure_consistent_read(&self) -> Result<()> {
7376 self.ensure_owner_process()?;
7377 if self.poisoned.load(Ordering::Relaxed) {
7378 return Err(MongrelError::Other(
7379 "database poisoned by post-commit failure; reopen required".into(),
7380 ));
7381 }
7382 Ok(())
7383 }
7384
7385 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
7386 self.replication_wal_retention_segments
7387 .store(segments, std::sync::atomic::Ordering::Relaxed);
7388 }
7389
7390 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
7395 let admin = crate::auth::Permission::Admin;
7396 self.require(&admin)?;
7397 let _operation = self.admit_operation()?;
7399 let operation_principal = self.principal_snapshot();
7400 let _barrier = self.replication_barrier.write();
7401 let _ddl = self.ddl_lock.lock();
7402 let _security = self.security_coordinator.gate.read();
7403 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7404 let mut handles: Vec<_> = self
7405 .tables
7406 .read()
7407 .iter()
7408 .map(|(id, handle)| (*id, handle.clone()))
7409 .collect();
7410 handles.sort_by_key(|(id, _)| *id);
7411 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7412 let _commit = self.commit_lock.lock();
7413 let mut wal = self.shared_wal.lock();
7414 wal.group_sync()?;
7415 let io_root = self.durable_root.io_path()?;
7416 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7417 let records = crate::wal::SharedWal::replay_with_dek(&io_root, wal_dek.as_ref())?;
7418 let epoch = records
7419 .iter()
7420 .filter_map(|record| match &record.op {
7421 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
7422 _ => None,
7423 })
7424 .max()
7425 .unwrap_or(0)
7426 .max(self.epoch.committed().0);
7427 let files = crate::replication::capture_files(&io_root)?;
7428 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7429 drop(wal);
7430 Ok(crate::replication::ReplicationSnapshot::new(
7431 source_id, epoch, files,
7432 ))
7433 }
7434
7435 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
7443 let control = crate::ExecutionControl::new(None);
7444 self.hot_backup_controlled(destination, &control, || true)
7445 }
7446
7447 pub(crate) fn hot_backup_to_durable_child(
7448 &self,
7449 parent: &crate::durable_file::DurableRoot,
7450 child: &Path,
7451 control: &crate::ExecutionControl,
7452 ) -> Result<crate::backup::BackupReport> {
7453 let mut components = child.components();
7454 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
7455 || components.next().is_some()
7456 {
7457 return Err(MongrelError::InvalidArgument(
7458 "durable backup child must be one relative path component".into(),
7459 ));
7460 }
7461 let destination_name = child.file_name().ok_or_else(|| {
7462 MongrelError::InvalidArgument("durable backup child has no filename".into())
7463 })?;
7464 let source_root = self.durable_root.io_path()?;
7465 let prepared = prepare_backup_destination_in(&source_root, parent, destination_name)?;
7466 self.hot_backup_prepared(prepared, control, || true)
7467 }
7468
7469 #[doc(hidden)]
7472 pub fn hot_backup_controlled<F>(
7473 &self,
7474 destination: impl AsRef<Path>,
7475 control: &crate::ExecutionControl,
7476 before_publish: F,
7477 ) -> Result<crate::backup::BackupReport>
7478 where
7479 F: FnOnce() -> bool,
7480 {
7481 let source_root = self.durable_root.io_path()?;
7482 let prepared = prepare_backup_destination(&source_root, destination.as_ref())?;
7483 self.hot_backup_prepared(prepared, control, before_publish)
7484 }
7485
7486 fn hot_backup_prepared<F>(
7487 &self,
7488 mut prepared: PreparedBackupDestination,
7489 control: &crate::ExecutionControl,
7490 before_publish: F,
7491 ) -> Result<crate::backup::BackupReport>
7492 where
7493 F: FnOnce() -> bool,
7494 {
7495 let admin = crate::auth::Permission::Admin;
7496 self.require(&admin)?;
7497 let _operation = self.admit_operation()?;
7500 let operation_principal = self.principal_snapshot();
7501 control.checkpoint()?;
7502 let destination = prepared.destination_path.clone();
7503 let mut before_publish = Some(before_publish);
7504
7505 let outcome = (|| {
7506 control.checkpoint()?;
7507 let barrier = self.replication_barrier.write();
7508 let ddl = self.ddl_lock.lock();
7509 let security = self.security_coordinator.gate.read();
7510 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7511 let mut handles: Vec<_> = self
7512 .tables
7513 .read()
7514 .iter()
7515 .map(|(id, handle)| (*id, handle.clone()))
7516 .collect();
7517 handles.sort_by_key(|(id, _)| *id);
7518 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7519 let commit = self.commit_lock.lock();
7520 let mut wal = self.shared_wal.lock();
7521 wal.group_sync()?;
7522 let epoch = self.epoch.committed().0;
7523 let boundary_unix_nanos = current_unix_nanos();
7524 let source_root = self.durable_root.io_path()?;
7525
7526 let pin_nonce = std::time::SystemTime::now()
7527 .duration_since(std::time::UNIX_EPOCH)
7528 .unwrap_or_default()
7529 .as_nanos();
7530 let file_pin_root = source_root
7531 .join(META_DIR)
7532 .join("backup-pins")
7533 .join(format!("{}-{pin_nonce}", std::process::id()));
7534 std::fs::create_dir_all(&file_pin_root)?;
7535 let _file_pins = BackupFilePins {
7536 root: file_pin_root.clone(),
7537 };
7538 let mut run_files = Vec::new();
7539 for (index, (table_id, _)) in handles.iter().enumerate() {
7540 if index % 256 == 0 {
7541 control.checkpoint()?;
7542 }
7543 let table = &table_guards[index];
7544 for (run_index, run) in table.run_refs().iter().enumerate() {
7545 if run_index % 256 == 0 {
7546 control.checkpoint()?;
7547 }
7548 let source = table.run_path(run.run_id as u64);
7549 let relative = Path::new(TABLES_DIR)
7550 .join(table_id.to_string())
7551 .join(crate::engine::RUNS_DIR)
7552 .join(format!("r-{}.sr", run.run_id));
7553 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
7554 if std::fs::hard_link(&source, &pinned).is_err() {
7555 crate::backup::copy_file_synced(&source, &pinned)?;
7556 }
7557 run_files.push(((*table_id, run.run_id), pinned, relative));
7558 }
7559 }
7560 crate::durable_file::sync_directory(&file_pin_root)?;
7561 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
7562 {
7563 let mut pins = self.backup_pins.lock();
7564 for key in &run_keys {
7565 *pins.entry(*key).or_insert(0) += 1;
7566 }
7567 }
7568 let _run_pins = RunPins {
7569 pins: Arc::clone(&self.backup_pins),
7570 runs: run_keys,
7571 };
7572 let _registry_pins: Vec<crate::retention::PinGuard> = table_guards
7579 .iter()
7580 .map(|table| {
7581 table
7582 .pin_registry()
7583 .pin(crate::retention::PinSource::BackupPitr, Epoch(epoch))
7584 })
7585 .collect();
7586 let deferred: HashSet<_> = run_files
7587 .iter()
7588 .map(|(_, _, relative)| relative.clone())
7589 .collect();
7590 let mut copied = Vec::new();
7591 copy_backup_boundary(
7592 &source_root,
7593 prepared.stage.as_deref().ok_or_else(|| {
7594 MongrelError::Other("backup staging root was already released".into())
7595 })?,
7596 &deferred,
7597 &mut copied,
7598 Some(control),
7599 )?;
7600
7601 drop(wal);
7602 drop(commit);
7603 drop(table_guards);
7604 drop(security);
7605 drop(ddl);
7606 drop(barrier);
7607
7608 if let Some(hook) = self.backup_hook.lock().as_ref() {
7609 hook();
7610 }
7611 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
7612 if index % 256 == 0 {
7613 control.checkpoint()?;
7614 }
7615 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
7616 prepared
7617 .stage
7618 .as_deref()
7619 .ok_or_else(|| {
7620 MongrelError::Other("backup staging root was already released".into())
7621 })?
7622 .copy_new_from(&relative, &mut source)?;
7623 copied.push(relative);
7624 }
7625
7626 let manifest = crate::backup::BackupManifest::create_controlled_durable(
7627 prepared.stage.as_deref().ok_or_else(|| {
7628 MongrelError::Other("backup staging root was already released".into())
7629 })?,
7630 epoch,
7631 &copied,
7632 control,
7633 )?;
7634 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
7635 MongrelError::Other("backup staging root was already released".into())
7636 })?)?;
7637 control.checkpoint()?;
7638 let publish = before_publish.take().ok_or_else(|| {
7639 MongrelError::Other("backup publication callback already consumed".into())
7640 })?;
7641 if !publish() {
7642 return Err(MongrelError::Cancelled);
7643 }
7644 let final_security = self.security_coordinator.gate.read();
7645 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7646 drop(prepared.stage.take().ok_or_else(|| {
7650 MongrelError::Other("backup staging root was already released".into())
7651 })?);
7652 let published = std::cell::Cell::new(false);
7653 if let Err(error) = prepared.parent.rename_directory_new_with_after(
7654 Path::new(&prepared.stage_name),
7655 &prepared.parent,
7656 Path::new(&prepared.destination_name),
7657 || published.set(true),
7658 ) {
7659 if published.get() {
7660 return Err(MongrelError::CommitOutcomeUnknown {
7661 epoch,
7662 message: format!("backup publication was not durable: {error}"),
7663 });
7664 }
7665 return Err(error.into());
7666 }
7667 drop(final_security);
7668 Ok(crate::backup::BackupReport {
7669 destination,
7670 epoch,
7671 boundary_unix_nanos,
7672 files: manifest.files.len(),
7673 bytes: manifest.total_bytes(),
7674 })
7675 })();
7676
7677 if outcome.is_err() {
7678 drop(prepared.stage.take());
7679 let _ = prepared
7680 .parent
7681 .remove_directory_all(Path::new(&prepared.stage_name));
7682 }
7683 outcome
7684 }
7685
7686 pub fn replication_batch_since(
7689 &self,
7690 since_epoch: u64,
7691 ) -> Result<crate::replication::ReplicationBatch> {
7692 use crate::wal::Op;
7693
7694 let admin = crate::auth::Permission::Admin;
7695 self.require(&admin)?;
7696 let _operation = self.admit_operation()?;
7698 let operation_principal = self.principal_snapshot();
7699
7700 let mut wal = self.shared_wal.lock();
7701 wal.group_sync()?;
7702 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7703 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
7704 drop(wal);
7705
7706 let commits: HashMap<u64, u64> = records
7707 .iter()
7708 .filter_map(|record| match &record.op {
7709 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
7710 _ => None,
7711 })
7712 .collect();
7713 let earliest_epoch = commits.values().copied().min();
7714 let current_epoch = commits
7715 .values()
7716 .copied()
7717 .max()
7718 .unwrap_or(0)
7719 .max(self.epoch.committed().0);
7720 let selected: HashSet<u64> = commits
7721 .iter()
7722 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
7723 .collect();
7724 let retention_gap = since_epoch < current_epoch
7725 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
7726 let spilled = records.iter().any(|record| {
7727 selected.contains(&record.txn_id)
7728 && matches!(
7729 &record.op,
7730 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
7731 )
7732 });
7733 let records = records
7734 .into_iter()
7735 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
7736 .filter(|record| selected.contains(&record.txn_id))
7737 .collect();
7738 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7739 let batch = crate::replication::ReplicationBatch::complete_for_source(
7740 source_id,
7741 since_epoch,
7742 current_epoch,
7743 earliest_epoch,
7744 retention_gap,
7745 spilled,
7746 records,
7747 )?;
7748 if let Some(hook) = self.replication_hook.lock().as_ref() {
7749 hook();
7750 }
7751 let _security = self.security_coordinator.gate.read();
7752 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7753 Ok(batch)
7754 }
7755
7756 pub fn append_replication_batch(
7760 &self,
7761 batch: &crate::replication::ReplicationBatch,
7762 ) -> Result<u64> {
7763 use crate::wal::Op;
7764
7765 if !self.read_only {
7766 return Err(MongrelError::InvalidArgument(
7767 "replication batches may only target a marked replica".into(),
7768 ));
7769 }
7770 let _operation = self.admit_operation()?;
7772 let current = crate::replication::replica_epoch(&self.root)?;
7773 if batch.is_source_bound() {
7774 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
7775 if batch.source_id != source_id {
7776 return Err(MongrelError::Conflict(
7777 "replication batch source does not match follower binding".into(),
7778 ));
7779 }
7780 }
7781 if batch.requires_snapshot {
7782 return Err(MongrelError::Conflict(
7783 "replication snapshot required for this batch".into(),
7784 ));
7785 }
7786 batch.validate_proof()?;
7787 if batch.from_epoch != current {
7788 if batch.from_epoch < current && batch.current_epoch == current {
7789 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7790 let _wal = self.shared_wal.lock();
7791 let existing: HashSet<(u64, u64)> =
7792 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7793 .into_iter()
7794 .filter_map(|record| match record.op {
7795 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7796 _ => None,
7797 })
7798 .collect();
7799 let already_applied = batch.records.iter().all(|record| match &record.op {
7800 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
7801 _ => true,
7802 });
7803 if already_applied {
7804 return Ok(current);
7805 }
7806 }
7807 return Err(MongrelError::Conflict(format!(
7808 "replication batch starts at epoch {}, follower is at epoch {current}",
7809 batch.from_epoch
7810 )));
7811 }
7812 if batch.current_epoch < current {
7813 return Err(MongrelError::InvalidArgument(format!(
7814 "replication batch current epoch {} precedes follower epoch {current}",
7815 batch.current_epoch
7816 )));
7817 }
7818 let records = &batch.records;
7819 let mut commits = HashMap::new();
7820 let mut commit_epochs = HashSet::new();
7821 let mut commit_timestamps = HashMap::new();
7822 for record in records {
7823 match &record.op {
7824 Op::TxnCommit { epoch, added_runs } => {
7825 if !added_runs.is_empty() {
7826 return Err(MongrelError::Conflict(
7827 "replication snapshot required for spilled-run transaction".into(),
7828 ));
7829 }
7830 if commits.insert(record.txn_id, *epoch).is_some() {
7831 return Err(MongrelError::InvalidArgument(format!(
7832 "duplicate commit for replication transaction {}",
7833 record.txn_id
7834 )));
7835 }
7836 if *epoch <= current || *epoch > batch.current_epoch {
7837 return Err(MongrelError::InvalidArgument(format!(
7838 "replication commit epoch {epoch} is outside ({current}, {}]",
7839 batch.current_epoch
7840 )));
7841 }
7842 if !commit_epochs.insert(*epoch) {
7843 return Err(MongrelError::InvalidArgument(format!(
7844 "duplicate replication commit epoch {epoch}"
7845 )));
7846 }
7847 }
7848 Op::CommitTimestamp { unix_nanos } => {
7849 commit_timestamps.insert(record.txn_id, *unix_nanos);
7850 }
7851 _ => {}
7852 }
7853 }
7854 for record in records {
7855 if record.txn_id == crate::wal::SYSTEM_TXN_ID
7856 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
7857 {
7858 return Err(MongrelError::InvalidArgument(
7859 "replication batch contains a non-committed record".into(),
7860 ));
7861 }
7862 if !commits.contains_key(&record.txn_id) {
7863 return Err(MongrelError::InvalidArgument(format!(
7864 "incomplete replication transaction {}",
7865 record.txn_id
7866 )));
7867 }
7868 }
7869 let target_epoch = commits
7870 .values()
7871 .copied()
7872 .filter(|epoch| *epoch > current)
7873 .max()
7874 .unwrap_or(current);
7875 if target_epoch != batch.current_epoch {
7876 return Err(MongrelError::InvalidArgument(format!(
7877 "replication batch ends at epoch {target_epoch}, expected {}",
7878 batch.current_epoch
7879 )));
7880 }
7881 let mut selected: HashSet<u64> = commits
7882 .iter()
7883 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
7884 .collect();
7885 if selected.is_empty() {
7886 return Ok(current);
7887 }
7888 let mut wal = self.shared_wal.lock();
7889 wal.group_sync()?;
7890 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7891 let existing: HashSet<(u64, u64)> =
7892 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7893 .into_iter()
7894 .filter_map(|record| match record.op {
7895 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7896 _ => None,
7897 })
7898 .collect();
7899 selected.retain(|txn_id| {
7900 commits
7901 .get(txn_id)
7902 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
7903 });
7904 for record in records {
7905 if !selected.contains(&record.txn_id) {
7906 continue;
7907 }
7908 match &record.op {
7909 Op::TxnCommit { epoch, added_runs } => {
7910 let timestamp = commit_timestamps
7911 .get(&record.txn_id)
7912 .copied()
7913 .unwrap_or_else(current_unix_nanos);
7914 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
7915 }
7916 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
7917 op => {
7918 wal.append(record.txn_id, 0, op.clone())?;
7919 }
7920 }
7921 }
7922 if !selected.is_empty() {
7923 wal.group_sync()?;
7924 }
7925 drop(wal);
7926
7927 let mut recovered_catalog = self.catalog.read().clone();
7931 if let Err(error) = recover_ddl_from_wal(
7932 &self.root,
7933 Some(&self.durable_root),
7934 &mut recovered_catalog,
7935 self.meta_dek.as_ref(),
7936 wal_dek.as_ref(),
7937 true,
7938 None,
7939 ) {
7940 return Err(MongrelError::DurableCommit {
7941 epoch: target_epoch,
7942 message: format!(
7943 "replication WAL is durable but catalog checkpoint failed: {error}"
7944 ),
7945 });
7946 }
7947 let _security = self.security_coordinator.gate.write();
7948 let old_security_version = self.catalog.read().security_version;
7949 let security_changed = old_security_version != recovered_catalog.security_version
7950 || self.catalog.read().require_auth != recovered_catalog.require_auth;
7951 let require_auth = recovered_catalog.require_auth;
7952 let principal = if security_changed {
7953 None
7954 } else {
7955 self.principal.read().as_ref().and_then(|principal| {
7956 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
7957 })
7958 };
7959 if require_auth {
7960 self.auth_state.set_require_auth(true);
7961 }
7962 self.auth_state.set_principal(principal.clone());
7963 *self.principal.write() = principal;
7964 let security_version = recovered_catalog.security_version;
7965 *self.catalog.write() = recovered_catalog;
7966 self.security_coordinator
7967 .version
7968 .store(security_version, Ordering::Release);
7969 if !require_auth {
7970 self.auth_state.set_require_auth(false);
7971 }
7972 if let Err(error) =
7973 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
7974 {
7975 return Err(MongrelError::DurableCommit {
7976 epoch: target_epoch,
7977 message: format!(
7978 "replication WAL and catalog are durable but follower watermark failed: {error}"
7979 ),
7980 });
7981 }
7982 Ok(target_epoch)
7983 }
7984
7985 pub fn table_id(&self, name: &str) -> Result<u64> {
7988 let cat = self.catalog.read();
7989 cat.live(name)
7990 .map(|e| e.table_id)
7991 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
7992 }
7993
7994 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
7999 let catalog = self.catalog.read();
8000 catalog
8001 .live(name)
8002 .map(|entry| (entry.table_id, entry.schema.schema_id))
8003 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
8004 }
8005
8006 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
8007 self.catalog
8008 .read()
8009 .building(name)
8010 .map(|entry| entry.table_id)
8011 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
8012 }
8013
8014 pub fn procedures(&self) -> Vec<StoredProcedure> {
8015 self.catalog
8016 .read()
8017 .procedures
8018 .iter()
8019 .map(|p| p.procedure.clone())
8020 .collect()
8021 }
8022
8023 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
8024 self.catalog
8025 .read()
8026 .procedures
8027 .iter()
8028 .find(|p| p.procedure.name == name)
8029 .map(|p| p.procedure.clone())
8030 }
8031
8032 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
8033 self.create_procedure_inner(procedure, None)
8034 }
8035
8036 pub fn create_procedure_controlled<F>(
8037 &self,
8038 procedure: StoredProcedure,
8039 mut before_publish: F,
8040 ) -> Result<StoredProcedure>
8041 where
8042 F: FnMut() -> Result<()>,
8043 {
8044 self.create_procedure_inner(procedure, Some(&mut before_publish))
8045 }
8046
8047 fn create_procedure_inner(
8048 &self,
8049 mut procedure: StoredProcedure,
8050 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8051 ) -> Result<StoredProcedure> {
8052 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8053 procedure: procedure.clone(),
8054 };
8055 self.require(&crate::catalog_cmds::required_permission(&command))?;
8056 let _g = self.ddl_lock.lock();
8057 let _security_write = self.security_write()?;
8058 self.require(&crate::catalog_cmds::required_permission(&command))?;
8059 procedure.validate()?;
8060 self.validate_procedure_references(&procedure)?;
8061 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8065 let commit_lock = Arc::clone(&self.commit_lock);
8066 let _c = commit_lock.lock();
8067 let epoch = self.epoch.bump_assigned();
8068 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8069 procedure.created_epoch = epoch.0;
8070 procedure.updated_epoch = epoch.0;
8071 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8072 procedure: procedure.clone(),
8073 };
8074 let mut next_catalog = self.catalog.read().clone();
8075 self.apply_catalog_command_to(&mut next_catalog, command)?;
8076 next_catalog.db_epoch = epoch.0;
8077 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8078 Ok(procedure)
8079 }
8080
8081 pub fn create_or_replace_procedure(
8082 &self,
8083 procedure: StoredProcedure,
8084 ) -> Result<StoredProcedure> {
8085 self.create_or_replace_procedure_inner(procedure, None)
8086 }
8087
8088 pub fn create_or_replace_procedure_controlled<F>(
8089 &self,
8090 procedure: StoredProcedure,
8091 mut before_publish: F,
8092 ) -> Result<StoredProcedure>
8093 where
8094 F: FnMut() -> Result<()>,
8095 {
8096 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
8097 }
8098
8099 fn create_or_replace_procedure_inner(
8100 &self,
8101 procedure: StoredProcedure,
8102 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8103 ) -> Result<StoredProcedure> {
8104 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8105 procedure: procedure.clone(),
8106 };
8107 self.require(&crate::catalog_cmds::required_permission(&command))?;
8108 let _g = self.ddl_lock.lock();
8109 let _security_write = self.security_write()?;
8110 self.require(&crate::catalog_cmds::required_permission(&command))?;
8111 procedure.validate()?;
8112 self.validate_procedure_references(&procedure)?;
8113 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8117 let commit_lock = Arc::clone(&self.commit_lock);
8118 let _c = commit_lock.lock();
8119 let epoch = self.epoch.bump_assigned();
8120 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8121 let mut next_catalog = self.catalog.read().clone();
8122 let replaced = match next_catalog
8127 .procedures
8128 .iter()
8129 .position(|p| p.procedure.name == procedure.name)
8130 {
8131 Some(idx) => next_catalog.procedures[idx]
8132 .procedure
8133 .replaced(procedure.clone(), epoch.0)?,
8134 None => {
8135 let mut next = procedure;
8136 next.created_epoch = epoch.0;
8137 next.updated_epoch = epoch.0;
8138 next
8139 }
8140 };
8141 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8142 procedure: replaced.clone(),
8143 };
8144 self.apply_catalog_command_to(&mut next_catalog, command)?;
8145 next_catalog.db_epoch = epoch.0;
8146 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8147 Ok(replaced)
8148 }
8149
8150 pub fn drop_procedure(&self, name: &str) -> Result<()> {
8151 self.drop_procedure_with_epoch(name).map(|_| ())
8152 }
8153
8154 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
8155 self.drop_procedure_with_epoch_inner(name, None)
8156 }
8157
8158 pub fn drop_procedure_with_epoch_controlled<F>(
8159 &self,
8160 name: &str,
8161 mut before_publish: F,
8162 ) -> Result<Epoch>
8163 where
8164 F: FnMut() -> Result<()>,
8165 {
8166 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
8167 }
8168
8169 fn drop_procedure_with_epoch_inner(
8170 &self,
8171 name: &str,
8172 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8173 ) -> Result<Epoch> {
8174 let command = crate::catalog_cmds::CatalogCommand::DropProcedure {
8175 name: name.to_string(),
8176 };
8177 self.require(&crate::catalog_cmds::required_permission(&command))?;
8178 let _g = self.ddl_lock.lock();
8179 let _security_write = self.security_write()?;
8180 self.require(&crate::catalog_cmds::required_permission(&command))?;
8181 let commit_lock = Arc::clone(&self.commit_lock);
8182 let _c = commit_lock.lock();
8183 let epoch = self.epoch.bump_assigned();
8184 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8185 let mut next_catalog = self.catalog.read().clone();
8186 self.apply_catalog_command_to(&mut next_catalog, command)?;
8187 next_catalog.db_epoch = epoch.0;
8188 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8189 Ok(epoch)
8190 }
8191
8192 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
8197 self.catalog.read().users.clone()
8198 }
8199
8200 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
8203 self.catalog
8204 .read()
8205 .users
8206 .iter()
8207 .find(|user| user.username == username)
8208 .map(|user| (user.id, user.created_epoch))
8209 }
8210
8211 pub fn user_scram_verifier(
8213 &self,
8214 username: &str,
8215 ) -> Option<crate::security_hardening::ScramVerifier> {
8216 self.catalog
8217 .read()
8218 .users
8219 .iter()
8220 .find(|user| user.username == username)
8221 .and_then(|user| user.scram_sha_256.clone())
8222 }
8223
8224 pub fn security_version(&self) -> u64 {
8227 self.catalog.read().security_version
8228 }
8229
8230 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
8232 self.catalog.read().roles.clone()
8233 }
8234
8235 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
8237 self.require(&crate::auth::Permission::Admin)?;
8238 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
8239 let scram = crate::auth::scram_verifier(password).map_err(MongrelError::Other)?;
8240 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(password);
8241 self.create_user_with_password_hash_inner(username, hash, Some((scram, mysql)), None)
8242 }
8243
8244 pub fn create_user_with_password_hash(
8246 &self,
8247 username: &str,
8248 hash: String,
8249 ) -> Result<crate::auth::UserEntry> {
8250 self.create_user_with_password_hash_inner(username, hash, None, None)
8251 }
8252
8253 pub fn create_user_with_password_hash_controlled<F>(
8254 &self,
8255 username: &str,
8256 hash: String,
8257 mut before_publish: F,
8258 ) -> Result<crate::auth::UserEntry>
8259 where
8260 F: FnMut() -> Result<()>,
8261 {
8262 self.create_user_with_password_hash_inner(username, hash, None, Some(&mut before_publish))
8263 }
8264
8265 fn create_user_with_password_hash_inner(
8266 &self,
8267 username: &str,
8268 hash: String,
8269 verifiers: Option<(
8270 crate::security_hardening::ScramVerifier,
8271 crate::auth::MysqlCachingSha2Verifier,
8272 )>,
8273 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8274 ) -> Result<crate::auth::UserEntry> {
8275 let command = match &verifiers {
8279 Some((scram_sha_256, mysql_caching_sha2)) => {
8280 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8281 username: username.to_string(),
8282 password_hash: hash.clone(),
8283 scram_sha_256: scram_sha_256.clone(),
8284 mysql_caching_sha2: mysql_caching_sha2.clone(),
8285 is_admin: false,
8286 created_epoch: 0,
8287 }
8288 }
8289 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8290 username: username.to_string(),
8291 password_hash: hash.clone(),
8292 is_admin: false,
8293 created_epoch: 0,
8294 },
8295 };
8296 self.require(&crate::catalog_cmds::required_permission(&command))?;
8297 let _ddl = self.ddl_lock.lock();
8298 let _security_write = self.security_write()?;
8299 self.require(&crate::catalog_cmds::required_permission(&command))?;
8300 let _commit = self.commit_lock.lock();
8301 let epoch = self.epoch.bump_assigned();
8302 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8303 let command = match verifiers {
8304 Some((scram_sha_256, mysql_caching_sha2)) => {
8305 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8306 username: username.to_string(),
8307 password_hash: hash,
8308 scram_sha_256,
8309 mysql_caching_sha2,
8310 is_admin: false,
8311 created_epoch: epoch.0,
8312 }
8313 }
8314 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8315 username: username.to_string(),
8316 password_hash: hash,
8317 is_admin: false,
8318 created_epoch: epoch.0,
8319 },
8320 };
8321 let mut next_catalog = self.catalog.read().clone();
8322 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8323 let crate::catalog_cmds::CatalogDelta::UserUpserted(entry) = delta else {
8324 return Err(MongrelError::Other(
8325 "CreateUser resolved to an unexpected catalog delta".into(),
8326 ));
8327 };
8328 next_catalog.db_epoch = epoch.0;
8329 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8330 Ok(entry)
8331 }
8332
8333 pub fn drop_user(&self, username: &str) -> Result<()> {
8335 self.drop_user_with_epoch(username).map(|_| ())
8336 }
8337
8338 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
8339 self.drop_user_with_epoch_inner(username, None)
8340 }
8341
8342 pub fn drop_user_with_epoch_controlled<F>(
8343 &self,
8344 username: &str,
8345 mut before_publish: F,
8346 ) -> Result<Epoch>
8347 where
8348 F: FnMut() -> Result<()>,
8349 {
8350 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
8351 }
8352
8353 fn drop_user_with_epoch_inner(
8354 &self,
8355 username: &str,
8356 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8357 ) -> Result<Epoch> {
8358 let command = crate::catalog_cmds::CatalogCommand::DropUser {
8359 username: username.to_string(),
8360 };
8361 self.require(&crate::catalog_cmds::required_permission(&command))?;
8362 let _ddl = self.ddl_lock.lock();
8363 let _security_write = self.security_write()?;
8364 self.require(&crate::catalog_cmds::required_permission(&command))?;
8365 let _commit = self.commit_lock.lock();
8366 let epoch = self.epoch.bump_assigned();
8367 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8368 let mut next_catalog = self.catalog.read().clone();
8369 self.apply_catalog_command_to(&mut next_catalog, command)?;
8370 next_catalog.db_epoch = epoch.0;
8371 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8372 Ok(epoch)
8373 }
8374
8375 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
8377 self.alter_user_password_with_epoch(username, new_password)
8378 .map(|_| ())
8379 }
8380
8381 pub fn alter_user_password_with_epoch(
8382 &self,
8383 username: &str,
8384 new_password: &str,
8385 ) -> Result<Epoch> {
8386 self.require(&crate::auth::Permission::Admin)?;
8387 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
8388 let scram = crate::auth::scram_verifier(new_password).map_err(MongrelError::Other)?;
8389 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(new_password);
8390 self.alter_user_password_hash_with_epoch_inner(username, hash, Some((scram, mysql)), None)
8391 }
8392
8393 pub fn alter_user_password_hash_with_epoch(
8394 &self,
8395 username: &str,
8396 hash: String,
8397 ) -> Result<Epoch> {
8398 self.alter_user_password_hash_with_epoch_inner(username, hash, None, None)
8399 }
8400
8401 pub fn alter_user_password_hash_with_epoch_controlled<F>(
8402 &self,
8403 username: &str,
8404 hash: String,
8405 mut before_publish: F,
8406 ) -> Result<Epoch>
8407 where
8408 F: FnMut() -> Result<()>,
8409 {
8410 self.alter_user_password_hash_with_epoch_inner(
8411 username,
8412 hash,
8413 None,
8414 Some(&mut before_publish),
8415 )
8416 }
8417
8418 fn alter_user_password_hash_with_epoch_inner(
8419 &self,
8420 username: &str,
8421 hash: String,
8422 verifiers: Option<(
8423 crate::security_hardening::ScramVerifier,
8424 crate::auth::MysqlCachingSha2Verifier,
8425 )>,
8426 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8427 ) -> Result<Epoch> {
8428 let command = match verifiers {
8429 Some((scram_sha_256, mysql_caching_sha2)) => {
8430 crate::catalog_cmds::CatalogCommand::AlterUserPasswordWithAuthVerifiers {
8431 username: username.to_string(),
8432 password_hash: hash,
8433 scram_sha_256,
8434 mysql_caching_sha2,
8435 }
8436 }
8437 None => crate::catalog_cmds::CatalogCommand::AlterUserPassword {
8438 username: username.to_string(),
8439 password_hash: hash,
8440 },
8441 };
8442 self.require(&crate::catalog_cmds::required_permission(&command))?;
8443 let _ddl = self.ddl_lock.lock();
8444 let _security_write = self.security_write()?;
8445 self.require(&crate::catalog_cmds::required_permission(&command))?;
8446 let _commit = self.commit_lock.lock();
8447 let epoch = self.epoch.bump_assigned();
8448 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8449 let mut next_catalog = self.catalog.read().clone();
8450 self.apply_catalog_command_to(&mut next_catalog, command)?;
8451 next_catalog.db_epoch = epoch.0;
8452 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8453 Ok(epoch)
8454 }
8455
8456 pub fn verify_user(
8459 &self,
8460 username: &str,
8461 password: &str,
8462 ) -> Result<Option<crate::auth::UserEntry>> {
8463 let cat = self.catalog.read();
8464 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
8465 return Ok(None);
8466 };
8467 if user.password_hash.is_empty() {
8468 return Ok(None);
8469 }
8470 let ok = crate::auth::verify_password(password, &user.password_hash)
8471 .map_err(MongrelError::Other)?;
8472 if ok {
8473 Ok(Some(user.clone()))
8474 } else {
8475 Ok(None)
8476 }
8477 }
8478
8479 pub fn authenticate_principal(
8483 &self,
8484 username: &str,
8485 password: &str,
8486 ) -> Result<Option<crate::auth::Principal>> {
8487 self.authenticate_principal_inner(username, password, || {})
8488 }
8489
8490 fn authenticate_principal_inner<F>(
8491 &self,
8492 username: &str,
8493 password: &str,
8494 after_verify: F,
8495 ) -> Result<Option<crate::auth::Principal>>
8496 where
8497 F: FnOnce(),
8498 {
8499 let catalog = self.catalog.read();
8500 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
8501 return Ok(None);
8502 };
8503 if user.password_hash.is_empty()
8504 || !crate::auth::verify_password(password, &user.password_hash)
8505 .map_err(MongrelError::Other)?
8506 {
8507 return Ok(None);
8508 }
8509 after_verify();
8510 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
8511 }
8512
8513 pub fn authenticate_mysql_caching_sha2_principal(
8516 &self,
8517 username: &str,
8518 nonce: &[u8],
8519 proof: &[u8],
8520 ) -> Option<crate::auth::Principal> {
8521 let catalog = self.catalog.read();
8522 let user = catalog
8523 .users
8524 .iter()
8525 .find(|user| user.username == username)?;
8526 if !user.mysql_caching_sha2.as_ref()?.verify(nonce, proof) {
8527 return None;
8528 }
8529 Self::resolve_user_principal_from_catalog(&catalog, user)
8530 }
8531
8532 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
8534 self.set_user_admin_with_epoch(username, is_admin)
8535 .map(|_| ())
8536 }
8537
8538 pub fn set_user_admin_with_epoch(
8539 &self,
8540 username: &str,
8541 is_admin: bool,
8542 ) -> Result<Option<Epoch>> {
8543 self.set_user_admin_with_epoch_inner(username, is_admin, None)
8544 }
8545
8546 pub fn set_user_admin_with_epoch_controlled<F>(
8547 &self,
8548 username: &str,
8549 is_admin: bool,
8550 mut before_publish: F,
8551 ) -> Result<Option<Epoch>>
8552 where
8553 F: FnMut() -> Result<()>,
8554 {
8555 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
8556 }
8557
8558 fn set_user_admin_with_epoch_inner(
8559 &self,
8560 username: &str,
8561 is_admin: bool,
8562 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8563 ) -> Result<Option<Epoch>> {
8564 let command = crate::catalog_cmds::CatalogCommand::SetUserAdmin {
8565 username: username.to_string(),
8566 is_admin,
8567 };
8568 self.require(&crate::catalog_cmds::required_permission(&command))?;
8569 let _ddl = self.ddl_lock.lock();
8570 let _security_write = self.security_write()?;
8571 self.require(&crate::catalog_cmds::required_permission(&command))?;
8572 let _commit = self.commit_lock.lock();
8573 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8577 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8578 return Ok(None);
8579 }
8580 let epoch = self.epoch.bump_assigned();
8581 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8582 let mut next_catalog = self.catalog.read().clone();
8583 self.apply_catalog_command_to(&mut next_catalog, command)?;
8584 next_catalog.db_epoch = epoch.0;
8585 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8586 Ok(Some(epoch))
8587 }
8588
8589 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
8591 self.create_role_inner(name, None)
8592 }
8593
8594 pub fn create_role_controlled<F>(
8595 &self,
8596 name: &str,
8597 mut before_publish: F,
8598 ) -> Result<crate::auth::RoleEntry>
8599 where
8600 F: FnMut() -> Result<()>,
8601 {
8602 self.create_role_inner(name, Some(&mut before_publish))
8603 }
8604
8605 fn create_role_inner(
8606 &self,
8607 name: &str,
8608 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8609 ) -> Result<crate::auth::RoleEntry> {
8610 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8611 name: name.to_string(),
8612 created_epoch: 0, };
8614 self.require(&crate::catalog_cmds::required_permission(&command))?;
8615 let _ddl = self.ddl_lock.lock();
8616 let _security_write = self.security_write()?;
8617 self.require(&crate::catalog_cmds::required_permission(&command))?;
8618 let _commit = self.commit_lock.lock();
8619 let epoch = self.epoch.bump_assigned();
8620 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8621 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8622 name: name.to_string(),
8623 created_epoch: epoch.0,
8624 };
8625 let mut next_catalog = self.catalog.read().clone();
8626 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8627 let crate::catalog_cmds::CatalogDelta::RoleUpserted(entry) = delta else {
8628 return Err(MongrelError::Other(
8629 "CreateRole resolved to an unexpected catalog delta".into(),
8630 ));
8631 };
8632 next_catalog.db_epoch = epoch.0;
8633 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8634 Ok(entry)
8635 }
8636
8637 pub fn drop_role(&self, name: &str) -> Result<()> {
8639 self.drop_role_with_epoch(name).map(|_| ())
8640 }
8641
8642 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
8643 self.drop_role_with_epoch_inner(name, None)
8644 }
8645
8646 pub fn drop_role_with_epoch_controlled<F>(
8647 &self,
8648 name: &str,
8649 mut before_publish: F,
8650 ) -> Result<Epoch>
8651 where
8652 F: FnMut() -> Result<()>,
8653 {
8654 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
8655 }
8656
8657 fn drop_role_with_epoch_inner(
8658 &self,
8659 name: &str,
8660 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8661 ) -> Result<Epoch> {
8662 let command = crate::catalog_cmds::CatalogCommand::DropRole {
8663 name: name.to_string(),
8664 };
8665 self.require(&crate::catalog_cmds::required_permission(&command))?;
8666 let _ddl = self.ddl_lock.lock();
8667 let _security_write = self.security_write()?;
8668 self.require(&crate::catalog_cmds::required_permission(&command))?;
8669 let _commit = self.commit_lock.lock();
8670 let epoch = self.epoch.bump_assigned();
8671 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8672 let mut next_catalog = self.catalog.read().clone();
8673 self.apply_catalog_command_to(&mut next_catalog, command)?;
8674 next_catalog.db_epoch = epoch.0;
8675 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8676 Ok(epoch)
8677 }
8678
8679 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
8681 self.grant_role_with_epoch(username, role_name).map(|_| ())
8682 }
8683
8684 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8685 self.grant_role_with_epoch_inner(username, role_name, None)
8686 }
8687
8688 pub fn grant_role_with_epoch_controlled<F>(
8689 &self,
8690 username: &str,
8691 role_name: &str,
8692 mut before_publish: F,
8693 ) -> Result<Option<Epoch>>
8694 where
8695 F: FnMut() -> Result<()>,
8696 {
8697 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8698 }
8699
8700 fn grant_role_with_epoch_inner(
8701 &self,
8702 username: &str,
8703 role_name: &str,
8704 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8705 ) -> Result<Option<Epoch>> {
8706 let command = crate::catalog_cmds::CatalogCommand::GrantRole {
8707 username: username.to_string(),
8708 role: role_name.to_string(),
8709 };
8710 self.require(&crate::catalog_cmds::required_permission(&command))?;
8711 let _ddl = self.ddl_lock.lock();
8712 let _security_write = self.security_write()?;
8713 self.require(&crate::catalog_cmds::required_permission(&command))?;
8714 let _commit = self.commit_lock.lock();
8715 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8719 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8720 return Ok(None);
8721 }
8722 let epoch = self.epoch.bump_assigned();
8723 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8724 let mut next_catalog = self.catalog.read().clone();
8725 self.apply_catalog_command_to(&mut next_catalog, command)?;
8726 next_catalog.db_epoch = epoch.0;
8727 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8728 Ok(Some(epoch))
8729 }
8730
8731 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
8733 self.revoke_role_with_epoch(username, role_name).map(|_| ())
8734 }
8735
8736 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8737 self.revoke_role_with_epoch_inner(username, role_name, None)
8738 }
8739
8740 pub fn revoke_role_with_epoch_controlled<F>(
8741 &self,
8742 username: &str,
8743 role_name: &str,
8744 mut before_publish: F,
8745 ) -> Result<Option<Epoch>>
8746 where
8747 F: FnMut() -> Result<()>,
8748 {
8749 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8750 }
8751
8752 fn revoke_role_with_epoch_inner(
8753 &self,
8754 username: &str,
8755 role_name: &str,
8756 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8757 ) -> Result<Option<Epoch>> {
8758 let command = crate::catalog_cmds::CatalogCommand::RevokeRole {
8759 username: username.to_string(),
8760 role: role_name.to_string(),
8761 };
8762 self.require(&crate::catalog_cmds::required_permission(&command))?;
8763 let _ddl = self.ddl_lock.lock();
8764 let _security_write = self.security_write()?;
8765 self.require(&crate::catalog_cmds::required_permission(&command))?;
8766 let _commit = self.commit_lock.lock();
8767 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8771 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8772 return Ok(None);
8773 }
8774 let epoch = self.epoch.bump_assigned();
8775 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8776 let mut next_catalog = self.catalog.read().clone();
8777 self.apply_catalog_command_to(&mut next_catalog, command)?;
8778 next_catalog.db_epoch = epoch.0;
8779 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8780 Ok(Some(epoch))
8781 }
8782
8783 pub fn grant_permission(
8785 &self,
8786 role_name: &str,
8787 permission: crate::auth::Permission,
8788 ) -> Result<()> {
8789 self.grant_permission_with_epoch(role_name, permission)
8790 .map(|_| ())
8791 }
8792
8793 pub fn grant_permission_with_epoch(
8794 &self,
8795 role_name: &str,
8796 permission: crate::auth::Permission,
8797 ) -> Result<Option<Epoch>> {
8798 self.grant_permission_with_epoch_inner(role_name, permission, None)
8799 }
8800
8801 pub fn grant_permission_with_epoch_controlled<F>(
8802 &self,
8803 role_name: &str,
8804 permission: crate::auth::Permission,
8805 mut before_publish: F,
8806 ) -> Result<Option<Epoch>>
8807 where
8808 F: FnMut() -> Result<()>,
8809 {
8810 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8811 }
8812
8813 fn grant_permission_with_epoch_inner(
8814 &self,
8815 role_name: &str,
8816 permission: crate::auth::Permission,
8817 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8818 ) -> Result<Option<Epoch>> {
8819 let command = crate::catalog_cmds::CatalogCommand::GrantPermission {
8820 role: role_name.to_string(),
8821 permission,
8822 };
8823 self.require(&crate::catalog_cmds::required_permission(&command))?;
8824 let _ddl = self.ddl_lock.lock();
8825 let _security_write = self.security_write()?;
8826 self.require(&crate::catalog_cmds::required_permission(&command))?;
8827 let _commit = self.commit_lock.lock();
8828 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8832 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8833 return Ok(None);
8834 }
8835 let epoch = self.epoch.bump_assigned();
8836 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8837 let mut next_catalog = self.catalog.read().clone();
8838 self.apply_catalog_command_to(&mut next_catalog, command)?;
8839 next_catalog.db_epoch = epoch.0;
8840 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8841 Ok(Some(epoch))
8842 }
8843
8844 pub fn revoke_permission(
8846 &self,
8847 role_name: &str,
8848 permission: crate::auth::Permission,
8849 ) -> Result<()> {
8850 self.revoke_permission_with_epoch(role_name, permission)
8851 .map(|_| ())
8852 }
8853
8854 pub fn revoke_permission_with_epoch(
8855 &self,
8856 role_name: &str,
8857 permission: crate::auth::Permission,
8858 ) -> Result<Option<Epoch>> {
8859 self.revoke_permission_with_epoch_inner(role_name, permission, None)
8860 }
8861
8862 pub fn revoke_permission_with_epoch_controlled<F>(
8863 &self,
8864 role_name: &str,
8865 permission: crate::auth::Permission,
8866 mut before_publish: F,
8867 ) -> Result<Option<Epoch>>
8868 where
8869 F: FnMut() -> Result<()>,
8870 {
8871 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8872 }
8873
8874 fn revoke_permission_with_epoch_inner(
8875 &self,
8876 role_name: &str,
8877 permission: crate::auth::Permission,
8878 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8879 ) -> Result<Option<Epoch>> {
8880 let command = crate::catalog_cmds::CatalogCommand::RevokePermission {
8881 role: role_name.to_string(),
8882 permission,
8883 };
8884 self.require(&crate::catalog_cmds::required_permission(&command))?;
8885 let _ddl = self.ddl_lock.lock();
8886 let _security_write = self.security_write()?;
8887 self.require(&crate::catalog_cmds::required_permission(&command))?;
8888 let _commit = self.commit_lock.lock();
8889 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8893 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8894 return Ok(None);
8895 }
8896 let epoch = self.epoch.bump_assigned();
8897 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8898 let mut next_catalog = self.catalog.read().clone();
8899 self.apply_catalog_command_to(&mut next_catalog, command)?;
8900 next_catalog.db_epoch = epoch.0;
8901 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8902 Ok(Some(epoch))
8903 }
8904
8905 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
8908 let cat = self.catalog.read();
8909 Self::resolve_principal_from_catalog(&cat, username)
8910 }
8911
8912 pub fn resolve_current_principal(
8915 &self,
8916 principal: &crate::auth::Principal,
8917 ) -> Option<crate::auth::Principal> {
8918 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
8919 }
8920
8921 fn resolve_principal_from_catalog(
8926 cat: &Catalog,
8927 username: &str,
8928 ) -> Option<crate::auth::Principal> {
8929 let user = cat.users.iter().find(|u| u.username == username)?;
8930 Self::resolve_user_principal_from_catalog(cat, user)
8931 }
8932
8933 fn resolve_bound_principal_from_catalog(
8934 cat: &Catalog,
8935 principal: &crate::auth::Principal,
8936 ) -> Option<crate::auth::Principal> {
8937 let user = cat.users.iter().find(|user| {
8938 user.id == principal.user_id
8939 && user.created_epoch == principal.created_epoch
8940 && user.username == principal.username
8941 })?;
8942 Self::resolve_user_principal_from_catalog(cat, user)
8943 }
8944
8945 fn resolve_user_principal_from_catalog(
8946 cat: &Catalog,
8947 user: &crate::auth::UserEntry,
8948 ) -> Option<crate::auth::Principal> {
8949 let mut permissions = Vec::new();
8950 for role_name in &user.roles {
8951 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
8952 permissions.extend(role.permissions.iter().cloned());
8953 }
8954 }
8955 Some(crate::auth::Principal {
8956 user_id: user.id,
8957 created_epoch: user.created_epoch,
8958 username: user.username.clone(),
8959 is_admin: user.is_admin,
8960 roles: user.roles.clone(),
8961 permissions,
8962 })
8963 }
8964
8965 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
8967 match self.resolve_principal(username) {
8968 Some(p) => p.has_permission(permission),
8969 None => false,
8970 }
8971 }
8972
8973 pub fn require_auth_enabled(&self) -> bool {
8977 self.catalog.read().require_auth
8978 }
8979
8980 pub fn principal(&self) -> Option<crate::auth::Principal> {
8984 self.principal.read().clone()
8985 }
8986
8987 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
8993 if self.shared {
8994 return None;
8995 }
8996 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
8997 self.auth_state.clone(),
8998 )))
8999 }
9000
9001 pub fn refresh_principal(&self) -> Result<()> {
9015 let previous = match self.principal.read().clone() {
9016 Some(principal) => principal,
9017 None => return Ok(()),
9018 };
9019 if previous.user_id == 0 {
9023 return Ok(());
9024 }
9025 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
9026 self.refresh_security_catalog_if_stale(observed_version)?;
9027 let cat = self.catalog.read();
9028 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
9029 Some(p) => {
9030 *self.principal.write() = Some(p.clone());
9031 self.auth_state.set_principal(Some(p));
9035 Ok(())
9036 }
9037 None => Err(MongrelError::InvalidCredentials {
9038 username: previous.username,
9039 }),
9040 }
9041 }
9042
9043 pub fn security_catalog_disk_read_count(&self) -> u64 {
9046 self.security_catalog_disk_reads.load(Ordering::Relaxed)
9047 }
9048
9049 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
9061 if self.shared {
9062 return Err(MongrelError::Conflict(
9068 "enable_auth requires an exclusive Database owner; shared cores reject \
9069 auth-mode transitions"
9070 .into(),
9071 ));
9072 }
9073 let password_hash =
9074 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
9075 let scram_sha_256 =
9076 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
9077 let mysql_caching_sha2 =
9078 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
9079 let _ddl = self.ddl_lock.lock();
9080 let _security_write = self.security_write()?;
9081 let _commit = self.commit_lock.lock();
9082 let epoch = self.epoch.bump_assigned();
9083 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9084 let mut next_catalog = self.catalog.read().clone();
9085 if next_catalog.require_auth {
9086 return Err(MongrelError::InvalidArgument(
9087 "database already has require_auth enabled".into(),
9088 ));
9089 }
9090 if next_catalog
9091 .users
9092 .iter()
9093 .any(|u| u.username == admin_username)
9094 {
9095 return Err(MongrelError::InvalidArgument(format!(
9096 "user {admin_username:?} already exists"
9097 )));
9098 }
9099 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
9100 let id = next_catalog.next_user_id;
9101 next_catalog.next_user_id = id
9102 .checked_add(1)
9103 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
9104 next_catalog.users.push(crate::auth::UserEntry {
9105 id,
9106 username: admin_username.to_string(),
9107 password_hash,
9108 scram_sha_256: Some(scram_sha_256),
9109 mysql_caching_sha2: Some(mysql_caching_sha2),
9110 roles: Vec::new(),
9111 is_admin: true,
9112 created_epoch: epoch.0,
9113 });
9114 next_catalog.require_auth = true;
9115 advance_security_version(&mut next_catalog)?;
9116 next_catalog.db_epoch = epoch.0;
9117 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9118 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9122 let principal = crate::auth::Principal {
9123 user_id: id,
9124 created_epoch: epoch.0,
9125 username: admin_username.to_string(),
9126 is_admin: true,
9127 roles: Vec::new(),
9128 permissions: Vec::new(),
9129 };
9130 *self.principal.write() = Some(principal.clone());
9131 self.auth_state.set_principal(Some(principal));
9132 }
9133 publish
9134 }
9135
9136 pub fn disable_auth(&self) -> Result<()> {
9153 let _ddl = self.ddl_lock.lock();
9154 let _security_write = self.security_write()?;
9155 let _commit = self.commit_lock.lock();
9156 let epoch = self.epoch.bump_assigned();
9157 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9158 let mut next_catalog = self.catalog.read().clone();
9159 if !next_catalog.require_auth {
9160 return Err(MongrelError::InvalidArgument(
9161 "database does not have require_auth enabled".into(),
9162 ));
9163 }
9164 next_catalog.require_auth = false;
9165 advance_security_version(&mut next_catalog)?;
9166 next_catalog.db_epoch = epoch.0;
9167 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9168 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9170 *self.principal.write() = None;
9171 }
9172 publish
9173 }
9174
9175 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
9182 self.ensure_owner_process()?;
9183 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
9184 return Err(MongrelError::ReadOnlyReplica);
9185 }
9186 if self.principal.read().is_some() {
9187 self.refresh_principal().map_err(|error| match error {
9188 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
9189 error => error,
9190 })?;
9191 }
9192 if !self.catalog.read().require_auth {
9193 return Ok(());
9194 }
9195 let guard = self.principal.read();
9196 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
9197 if p.has_permission(perm) {
9198 Ok(())
9199 } else {
9200 Err(MongrelError::PermissionDenied {
9201 required: perm.clone(),
9202 principal: p.username.clone(),
9203 })
9204 }
9205 }
9206
9207 pub fn require_table(
9212 &self,
9213 table: &str,
9214 perm: crate::auth_state::RequiredPermission,
9215 ) -> Result<()> {
9216 self.require(&perm.into_permission(table))
9217 }
9218
9219 pub fn triggers(&self) -> Vec<StoredTrigger> {
9220 self.catalog
9221 .read()
9222 .triggers
9223 .iter()
9224 .map(|t| t.trigger.clone())
9225 .collect()
9226 }
9227
9228 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
9229 self.catalog
9230 .read()
9231 .triggers
9232 .iter()
9233 .find(|t| t.trigger.name == name)
9234 .map(|t| t.trigger.clone())
9235 }
9236
9237 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9238 self.create_trigger_inner(trigger, None, None)
9239 }
9240
9241 pub fn create_trigger_controlled<F>(
9242 &self,
9243 trigger: StoredTrigger,
9244 mut before_publish: F,
9245 ) -> Result<StoredTrigger>
9246 where
9247 F: FnMut() -> Result<()>,
9248 {
9249 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
9250 }
9251
9252 pub fn create_trigger_as_controlled<F>(
9253 &self,
9254 trigger: StoredTrigger,
9255 principal: Option<&crate::auth::Principal>,
9256 mut before_publish: F,
9257 ) -> Result<StoredTrigger>
9258 where
9259 F: FnMut() -> Result<()>,
9260 {
9261 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
9262 }
9263
9264 fn create_trigger_inner(
9265 &self,
9266 mut trigger: StoredTrigger,
9267 principal: Option<&crate::auth::Principal>,
9268 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9269 ) -> Result<StoredTrigger> {
9270 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9271 trigger: trigger.clone(),
9272 };
9273 self.require_for(
9274 principal,
9275 &crate::catalog_cmds::required_permission(&command),
9276 )?;
9277 let _g = self.ddl_lock.lock();
9278 let _security_write = self.security_write()?;
9279 self.require_for(
9280 principal,
9281 &crate::catalog_cmds::required_permission(&command),
9282 )?;
9283 trigger.validate()?;
9284 self.validate_trigger_references(&trigger)
9285 .map_err(trigger_validation_error)?;
9286 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9290 let commit_lock = Arc::clone(&self.commit_lock);
9291 let _c = commit_lock.lock();
9292 let epoch = self.epoch.bump_assigned();
9293 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9294 trigger.created_epoch = epoch.0;
9295 trigger.updated_epoch = epoch.0;
9296 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9297 trigger: trigger.clone(),
9298 };
9299 let mut next_catalog = self.catalog.read().clone();
9300 self.apply_catalog_command_to(&mut next_catalog, command)?;
9301 next_catalog.db_epoch = epoch.0;
9302 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9303 Ok(trigger)
9304 }
9305
9306 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9307 self.create_or_replace_trigger_inner(trigger, None, None)
9308 }
9309
9310 pub fn create_or_replace_trigger_controlled<F>(
9311 &self,
9312 trigger: StoredTrigger,
9313 mut before_publish: F,
9314 ) -> Result<StoredTrigger>
9315 where
9316 F: FnMut() -> Result<()>,
9317 {
9318 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
9319 }
9320
9321 pub fn create_or_replace_trigger_as_controlled<F>(
9322 &self,
9323 trigger: StoredTrigger,
9324 principal: Option<&crate::auth::Principal>,
9325 mut before_publish: F,
9326 ) -> Result<StoredTrigger>
9327 where
9328 F: FnMut() -> Result<()>,
9329 {
9330 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
9331 }
9332
9333 fn create_or_replace_trigger_inner(
9334 &self,
9335 trigger: StoredTrigger,
9336 principal: Option<&crate::auth::Principal>,
9337 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9338 ) -> Result<StoredTrigger> {
9339 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9340 trigger: trigger.clone(),
9341 };
9342 self.require_for(
9343 principal,
9344 &crate::catalog_cmds::required_permission(&command),
9345 )?;
9346 let _g = self.ddl_lock.lock();
9347 let _security_write = self.security_write()?;
9348 self.require_for(
9349 principal,
9350 &crate::catalog_cmds::required_permission(&command),
9351 )?;
9352 trigger.validate()?;
9353 self.validate_trigger_references(&trigger)
9354 .map_err(trigger_validation_error)?;
9355 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9359 let commit_lock = Arc::clone(&self.commit_lock);
9360 let _c = commit_lock.lock();
9361 let epoch = self.epoch.bump_assigned();
9362 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9363 let mut next_catalog = self.catalog.read().clone();
9364 let replaced = match next_catalog
9369 .triggers
9370 .iter()
9371 .position(|t| t.trigger.name == trigger.name)
9372 {
9373 Some(idx) => next_catalog.triggers[idx]
9374 .trigger
9375 .replaced(trigger.clone(), epoch.0)?,
9376 None => {
9377 let mut next = trigger;
9378 next.created_epoch = epoch.0;
9379 next.updated_epoch = epoch.0;
9380 next
9381 }
9382 };
9383 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9384 trigger: replaced.clone(),
9385 };
9386 self.apply_catalog_command_to(&mut next_catalog, command)?;
9387 next_catalog.db_epoch = epoch.0;
9388 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9389 Ok(replaced)
9390 }
9391
9392 pub fn drop_trigger(&self, name: &str) -> Result<()> {
9393 self.drop_trigger_with_epoch(name).map(|_| ())
9394 }
9395
9396 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
9398 self.drop_triggers_with_epoch(&[name.to_string()])
9399 }
9400
9401 pub fn drop_trigger_with_epoch_controlled<F>(
9402 &self,
9403 name: &str,
9404 before_publish: F,
9405 ) -> Result<Epoch>
9406 where
9407 F: FnMut() -> Result<()>,
9408 {
9409 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
9410 }
9411
9412 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
9414 self.drop_triggers_with_epoch_inner(names, None, None)
9415 }
9416
9417 pub fn drop_triggers_with_epoch_controlled<F>(
9418 &self,
9419 names: &[String],
9420 mut before_publish: F,
9421 ) -> Result<Epoch>
9422 where
9423 F: FnMut() -> Result<()>,
9424 {
9425 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
9426 }
9427
9428 pub fn drop_triggers_with_epoch_as_controlled<F>(
9429 &self,
9430 names: &[String],
9431 principal: Option<&crate::auth::Principal>,
9432 mut before_publish: F,
9433 ) -> Result<Epoch>
9434 where
9435 F: FnMut() -> Result<()>,
9436 {
9437 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
9438 }
9439
9440 fn drop_triggers_with_epoch_inner(
9441 &self,
9442 names: &[String],
9443 principal: Option<&crate::auth::Principal>,
9444 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9445 ) -> Result<Epoch> {
9446 if names.is_empty() {
9447 return Err(MongrelError::InvalidArgument(
9448 "at least one trigger name is required".into(),
9449 ));
9450 }
9451 let commands: Vec<crate::catalog_cmds::CatalogCommand> = names
9452 .iter()
9453 .map(|name| crate::catalog_cmds::CatalogCommand::DropTrigger { name: name.clone() })
9454 .collect();
9455 for command in &commands {
9456 self.require_for(
9457 principal,
9458 &crate::catalog_cmds::required_permission(command),
9459 )?;
9460 }
9461 let _g = self.ddl_lock.lock();
9462 let _security_write = self.security_write()?;
9463 for command in &commands {
9464 self.require_for(
9465 principal,
9466 &crate::catalog_cmds::required_permission(command),
9467 )?;
9468 }
9469 {
9474 let cat = self.catalog.read();
9475 for command in &commands {
9476 crate::catalog_cmds::apply(&cat, command)?;
9477 }
9478 }
9479 let commit_lock = Arc::clone(&self.commit_lock);
9480 let _c = commit_lock.lock();
9481 let epoch = self.epoch.bump_assigned();
9482 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9483 let mut next_catalog = self.catalog.read().clone();
9484 for command in commands {
9485 self.apply_catalog_command_to(&mut next_catalog, command)?;
9486 }
9487 next_catalog.db_epoch = epoch.0;
9488 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9489 Ok(epoch)
9490 }
9491
9492 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
9493 self.catalog.read().external_tables.clone()
9494 }
9495
9496 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
9497 self.catalog
9498 .read()
9499 .external_tables
9500 .iter()
9501 .find(|t| t.name == name)
9502 .cloned()
9503 }
9504
9505 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
9506 self.create_external_table_inner(entry, None)
9507 }
9508
9509 pub fn create_external_table_controlled<F>(
9510 &self,
9511 entry: ExternalTableEntry,
9512 mut before_publish: F,
9513 ) -> Result<ExternalTableEntry>
9514 where
9515 F: FnMut() -> Result<()>,
9516 {
9517 self.create_external_table_inner(entry, Some(&mut before_publish))
9518 }
9519
9520 fn create_external_table_inner(
9521 &self,
9522 mut entry: ExternalTableEntry,
9523 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9524 ) -> Result<ExternalTableEntry> {
9525 self.require(&crate::auth::Permission::Ddl)?;
9526 let _g = self.ddl_lock.lock();
9527 let _security_write = self.security_write()?;
9528 self.require(&crate::auth::Permission::Ddl)?;
9529 entry.validate()?;
9530 {
9531 let cat = self.catalog.read();
9532 if cat.live(&entry.name).is_some()
9533 || cat.external_tables.iter().any(|t| t.name == entry.name)
9534 {
9535 return Err(MongrelError::InvalidArgument(format!(
9536 "table {:?} already exists",
9537 entry.name
9538 )));
9539 }
9540 }
9541 let commit_lock = Arc::clone(&self.commit_lock);
9542 let _c = commit_lock.lock();
9543 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
9547 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
9548 let epoch = self.epoch.bump_assigned();
9549 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9550 entry.created_epoch = epoch.0;
9551 let mut next_catalog = self.catalog.read().clone();
9552 next_catalog.external_tables.push(entry.clone());
9553 next_catalog.db_epoch = epoch.0;
9554 self.publish_catalog_candidate_with_prelude(
9555 next_catalog,
9556 epoch,
9557 &mut _epoch_guard,
9558 before_publish,
9559 vec![(
9560 EXTERNAL_TABLE_ID,
9561 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9562 name: entry.name.clone(),
9563 generation_epoch: epoch.0,
9564 }),
9565 )],
9566 )?;
9567 Ok(entry)
9568 }
9569
9570 pub fn drop_external_table(&self, name: &str) -> Result<()> {
9571 self.drop_external_table_with_epoch(name).map(|_| ())
9572 }
9573
9574 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
9576 self.drop_external_table_with_epoch_inner(name, None)
9577 }
9578
9579 pub fn drop_external_table_with_epoch_controlled<F>(
9580 &self,
9581 name: &str,
9582 mut before_publish: F,
9583 ) -> Result<Epoch>
9584 where
9585 F: FnMut() -> Result<()>,
9586 {
9587 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
9588 }
9589
9590 fn drop_external_table_with_epoch_inner(
9591 &self,
9592 name: &str,
9593 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9594 ) -> Result<Epoch> {
9595 self.require(&crate::auth::Permission::Ddl)?;
9596 let _g = self.ddl_lock.lock();
9597 let _security_write = self.security_write()?;
9598 self.require(&crate::auth::Permission::Ddl)?;
9599 let commit_lock = Arc::clone(&self.commit_lock);
9600 let _c = commit_lock.lock();
9601 let epoch = self.epoch.bump_assigned();
9602 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9603 let mut next_catalog = self.catalog.read().clone();
9604 let before = next_catalog.external_tables.len();
9605 next_catalog.external_tables.retain(|t| t.name != name);
9606 if next_catalog.external_tables.len() == before {
9607 return Err(MongrelError::NotFound(format!(
9608 "external table {name:?} not found"
9609 )));
9610 }
9611 next_catalog.db_epoch = epoch.0;
9612 self.publish_catalog_candidate_with_prelude(
9613 next_catalog,
9614 epoch,
9615 &mut _epoch_guard,
9616 before_publish,
9617 vec![(
9618 EXTERNAL_TABLE_ID,
9619 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9620 name: name.to_string(),
9621 generation_epoch: epoch.0,
9622 }),
9623 )],
9624 )?;
9625 let state_dir = self.root.join(VTAB_DIR).join(name);
9626 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
9627 return Err(MongrelError::DurableCommit {
9628 epoch: epoch.0,
9629 message: format!(
9630 "external table was dropped but connector-state cleanup failed: {error}"
9631 ),
9632 });
9633 }
9634 Ok(epoch)
9635 }
9636
9637 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
9638 let txn_id = self.alloc_txn_id()?;
9639 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9640 self.commit_transaction_with_external_states(
9641 txn_id,
9642 self.epoch.visible(),
9643 Vec::new(),
9644 vec![(name.to_string(), state.to_vec())],
9645 Vec::new(),
9646 principal,
9647 catalog_bound,
9648 None,
9649 crate::txn::TxnCommitContext::internal(),
9650 )
9651 .map(|(epoch, _)| epoch)
9652 }
9653
9654 pub fn trigger_config(&self) -> TriggerConfig {
9655 use std::sync::atomic::Ordering;
9656 TriggerConfig {
9657 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
9658 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
9659 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
9660 }
9661 }
9662
9663 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
9664 use std::sync::atomic::Ordering;
9665 if config.max_depth == 0 {
9666 return Err(MongrelError::InvalidArgument(
9667 "trigger max_depth must be greater than 0".into(),
9668 ));
9669 }
9670 self.trigger_recursive
9671 .store(config.recursive_triggers, Ordering::Relaxed);
9672 self.trigger_max_depth
9673 .store(config.max_depth, Ordering::Relaxed);
9674 self.trigger_max_loop_iterations
9675 .store(config.max_loop_iterations, Ordering::Relaxed);
9676 Ok(())
9677 }
9678
9679 pub fn set_recursive_triggers(&self, recursive: bool) {
9680 use std::sync::atomic::Ordering;
9681 self.trigger_recursive.store(recursive, Ordering::Relaxed);
9682 }
9683
9684 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
9688 self.notify.subscribe()
9689 }
9690
9691 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
9692 self.change_wake.subscribe()
9693 }
9694
9695 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
9700 let control = crate::ExecutionControl::new(None);
9701 self.change_events_since_controlled(last_event_id, &control)
9702 }
9703
9704 pub fn change_events_since_controlled(
9706 &self,
9707 last_event_id: Option<&str>,
9708 control: &crate::ExecutionControl,
9709 ) -> Result<CdcBatch> {
9710 use crate::wal::Op;
9711
9712 control.checkpoint()?;
9713 let resume = match last_event_id {
9714 Some(id) => {
9715 let (epoch, index) = id.split_once(':').ok_or_else(|| {
9716 MongrelError::InvalidArgument(format!(
9717 "invalid CDC event id {id:?}; expected <epoch>:<index>"
9718 ))
9719 })?;
9720 Some((
9721 epoch.parse::<u64>().map_err(|error| {
9722 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
9723 })?,
9724 index.parse::<u32>().map_err(|error| {
9725 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
9726 })?,
9727 ))
9728 }
9729 None => None,
9730 };
9731
9732 let mut wal = self.shared_wal.lock();
9733 wal.group_sync()?;
9734 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
9735 let records = crate::wal::SharedWal::replay_with_dek_controlled(
9736 &self.root,
9737 wal_dek.as_ref(),
9738 control,
9739 CDC_MAX_WAL_RECORDS,
9740 CDC_MAX_WAL_REPLAY_BYTES,
9741 )?;
9742 drop(wal);
9743 control.checkpoint()?;
9744
9745 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
9746 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
9747 for (index, record) in records.iter().enumerate() {
9748 if index % 256 == 0 {
9749 control.checkpoint()?;
9750 }
9751 if let Op::TxnCommit { epoch, added_runs } = &record.op {
9752 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
9753 }
9754 if let Op::SpilledRows { table_id, rows } = &record.op {
9755 spilled_payloads
9756 .entry((record.txn_id, *table_id))
9757 .or_default()
9758 .push(rows);
9759 }
9760 }
9761 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
9762 let current_epoch = self.epoch.committed().0;
9763 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
9764 let gap = resume.is_some_and(|(epoch, _)| {
9765 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
9766 });
9767 if gap {
9768 return Ok(CdcBatch {
9769 events: Vec::new(),
9770 current_epoch,
9771 earliest_epoch,
9772 gap: true,
9773 });
9774 }
9775
9776 let table_names: HashMap<u64, String> = self
9777 .catalog
9778 .read()
9779 .tables
9780 .iter()
9781 .map(|entry| (entry.table_id, entry.name.clone()))
9782 .collect();
9783 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
9784 let mut retained_bytes = 0_usize;
9785 for (index, record) in records.iter().enumerate() {
9786 if index % 256 == 0 {
9787 control.checkpoint()?;
9788 }
9789 if !commits.contains_key(&record.txn_id) {
9790 continue;
9791 }
9792 let Op::BeforeImage {
9793 table_id,
9794 row_id,
9795 row,
9796 } = &record.op
9797 else {
9798 continue;
9799 };
9800 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9801 return Err(MongrelError::ResourceLimitExceeded {
9802 resource: "CDC before-image bytes",
9803 requested: row.len(),
9804 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9805 });
9806 }
9807 let before: crate::memtable::Row = bincode::deserialize(row)?;
9808 if before_images.len() >= CDC_MAX_ROWS {
9809 return Err(MongrelError::ResourceLimitExceeded {
9810 resource: "CDC before-image rows",
9811 requested: before_images.len().saturating_add(1),
9812 limit: CDC_MAX_ROWS,
9813 });
9814 }
9815 charge_cdc_bytes(
9816 &mut retained_bytes,
9817 cdc_row_storage_bytes(&before),
9818 "CDC retained bytes",
9819 )?;
9820 before_images.insert((record.txn_id, *table_id, row_id.0), before);
9821 }
9822 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
9823 let mut events = Vec::new();
9824 let mut decoded_rows = before_images.len();
9825 for (record_index, record) in records.iter().enumerate() {
9826 if record_index % 256 == 0 {
9827 control.checkpoint()?;
9828 }
9829 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
9830 continue;
9831 };
9832 let event = match &record.op {
9833 Op::Put { table_id, rows } => {
9834 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9835 return Err(MongrelError::ResourceLimitExceeded {
9836 resource: "CDC inline row bytes",
9837 requested: rows.len(),
9838 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9839 });
9840 }
9841 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
9842 decoded_rows = decoded_rows.saturating_add(rows.len());
9843 if decoded_rows > CDC_MAX_ROWS {
9844 return Err(MongrelError::ResourceLimitExceeded {
9845 resource: "CDC decoded rows",
9846 requested: decoded_rows,
9847 limit: CDC_MAX_ROWS,
9848 });
9849 }
9850 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
9851 let mut peak_bytes = retained_bytes;
9852 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9853 let data = serde_json::to_value(rows)
9854 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
9855 Some((*table_id, "put", data, event_bytes))
9856 }
9857 Op::Delete { table_id, row_ids } => {
9858 let before = row_ids
9859 .iter()
9860 .filter_map(|row_id| {
9861 before_images
9862 .get(&(record.txn_id, *table_id, row_id.0))
9863 .cloned()
9864 })
9865 .collect::<Vec<_>>();
9866 let event_bytes = cdc_rows_json_bytes(&before)
9867 .saturating_add(
9868 row_ids
9869 .len()
9870 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
9871 )
9872 .saturating_add(512);
9873 let mut peak_bytes = retained_bytes;
9874 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9875 Some((
9876 *table_id,
9877 "delete",
9878 serde_json::json!({
9879 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
9880 "before": before,
9881 }),
9882 event_bytes,
9883 ))
9884 }
9885 Op::TruncateTable { table_id } => {
9886 Some((*table_id, "truncate", serde_json::Value::Null, 512))
9887 }
9888 _ => None,
9889 };
9890 if let Some((table_id, op, data, event_bytes)) = event {
9891 let index = operation_indices.entry(record.txn_id).or_insert(0);
9892 let event_position = (*commit_epoch, *index);
9893 *index = index.saturating_add(1);
9894 if resume.is_some_and(|position| event_position <= position) {
9895 continue;
9896 }
9897 if events.len() >= CDC_MAX_EVENTS {
9898 return Err(MongrelError::ResourceLimitExceeded {
9899 resource: "CDC events",
9900 requested: events.len().saturating_add(1),
9901 limit: CDC_MAX_EVENTS,
9902 });
9903 }
9904 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
9905 events.push(ChangeEvent {
9906 id: Some(format!("{}:{}", event_position.0, event_position.1)),
9907 channel: "changes".into(),
9908 table_id: Some(table_id),
9909 table: table_names.get(&table_id).cloned().unwrap_or_default(),
9910 op: op.into(),
9911 epoch: *commit_epoch,
9912 txn_id: Some(record.txn_id),
9913 message: None,
9914 data: Some(data),
9915 });
9916 }
9917 if let Op::TxnCommit { added_runs, .. } = &record.op {
9918 for run in added_runs {
9919 control.checkpoint()?;
9920 let index = operation_indices.entry(record.txn_id).or_insert(0);
9921 let event_position = (*commit_epoch, *index);
9922 *index = index.saturating_add(1);
9923 if resume.is_some_and(|position| event_position <= position) {
9924 continue;
9925 }
9926 let mut rows = if let Some(payloads) =
9927 spilled_payloads.get(&(record.txn_id, run.table_id))
9928 {
9929 let mut rows = Vec::new();
9930 for payload in payloads {
9931 control.checkpoint()?;
9932 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9933 return Err(MongrelError::ResourceLimitExceeded {
9934 resource: "CDC spilled row bytes",
9935 requested: payload.len(),
9936 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9937 });
9938 }
9939 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
9940 if decoded_rows
9941 .saturating_add(rows.len())
9942 .saturating_add(chunk.len())
9943 > CDC_MAX_ROWS
9944 {
9945 return Err(MongrelError::ResourceLimitExceeded {
9946 resource: "CDC decoded rows",
9947 requested: decoded_rows
9948 .saturating_add(rows.len())
9949 .saturating_add(chunk.len()),
9950 limit: CDC_MAX_ROWS,
9951 });
9952 }
9953 rows.extend(chunk);
9954 }
9955 rows
9956 } else {
9957 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
9958 return Ok(CdcBatch {
9959 events: Vec::new(),
9960 current_epoch,
9961 earliest_epoch,
9962 gap: true,
9963 });
9964 };
9965 let table = handle.lock();
9966 let mut reader = match table.open_reader(run.run_id) {
9967 Ok(reader) => reader,
9968 Err(_) => {
9969 return Ok(CdcBatch {
9970 events: Vec::new(),
9971 current_epoch,
9972 earliest_epoch,
9973 gap: true,
9974 })
9975 }
9976 };
9977 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
9978 let rows = reader.all_rows_controlled(control, remaining)?;
9979 drop(reader);
9980 drop(table);
9981 rows
9982 };
9983 for row in &mut rows {
9984 row.committed_epoch = Epoch(*commit_epoch);
9985 }
9986 decoded_rows = decoded_rows.saturating_add(rows.len());
9987 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
9988 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
9989 if events.len() >= CDC_MAX_EVENTS {
9990 return Err(MongrelError::ResourceLimitExceeded {
9991 resource: "CDC events",
9992 requested: events.len().saturating_add(1),
9993 limit: CDC_MAX_EVENTS,
9994 });
9995 }
9996 events.push(ChangeEvent {
9997 id: Some(format!("{}:{}", event_position.0, event_position.1)),
9998 channel: "changes".into(),
9999 table_id: Some(run.table_id),
10000 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
10001 op: "put_run".into(),
10002 epoch: *commit_epoch,
10003 txn_id: Some(record.txn_id),
10004 message: None,
10005 data: Some(serde_json::json!({
10006 "run_id": run.run_id.to_string(),
10007 "row_count": run.row_count,
10008 "min_row_id": run.min_row_id,
10009 "max_row_id": run.max_row_id,
10010 "rows": rows,
10011 })),
10012 });
10013 }
10014 }
10015 }
10016 control.checkpoint()?;
10017 Ok(CdcBatch {
10018 events,
10019 current_epoch,
10020 earliest_epoch,
10021 gap: false,
10022 })
10023 }
10024
10025 pub fn notify(&self, channel: &str, message: Option<String>) {
10028 let _ = self.notify.send(ChangeEvent {
10029 id: None,
10030 channel: channel.to_string(),
10031 table_id: None,
10032 table: String::new(),
10033 op: "notify".into(),
10034 epoch: self.epoch.visible().0,
10035 txn_id: None,
10036 message,
10037 data: None,
10038 });
10039 }
10040
10041 pub fn call_procedure(
10042 &self,
10043 name: &str,
10044 args: HashMap<String, crate::Value>,
10045 ) -> Result<ProcedureCallResult> {
10046 self.call_procedure_as(name, args, None)
10047 }
10048
10049 pub fn call_procedure_as(
10050 &self,
10051 name: &str,
10052 args: HashMap<String, crate::Value>,
10053 principal: Option<&crate::auth::Principal>,
10054 ) -> Result<ProcedureCallResult> {
10055 let control = crate::ExecutionControl::new(None);
10056 self.call_procedure_as_controlled(name, args, principal, &control, || true)
10057 }
10058
10059 #[doc(hidden)]
10062 pub fn call_procedure_as_bound(
10063 &self,
10064 expected: &StoredProcedure,
10065 args: HashMap<String, crate::Value>,
10066 principal: Option<&crate::auth::Principal>,
10067 ) -> Result<ProcedureCallResult> {
10068 self.require_for(principal, &crate::auth::Permission::All)?;
10069 let procedure = self.procedure(&expected.name).ok_or_else(|| {
10070 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
10071 })?;
10072 if &procedure != expected {
10073 return Err(MongrelError::Conflict(format!(
10074 "procedure {:?} changed after request authorization",
10075 expected.name
10076 )));
10077 }
10078 let control = crate::ExecutionControl::new(None);
10079 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
10080 }
10081
10082 #[doc(hidden)]
10087 pub fn call_procedure_as_controlled<F>(
10088 &self,
10089 name: &str,
10090 args: HashMap<String, crate::Value>,
10091 principal: Option<&crate::auth::Principal>,
10092 control: &crate::ExecutionControl,
10093 before_commit: F,
10094 ) -> Result<ProcedureCallResult>
10095 where
10096 F: FnOnce() -> bool,
10097 {
10098 self.require_for(principal, &crate::auth::Permission::All)?;
10102 let procedure = self
10103 .procedure(name)
10104 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
10105 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
10106 }
10107
10108 fn execute_procedure_as_controlled<F>(
10109 &self,
10110 procedure: StoredProcedure,
10111 args: HashMap<String, crate::Value>,
10112 principal: Option<&crate::auth::Principal>,
10113 control: &crate::ExecutionControl,
10114 before_commit: F,
10115 ) -> Result<ProcedureCallResult>
10116 where
10117 F: FnOnce() -> bool,
10118 {
10119 let args = bind_procedure_args(&procedure, args)?;
10120 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
10121 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
10122 if has_writes {
10123 let mut tx = self.begin_as(principal.cloned());
10124 let run = (|| {
10125 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10126 if step_index % 256 == 0 {
10127 control.checkpoint()?;
10128 }
10129 let output = self.execute_procedure_step(
10130 step,
10131 &args,
10132 &outputs,
10133 Some(&mut tx),
10134 principal,
10135 Some(control),
10136 )?;
10137 outputs.insert(step.id().to_string(), output);
10138 }
10139 control.checkpoint()?;
10140 eval_return_output(&procedure.body.return_value, &args, &outputs)
10141 })();
10142 match run {
10143 Ok(output) => {
10144 control.checkpoint()?;
10145 if !before_commit() {
10146 tx.rollback();
10147 return Err(MongrelError::Cancelled);
10148 }
10149 let epoch = tx.commit()?.0;
10150 Ok(ProcedureCallResult {
10151 epoch: Some(epoch),
10152 output,
10153 })
10154 }
10155 Err(e) => {
10156 tx.rollback();
10157 Err(e)
10158 }
10159 }
10160 } else {
10161 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10162 if step_index % 256 == 0 {
10163 control.checkpoint()?;
10164 }
10165 let output = self.execute_procedure_step(
10166 step,
10167 &args,
10168 &outputs,
10169 None,
10170 principal,
10171 Some(control),
10172 )?;
10173 outputs.insert(step.id().to_string(), output);
10174 }
10175 control.checkpoint()?;
10176 Ok(ProcedureCallResult {
10177 epoch: None,
10178 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
10179 })
10180 }
10181 }
10182
10183 fn execute_procedure_step(
10184 &self,
10185 step: &ProcedureStep,
10186 args: &HashMap<String, crate::Value>,
10187 outputs: &HashMap<String, ProcedureCallOutput>,
10188 tx: Option<&mut crate::txn::Transaction<'_>>,
10189 principal: Option<&crate::auth::Principal>,
10190 control: Option<&crate::ExecutionControl>,
10191 ) -> Result<ProcedureCallOutput> {
10192 if let Some(control) = control {
10193 control.checkpoint()?;
10194 }
10195 match step {
10196 ProcedureStep::NativeQuery {
10197 table,
10198 conditions,
10199 projection,
10200 limit,
10201 ..
10202 } => {
10203 let mut q = crate::Query::new();
10204 for condition in conditions {
10205 q = q.and(eval_condition(condition, args, outputs)?);
10206 }
10207 let fallback_control = crate::ExecutionControl::new(None);
10208 let query_control = control.unwrap_or(&fallback_control);
10209 let mut rows = self.query_for_principal_controlled(
10210 table,
10211 &q,
10212 projection.as_deref(),
10213 principal,
10214 false,
10215 query_control,
10216 )?;
10217 if let Some(limit) = limit {
10218 rows.truncate(*limit);
10219 }
10220 let mut output = Vec::with_capacity(rows.len());
10221 for (row_index, row) in rows.into_iter().enumerate() {
10222 if row_index % 256 == 0 {
10223 if let Some(control) = control {
10224 control.checkpoint()?;
10225 }
10226 }
10227 output.push(ProcedureCallRow {
10228 row_id: Some(row.row_id),
10229 columns: row.columns,
10230 });
10231 }
10232 Ok(ProcedureCallOutput::Rows(output))
10233 }
10234 ProcedureStep::Put {
10235 table,
10236 cells,
10237 returning,
10238 ..
10239 } => {
10240 let tx = tx.ok_or_else(|| {
10241 MongrelError::InvalidArgument(
10242 "write procedure step requires a transaction".into(),
10243 )
10244 })?;
10245 let cells = eval_cells(cells, args, outputs)?;
10246 if *returning {
10247 let out = tx.put_returning(table, cells)?;
10248 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10249 row_id: None,
10250 columns: out.row.columns.into_iter().collect(),
10251 }))
10252 } else {
10253 tx.put(table, cells)?;
10254 Ok(ProcedureCallOutput::Null)
10255 }
10256 }
10257 ProcedureStep::Upsert {
10258 table,
10259 cells,
10260 update_cells,
10261 returning,
10262 ..
10263 } => {
10264 let tx = tx.ok_or_else(|| {
10265 MongrelError::InvalidArgument(
10266 "write procedure step requires a transaction".into(),
10267 )
10268 })?;
10269 let cells = eval_cells(cells, args, outputs)?;
10270 let action = match update_cells {
10271 Some(update_cells) => {
10272 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
10273 }
10274 None => crate::UpsertAction::DoNothing,
10275 };
10276 let out = tx.upsert(table, cells, action)?;
10277 if *returning {
10278 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10279 row_id: None,
10280 columns: out.row.columns.into_iter().collect(),
10281 }))
10282 } else {
10283 Ok(ProcedureCallOutput::Null)
10284 }
10285 }
10286 ProcedureStep::DeleteByPk { table, pk, .. } => {
10287 let tx = tx.ok_or_else(|| {
10288 MongrelError::InvalidArgument(
10289 "write procedure step requires a transaction".into(),
10290 )
10291 })?;
10292 let pk = eval_value(pk, args, outputs)?;
10293 let handle = self.table(table)?;
10294 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
10295 MongrelError::NotFound("procedure delete_by_pk target not found".into())
10296 })?;
10297 tx.delete(table, row_id)?;
10298 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
10299 }
10300 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
10301 "DeleteRows procedure step is not supported by the core executor yet".into(),
10302 )),
10303 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
10304 "SqlQuery procedure step must be executed by mongreldb-query".into(),
10305 )),
10306 }
10307 }
10308
10309 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
10310 let cat = self.catalog.read();
10311 for step in &procedure.body.steps {
10312 let Some(table_name) = step.table() else {
10313 continue;
10314 };
10315 let schema = &cat
10316 .live(table_name)
10317 .ok_or_else(|| {
10318 MongrelError::InvalidArgument(format!(
10319 "procedure {:?} references unknown table {table_name:?}",
10320 procedure.name
10321 ))
10322 })?
10323 .schema;
10324 match step {
10325 ProcedureStep::NativeQuery {
10326 conditions,
10327 projection,
10328 ..
10329 } => {
10330 for condition in conditions {
10331 validate_condition_columns(condition, schema)?;
10332 }
10333 if let Some(projection) = projection {
10334 for id in projection {
10335 validate_column_id(*id, schema)?;
10336 }
10337 }
10338 }
10339 ProcedureStep::Put { cells, .. } => {
10340 for cell in cells {
10341 validate_column_id(cell.column_id, schema)?;
10342 }
10343 }
10344 ProcedureStep::Upsert {
10345 cells,
10346 update_cells,
10347 ..
10348 } => {
10349 for cell in cells {
10350 validate_column_id(cell.column_id, schema)?;
10351 }
10352 if let Some(update_cells) = update_cells {
10353 for cell in update_cells {
10354 validate_column_id(cell.column_id, schema)?;
10355 }
10356 }
10357 }
10358 ProcedureStep::DeleteByPk { .. } => {
10359 if schema.primary_key().is_none() {
10360 return Err(MongrelError::InvalidArgument(format!(
10361 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
10362 procedure.name
10363 )));
10364 }
10365 }
10366 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
10367 }
10368 }
10369 Ok(())
10370 }
10371
10372 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
10373 let cat = self.catalog.read();
10374 let target_schema = match &trigger.target {
10375 TriggerTarget::Table(target_name) => cat
10376 .live(target_name)
10377 .ok_or_else(|| {
10378 MongrelError::InvalidArgument(format!(
10379 "trigger {:?} references unknown target table {target_name:?}",
10380 trigger.name
10381 ))
10382 })?
10383 .schema
10384 .clone(),
10385 TriggerTarget::View(_) => Schema {
10386 columns: trigger.target_columns.clone(),
10387 ..Schema::default()
10388 },
10389 };
10390 for col in &trigger.update_of {
10391 if target_schema.column(col).is_none() {
10392 return Err(MongrelError::InvalidArgument(format!(
10393 "trigger {:?} UPDATE OF references unknown column {col:?}",
10394 trigger.name
10395 )));
10396 }
10397 }
10398 if let Some(expr) = &trigger.when {
10399 validate_trigger_expr(expr, &target_schema, trigger.event)?;
10400 }
10401 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
10402 for step in &trigger.program.steps {
10403 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
10404 {
10405 return Err(MongrelError::InvalidArgument(
10406 "SetNew trigger steps are only valid in BEFORE triggers".into(),
10407 ));
10408 }
10409 validate_trigger_step(
10410 step,
10411 &cat,
10412 &target_schema,
10413 trigger.event,
10414 &mut select_schemas,
10415 )?;
10416 }
10417 Ok(())
10418 }
10419
10420 pub fn begin(&self) -> crate::txn::Transaction<'_> {
10422 self.begin_with_isolation(crate::txn::IsolationLevel::default())
10423 }
10424
10425 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
10426 let principal = self.principal.read().clone();
10427 let catalog_bound = principal
10428 .as_ref()
10429 .is_some_and(|principal| principal.user_id != 0);
10430 (principal, catalog_bound)
10431 }
10432
10433 pub fn begin_as(
10434 &self,
10435 principal: Option<crate::auth::Principal>,
10436 ) -> crate::txn::Transaction<'_> {
10437 let catalog_bound = principal
10438 .as_ref()
10439 .is_some_and(|principal| principal.user_id != 0);
10440 let txn_id = self.alloc_txn_id();
10441 let read = self.visible_snapshot();
10442 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10443 .with_principal(principal, catalog_bound)
10444 }
10445
10446 pub fn begin_with_isolation(
10448 &self,
10449 level: crate::txn::IsolationLevel,
10450 ) -> crate::txn::Transaction<'_> {
10451 let txn_id = self.alloc_txn_id();
10452 let read = self.visible_snapshot();
10455 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10456 crate::txn::Transaction::new(self, txn_id, read, level)
10457 .with_principal(principal, catalog_bound)
10458 }
10459
10460 pub fn begin_with_external_trigger_bridge<'a>(
10463 &'a self,
10464 bridge: &'a dyn ExternalTriggerBridge,
10465 ) -> crate::txn::Transaction<'a> {
10466 let txn_id = self.alloc_txn_id();
10467 let read = self.visible_snapshot();
10468 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10469 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10470 .with_external_trigger_bridge(bridge)
10471 .with_principal(principal, catalog_bound)
10472 }
10473
10474 pub fn begin_with_external_trigger_bridge_as<'a>(
10475 &'a self,
10476 bridge: &'a dyn ExternalTriggerBridge,
10477 principal: Option<crate::auth::Principal>,
10478 ) -> crate::txn::Transaction<'a> {
10479 let catalog_bound = principal
10480 .as_ref()
10481 .is_some_and(|principal| principal.user_id != 0);
10482 let txn_id = self.alloc_txn_id();
10483 let read = self.visible_snapshot();
10484 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10485 .with_external_trigger_bridge(bridge)
10486 .with_principal(principal, catalog_bound)
10487 }
10488
10489 pub fn transaction<T>(
10491 &self,
10492 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10493 ) -> Result<T> {
10494 let mut tx = self.begin();
10495 match f(&mut tx) {
10496 Ok(out) => {
10497 tx.commit()?;
10498 Ok(out)
10499 }
10500 Err(e) => {
10501 tx.rollback();
10502 Err(e)
10503 }
10504 }
10505 }
10506
10507 pub fn transaction_with_row_ids<T>(
10508 &self,
10509 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10510 ) -> Result<(T, Vec<RowId>)> {
10511 let mut tx = self.begin();
10512 match f(&mut tx) {
10513 Ok(output) => {
10514 let (_, row_ids) = tx.commit_with_row_ids()?;
10515 Ok((output, row_ids))
10516 }
10517 Err(error) => {
10518 tx.rollback();
10519 Err(error)
10520 }
10521 }
10522 }
10523
10524 pub fn transaction_for_current_principal<T>(
10525 &self,
10526 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10527 ) -> Result<T> {
10528 if self.principal.read().is_some() {
10529 self.refresh_principal()?;
10530 }
10531 let mut transaction = self.begin_as(self.principal.read().clone());
10532 match f(&mut transaction) {
10533 Ok(output) => {
10534 transaction.commit()?;
10535 Ok(output)
10536 }
10537 Err(error) => {
10538 transaction.rollback();
10539 Err(error)
10540 }
10541 }
10542 }
10543
10544 pub fn transaction_for_current_principal_with_epoch<T>(
10545 &self,
10546 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10547 ) -> Result<(Epoch, T)> {
10548 if self.principal.read().is_some() {
10549 self.refresh_principal()?;
10550 }
10551 let mut transaction = self.begin_as(self.principal.read().clone());
10552 match f(&mut transaction) {
10553 Ok(output) => {
10554 let epoch = transaction.commit()?;
10555 Ok((epoch, output))
10556 }
10557 Err(error) => {
10558 transaction.rollback();
10559 Err(error)
10560 }
10561 }
10562 }
10563
10564 pub fn transaction_with_row_ids_for_current_principal<T>(
10565 &self,
10566 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10567 ) -> Result<(T, Vec<RowId>)> {
10568 if self.principal.read().is_some() {
10569 self.refresh_principal()?;
10570 }
10571 let mut transaction = self.begin_as(self.principal.read().clone());
10572 match f(&mut transaction) {
10573 Ok(output) => {
10574 let (_, row_ids) = transaction.commit_with_row_ids()?;
10575 Ok((output, row_ids))
10576 }
10577 Err(error) => {
10578 transaction.rollback();
10579 Err(error)
10580 }
10581 }
10582 }
10583
10584 pub fn transaction_with_external_trigger_bridge<'a, T>(
10587 &'a self,
10588 bridge: &'a dyn ExternalTriggerBridge,
10589 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10590 ) -> Result<T> {
10591 let mut tx = self.begin_with_external_trigger_bridge(bridge);
10592 match f(&mut tx) {
10593 Ok(out) => {
10594 tx.commit()?;
10595 Ok(out)
10596 }
10597 Err(e) => {
10598 tx.rollback();
10599 Err(e)
10600 }
10601 }
10602 }
10603
10604 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
10605 &'a self,
10606 bridge: &'a dyn ExternalTriggerBridge,
10607 principal: Option<crate::auth::Principal>,
10608 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10609 ) -> Result<T> {
10610 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
10611 match f(&mut tx) {
10612 Ok(output) => {
10613 tx.commit()?;
10614 Ok(output)
10615 }
10616 Err(error) => {
10617 tx.rollback();
10618 Err(error)
10619 }
10620 }
10621 }
10622
10623 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
10626 self.active_txns.register(epoch)
10627 }
10628
10629 fn fill_auto_increment_for_staging(
10630 &self,
10631 txn_id: u64,
10632 staging: &mut [(u64, crate::txn::Staged)],
10633 control: Option<&crate::ExecutionControl>,
10634 ) -> Result<()> {
10635 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
10636 for (index, (table_id, staged)) in staging.iter().enumerate() {
10637 commit_prepare_checkpoint(control, index)?;
10638 if matches!(staged, crate::txn::Staged::Put(_)) {
10639 puts_by_table.entry(*table_id).or_default().push(index);
10640 }
10641 }
10642
10643 {
10650 let mut barrier_tables: Vec<u64> = puts_by_table
10651 .iter()
10652 .filter(|(table_id, indexes)| {
10653 indexes.iter().any(|index| {
10654 matches!(
10655 &staging[*index].1,
10656 crate::txn::Staged::Put(cells)
10657 if self.table_auto_inc_would_allocate(**table_id, cells)
10658 )
10659 })
10660 })
10661 .map(|(table_id, _)| *table_id)
10662 .collect();
10663 barrier_tables.sort_unstable();
10664 for table_id in barrier_tables {
10665 self.acquire_txn_lock(
10666 txn_id,
10667 crate::locks::LockKey::sequence_barrier(&format!("auto_inc:{table_id}")),
10668 crate::locks::LockMode::Exclusive,
10669 control,
10670 )?;
10671 }
10672 }
10673
10674 let tables = self.tables.read();
10675 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
10676 commit_prepare_checkpoint(control, table_index)?;
10677 if let Some(handle) = tables.get(&table_id) {
10678 #[cfg(test)]
10679 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10680 let mut t = handle.lock();
10681 for (fill_index, index) in indexes.into_iter().enumerate() {
10682 commit_prepare_checkpoint(control, fill_index)?;
10683 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
10684 t.fill_auto_inc(cells)?;
10685 }
10686 }
10687 }
10688 }
10689 Ok(())
10690 }
10691
10692 fn expand_table_triggers(
10693 &self,
10694 txn_id: u64,
10695 staging: &mut Vec<(u64, crate::txn::Staged)>,
10696 read_epoch: Epoch,
10697 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
10698 external_states: &mut Vec<(String, Vec<u8>)>,
10699 control: Option<&crate::ExecutionControl>,
10700 ) -> Result<()> {
10701 commit_prepare_checkpoint(control, 0)?;
10702 let mut external_writes = Vec::new();
10703 let config = self.trigger_config();
10704 if config.recursive_triggers {
10705 let chunk = std::mem::take(staging);
10706 let stacks = vec![Vec::new(); chunk.len()];
10707 *staging = self.expand_trigger_chunk(
10708 txn_id,
10709 chunk,
10710 stacks,
10711 read_epoch,
10712 0,
10713 config.max_depth,
10714 &mut external_writes,
10715 &config,
10716 control,
10717 )?;
10718 self.apply_external_trigger_writes(
10719 external_writes,
10720 external_trigger_bridge,
10721 external_states,
10722 staging,
10723 control,
10724 )?;
10725 return Ok(());
10726 }
10727
10728 let mut expansion =
10729 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
10730 if !expansion.before.is_empty() {
10731 let mut final_staging = expansion.before;
10732 final_staging.extend(filter_ignored_staging(
10733 std::mem::take(staging),
10734 &expansion.ignored_indices,
10735 ));
10736 *staging = final_staging;
10737 } else if !expansion.ignored_indices.is_empty() {
10738 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
10739 }
10740 staging.append(&mut expansion.after);
10741 external_writes.append(&mut expansion.before_external);
10742 external_writes.append(&mut expansion.after_external);
10743 self.apply_external_trigger_writes(
10744 external_writes,
10745 external_trigger_bridge,
10746 external_states,
10747 staging,
10748 control,
10749 )?;
10750 Ok(())
10751 }
10752
10753 #[allow(clippy::too_many_arguments)]
10754 fn expand_trigger_chunk(
10755 &self,
10756 txn_id: u64,
10757 mut chunk: Vec<(u64, crate::txn::Staged)>,
10758 stacks: Vec<Vec<String>>,
10759 read_epoch: Epoch,
10760 depth: u32,
10761 max_depth: u32,
10762 external_writes: &mut Vec<ExternalTriggerWrite>,
10763 config: &TriggerConfig,
10764 control: Option<&crate::ExecutionControl>,
10765 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
10766 if chunk.is_empty() {
10767 return Ok(Vec::new());
10768 }
10769 commit_prepare_checkpoint(control, 0)?;
10770 self.fill_auto_increment_for_staging(txn_id, &mut chunk, control)?;
10771 let expansion = self.expand_table_triggers_once(
10772 &mut chunk,
10773 read_epoch,
10774 Some(&stacks),
10775 config,
10776 control,
10777 )?;
10778 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
10779 let stack = expansion
10780 .before_stacks
10781 .first()
10782 .or_else(|| expansion.after_stacks.first())
10783 .cloned()
10784 .unwrap_or_default();
10785 return Err(MongrelError::TriggerValidation(format!(
10786 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
10787 Self::format_trigger_stack(&stack)
10788 )));
10789 }
10790
10791 let mut out = Vec::new();
10792 external_writes.extend(expansion.before_external);
10793 out.extend(self.expand_trigger_chunk(
10794 txn_id,
10795 expansion.before,
10796 expansion.before_stacks,
10797 read_epoch,
10798 depth + 1,
10799 max_depth,
10800 external_writes,
10801 config,
10802 control,
10803 )?);
10804 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
10805 external_writes.extend(expansion.after_external);
10806 out.extend(self.expand_trigger_chunk(
10807 txn_id,
10808 expansion.after,
10809 expansion.after_stacks,
10810 read_epoch,
10811 depth + 1,
10812 max_depth,
10813 external_writes,
10814 config,
10815 control,
10816 )?);
10817 Ok(out)
10818 }
10819
10820 fn apply_external_trigger_writes(
10821 &self,
10822 writes: Vec<ExternalTriggerWrite>,
10823 bridge: Option<&dyn ExternalTriggerBridge>,
10824 external_states: &mut Vec<(String, Vec<u8>)>,
10825 staging: &mut Vec<(u64, crate::txn::Staged)>,
10826 control: Option<&crate::ExecutionControl>,
10827 ) -> Result<()> {
10828 if writes.is_empty() {
10829 return Ok(());
10830 }
10831 let bridge = bridge.ok_or_else(|| {
10832 MongrelError::TriggerValidation(
10833 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
10834 )
10835 })?;
10836 for (write_index, write) in writes.into_iter().enumerate() {
10837 commit_prepare_checkpoint(control, write_index)?;
10838 let table = write.table().to_string();
10839 let entry = self.external_table(&table).ok_or_else(|| {
10840 MongrelError::NotFound(format!("external table {table:?} not found"))
10841 })?;
10842 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
10843 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
10844 external_states.push((table, result.state));
10845 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
10846 commit_prepare_checkpoint(control, base_index)?;
10847 match base_write {
10848 ExternalTriggerBaseWrite::Put { table, cells } => {
10849 let table_id = self.table_id(&table)?;
10850 staging.push((table_id, crate::txn::Staged::Put(cells)));
10851 }
10852 ExternalTriggerBaseWrite::Delete { table, row_id } => {
10853 let table_id = self.table_id(&table)?;
10854 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
10855 }
10856 }
10857 }
10858 }
10859 dedup_external_states_in_place(external_states);
10860 Ok(())
10861 }
10862
10863 fn expand_table_triggers_once(
10864 &self,
10865 staging: &mut Vec<(u64, crate::txn::Staged)>,
10866 read_epoch: Epoch,
10867 trigger_stacks: Option<&[Vec<String>]>,
10868 config: &TriggerConfig,
10869 control: Option<&crate::ExecutionControl>,
10870 ) -> Result<TriggerExpansion> {
10871 commit_prepare_checkpoint(control, 0)?;
10872 let triggers: Vec<StoredTrigger> = self
10873 .catalog
10874 .read()
10875 .triggers
10876 .iter()
10877 .filter(|entry| {
10878 entry.trigger.enabled
10879 && matches!(
10880 entry.trigger.timing,
10881 TriggerTiming::Before | TriggerTiming::After
10882 )
10883 && matches!(entry.trigger.target, TriggerTarget::Table(_))
10884 })
10885 .map(|entry| entry.trigger.clone())
10886 .collect();
10887 if triggers.is_empty() || staging.is_empty() {
10888 return Ok(TriggerExpansion::default());
10889 }
10890
10891 let before_triggers = triggers
10892 .iter()
10893 .filter(|trigger| trigger.timing == TriggerTiming::Before)
10894 .cloned()
10895 .collect::<Vec<_>>();
10896 let after_triggers = triggers
10897 .iter()
10898 .filter(|trigger| trigger.timing == TriggerTiming::After)
10899 .cloned()
10900 .collect::<Vec<_>>();
10901
10902 let mut before_added = Vec::new();
10903 let mut before_stacks = Vec::new();
10904 let mut before_external = Vec::new();
10905 let mut ignored_indices = std::collections::BTreeSet::new();
10906 if !before_triggers.is_empty() {
10907 let before_events =
10908 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
10909 let mut out = TriggerProgramOutput {
10910 added: &mut before_added,
10911 added_stacks: &mut before_stacks,
10912 added_external: &mut before_external,
10913 ignored_indices: &mut ignored_indices,
10914 };
10915 self.execute_triggers_for_events(
10916 &before_triggers,
10917 &before_events,
10918 Some(staging),
10919 &mut out,
10920 config,
10921 read_epoch,
10922 control,
10923 )?;
10924 }
10925
10926 let after_events = if after_triggers.is_empty() {
10927 Vec::new()
10928 } else {
10929 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
10930 .into_iter()
10931 .filter(|event| {
10932 !event
10933 .op_indices
10934 .iter()
10935 .any(|idx| ignored_indices.contains(idx))
10936 })
10937 .collect()
10938 };
10939
10940 let mut after_added = Vec::new();
10941 let mut after_stacks = Vec::new();
10942 let mut after_external = Vec::new();
10943 let mut out = TriggerProgramOutput {
10944 added: &mut after_added,
10945 added_stacks: &mut after_stacks,
10946 added_external: &mut after_external,
10947 ignored_indices: &mut ignored_indices,
10948 };
10949 self.execute_triggers_for_events(
10950 &after_triggers,
10951 &after_events,
10952 None,
10953 &mut out,
10954 config,
10955 read_epoch,
10956 control,
10957 )?;
10958 Ok(TriggerExpansion {
10959 before: before_added,
10960 before_stacks,
10961 before_external,
10962 after: after_added,
10963 after_stacks,
10964 after_external,
10965 ignored_indices,
10966 })
10967 }
10968
10969 #[allow(clippy::too_many_arguments)]
10970 fn execute_triggers_for_events(
10971 &self,
10972 triggers: &[StoredTrigger],
10973 events: &[WriteEvent],
10974 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
10975 out: &mut TriggerProgramOutput<'_>,
10976 config: &TriggerConfig,
10977 read_epoch: Epoch,
10978 control: Option<&crate::ExecutionControl>,
10979 ) -> Result<()> {
10980 let mut checkpoint_index = 0_usize;
10981 for event in events {
10982 for trigger in triggers {
10983 commit_prepare_checkpoint(control, checkpoint_index)?;
10984 checkpoint_index += 1;
10985 if event
10986 .op_indices
10987 .iter()
10988 .any(|idx| out.ignored_indices.contains(idx))
10989 {
10990 break;
10991 }
10992 let matches = {
10993 let cat = self.catalog.read();
10994 trigger_matches_event(trigger, event, &cat)?
10995 };
10996 if !matches {
10997 continue;
10998 }
10999 if let Some(when) = &trigger.when {
11000 if !eval_trigger_expr(when, event)? {
11001 continue;
11002 }
11003 }
11004 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
11005 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
11006 return Err(MongrelError::TriggerValidation(format!(
11007 "trigger recursion cycle detected; trigger stack: {}",
11008 Self::format_trigger_stack(&trigger_stack)
11009 )));
11010 }
11011 let outcome = match staging.as_mut() {
11012 Some(staging) => self.execute_trigger_program(
11013 trigger,
11014 event,
11015 Some(&mut **staging),
11016 out,
11017 &trigger_stack,
11018 config,
11019 read_epoch,
11020 control,
11021 )?,
11022 None => self.execute_trigger_program(
11023 trigger,
11024 event,
11025 None,
11026 out,
11027 &trigger_stack,
11028 config,
11029 read_epoch,
11030 control,
11031 )?,
11032 };
11033 if outcome == TriggerProgramOutcome::Ignore {
11034 out.ignored_indices.extend(event.op_indices.iter().copied());
11035 break;
11036 }
11037 }
11038 }
11039 Ok(())
11040 }
11041
11042 fn trigger_events_for_staging(
11043 &self,
11044 staging: &[(u64, crate::txn::Staged)],
11045 read_epoch: Epoch,
11046 trigger_stacks: Option<&[Vec<String>]>,
11047 control: Option<&crate::ExecutionControl>,
11048 ) -> Result<Vec<WriteEvent>> {
11049 use crate::txn::Staged;
11050 use std::collections::{HashMap, VecDeque};
11051
11052 let snapshot = self.snapshot_for_epoch(read_epoch);
11053 let cat = self.catalog.read();
11054 let mut table_names = HashMap::new();
11055 let mut table_schemas = HashMap::new();
11056 for entry in cat
11057 .tables
11058 .iter()
11059 .filter(|entry| matches!(entry.state, TableState::Live))
11060 {
11061 table_names.insert(entry.table_id, entry.name.clone());
11062 table_schemas.insert(entry.table_id, entry.schema.clone());
11063 }
11064 drop(cat);
11065
11066 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
11067 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11068 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11069
11070 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11071 commit_prepare_checkpoint(control, idx)?;
11072 let Some(schema) = table_schemas.get(table_id) else {
11073 continue;
11074 };
11075 let Some(pk) = schema.primary_key() else {
11076 continue;
11077 };
11078 match staged {
11079 Staged::Delete(row_id) => {
11080 let handle = self.table_by_id(*table_id)?;
11081 let Some(row) = handle.lock().get(*row_id, snapshot) else {
11082 continue;
11083 };
11084 let Some(pk_value) = row.columns.get(&pk.id) else {
11085 continue;
11086 };
11087 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
11088 delete_by_key
11089 .entry((*table_id, pk_value.encode_key()))
11090 .or_default()
11091 .push_back(idx);
11092 }
11093 Staged::Put(cells) => {
11094 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
11095 put_by_key
11096 .entry((*table_id, value.encode_key()))
11097 .or_default()
11098 .push_back(idx);
11099 }
11100 }
11101 Staged::Update { row_id, .. } => {
11102 let handle = self.table_by_id(*table_id)?;
11103 let row = handle.lock().get(*row_id, snapshot);
11104 if let Some(row) = row {
11105 old_rows.insert(idx, TriggerRowImage::from_row(row));
11106 }
11107 }
11108 Staged::Truncate => {}
11109 }
11110 }
11111
11112 let mut paired_delete = std::collections::HashSet::new();
11113 let mut paired_put = std::collections::HashSet::new();
11114 let mut events = Vec::new();
11115
11116 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
11117 commit_prepare_checkpoint(control, pair_index)?;
11118 let Some(puts) = put_by_key.get_mut(key) else {
11119 continue;
11120 };
11121 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
11122 paired_delete.insert(delete_idx);
11123 paired_put.insert(put_idx);
11124 let (table_id, _) = &staging[put_idx];
11125 let Some(table_name) = table_names.get(table_id).cloned() else {
11126 continue;
11127 };
11128 let old = old_rows.get(&delete_idx).cloned();
11129 let new = match &staging[put_idx].1 {
11130 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
11131 _ => None,
11132 };
11133 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11134 events.push(WriteEvent {
11135 table: table_name,
11136 kind: TriggerEvent::Update,
11137 old,
11138 new,
11139 changed_columns,
11140 op_indices: vec![delete_idx, put_idx],
11141 put_idx: Some(put_idx),
11142 trigger_stack: Self::trigger_stack_for_indices(
11143 trigger_stacks,
11144 &[delete_idx, put_idx],
11145 ),
11146 });
11147 }
11148 }
11149
11150 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11151 commit_prepare_checkpoint(control, idx)?;
11152 let Some(table_name) = table_names.get(table_id).cloned() else {
11153 continue;
11154 };
11155 match staged {
11156 Staged::Put(cells) if !paired_put.contains(&idx) => {
11157 let new = Some(TriggerRowImage::from_cells(cells));
11158 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
11159 events.push(WriteEvent {
11160 table: table_name,
11161 kind: TriggerEvent::Insert,
11162 old: None,
11163 new,
11164 changed_columns,
11165 op_indices: vec![idx],
11166 put_idx: Some(idx),
11167 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11168 });
11169 }
11170 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
11171 let old = match old_rows.get(&idx).cloned() {
11172 Some(old) => Some(old),
11173 None => {
11174 let handle = self.table_by_id(*table_id)?;
11175 let row = handle.lock().get(*row_id, snapshot);
11176 row.map(TriggerRowImage::from_row)
11177 }
11178 };
11179 let Some(old) = old else {
11180 continue;
11181 };
11182 let changed_columns = old.columns.keys().copied().collect();
11183 events.push(WriteEvent {
11184 table: table_name,
11185 kind: TriggerEvent::Delete,
11186 old: Some(old),
11187 new: None,
11188 changed_columns,
11189 op_indices: vec![idx],
11190 put_idx: None,
11191 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11192 });
11193 }
11194 Staged::Update { new_row: cells, .. } => {
11195 let old = old_rows.get(&idx).cloned();
11196 let new = Some(TriggerRowImage::from_cells(cells));
11197 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11198 events.push(WriteEvent {
11199 table: table_name,
11200 kind: TriggerEvent::Update,
11201 old,
11202 new,
11203 changed_columns,
11204 op_indices: vec![idx],
11205 put_idx: Some(idx),
11206 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11207 });
11208 }
11209 Staged::Truncate => {}
11210 _ => {}
11211 }
11212 }
11213
11214 Ok(events)
11215 }
11216
11217 #[allow(clippy::too_many_arguments)]
11218 fn execute_trigger_program(
11219 &self,
11220 trigger: &StoredTrigger,
11221 event: &WriteEvent,
11222 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11223 out: &mut TriggerProgramOutput<'_>,
11224 trigger_stack: &[String],
11225 config: &TriggerConfig,
11226 read_epoch: Epoch,
11227 control: Option<&crate::ExecutionControl>,
11228 ) -> Result<TriggerProgramOutcome> {
11229 let mut event = event.clone();
11230 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
11231 self.execute_trigger_steps(
11232 trigger,
11233 &trigger.program.steps,
11234 &mut event,
11235 staging,
11236 out,
11237 trigger_stack,
11238 config,
11239 &mut select_results,
11240 0,
11241 None,
11242 read_epoch,
11243 control,
11244 )
11245 }
11246
11247 #[allow(clippy::too_many_arguments)]
11248 fn execute_trigger_steps(
11249 &self,
11250 trigger: &StoredTrigger,
11251 steps: &[TriggerStep],
11252 event: &mut WriteEvent,
11253 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11254 out: &mut TriggerProgramOutput<'_>,
11255 trigger_stack: &[String],
11256 config: &TriggerConfig,
11257 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
11258 depth: u32,
11259 selected: Option<&TriggerRowImage>,
11260 read_epoch: Epoch,
11261 control: Option<&crate::ExecutionControl>,
11262 ) -> Result<TriggerProgramOutcome> {
11263 let _ = depth;
11264 for (step_index, step) in steps.iter().enumerate() {
11265 commit_prepare_checkpoint(control, step_index)?;
11266 match step {
11267 TriggerStep::SetNew { cells } => {
11268 if trigger.timing != TriggerTiming::Before {
11269 return Err(MongrelError::InvalidArgument(
11270 "SetNew trigger step is only valid in BEFORE triggers".into(),
11271 ));
11272 }
11273 let put_idx = event.put_idx.ok_or_else(|| {
11274 MongrelError::InvalidArgument(
11275 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
11276 )
11277 })?;
11278 let staging = staging.as_deref_mut().ok_or_else(|| {
11279 MongrelError::InvalidArgument(
11280 "SetNew trigger step requires mutable trigger staging".into(),
11281 )
11282 })?;
11283 let mut update_changed_columns = None;
11284 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
11285 Some(crate::txn::Staged::Put(cells)) => cells,
11286 Some(crate::txn::Staged::Update {
11287 new_row,
11288 changed_columns,
11289 ..
11290 }) => {
11291 update_changed_columns = Some(changed_columns);
11292 new_row
11293 }
11294 _ => {
11295 return Err(MongrelError::InvalidArgument(
11296 "SetNew trigger step target row is not mutable".into(),
11297 ))
11298 }
11299 };
11300 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
11301 row_cells.retain(|(id, _)| *id != column_id);
11302 row_cells.push((column_id, value.clone()));
11303 if let Some(changed_columns) = &mut update_changed_columns {
11304 changed_columns.push(column_id);
11305 }
11306 if let Some(new) = &mut event.new {
11307 new.columns.insert(column_id, value);
11308 }
11309 }
11310 row_cells.sort_by_key(|(id, _)| *id);
11311 if let Some(changed_columns) = update_changed_columns {
11312 changed_columns.sort_unstable();
11313 changed_columns.dedup();
11314 }
11315 }
11316 TriggerStep::Insert { table, cells } => {
11317 let cells = eval_trigger_cells(cells, event, selected)?;
11318 if let Ok(table_id) = self.table_id(table) {
11319 out.added.push((table_id, crate::txn::Staged::Put(cells)));
11320 out.added_stacks.push(trigger_stack.to_vec());
11321 } else if self.external_table(table).is_some() {
11322 out.added_external.push(ExternalTriggerWrite::Insert {
11323 table: table.clone(),
11324 cells,
11325 });
11326 } else {
11327 return Err(MongrelError::NotFound(format!(
11328 "trigger {:?} insert target {table:?} not found",
11329 trigger.name
11330 )));
11331 }
11332 }
11333 TriggerStep::UpdateByPk { table, pk, cells } => {
11334 let pk = eval_trigger_value(pk, event, selected)?;
11335 let cells = eval_trigger_cells(cells, event, selected)?;
11336 if self.external_table(table).is_some() {
11337 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
11338 table: table.clone(),
11339 pk,
11340 cells,
11341 });
11342 } else {
11343 let row_id = self
11344 .table(table)?
11345 .lock()
11346 .lookup_pk(&pk.encode_key())
11347 .ok_or_else(|| {
11348 MongrelError::NotFound(format!(
11349 "trigger {:?} update target not found",
11350 trigger.name
11351 ))
11352 })?;
11353 let handle = self.table(table)?;
11354 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
11355 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
11356 MongrelError::NotFound(format!(
11357 "trigger {:?} update target not visible",
11358 trigger.name
11359 ))
11360 })?;
11361 let mut changed_columns = cells
11362 .iter()
11363 .map(|(column_id, _)| *column_id)
11364 .collect::<Vec<_>>();
11365 changed_columns.sort_unstable();
11366 changed_columns.dedup();
11367 let mut merged = old.columns;
11368 for (column_id, value) in cells {
11369 merged.insert(column_id, value);
11370 }
11371 out.added.push((
11372 self.table_id(table)?,
11373 crate::txn::Staged::Update {
11374 row_id,
11375 new_row: merged.into_iter().collect(),
11376 changed_columns,
11377 },
11378 ));
11379 out.added_stacks.push(trigger_stack.to_vec());
11380 }
11381 }
11382 TriggerStep::DeleteByPk { table, pk } => {
11383 let pk = eval_trigger_value(pk, event, selected)?;
11384 if self.external_table(table).is_some() {
11385 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
11386 table: table.clone(),
11387 pk,
11388 });
11389 } else {
11390 let row_id = self
11391 .table(table)?
11392 .lock()
11393 .lookup_pk(&pk.encode_key())
11394 .ok_or_else(|| {
11395 MongrelError::NotFound(format!(
11396 "trigger {:?} delete target not found",
11397 trigger.name
11398 ))
11399 })?;
11400 out.added
11401 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
11402 out.added_stacks.push(trigger_stack.to_vec());
11403 }
11404 }
11405 TriggerStep::Select {
11406 id,
11407 table,
11408 conditions,
11409 } => {
11410 let schema = self.table(table)?.lock().schema().clone();
11411 let snapshot = self.snapshot_for_epoch(read_epoch);
11412 let handle = self.table(table)?;
11413 let rows = match control {
11414 Some(control) => {
11415 handle.lock().visible_rows_controlled(snapshot, control)?
11416 }
11417 None => handle.lock().visible_rows(snapshot)?,
11418 };
11419 let mut matched = Vec::new();
11420 for (row_index, row) in rows.into_iter().enumerate() {
11421 commit_prepare_checkpoint(control, row_index)?;
11422 let image = TriggerRowImage::from_row(row);
11423 let passes = conditions
11424 .iter()
11425 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11426 .collect::<Result<Vec<_>>>()?
11427 .into_iter()
11428 .all(|b| b);
11429 if passes {
11430 matched.push(image);
11431 }
11432 }
11433 if let Some(pk) = schema.primary_key() {
11434 matched.sort_by(|a, b| {
11435 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
11436 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
11437 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
11438 });
11439 }
11440 select_results.insert(id.clone(), matched);
11441 }
11442 TriggerStep::Foreach { id, steps } => {
11443 let rows = select_results.get(id).ok_or_else(|| {
11444 MongrelError::InvalidArgument(format!(
11445 "trigger {:?} foreach references unknown select id {id:?}",
11446 trigger.name
11447 ))
11448 })?;
11449 if rows.len() > config.max_loop_iterations as usize {
11450 return Err(MongrelError::InvalidArgument(format!(
11451 "trigger {:?} foreach exceeded max_loop_iterations ({})",
11452 trigger.name, config.max_loop_iterations
11453 )));
11454 }
11455 for (row_index, row) in rows.clone().into_iter().enumerate() {
11456 commit_prepare_checkpoint(control, row_index)?;
11457 let result = self.execute_trigger_steps(
11458 trigger,
11459 steps,
11460 event,
11461 staging.as_deref_mut(),
11462 out,
11463 trigger_stack,
11464 config,
11465 select_results,
11466 depth + 1,
11467 Some(&row),
11468 read_epoch,
11469 control,
11470 )?;
11471 if result == TriggerProgramOutcome::Ignore {
11472 return Ok(TriggerProgramOutcome::Ignore);
11473 }
11474 }
11475 }
11476 TriggerStep::DeleteWhere { table, conditions } => {
11477 let schema = self.table(table)?.lock().schema().clone();
11478 let snapshot = self.snapshot_for_epoch(read_epoch);
11479 let handle = self.table(table)?;
11480 let rows = match control {
11481 Some(control) => {
11482 handle.lock().visible_rows_controlled(snapshot, control)?
11483 }
11484 None => handle.lock().visible_rows(snapshot)?,
11485 };
11486 let table_id = self.table_id(table)?;
11487 let mut to_delete = Vec::new();
11488 for (row_index, row) in rows.into_iter().enumerate() {
11489 commit_prepare_checkpoint(control, row_index)?;
11490 let image = TriggerRowImage::from_row(row.clone());
11491 let passes = conditions
11492 .iter()
11493 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11494 .collect::<Result<Vec<_>>>()?
11495 .into_iter()
11496 .all(|b| b);
11497 if passes {
11498 to_delete.push((table_id, row.row_id));
11499 }
11500 }
11501 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
11502 commit_prepare_checkpoint(control, row_index)?;
11503 out.added
11504 .push((table_id, crate::txn::Staged::Delete(row_id)));
11505 out.added_stacks.push(trigger_stack.to_vec());
11506 }
11507 }
11508 TriggerStep::UpdateWhere {
11509 table,
11510 conditions,
11511 cells,
11512 } => {
11513 let schema = self.table(table)?.lock().schema().clone();
11514 let snapshot = self.snapshot_for_epoch(read_epoch);
11515 let handle = self.table(table)?;
11516 let rows = match control {
11517 Some(control) => {
11518 handle.lock().visible_rows_controlled(snapshot, control)?
11519 }
11520 None => handle.lock().visible_rows(snapshot)?,
11521 };
11522 let table_id = self.table_id(table)?;
11523 let mut changed_columns =
11524 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
11525 changed_columns.sort_unstable();
11526 changed_columns.dedup();
11527 let mut to_update = Vec::new();
11528 for (row_index, row) in rows.into_iter().enumerate() {
11529 commit_prepare_checkpoint(control, row_index)?;
11530 let image = TriggerRowImage::from_row(row.clone());
11531 let passes = conditions
11532 .iter()
11533 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11534 .collect::<Result<Vec<_>>>()?
11535 .into_iter()
11536 .all(|b| b);
11537 if passes {
11538 let new_cells = cells
11539 .iter()
11540 .map(|cell| {
11541 Ok((
11542 cell.column_id,
11543 eval_trigger_value(&cell.value, event, Some(&image))?,
11544 ))
11545 })
11546 .collect::<Result<Vec<_>>>()?;
11547 let mut merged = row.columns.clone();
11548 for (column_id, value) in new_cells {
11549 merged.insert(column_id, value);
11550 }
11551 to_update.push((table_id, row.row_id, merged));
11552 }
11553 }
11554 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
11555 {
11556 commit_prepare_checkpoint(control, row_index)?;
11557 out.added.push((
11558 table_id,
11559 crate::txn::Staged::Update {
11560 row_id,
11561 new_row: merged.into_iter().collect(),
11562 changed_columns: changed_columns.clone(),
11563 },
11564 ));
11565 out.added_stacks.push(trigger_stack.to_vec());
11566 }
11567 }
11568 TriggerStep::Raise { action, message } => match action {
11569 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
11570 TriggerRaiseAction::Abort
11571 | TriggerRaiseAction::Fail
11572 | TriggerRaiseAction::Rollback => {
11573 let message = eval_trigger_value(message, event, selected)?;
11574 return Err(MongrelError::TriggerValidation(format!(
11575 "trigger {:?} raised: {}; trigger stack: {}",
11576 trigger.name,
11577 trigger_message(message),
11578 Self::format_trigger_stack(trigger_stack)
11579 )));
11580 }
11581 },
11582 }
11583 }
11584 Ok(TriggerProgramOutcome::Continue)
11585 }
11586
11587 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
11588 let Some(stacks) = stacks else {
11589 return Vec::new();
11590 };
11591 let mut out = Vec::new();
11592 for idx in indices {
11593 let Some(stack) = stacks.get(*idx) else {
11594 continue;
11595 };
11596 for name in stack {
11597 if !out.iter().any(|existing| existing == name) {
11598 out.push(name.clone());
11599 }
11600 }
11601 }
11602 out
11603 }
11604
11605 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
11606 let mut out = stack.to_vec();
11607 out.push(trigger_name.to_string());
11608 out
11609 }
11610
11611 fn format_trigger_stack(stack: &[String]) -> String {
11612 if stack.is_empty() {
11613 "<root>".into()
11614 } else {
11615 stack.join(" -> ")
11616 }
11617 }
11618
11619 fn acquire_unique_key_claims(
11640 &self,
11641 txn_id: u64,
11642 staging: &[(u64, crate::txn::Staged)],
11643 control: Option<&crate::ExecutionControl>,
11644 ) -> Result<()> {
11645 let catalog = self.catalog.read();
11646 let has_uniques = staging.iter().any(|(table_id, staged)| {
11647 matches!(
11648 staged,
11649 crate::txn::Staged::Put(_) | crate::txn::Staged::Update { .. }
11650 ) && catalog.tables.iter().any(|entry| {
11651 entry.table_id == *table_id
11652 && (entry.schema.primary_key().is_some()
11653 || !entry.schema.constraints.uniques.is_empty())
11654 })
11655 });
11656 if !has_uniques {
11657 return Ok(());
11658 }
11659 let mut claims: Vec<(u64, Vec<u8>)> = Vec::new();
11660 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11661 commit_prepare_checkpoint(control, staged_index)?;
11662 let cells = match staged {
11663 crate::txn::Staged::Put(cells) => cells,
11664 crate::txn::Staged::Update { new_row, .. } => new_row,
11665 _ => continue,
11666 };
11667 let Some(entry) = catalog
11668 .tables
11669 .iter()
11670 .find(|entry| entry.table_id == *table_id)
11671 else {
11672 continue;
11673 };
11674 for column in &entry.schema.columns {
11675 if !column
11676 .flags
11677 .contains(crate::schema::ColumnFlags::PRIMARY_KEY)
11678 {
11679 continue;
11680 }
11681 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == column.id) {
11682 let mut key = b"pk:".to_vec();
11683 key.extend_from_slice(&value.encode_key());
11684 claims.push((*table_id, key));
11685 }
11686 }
11687 let cells_map: HashMap<u16, Value> = cells.iter().cloned().collect();
11692 for uc in &entry.schema.constraints.uniques {
11693 if let Some(composite) =
11694 crate::constraint::encode_composite_key(&uc.columns, &cells_map)
11695 {
11696 let mut key = format!("uq{}:", uc.id).into_bytes();
11697 key.extend_from_slice(&composite);
11698 claims.push((*table_id, key));
11699 }
11700 }
11701 }
11702 claims.sort();
11703 claims.dedup();
11704 for (table_id, key) in claims {
11705 self.acquire_txn_lock(
11706 txn_id,
11707 crate::locks::LockKey::key(table_id, key),
11708 crate::locks::LockMode::Exclusive,
11709 control,
11710 )?;
11711 }
11712 Ok(())
11713 }
11714
11715 fn acquire_fk_lock(
11719 &self,
11720 txn_id: u64,
11721 table_id: u64,
11722 key: &[u8],
11723 mode: crate::locks::LockMode,
11724 control: Option<&crate::ExecutionControl>,
11725 ) -> Result<()> {
11726 let mut namespaced = b"fk:".to_vec();
11727 namespaced.extend_from_slice(key);
11728 self.acquire_txn_lock(
11729 txn_id,
11730 crate::locks::LockKey::key(table_id, namespaced),
11731 mode,
11732 control,
11733 )?;
11734 let hook = self.fk_lock_hook.lock().clone();
11737 if let Some(hook) = hook {
11738 hook();
11739 }
11740 Ok(())
11741 }
11742
11743 fn validate_constraints(
11744 &self,
11745 txn_id: u64,
11746 staging: &mut Vec<(u64, crate::txn::Staged)>,
11747 read_epoch: Epoch,
11748 principal: Option<&crate::auth::Principal>,
11749 control: Option<&crate::ExecutionControl>,
11750 ) -> Result<()> {
11751 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
11752 use crate::memtable::Row;
11753 use crate::txn::Staged;
11754 use std::collections::HashSet;
11755
11756 commit_prepare_checkpoint(control, 0)?;
11757 let snapshot = self.snapshot_for_epoch(read_epoch);
11760 let cat = self.catalog.read().clone();
11761
11762 let live: Vec<(u64, String, crate::schema::Schema)> = cat
11764 .tables
11765 .iter()
11766 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
11767 .map(|e| (e.table_id, e.name.clone(), e.schema.clone()))
11768 .collect();
11769
11770 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
11772 if !any_constraints {
11773 self.materialize_generated_embeddings(staging, control)?;
11774 return Ok(());
11775 }
11776
11777 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
11779 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
11780 if let Some(r) = rows_cache.get(&table_id) {
11781 return Ok(r.clone());
11782 }
11783 let handle = self.table_by_id(table_id)?;
11784 let rows = match control {
11785 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
11786 None => handle.lock().visible_rows(snapshot)?,
11787 };
11788 rows_cache.insert(table_id, rows.clone());
11789 Ok(rows)
11790 };
11791
11792 let mut processed_updates = HashSet::new();
11797 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
11798 let mut update_pass = 0_usize;
11799 loop {
11800 commit_prepare_checkpoint(control, update_pass)?;
11801 update_pass += 1;
11802 let updates: Vec<PendingUpdate> = staging
11803 .iter()
11804 .enumerate()
11805 .filter_map(|(index, (table_id, op))| match op {
11806 Staged::Update {
11807 row_id,
11808 new_row: cells,
11809 ..
11810 } if !processed_updates.contains(&index) => {
11811 Some((index, *table_id, *row_id, cells.clone()))
11812 }
11813 _ => None,
11814 })
11815 .collect();
11816 if updates.is_empty() {
11817 break;
11818 }
11819 let mut new_ops = Vec::new();
11820 for (update_index, (index, table_id, row_id, new_cells)) in
11821 updates.into_iter().enumerate()
11822 {
11823 commit_prepare_checkpoint(control, update_index)?;
11824 processed_updates.insert(index);
11825 let Some(tname) = live
11826 .iter()
11827 .find(|(id, _, _)| *id == table_id)
11828 .map(|(_, name, _)| name.as_str())
11829 else {
11830 continue;
11831 };
11832 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
11833 continue;
11834 };
11835 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
11836 for (child_id, _child_name, child_schema) in &live {
11837 for fk in &child_schema.constraints.foreign_keys {
11838 if fk.ref_table != tname {
11839 continue;
11840 }
11841 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
11842 else {
11843 continue;
11844 };
11845 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
11846 == Some(old_key.as_slice())
11847 {
11848 continue;
11849 }
11850 if fk.on_update == FkAction::Restrict {
11851 continue;
11852 }
11853 self.acquire_fk_lock(
11858 txn_id,
11859 table_id,
11860 &old_key,
11861 crate::locks::LockMode::Exclusive,
11862 control,
11863 )?;
11864 let child_rows = load_rows(*child_id)?;
11865 for (child_index, child) in child_rows.into_iter().enumerate() {
11866 commit_prepare_checkpoint(control, child_index)?;
11867 if encode_composite_key(&fk.columns, &child.columns).as_deref()
11868 != Some(old_key.as_slice())
11869 {
11870 continue;
11871 }
11872 if staging.iter().any(|(id, op)| {
11873 *id == *child_id
11874 && matches!(op, Staged::Delete(id) if *id == child.row_id)
11875 }) {
11876 continue;
11877 }
11878 let mut cells: Vec<(u16, Value)> = child
11879 .columns
11880 .iter()
11881 .map(|(column_id, value)| (*column_id, value.clone()))
11882 .collect();
11883 for (child_column, parent_column) in
11884 fk.columns.iter().zip(&fk.ref_columns)
11885 {
11886 cells.retain(|(column_id, _)| column_id != child_column);
11887 let value = match fk.on_update {
11888 FkAction::Cascade => {
11889 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
11890 }
11891 FkAction::SetNull => Value::Null,
11892 FkAction::Restrict => {
11893 return Err(MongrelError::Other(
11894 "restricted foreign-key update reached cascade preparation"
11895 .into(),
11896 ));
11897 }
11898 };
11899 cells.push((*child_column, value));
11900 }
11901 cells.sort_by_key(|(column_id, _)| *column_id);
11902 if let Some(existing_index) = staging.iter().position(|(id, op)| {
11903 *id == *child_id
11904 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
11905 }) {
11906 if let Staged::Update {
11907 new_row: existing,
11908 changed_columns,
11909 ..
11910 } = &mut staging[existing_index].1 {
11911 changed_columns.extend(fk.columns.iter().copied());
11912 changed_columns.sort_unstable();
11913 changed_columns.dedup();
11914 if *existing != cells {
11915 *existing = cells;
11916 processed_updates.remove(&existing_index);
11917 }
11918 }
11919 } else {
11920 new_ops.push((
11921 *child_id,
11922 Staged::Update {
11923 row_id: child.row_id,
11924 new_row: cells,
11925 changed_columns: fk.columns.clone(),
11926 },
11927 ));
11928 }
11929 }
11930 }
11931 }
11932 }
11933 staging.extend(new_ops);
11934 }
11935
11936 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
11941 let mut cascade_pass = 0_usize;
11942 loop {
11943 commit_prepare_checkpoint(control, cascade_pass)?;
11944 cascade_pass += 1;
11945 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
11946 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
11947 .iter()
11948 .filter_map(|(t, op)| match op {
11949 Staged::Delete(rid) => Some((*t, *rid)),
11950 _ => None,
11951 })
11952 .collect();
11953 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
11954 commit_prepare_checkpoint(control, delete_index)?;
11955 if !cascaded.insert((table_id, rid.0)) {
11956 continue;
11957 }
11958 let Some(tname) = live
11959 .iter()
11960 .find(|(t, _, _)| *t == table_id)
11961 .map(|(_, n, _)| n.as_str())
11962 else {
11963 continue;
11964 };
11965 let parent_handle = self.table_by_id(table_id)?;
11966 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
11967 continue;
11968 };
11969 for (child_id, _child_name, child_schema) in &live {
11970 for fk in &child_schema.constraints.foreign_keys {
11971 if fk.ref_table != tname {
11972 continue;
11973 }
11974 let Some(parent_key) =
11975 encode_composite_key(&fk.ref_columns, &parent_row.columns)
11976 else {
11977 continue;
11978 };
11979 let key_preserved = staging.iter().any(|(t, op)| {
11988 if *t != table_id {
11989 return false;
11990 }
11991 let Staged::Put(cells) = op else {
11992 return false;
11993 };
11994 let map: HashMap<u16, crate::memtable::Value> =
11995 cells.iter().cloned().collect();
11996 encode_composite_key(&fk.ref_columns, &map).as_deref()
11997 == Some(parent_key.as_slice())
11998 });
11999 if key_preserved {
12000 continue;
12001 }
12002 self.acquire_fk_lock(
12009 txn_id,
12010 table_id,
12011 &parent_key,
12012 crate::locks::LockMode::Exclusive,
12013 control,
12014 )?;
12015 match fk.on_delete {
12016 FkAction::Restrict => continue,
12017 FkAction::Cascade => {
12018 let child_rows = load_rows(*child_id)?;
12019 for (child_index, cr) in child_rows.iter().enumerate() {
12020 commit_prepare_checkpoint(control, child_index)?;
12021 if !cascaded.contains(&(*child_id, cr.row_id.0))
12022 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12023 == Some(parent_key.as_slice())
12024 {
12025 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
12026 }
12027 }
12028 }
12029 FkAction::SetNull => {
12030 let child_rows = load_rows(*child_id)?;
12031 for (child_index, cr) in child_rows.iter().enumerate() {
12032 commit_prepare_checkpoint(control, child_index)?;
12033 if !cascaded.contains(&(*child_id, cr.row_id.0))
12034 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12035 == Some(parent_key.as_slice())
12036 {
12037 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
12040 .columns
12041 .iter()
12042 .map(|(k, v)| (*k, v.clone()))
12043 .collect();
12044 for cid in &fk.columns {
12045 cells.retain(|(k, _)| k != cid);
12046 cells.push((*cid, crate::memtable::Value::Null));
12047 }
12048 new_ops.push((
12049 *child_id,
12050 Staged::Update {
12051 row_id: cr.row_id,
12052 new_row: cells,
12053 changed_columns: fk.columns.clone(),
12054 },
12055 ));
12056 }
12057 }
12058 }
12059 }
12060 }
12061 }
12062 }
12063 if new_ops.is_empty() {
12064 break;
12065 }
12066 staging.extend(new_ops);
12067 }
12068
12069 self.validate_write_permissions(staging, principal, control)?;
12073 self.validate_security_writes(staging, read_epoch, principal, control)?;
12074 self.materialize_generated_embeddings(staging, control)?;
12075
12076 let staged_deletes: HashSet<(u64, u64)> = staging
12080 .iter()
12081 .filter_map(|(t, op)| match op {
12082 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
12083 _ => None,
12084 })
12085 .collect();
12086
12087 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
12089
12090 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
12092 commit_prepare_checkpoint(control, operation_index)?;
12093 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id) else {
12094 continue;
12095 };
12096 let cells_map: HashMap<u16, crate::memtable::Value>;
12097 match op {
12098 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
12099 cells_map = cells.iter().cloned().collect();
12100
12101 if !schema.constraints.checks.is_empty() {
12103 validate_checks(&schema.constraints.checks, &cells_map)?;
12104 }
12105
12106 for uc in &schema.constraints.uniques {
12108 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
12109 continue; };
12111 let marker = (*table_id, uc.id, key.clone());
12112 if !seen_unique.insert(marker) {
12113 return Err(MongrelError::Conflict(format!(
12114 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
12115 uc.name
12116 )));
12117 }
12118 let rows = load_rows(*table_id)?;
12119 for (row_index, r) in rows.iter().enumerate() {
12120 commit_prepare_checkpoint(control, row_index)?;
12121 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
12124 continue;
12125 }
12126 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
12127 if theirs == key {
12128 return Err(MongrelError::Conflict(format!(
12129 "UNIQUE constraint '{}' on table '{tname}' violated",
12130 uc.name
12131 )));
12132 }
12133 }
12134 }
12135 }
12136
12137 for fk in &schema.constraints.foreign_keys {
12139 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
12140 continue; };
12142 let Some(parent_id) = cat
12143 .tables
12144 .iter()
12145 .find(|t| t.name == fk.ref_table)
12146 .map(|t| t.table_id)
12147 else {
12148 return Err(MongrelError::InvalidArgument(format!(
12149 "FOREIGN KEY '{}' references unknown table '{}'",
12150 fk.name, fk.ref_table
12151 )));
12152 };
12153 self.acquire_fk_lock(
12158 txn_id,
12159 parent_id,
12160 &child_key,
12161 crate::locks::LockMode::Shared,
12162 control,
12163 )?;
12164 let parent_rows = load_rows(parent_id)?;
12165 let mut found = false;
12166 for (row_index, r) in parent_rows.iter().enumerate() {
12167 commit_prepare_checkpoint(control, row_index)?;
12168 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
12169 continue;
12170 }
12171 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
12172 if pkey == child_key {
12173 found = true;
12174 break;
12175 }
12176 }
12177 }
12178 if !found {
12185 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
12186 commit_prepare_checkpoint(control, staged_index)?;
12187 if *st_table != parent_id {
12188 continue;
12189 }
12190 if let Staged::Put(pcells)
12191 | Staged::Update {
12192 new_row: pcells, ..
12193 } = st_op
12194 {
12195 let pmap: HashMap<u16, crate::memtable::Value> =
12196 pcells.iter().cloned().collect();
12197 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
12198 {
12199 if pkey == child_key {
12200 found = true;
12201 break;
12202 }
12203 }
12204 }
12205 }
12206 }
12207 if !found {
12208 return Err(MongrelError::Conflict(format!(
12209 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
12210 fk.name, fk.ref_table
12211 )));
12212 }
12213 }
12214
12215 if let Staged::Update { row_id, .. } = op {
12220 let parent_handle = self.table_by_id(*table_id)?;
12221 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
12222 continue;
12223 };
12224 for (child_id, child_name, child_schema) in &live {
12225 for fk in &child_schema.constraints.foreign_keys {
12226 if fk.ref_table != *tname || fk.on_update != FkAction::Restrict {
12227 continue;
12228 }
12229 let Some(old_key) =
12230 encode_composite_key(&fk.ref_columns, &old_parent.columns)
12231 else {
12232 continue;
12233 };
12234 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
12235 == Some(old_key.as_slice())
12236 {
12237 continue;
12238 }
12239 self.acquire_fk_lock(
12244 txn_id,
12245 *table_id,
12246 &old_key,
12247 crate::locks::LockMode::Exclusive,
12248 control,
12249 )?;
12250 for (child_index, child) in
12251 load_rows(*child_id)?.into_iter().enumerate()
12252 {
12253 commit_prepare_checkpoint(control, child_index)?;
12254 if encode_composite_key(&fk.columns, &child.columns).as_deref()
12255 != Some(old_key.as_slice())
12256 {
12257 continue;
12258 }
12259 let replacement = staging.iter().find_map(|(id, op)| {
12260 if *id != *child_id {
12261 return None;
12262 }
12263 match op {
12264 Staged::Delete(id) if *id == child.row_id => Some(None),
12265 Staged::Update {
12266 row_id,
12267 new_row: cells,
12268 ..
12269 } if *row_id == child.row_id => {
12270 let map: HashMap<u16, Value> =
12271 cells.iter().cloned().collect();
12272 Some(encode_composite_key(&fk.columns, &map))
12273 }
12274 _ => None,
12275 }
12276 });
12277 if replacement.is_some_and(|key| {
12278 key.as_deref() != Some(old_key.as_slice())
12279 }) {
12280 continue;
12281 }
12282 return Err(MongrelError::Conflict(format!(
12283 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
12284 fk.name
12285 )));
12286 }
12287 }
12288 }
12289 }
12290 }
12291 Staged::Delete(rid) => {
12292 let parent_handle = self.table_by_id(*table_id)?;
12296 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
12297 continue;
12298 };
12299 for (child_id, child_name, child_schema) in &live {
12300 for fk in &child_schema.constraints.foreign_keys {
12301 if fk.ref_table != *tname || fk.on_delete != FkAction::Restrict {
12302 continue;
12303 }
12304 let Some(parent_key) =
12305 encode_composite_key(&fk.ref_columns, &parent_row.columns)
12306 else {
12307 continue;
12308 };
12309 let child_rows = load_rows(*child_id)?;
12310 for (row_index, r) in child_rows.iter().enumerate() {
12311 commit_prepare_checkpoint(control, row_index)?;
12312 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
12315 continue;
12316 }
12317 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
12318 if ck == parent_key {
12319 return Err(MongrelError::Conflict(format!(
12320 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
12321 fk.name
12322 )));
12323 }
12324 }
12325 }
12326 }
12327 }
12328 }
12329 Staged::Truncate => {
12330 for (child_id, child_name, child_schema) in &live {
12334 for fk in &child_schema.constraints.foreign_keys {
12335 if fk.ref_table != *tname {
12336 continue;
12337 }
12338 let child_rows = load_rows(*child_id)?;
12339 if child_rows
12340 .iter()
12341 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
12342 {
12343 return Err(MongrelError::Conflict(format!(
12344 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
12345 fk.name
12346 )));
12347 }
12348 }
12349 }
12350 }
12351 }
12352 }
12353 Ok(())
12354 }
12355
12356 fn validate_write_permissions(
12357 &self,
12358 staging: &[(u64, crate::txn::Staged)],
12359 principal: Option<&crate::auth::Principal>,
12360 control: Option<&crate::ExecutionControl>,
12361 ) -> Result<()> {
12362 commit_prepare_checkpoint(control, 0)?;
12363 if principal.is_none() && !self.auth_state.require_auth() {
12364 return Ok(());
12365 }
12366 let principal = principal.ok_or(MongrelError::AuthRequired)?;
12367 let needs = summarize_write_permissions(staging);
12368 let catalog = self.catalog.read();
12369
12370 if needs.values().any(|need| need.truncate) {
12371 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
12372 }
12373 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
12374 commit_prepare_checkpoint(control, need_index)?;
12375 let entry = catalog
12376 .tables
12377 .iter()
12378 .find(|entry| {
12379 entry.table_id == table_id
12380 && matches!(entry.state, TableState::Live | TableState::Building { .. })
12381 })
12382 .ok_or_else(|| {
12383 MongrelError::NotFound(format!(
12384 "live table {table_id} not found during write validation"
12385 ))
12386 })?;
12387 if matches!(entry.state, TableState::Building { .. }) {
12388 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
12389 continue;
12390 }
12391 if need.insert {
12392 Self::require_columns_for_principal(
12393 &entry.name,
12394 &entry.schema,
12395 crate::auth::ColumnOperation::Insert,
12396 &need.insert_columns,
12397 principal,
12398 )?;
12399 }
12400 if need.update {
12401 Self::require_columns_for_principal(
12402 &entry.name,
12403 &entry.schema,
12404 crate::auth::ColumnOperation::Update,
12405 &need.update_columns,
12406 principal,
12407 )?;
12408 }
12409 if need.delete {
12410 self.require_for(
12411 Some(principal),
12412 &crate::auth::Permission::Delete {
12413 table: entry.name.clone(),
12414 },
12415 )?;
12416 }
12417 }
12418 Ok(())
12419 }
12420
12421 fn validate_security_writes(
12422 &self,
12423 staging: &[(u64, crate::txn::Staged)],
12424 read_epoch: Epoch,
12425 explicit_principal: Option<&crate::auth::Principal>,
12426 control: Option<&crate::ExecutionControl>,
12427 ) -> Result<()> {
12428 commit_prepare_checkpoint(control, 0)?;
12429 use crate::security::PolicyCommand;
12430 use crate::txn::Staged;
12431
12432 let catalog = self.catalog.read();
12433 if catalog.security.rls_tables.is_empty() {
12434 return Ok(());
12435 }
12436 let security = catalog.security.clone();
12437 let table_names = catalog
12438 .tables
12439 .iter()
12440 .filter(|entry| matches!(entry.state, TableState::Live))
12441 .map(|entry| (entry.table_id, entry.name.clone()))
12442 .collect::<HashMap<_, _>>();
12443 drop(catalog);
12444 if !staging.iter().any(|(table_id, _)| {
12445 table_names
12446 .get(table_id)
12447 .is_some_and(|table| security.rls_enabled(table))
12448 }) {
12449 return Ok(());
12450 }
12451 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
12452
12453 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
12454 commit_prepare_checkpoint(control, operation_index)?;
12455 let Some(table) = table_names.get(table_id) else {
12456 continue;
12457 };
12458 if !security.rls_enabled(table) || principal.is_admin {
12459 continue;
12460 }
12461 let denied = |command| MongrelError::PermissionDenied {
12462 required: match command {
12463 PolicyCommand::Insert => crate::auth::Permission::Insert {
12464 table: table.clone(),
12465 },
12466 PolicyCommand::Update => crate::auth::Permission::Update {
12467 table: table.clone(),
12468 },
12469 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
12470 crate::auth::Permission::Delete {
12471 table: table.clone(),
12472 }
12473 }
12474 },
12475 principal: principal.username.clone(),
12476 };
12477 match operation {
12478 Staged::Put(cells) => {
12479 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
12480 row.columns.extend(cells.iter().cloned());
12481 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
12482 return Err(denied(PolicyCommand::Insert));
12483 }
12484 }
12485 Staged::Update {
12486 row_id,
12487 new_row: cells,
12488 ..
12489 } => {
12490 let old = self
12491 .table_by_id(*table_id)?
12492 .lock()
12493 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12494 .ok_or_else(|| {
12495 MongrelError::NotFound(format!("row {} not found", row_id.0))
12496 })?;
12497 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
12498 return Err(denied(PolicyCommand::Update));
12499 }
12500 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
12501 new.columns.extend(cells.iter().cloned());
12502 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
12503 return Err(denied(PolicyCommand::Update));
12504 }
12505 }
12506 Staged::Delete(row_id) => {
12507 let old = self
12508 .table_by_id(*table_id)?
12509 .lock()
12510 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12511 .ok_or_else(|| {
12512 MongrelError::NotFound(format!("row {} not found", row_id.0))
12513 })?;
12514 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
12515 return Err(denied(PolicyCommand::Delete));
12516 }
12517 }
12518 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
12519 }
12520 }
12521 Ok(())
12522 }
12523
12524 #[allow(clippy::too_many_arguments)]
12531 pub(crate) fn commit_transaction_with_external_states(
12532 &self,
12533 txn_id: u64,
12534 read_epoch: Epoch,
12535 staging: Vec<(u64, crate::txn::Staged)>,
12536 external_states: Vec<(String, Vec<u8>)>,
12537 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12538 security_principal: Option<crate::auth::Principal>,
12539 principal_catalog_bound: bool,
12540 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12541 context: crate::txn::TxnCommitContext,
12542 ) -> Result<(Epoch, Vec<RowId>)> {
12543 self.commit_transaction_with_external_states_inner(
12544 txn_id,
12545 read_epoch,
12546 staging,
12547 external_states,
12548 materialized_view_updates,
12549 security_principal,
12550 principal_catalog_bound,
12551 external_trigger_bridge,
12552 context,
12553 None,
12554 None,
12555 )
12556 }
12557
12558 #[allow(clippy::too_many_arguments)]
12559 pub(crate) fn commit_transaction_with_external_states_controlled(
12560 &self,
12561 txn_id: u64,
12562 read_epoch: Epoch,
12563 staging: Vec<(u64, crate::txn::Staged)>,
12564 external_states: Vec<(String, Vec<u8>)>,
12565 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12566 security_principal: Option<crate::auth::Principal>,
12567 principal_catalog_bound: bool,
12568 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12569 context: crate::txn::TxnCommitContext,
12570 control: &crate::ExecutionControl,
12571 before_commit: &mut dyn FnMut() -> Result<()>,
12572 ) -> Result<(Epoch, Vec<RowId>)> {
12573 self.commit_transaction_with_external_states_inner(
12574 txn_id,
12575 read_epoch,
12576 staging,
12577 external_states,
12578 materialized_view_updates,
12579 security_principal,
12580 principal_catalog_bound,
12581 external_trigger_bridge,
12582 context,
12583 Some(control),
12584 Some(before_commit),
12585 )
12586 }
12587
12588 #[allow(clippy::too_many_arguments)]
12589 fn commit_transaction_with_external_states_inner(
12590 &self,
12591 txn_id: u64,
12592 read_epoch: Epoch,
12593 mut staging: Vec<(u64, crate::txn::Staged)>,
12594 external_states: Vec<(String, Vec<u8>)>,
12595 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12596 mut security_principal: Option<crate::auth::Principal>,
12597 principal_catalog_bound: bool,
12598 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12599 context: crate::txn::TxnCommitContext,
12600 control: Option<&crate::ExecutionControl>,
12601 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
12602 ) -> Result<(Epoch, Vec<RowId>)> {
12603 use crate::memtable::Row;
12604 use crate::txn::{Staged, StagedOp, WriteKey};
12605 use crate::wal::Op;
12606 use std::collections::hash_map::DefaultHasher;
12607 use std::hash::{Hash, Hasher};
12608 use std::sync::atomic::Ordering;
12609
12610 if txn_id == crate::wal::SYSTEM_TXN_ID {
12611 return Err(MongrelError::Full(
12612 "per-open transaction id namespace exhausted; reopen the database".into(),
12613 ));
12614 }
12615 if self.read_only {
12616 return Err(MongrelError::ReadOnlyReplica);
12617 }
12618 commit_prepare_checkpoint(control, 0)?;
12619 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
12620 self.refresh_security_catalog_if_stale(observed_security_version)?;
12621 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
12622 if self.auth_state.require_auth() && security_principal.is_none() {
12623 return Err(MongrelError::AuthRequired);
12624 }
12625 {
12626 let catalog = self.catalog.read();
12627 if principal_catalog_bound
12628 || security_principal
12629 .as_ref()
12630 .is_some_and(|principal| principal.user_id != 0)
12631 {
12632 let principal = security_principal
12633 .as_ref()
12634 .ok_or(MongrelError::AuthRequired)?;
12635 security_principal =
12636 Self::resolve_bound_principal_from_catalog(&catalog, principal);
12637 if security_principal.is_none() {
12638 return Err(MongrelError::AuthRequired);
12639 }
12640 }
12641 }
12642 let _replication_guard = self.replication_barrier.read();
12643 if self.poisoned.load(Ordering::Relaxed) {
12644 return Err(MongrelError::Other(
12645 "database poisoned by fsync error".into(),
12646 ));
12647 }
12648 let _operation = self.admit_operation()?;
12652
12653 if context.state.is_some() {
12662 self.acquire_txn_lock(
12663 txn_id,
12664 crate::locks::LockKey::schema_barrier(),
12665 crate::locks::LockMode::Shared,
12666 control,
12667 )?;
12668 }
12669 let _txn_lock_guard = TxnLockGuard {
12670 locks: &self.lock_manager,
12671 txn_id,
12672 };
12673
12674 let idempotency_request = match &context.idempotency {
12680 Some(request) => match self.idempotency.check_and_reserve(request)? {
12681 crate::txn::IdempotencyCheck::Replay(receipt) => {
12682 let epoch = Epoch(receipt.log_position.index);
12683 if let Some(state) = &context.state {
12684 state.committed(receipt);
12685 }
12686 return Ok((epoch, Vec::new()));
12687 }
12688 crate::txn::IdempotencyCheck::Reserved => Some(request.clone()),
12689 },
12690 None => None,
12691 };
12692 let mut idempotency_guard = idempotency_request.as_ref().map(|request| {
12694 crate::txn::IdempotencyReservationGuard::new(&self.idempotency, request.clone())
12695 });
12696 let mut external_states = dedup_external_states(external_states);
12697 if !external_states.is_empty() {
12698 let cat = self.catalog.read();
12699 for (name, _) in &external_states {
12700 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
12701 return Err(MongrelError::NotFound(format!(
12702 "external table {name:?} not found"
12703 )));
12704 }
12705 }
12706 }
12707 let prepared_materialized_views = {
12708 let mut deduplicated = HashMap::new();
12709 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
12710 {
12711 commit_prepare_checkpoint(control, definition_index)?;
12712 if definition.name.is_empty() || definition.query.trim().is_empty() {
12713 return Err(MongrelError::InvalidArgument(
12714 "materialized view name and query must not be empty".into(),
12715 ));
12716 }
12717 deduplicated.insert(definition.name.clone(), definition);
12718 }
12719 let catalog = self.catalog.read();
12720 let mut prepared = Vec::with_capacity(deduplicated.len());
12721 for (definition_index, definition) in deduplicated.into_values().enumerate() {
12722 commit_prepare_checkpoint(control, definition_index)?;
12723 let table_id = catalog
12724 .live(&definition.name)
12725 .ok_or_else(|| {
12726 MongrelError::NotFound(format!(
12727 "materialized view table {:?} not found",
12728 definition.name
12729 ))
12730 })?
12731 .table_id;
12732 prepared.push((table_id, definition));
12733 }
12734 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
12735 prepared
12736 };
12737
12738 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12741 self.expand_table_triggers(
12742 txn_id,
12743 &mut staging,
12744 read_epoch,
12745 external_trigger_bridge,
12746 &mut external_states,
12747 control,
12748 )?;
12749 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12750 external_states = dedup_external_states(external_states);
12751 let expected_external_generations = {
12752 let catalog = self.catalog.read();
12753 let mut generations = HashMap::with_capacity(external_states.len());
12754 for (name, _) in &external_states {
12755 let entry = catalog
12756 .external_tables
12757 .iter()
12758 .find(|entry| entry.name == *name)
12759 .ok_or_else(|| {
12760 MongrelError::NotFound(format!("external table {name:?} not found"))
12761 })?;
12762 generations.insert(name.clone(), entry.created_epoch);
12763 }
12764 generations
12765 };
12766
12767 self.acquire_unique_key_claims(txn_id, &staging, control)?;
12773 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
12777 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
12778 self.validate_constraints(
12784 txn_id,
12785 &mut staging,
12786 read_epoch,
12787 security_principal.as_ref(),
12788 control,
12789 )?;
12790 let mut normalized = Vec::with_capacity(staging.len() * 2);
12791 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
12792 commit_prepare_checkpoint(control, staged_index)?;
12793 match op {
12794 crate::txn::Staged::Update {
12795 row_id,
12796 new_row: cells,
12797 ..
12798 } => {
12799 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
12800 normalized.push((table_id, crate::txn::Staged::Put(cells)));
12801 }
12802 op => normalized.push((table_id, op)),
12803 }
12804 }
12805 staging = normalized;
12806 let has_changes = !staging.is_empty()
12807 || !external_states.is_empty()
12808 || !prepared_materialized_views.is_empty();
12809 let truncated_tables: HashSet<u64> = staging
12810 .iter()
12811 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
12812 .collect();
12813
12814 let write_keys = {
12815 let cat = self.catalog.read();
12816 let mut keys: Vec<WriteKey> = Vec::new();
12817 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12818 commit_prepare_checkpoint(control, staged_index)?;
12819 match staged {
12820 Staged::Put(cells) => {
12821 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
12822 for col in &entry.schema.columns {
12823 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
12824 if let Some((_, val)) =
12825 cells.iter().find(|(id, _)| *id == col.id)
12826 {
12827 let mut h = DefaultHasher::new();
12828 val.encode_key().hash(&mut h);
12829 keys.push(WriteKey::Unique {
12830 table_id: *table_id,
12831 index_id: 0,
12832 key_hash: h.finish(),
12833 });
12834 }
12835 }
12836 }
12837 for uc in &entry.schema.constraints.uniques {
12844 if let Some(key_bytes) = crate::constraint::encode_composite_key(
12845 &uc.columns,
12846 &cells.iter().cloned().collect(),
12847 ) {
12848 let mut h = DefaultHasher::new();
12849 key_bytes.hash(&mut h);
12850 keys.push(WriteKey::Unique {
12851 table_id: *table_id,
12852 index_id: uc.id | 0x8000,
12853 key_hash: h.finish(),
12854 });
12855 }
12856 }
12857 }
12858 }
12859 Staged::Delete(rid) => keys.push(WriteKey::Row {
12860 table_id: *table_id,
12861 row_id: rid.0,
12862 }),
12863 Staged::Truncate => keys.push(WriteKey::Table {
12864 table_id: *table_id,
12865 }),
12866 Staged::Update { .. } => {
12867 return Err(MongrelError::Other(
12868 "transaction contains an unnormalized update during preparation".into(),
12869 ));
12870 }
12871 }
12872 }
12873 for (external_index, (name, _)) in external_states.iter().enumerate() {
12874 commit_prepare_checkpoint(control, external_index)?;
12875 let mut h = DefaultHasher::new();
12876 name.hash(&mut h);
12877 keys.push(WriteKey::Unique {
12878 table_id: EXTERNAL_TABLE_ID,
12879 index_id: 0,
12880 key_hash: h.finish(),
12881 });
12882 }
12883 keys
12884 };
12885
12886 let min_active = self.active_txns.min_read_epoch();
12888 if min_active < u64::MAX {
12889 self.conflicts.prune_below(Epoch(min_active));
12890 }
12891
12892 let ssi_keys = match context.isolation.canonical() {
12899 crate::txn::IsolationLevel::Serializable => {
12900 crate::txn::ssi_validation_keys(&context.read_set, &context.predicate_set)
12901 }
12902 _ => Vec::new(),
12903 };
12904
12905 if self.conflicts.conflicts(&write_keys, read_epoch) {
12909 return Err(MongrelError::Conflict(
12910 "write-write conflict (pre-validate, first-committer-wins)".into(),
12911 ));
12912 }
12913 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
12914 return Err(MongrelError::SerializationFailure {
12915 message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
12916 .into(),
12917 });
12918 }
12919 let pre_validate_version = self.conflicts.version();
12920
12921 let mut spilled: Vec<SpilledRun> = Vec::new();
12925 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
12926 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
12930 {
12931 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
12932 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
12933 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12934 commit_prepare_checkpoint(control, staged_index)?;
12935 if let Staged::Put(cells) = staged {
12936 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
12937 bytes.saturating_add(value.estimated_bytes())
12938 });
12939 let table_bytes = table_bytes.entry(*table_id).or_default();
12940 *table_bytes = table_bytes.saturating_add(bytes);
12941 put_indexes.entry(*table_id).or_default().push(staged_index);
12942 }
12943 }
12944 let tables = self.tables.read();
12945 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
12946 commit_prepare_checkpoint(control, table_index)?;
12947 if bytes
12948 <= self
12949 .spill_threshold
12950 .load(std::sync::atomic::Ordering::Relaxed)
12951 {
12952 continue;
12953 }
12954 let Some(handle) = tables.get(&table_id) else {
12955 continue;
12956 };
12957 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
12958 let mut t = handle.lock();
12959 let tdir = t.table_dir().to_path_buf();
12960 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
12961 std::fs::create_dir_all(&txn_dir)?;
12962 let run_id = t.alloc_run_id()? as u128;
12963 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
12964 let final_path = t.run_path(run_id as u64);
12965
12966 let mut rows: Vec<Row> = Vec::new();
12967 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
12968 commit_prepare_checkpoint(control, put_index)?;
12969 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
12970 return Err(MongrelError::Other(
12971 "transaction put index no longer references a put".into(),
12972 ));
12973 };
12974 t.validate_cells_not_null(cells)?;
12975 let row_id = t.alloc_row_id()?;
12976 let mut row = Row::new(row_id, Epoch(0));
12977 row.columns.extend(std::mem::take(cells));
12978 rows.push(row);
12979 }
12980 let schema = t.schema_ref().clone();
12981 let kek = t.kek_ref().cloned();
12982 let specs = t.indexable_column_specs();
12983 drop(t);
12984
12985 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
12986 .uniform_epoch(true);
12987 if let Some(ref kek) = kek {
12988 writer = writer.with_encryption(kek.as_ref(), specs);
12989 }
12990 commit_prepare_checkpoint(control, 0)?;
12991 let header = writer.write(&pending_path, &rows)?;
12992 commit_prepare_checkpoint(control, 0)?;
12993 let row_count = header.row_count;
12994 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
12995 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
12996
12997 spilled.push(SpilledRun {
12998 table_id,
12999 run_id,
13000 pending_path,
13001 final_path,
13002 rows,
13003 row_count,
13004 min_rid,
13005 max_rid,
13006 content_hash: header.content_hash,
13007 });
13008 spilled_tables.insert(table_id);
13009 }
13010 }
13011
13012 if spill_guard.is_some() {
13014 if let Some(hook) = self.spill_hook.lock().as_ref() {
13015 hook();
13016 }
13017 }
13018
13019 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13030 .take(staging.len())
13031 .collect();
13032 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13033 .take(staging.len())
13034 .collect();
13035 {
13036 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
13037 for (index, (table_id, staged)) in staging.iter().enumerate() {
13038 commit_prepare_checkpoint(control, index)?;
13039 if matches!(staged, Staged::Delete(_))
13040 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
13041 {
13042 indexes_by_table.entry(*table_id).or_default().push(index);
13043 }
13044 }
13045 let tables = self.tables.read();
13046 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
13047 commit_prepare_checkpoint(control, table_index)?;
13048 let handle = tables.get(&table_id).ok_or_else(|| {
13049 MongrelError::NotFound(format!("table {table_id} not mounted"))
13050 })?;
13051 #[cfg(test)]
13052 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13053 let mut t = handle.lock();
13054 for (prepare_index, index) in indexes.into_iter().enumerate() {
13055 commit_prepare_checkpoint(control, prepare_index)?;
13056 match &staging[index].1 {
13057 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
13058 t.validate_cells_not_null(cells)?;
13059 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
13060 for (column, value) in cells {
13061 row.columns.insert(*column, value.clone());
13062 }
13063 prebuilt[index] = Some(row);
13064 }
13065 Staged::Delete(row_id) => {
13066 delete_images[index] =
13067 t.get(*row_id, self.snapshot_for_epoch(read_epoch));
13068 }
13069 Staged::Put(_) | Staged::Truncate => {}
13070 Staged::Update { .. } => {
13071 return Err(MongrelError::Other(
13072 "transaction contains an unnormalized update during row preparation"
13073 .into(),
13074 ));
13075 }
13076 }
13077 }
13078 }
13079 }
13080
13081 let prepared_table_handles = {
13085 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
13086 let put_table_ids: HashSet<u64> = staging
13087 .iter()
13088 .filter_map(|(table_id, staged)| {
13089 matches!(staged, Staged::Put(_)).then_some(*table_id)
13090 })
13091 .collect();
13092 let tables = self.tables.read();
13093 let mut handles = HashMap::with_capacity(table_ids.len());
13094 for (table_index, table_id) in table_ids.into_iter().enumerate() {
13095 commit_prepare_checkpoint(control, table_index)?;
13096 let handle = tables.get(&table_id).ok_or_else(|| {
13097 MongrelError::NotFound(format!("table {table_id} not mounted"))
13098 })?;
13099 if put_table_ids.contains(&table_id) {
13100 match control {
13101 Some(control) => {
13102 handle.lock().prepare_durable_publish_controlled(control)?
13103 }
13104 None => handle.lock().prepare_durable_publish()?,
13105 }
13106 }
13107 handles.insert(table_id, handle.clone());
13108 }
13109 handles
13110 };
13111
13112 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
13116
13117 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
13118 .iter()
13119 .map(|run| {
13120 (
13121 run.table_id,
13122 run.rows.iter().map(|row| row.row_id).collect(),
13123 )
13124 })
13125 .collect();
13126 let committed_row_ids = staging
13127 .iter()
13128 .enumerate()
13129 .filter_map(|(index, (table_id, staged))| {
13130 if !matches!(staged, Staged::Put(_)) {
13131 return None;
13132 }
13133 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
13134 spilled_row_ids
13135 .get_mut(table_id)
13136 .and_then(VecDeque::pop_front)
13137 })
13138 })
13139 .collect();
13140
13141 let mut prepared_external = Vec::with_capacity(external_states.len());
13142 for (external_index, (name, state)) in external_states.iter().enumerate() {
13143 commit_prepare_checkpoint(control, external_index)?;
13144 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
13145 prepared_external.push((name.clone(), state.clone(), pending));
13146 }
13147
13148 if context.state.is_some() {
13158 self.lock_manager
13159 .release(txn_id, &crate::locks::LockKey::schema_barrier());
13160 }
13161 let added_runs: Vec<crate::wal::AddedRun> = spilled
13162 .iter()
13163 .map(|s| crate::wal::AddedRun {
13164 table_id: s.table_id,
13165 run_id: s.run_id,
13166 row_count: s.row_count,
13167 level: 0,
13168 min_row_id: s.min_rid,
13169 max_row_id: s.max_rid,
13170 content_hash: s.content_hash,
13171 })
13172 .collect();
13173 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
13174 hook();
13175 }
13176 let security_guard = self.security_coordinator.gate.read();
13180 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
13181 return Err(MongrelError::Conflict(
13182 "security policy changed during write".into(),
13183 ));
13184 }
13185 if spill_guard.is_some() {
13186 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
13187 hook();
13188 }
13189 }
13190 let commit_guard = self.commit_lock.lock();
13191 let catalog_generation_result = (|| {
13192 {
13193 let catalog = self.catalog.read();
13194 for table_id in prepared_table_handles.keys() {
13195 let is_current = catalog.tables.iter().any(|entry| {
13196 entry.table_id == *table_id
13197 && matches!(entry.state, TableState::Live | TableState::Building { .. })
13198 });
13199 if !is_current {
13200 return Err(MongrelError::Conflict(format!(
13201 "table {table_id} changed during transaction preparation"
13202 )));
13203 }
13204 }
13205 for (name, created_epoch) in &expected_external_generations {
13206 let current = catalog
13207 .external_tables
13208 .iter()
13209 .find(|entry| entry.name == *name)
13210 .map(|entry| entry.created_epoch);
13211 if current != Some(*created_epoch) {
13212 return Err(MongrelError::Conflict(format!(
13213 "external table {name:?} changed during transaction preparation"
13214 )));
13215 }
13216 }
13217 for (table_id, definition) in &prepared_materialized_views {
13218 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
13219 if current != Some(*table_id) {
13220 return Err(MongrelError::Conflict(format!(
13221 "materialized view {:?} changed during transaction preparation",
13222 definition.name
13223 )));
13224 }
13225 }
13226 if trigger_catalog_binding(&catalog) != trigger_binding {
13227 return Err(MongrelError::Conflict(
13228 "trigger or referenced table generation changed during transaction preparation"
13229 .into(),
13230 ));
13231 }
13232 }
13233 let tables = self.tables.read();
13234 for (table_id, prepared) in &prepared_table_handles {
13235 if !tables
13236 .get(table_id)
13237 .is_some_and(|current| current.ptr_eq(prepared))
13238 {
13239 return Err(MongrelError::Conflict(format!(
13240 "table {table_id} mount changed during transaction preparation"
13241 )));
13242 }
13243 }
13244 Ok(())
13245 })();
13246 if let Err(error) = catalog_generation_result {
13247 drop(commit_guard);
13248 for (_, _, pending) in &prepared_external {
13249 let _ = std::fs::remove_file(pending);
13250 }
13251 return Err(error);
13252 }
13253 let new_epoch = self.epoch.assigned().next();
13257 let commit_ts = match context.read_ts {
13263 Some(read_ts) => self.hlc.commit_timestamp([read_ts]),
13264 None => self.hlc.now().map_err(|skew| {
13265 MongrelError::Other(format!(
13266 "clock skew rejected commit timestamp allocation: {skew}"
13267 ))
13268 })?,
13269 };
13270 let mut spilled_wal_bytes = 0;
13271 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
13272 let spill_prepare = (|| {
13273 for run in &mut spilled {
13274 for row in &mut run.rows {
13275 row.committed_epoch = new_epoch;
13276 row.commit_ts = Some(commit_ts);
13277 }
13278 for rows in encode_spilled_row_chunks(
13279 &run.rows,
13280 &mut spilled_wal_bytes,
13281 SPILLED_WAL_TOTAL_MAX_BYTES,
13282 control,
13283 )? {
13284 spilled_wal_records.push((
13285 run.table_id,
13286 Op::SpilledRows {
13287 table_id: run.table_id,
13288 rows,
13289 },
13290 ));
13291 }
13292 }
13293 Result::<()>::Ok(())
13294 })();
13295 if let Err(error) = spill_prepare {
13296 for (_, _, pending) in &prepared_external {
13297 let _ = std::fs::remove_file(pending);
13298 }
13299 return Err(error);
13300 }
13301 let (
13302 new_epoch,
13303 mut _epoch_guard,
13304 applies,
13305 committed_materialized_views,
13306 commit_seq,
13307 commit_ts,
13308 ) = {
13309 let mut wal = self.shared_wal.lock();
13310
13311 if self.conflicts.version() != pre_validate_version {
13316 if self.conflicts.conflicts(&write_keys, read_epoch) {
13317 drop(wal);
13320 for (_, _, pending) in &prepared_external {
13321 let _ = std::fs::remove_file(pending);
13322 }
13323 return Err(MongrelError::Conflict(
13324 "write-write conflict (sequencer delta re-check)".into(),
13325 ));
13326 }
13327 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
13328 drop(wal);
13332 for (_, _, pending) in &prepared_external {
13333 let _ = std::fs::remove_file(pending);
13334 }
13335 return Err(MongrelError::SerializationFailure {
13336 message:
13337 "a concurrent commit invalidated this transaction's reads (sequencer re-check)"
13338 .into(),
13339 });
13340 }
13341 }
13342
13343 if let Some(control) = control {
13344 if let Err(error) = control.checkpoint() {
13345 drop(wal);
13346 for (_, _, pending) in &prepared_external {
13347 let _ = std::fs::remove_file(pending);
13348 }
13349 return Err(error);
13350 }
13351 }
13352 let mut applies = Vec::<TableApplyBatch>::new();
13353 let mut apply_indexes = HashMap::<u64, usize>::new();
13354 let mut committed_materialized_views = Vec::new();
13355 let mut wal_records = spilled_wal_records;
13356
13357 let mut index = 0;
13358 while index < staging.len() {
13359 let table_id = staging[index].0;
13360 let handle = prepared_table_handles
13361 .get(&table_id)
13362 .cloned()
13363 .ok_or_else(|| {
13364 MongrelError::NotFound(format!("table {table_id} not prepared"))
13365 })?;
13366 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
13367 let index = applies.len();
13368 applies.push(TableApplyBatch {
13369 table_id,
13370 handle,
13371 ops: Vec::new(),
13372 });
13373 index
13374 });
13375
13376 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
13379 {
13380 index += 1;
13381 continue;
13382 }
13383
13384 match &staging[index].1 {
13385 Staged::Put(_) => {
13386 let mut rows = Vec::new();
13387 while index < staging.len()
13388 && staging[index].0 == table_id
13389 && matches!(&staging[index].1, Staged::Put(_))
13390 {
13391 let mut row = prebuilt[index].take().ok_or_else(|| {
13392 MongrelError::Other(
13393 "transaction prepare lost a prebuilt put row".into(),
13394 )
13395 })?;
13396 row.committed_epoch = new_epoch;
13397 row.commit_ts = Some(commit_ts);
13398 rows.push(row);
13399 index += 1;
13400 }
13401 let payload = bincode::serialize(&rows)
13402 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
13403 wal_records.push((
13404 table_id,
13405 Op::Put {
13406 table_id,
13407 rows: payload,
13408 },
13409 ));
13410 applies[batch_index].ops.push(StagedOp::Put(rows));
13411 }
13412 Staged::Delete(_) => {
13413 let mut row_ids = Vec::new();
13414 while index < staging.len()
13415 && staging[index].0 == table_id
13416 && matches!(&staging[index].1, Staged::Delete(_))
13417 {
13418 let Staged::Delete(row_id) = &staging[index].1 else {
13419 return Err(MongrelError::Other(
13420 "transaction delete batch changed during WAL preparation"
13421 .into(),
13422 ));
13423 };
13424 if let Some(before) = &delete_images[index] {
13425 wal_records.push((
13426 table_id,
13427 Op::BeforeImage {
13428 table_id,
13429 row_id: *row_id,
13430 row: bincode::serialize(before).map_err(|error| {
13431 MongrelError::Other(format!(
13432 "before-image serialize: {error}"
13433 ))
13434 })?,
13435 },
13436 ));
13437 }
13438 row_ids.push(*row_id);
13439 index += 1;
13440 }
13441 wal_records.push((
13442 table_id,
13443 Op::Delete {
13444 table_id,
13445 row_ids: row_ids.clone(),
13446 },
13447 ));
13448 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
13449 }
13450 Staged::Truncate => {
13451 wal_records.push((table_id, Op::TruncateTable { table_id }));
13452 applies[batch_index].ops.push(StagedOp::Truncate);
13453 index += 1;
13454 }
13455 Staged::Update { .. } => {
13456 return Err(MongrelError::Other(
13457 "transaction contains an unnormalized update at the sequencer".into(),
13458 ));
13459 }
13460 }
13461 }
13462
13463 for (name, state, _) in &prepared_external {
13464 wal_records.push((
13465 EXTERNAL_TABLE_ID,
13466 Op::ExternalTableState {
13467 name: name.clone(),
13468 state: state.clone(),
13469 },
13470 ));
13471 }
13472
13473 for (table_id, definition) in &prepared_materialized_views {
13474 let mut definition = definition.clone();
13475 definition.last_refresh_epoch = new_epoch.0;
13476 wal_records.push((
13477 *table_id,
13478 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
13479 name: definition.name.clone(),
13480 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
13481 }),
13482 ));
13483 committed_materialized_views.push(definition);
13484 }
13485 if !committed_materialized_views.is_empty() {
13486 let mut next_catalog = self.catalog.read().clone();
13487 for definition in &committed_materialized_views {
13488 if let Some(existing) = next_catalog
13489 .materialized_views
13490 .iter_mut()
13491 .find(|existing| existing.name == definition.name)
13492 {
13493 *existing = definition.clone();
13494 } else {
13495 next_catalog.materialized_views.push(definition.clone());
13496 }
13497 }
13498 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13499 wal_records.push((
13500 WAL_TABLE_ID,
13501 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
13502 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
13503 }),
13504 ));
13505 }
13506
13507 if let Some(control) = control {
13508 if let Err(error) = control.checkpoint() {
13509 drop(wal);
13510 for (_, _, pending) in &prepared_external {
13511 let _ = std::fs::remove_file(pending);
13512 }
13513 return Err(error);
13514 }
13515 }
13516 if let Some(before_commit) = before_commit.as_mut() {
13517 if let Err(error) = before_commit() {
13518 drop(wal);
13519 for (_, _, pending) in &prepared_external {
13520 let _ = std::fs::remove_file(pending);
13521 }
13522 return Err(error);
13523 }
13524 }
13525
13526 let assigned_epoch = self.epoch.bump_assigned();
13527 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
13528 if assigned_epoch != new_epoch {
13529 for (_, _, pending) in &prepared_external {
13530 let _ = std::fs::remove_file(pending);
13531 }
13532 return Err(MongrelError::Conflict(
13533 "commit epoch changed while sequencer lock was held".into(),
13534 ));
13535 }
13536
13537 if let Err(fault) = mongreldb_fault::inject("txn.decision.before") {
13544 for (_, _, pending) in &prepared_external {
13545 let _ = std::fs::remove_file(pending);
13546 }
13547 return Err(crate::commit_log::fault_as_io(fault));
13548 }
13549 if let Some(state) = &context.state {
13550 state.enter_commit_critical();
13551 }
13552
13553 prepared_run_links.disarm();
13557
13558 let commit_seq = self
13562 .standalone_commit_log
13563 .append_transaction(
13564 &mut wal,
13565 txn_id,
13566 new_epoch,
13567 commit_ts,
13568 wal_records,
13569 &added_runs,
13570 )
13571 .map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
13572
13573 self.conflicts.record(&write_keys, new_epoch);
13578 (
13579 new_epoch,
13580 _epoch_guard,
13581 applies,
13582 committed_materialized_views,
13583 commit_seq,
13584 commit_ts,
13585 )
13586 };
13587 drop(commit_guard);
13588
13589 let receipt =
13591 self.await_durable_commit_with_ts(txn_id, commit_seq, new_epoch, commit_ts)?;
13592 drop(security_guard);
13593
13594 if let Some(request) = &idempotency_request {
13598 if let Err(error) =
13599 self.idempotency
13600 .complete(request, txn_id, new_epoch, receipt.commit_ts)
13601 {
13602 return Err(MongrelError::DurableCommit {
13603 epoch: new_epoch.0,
13604 message: format!("idempotency record was not durable: {error}"),
13605 });
13606 }
13607 if let Some(guard) = idempotency_guard.as_mut() {
13608 guard.disarm();
13609 }
13610 }
13611
13612 let publish_result: Result<()> = {
13614 let mut first_error = None;
13615 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
13616 for run in &spilled {
13617 spilled_by_table.entry(run.table_id).or_default().push(run);
13618 }
13619 let mut modified_tables = Vec::with_capacity(applies.len());
13620 for batch in applies {
13623 #[cfg(test)]
13624 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13625 let mut t = batch.handle.lock();
13626 for op in batch.ops {
13627 match op {
13628 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
13629 StagedOp::Delete(row_ids) => {
13630 for row_id in row_ids {
13631 t.apply_delete_at(row_id, new_epoch, Some(commit_ts));
13632 }
13633 }
13634 StagedOp::Truncate => t.apply_truncate(new_epoch),
13635 }
13636 }
13637 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
13638 for run in runs {
13639 t.link_run(crate::manifest::RunRef {
13640 run_id: run.run_id,
13641 level: 0,
13642 epoch_created: new_epoch.0,
13643 row_count: run.row_count,
13644 });
13645 t.apply_run_metadata_prepared(&run.rows)?;
13646 if truncated_tables.contains(&batch.table_id) {
13647 t.set_flushed_epoch(new_epoch);
13652 }
13653 }
13654 }
13655 t.invalidate_pending_cache();
13656 drop(t);
13657 modified_tables.push(batch.handle);
13658 }
13659
13660 for handle in modified_tables {
13664 #[cfg(test)]
13665 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
13666 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
13667 first_error.get_or_insert(error);
13668 }
13669 }
13670 for (name, _, pending) in &prepared_external {
13671 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
13672 first_error.get_or_insert(error);
13673 }
13674 }
13675 if !committed_materialized_views.is_empty() {
13676 let mut next_catalog = self.catalog.read().clone();
13677 for definition in committed_materialized_views {
13678 if let Some(existing) = next_catalog
13679 .materialized_views
13680 .iter_mut()
13681 .find(|existing| existing.name == definition.name)
13682 {
13683 *existing = definition;
13684 } else {
13685 next_catalog.materialized_views.push(definition);
13686 }
13687 }
13688 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13689 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
13690 first_error.get_or_insert(error);
13691 }
13692 }
13693 match first_error {
13694 Some(error) => Err(error),
13695 None => Ok(()),
13696 }
13697 };
13698
13699 if has_changes {
13700 let _ = self.change_wake.send(());
13701 }
13702 self.finish_durable_publish(new_epoch, &mut _epoch_guard, &receipt, publish_result)?;
13703 if let Some(state) = &context.state {
13707 state.committed(receipt.clone());
13708 }
13709 mongreldb_fault::inject("txn.decision.after").map_err(|fault| {
13713 MongrelError::DurableCommit {
13714 epoch: new_epoch.0,
13715 message: fault.to_string(),
13716 }
13717 })?;
13718 Ok((new_epoch, committed_row_ids))
13719 }
13720
13721 pub fn visible_snapshot(&self) -> Snapshot {
13725 self.snapshot_for_epoch(self.epoch.visible())
13726 }
13727
13728 pub fn snapshot_for_epoch(&self, epoch: Epoch) -> Snapshot {
13733 let commit_ts = self
13734 .commit_ts_for_epoch(epoch)
13735 .filter(|ts| *ts != mongreldb_types::hlc::HlcTimestamp::ZERO)
13736 .or_else(|| self.hlc.now().ok())
13737 .unwrap_or(mongreldb_types::hlc::HlcTimestamp::MAX);
13738 Snapshot::at_hlc(epoch, commit_ts)
13739 }
13740
13741 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
13744 let snap = self.visible_snapshot();
13745 let g = self.snapshots.register(snap.epoch);
13746 (snap, g)
13747 }
13748
13749 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
13752 let snap = self.visible_snapshot();
13753 let g = self.snapshots.register_owned(snap.epoch);
13754 (snap, g)
13755 }
13756
13757 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
13762 let _guard = self.ddl_lock.lock();
13763 let current = self.epoch.visible();
13764 let (old_epochs, old_start) = self.snapshots.history_config();
13765 let earliest_already_guaranteed = if old_epochs == 0 {
13766 current
13767 } else {
13768 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
13769 };
13770 let start = if epochs == 0 {
13771 current
13772 } else {
13773 earliest_already_guaranteed
13774 };
13775 let published = std::cell::Cell::new(false);
13776 let result = write_history_retention(&self.root, epochs, start, || {
13777 self.snapshots.configure_history(epochs, start);
13778 published.set(true);
13779 });
13780 match result {
13781 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
13782 epoch: current.0,
13783 message: format!("history-retention publication was not durable: {error}"),
13784 }),
13785 result => result,
13786 }
13787 }
13788
13789 pub fn history_retention_epochs(&self) -> u64 {
13790 self.snapshots.history_config().0
13791 }
13792
13793 pub fn earliest_retained_epoch(&self) -> Epoch {
13794 let current = self.epoch.visible();
13795 self.snapshots.history_floor(current).unwrap_or(current)
13796 }
13797
13798 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
13803 let current = self.epoch.visible();
13804 if epoch > current {
13805 return Err(MongrelError::InvalidArgument(format!(
13806 "epoch {} is in the future; current epoch is {}",
13807 epoch.0, current.0
13808 )));
13809 }
13810 let earliest = self.earliest_retained_epoch();
13811 if epoch < earliest {
13812 return Err(MongrelError::InvalidArgument(format!(
13813 "epoch {} is no longer retained; earliest available epoch is {}",
13814 epoch.0, earliest.0
13815 )));
13816 }
13817 let guard = self.snapshots.register_owned(epoch);
13818 let snap = match self.commit_ts_for_epoch(epoch) {
13819 Some(commit_ts) => Snapshot::at_hlc(epoch, commit_ts),
13820 None => Snapshot::at(epoch),
13821 };
13822 Ok((snap, guard))
13823 }
13824
13825 pub fn table_names(&self) -> Vec<String> {
13827 self.catalog
13828 .read()
13829 .tables
13830 .iter()
13831 .filter(|t| matches!(t.state, TableState::Live))
13832 .map(|t| t.name.clone())
13833 .collect()
13834 }
13835
13836 pub fn close(&self) -> Result<()> {
13842 for name in self.table_names() {
13843 if let Ok(handle) = self.table(&name) {
13844 if let Err(e) = handle.lock().close() {
13845 eprintln!("[close] flush failed for {name}: {e}");
13846 }
13847 }
13848 }
13849 Ok(())
13850 }
13851
13852 pub fn compact(&self) -> Result<(usize, usize)> {
13859 self.require(&crate::auth::Permission::Ddl)?;
13860 let _operation = self.admit_operation()?;
13862 let mut compacted = 0;
13863 let mut skipped = 0;
13864 for name in self.table_names() {
13865 let Ok(handle) = self.table(&name) else {
13866 continue;
13867 };
13868 {
13869 let mut t = handle.lock();
13870 let before = t.run_count();
13871 if before < 2 && !t.should_compact() {
13872 skipped += 1;
13873 continue;
13874 }
13875 match t.compact() {
13876 Ok(()) => {
13877 let after = t.run_count();
13878 compacted += 1;
13879 eprintln!("[compact] {name}: {before} -> {after} runs");
13880 }
13881 Err(e) => {
13882 eprintln!("[compact] {name}: compaction failed: {e}");
13883 skipped += 1;
13884 }
13885 }
13886 }
13887 }
13888 Ok((compacted, skipped))
13889 }
13890
13891 pub fn compact_table(&self, name: &str) -> Result<bool> {
13894 self.require(&crate::auth::Permission::Ddl)?;
13895 let handle = self.table(name)?;
13896 let mut t = handle.lock();
13897 let before = t.run_count();
13898 if before < 2 {
13899 return Ok(false);
13900 }
13901 t.compact()?;
13902 Ok(t.run_count() < before)
13903 }
13904
13905 pub fn table(&self, name: &str) -> Result<TableHandle> {
13907 self.ensure_owner_process()?;
13908 let cat = self.catalog.read();
13909 let entry = cat
13910 .live(name)
13911 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
13912 let id = entry.table_id;
13913 drop(cat);
13914 self.tables
13915 .read()
13916 .get(&id)
13917 .cloned()
13918 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
13919 }
13920
13921 pub fn has_ttl_tables(&self) -> bool {
13924 self.tables
13925 .read()
13926 .values()
13927 .any(|table| table.lock().ttl().is_some())
13928 }
13929
13930 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
13933 self.tables
13934 .read()
13935 .get(&id)
13936 .cloned()
13937 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
13938 }
13939
13940 pub(crate) fn table_auto_inc_would_allocate(
13946 &self,
13947 table_id: u64,
13948 cells: &[(u16, Value)],
13949 ) -> bool {
13950 let catalog = self.catalog.read();
13951 let Some(entry) = catalog
13952 .tables
13953 .iter()
13954 .find(|entry| entry.table_id == table_id)
13955 else {
13956 return false;
13957 };
13958 let Some(column) = entry.schema.auto_increment_column() else {
13959 return false;
13960 };
13961 matches!(
13962 cells.iter().find(|(id, _)| *id == column.id),
13963 None | Some((_, Value::Null))
13964 )
13965 }
13966
13967 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
13973 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
13974 return Err(MongrelError::InvalidArgument(format!(
13975 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
13976 )));
13977 }
13978 self.create_table_with_state(name, schema, TableState::Live)
13979 }
13980
13981 #[doc(hidden)]
13983 pub fn create_building_table(
13984 &self,
13985 build_name: &str,
13986 intended_name: &str,
13987 query_id: &str,
13988 schema: Schema,
13989 ) -> Result<u64> {
13990 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13991 || intended_name.is_empty()
13992 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
13993 || query_id.is_empty()
13994 {
13995 return Err(MongrelError::InvalidArgument(
13996 "invalid CTAS building-table identity".into(),
13997 ));
13998 }
13999 self.create_table_with_state(
14000 build_name,
14001 schema,
14002 TableState::Building {
14003 intended_name: intended_name.to_string(),
14004 query_id: query_id.to_string(),
14005 created_at_unix_nanos: current_unix_nanos(),
14006 replaces_table_id: None,
14007 },
14008 )
14009 }
14010
14011 #[doc(hidden)]
14014 pub fn create_rebuilding_table(
14015 &self,
14016 build_name: &str,
14017 intended_name: &str,
14018 query_id: &str,
14019 schema: Schema,
14020 ) -> Result<u64> {
14021 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14022 || intended_name.is_empty()
14023 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14024 || query_id.is_empty()
14025 {
14026 return Err(MongrelError::InvalidArgument(
14027 "invalid rebuilding-table identity".into(),
14028 ));
14029 }
14030 let replaces_table_id = self
14031 .catalog
14032 .read()
14033 .live(intended_name)
14034 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
14035 .table_id;
14036 self.create_table_with_state(
14037 build_name,
14038 schema,
14039 TableState::Building {
14040 intended_name: intended_name.to_string(),
14041 query_id: query_id.to_string(),
14042 created_at_unix_nanos: current_unix_nanos(),
14043 replaces_table_id: Some(replaces_table_id),
14044 },
14045 )
14046 }
14047
14048 fn create_table_with_state(
14049 &self,
14050 name: &str,
14051 schema: Schema,
14052 state: TableState,
14053 ) -> Result<u64> {
14054 use crate::wal::DdlOp;
14055 use std::sync::atomic::Ordering;
14056
14057 self.require(&crate::auth::Permission::Ddl)?;
14058 if self.poisoned.load(Ordering::Relaxed) {
14059 return Err(MongrelError::Other(
14060 "database poisoned by fsync error".into(),
14061 ));
14062 }
14063 let _operation = self.admit_operation()?;
14065 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14066 let _g = self.ddl_lock.lock();
14067 let _security_write = self.security_write()?;
14068 self.require(&crate::auth::Permission::Ddl)?;
14069 {
14070 let cat = self.catalog.read();
14071 match &state {
14072 TableState::Live => {
14073 if cat.live(name).is_some() || cat.building_for(name).is_some() {
14074 return Err(MongrelError::InvalidArgument(format!(
14075 "table {name:?} already exists or is being built"
14076 )));
14077 }
14078 }
14079 TableState::Building {
14080 intended_name,
14081 replaces_table_id,
14082 ..
14083 } => {
14084 let target_matches = match replaces_table_id {
14085 Some(table_id) => cat
14086 .live(intended_name)
14087 .is_some_and(|entry| entry.table_id == *table_id),
14088 None => cat.live(intended_name).is_none(),
14089 };
14090 if !target_matches || cat.building_for(intended_name).is_some() {
14091 return Err(MongrelError::InvalidArgument(format!(
14092 "table {intended_name:?} changed or is already being built"
14093 )));
14094 }
14095 if cat.building(name).is_some() {
14096 return Err(MongrelError::InvalidArgument(format!(
14097 "building table {name:?} already exists"
14098 )));
14099 }
14100 }
14101 TableState::Dropped { .. } => {
14102 return Err(MongrelError::InvalidArgument(
14103 "cannot create a dropped table".into(),
14104 ));
14105 }
14106 }
14107 }
14108
14109 let commit_lock = Arc::clone(&self.commit_lock);
14114 let _c = commit_lock.lock();
14115 let live_create = matches!(state, TableState::Live);
14116 let legacy_table_id = if live_create {
14117 None
14118 } else {
14119 let mut cat = self.catalog.write();
14120 let id = cat.next_table_id;
14121 cat.next_table_id = id
14122 .checked_add(1)
14123 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
14124 Some(id)
14125 };
14126 let epoch = self.epoch.bump_assigned();
14127 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14128 let txn_id = self.alloc_txn_id()?;
14129
14130 let mut schema = schema;
14131 if let Some(table_id) = legacy_table_id {
14132 schema.schema_id = table_id;
14136 schema.validate_auto_increment()?;
14144 schema.validate_defaults()?;
14145 schema.validate_ai()?;
14146 for index in &schema.indexes {
14147 index.validate_options()?;
14148 }
14149 for constraint in &schema.constraints.checks {
14150 constraint.expr.validate()?;
14151 }
14152 }
14153
14154 let mut next_catalog = self.catalog.read().clone();
14155 let (table_id, schema) = match legacy_table_id {
14156 Some(table_id) => {
14157 next_catalog.tables.push(CatalogEntry {
14158 table_id,
14159 name: name.to_string(),
14160 schema: schema.clone(),
14161 state: state.clone(),
14162 created_epoch: epoch.0,
14163 });
14164 (table_id, schema)
14165 }
14166 None => {
14167 let command = crate::catalog_cmds::CatalogCommand::CreateTable {
14171 name: name.to_string(),
14172 schema,
14173 created_epoch: epoch.0,
14174 };
14175 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
14176 let crate::catalog_cmds::CatalogDelta::TableCreated { entry } = delta else {
14177 return Err(MongrelError::Other(
14178 "CreateTable resolved to an unexpected catalog delta".into(),
14179 ));
14180 };
14181 (entry.table_id, entry.schema)
14182 }
14183 };
14184 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14185
14186 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
14189 let canonical_tdir = self.root.join(&table_relative);
14190 let table_root = Arc::new(
14191 self.durable_root
14192 .create_directory_all_pinned(&table_relative)?,
14193 );
14194 let tdir = table_root.io_path()?;
14195 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
14196 let ctx = SharedCtx {
14197 root_guard: Some(table_root),
14198 epoch: Arc::clone(&self.epoch),
14199 page_cache: Arc::clone(&self.page_cache),
14200 decoded_cache: Arc::clone(&self.decoded_cache),
14201 snapshots: Arc::clone(&self.snapshots),
14202 kek: self.kek.clone(),
14203 commit_lock: Arc::clone(&self.commit_lock),
14204 shared: Some(crate::engine::SharedWalCtx {
14205 wal: Arc::clone(&self.shared_wal),
14206 group: Arc::clone(&self.group),
14207 poisoned: Arc::clone(&self.poisoned),
14208 txn_ids: Arc::clone(&self.next_txn_id),
14209 change_wake: self.change_wake.clone(),
14210 lifecycle: Arc::clone(&self.lifecycle),
14211 hlc: Arc::clone(&self.hlc),
14212 }),
14213 table_name: Some(name.to_string()),
14214 auth: self.table_auth_checker(),
14215 read_only: self.read_only,
14216 };
14217 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
14218
14219 let schema_json = DdlOp::encode_schema(&schema)?;
14222 let ddl = match &state {
14223 TableState::Live => DdlOp::CreateTable {
14224 table_id,
14225 name: name.to_string(),
14226 schema_json,
14227 },
14228 TableState::Building {
14229 intended_name,
14230 query_id,
14231 created_at_unix_nanos,
14232 replaces_table_id,
14233 } => match replaces_table_id {
14234 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
14235 table_id,
14236 build_name: name.to_string(),
14237 intended_name: intended_name.clone(),
14238 query_id: query_id.clone(),
14239 created_at_unix_nanos: *created_at_unix_nanos,
14240 replaces_table_id: *replaces_table_id,
14241 schema_json,
14242 },
14243 None => DdlOp::CreateBuildingTable {
14244 table_id,
14245 build_name: name.to_string(),
14246 intended_name: intended_name.clone(),
14247 query_id: query_id.clone(),
14248 created_at_unix_nanos: *created_at_unix_nanos,
14249 schema_json,
14250 },
14251 },
14252 TableState::Dropped { .. } => {
14253 return Err(MongrelError::InvalidArgument(
14254 "cannot create a table in dropped state".into(),
14255 ));
14256 }
14257 };
14258 let commit_seq = {
14259 let mut wal = self.shared_wal.lock();
14260 let append: Result<u64> = (|| {
14261 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
14262 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14263 wal.append_commit(txn_id, epoch, &[])
14264 })();
14265 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14266 };
14267 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14268 pending_table_dir.disarm();
14269
14270 self.tables
14273 .write()
14274 .insert(table_id, TableHandle::new(table));
14275 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14276 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14277 Ok(table_id)
14278 }
14279
14280 pub fn drop_table(&self, name: &str) -> Result<()> {
14282 self.drop_table_with_epoch(name).map(|_| ())
14283 }
14284
14285 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
14287 self.drop_table_with_state(name, false, None)
14288 }
14289
14290 pub fn drop_table_with_epoch_controlled<F>(
14291 &self,
14292 name: &str,
14293 mut before_commit: F,
14294 ) -> Result<Epoch>
14295 where
14296 F: FnMut() -> Result<()>,
14297 {
14298 self.drop_table_with_state(name, false, Some(&mut before_commit))
14299 }
14300
14301 #[doc(hidden)]
14303 pub fn discard_building_table(&self, name: &str) -> Result<()> {
14304 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
14305 return Err(MongrelError::InvalidArgument(
14306 "not a CTAS building table".into(),
14307 ));
14308 }
14309 self.drop_table_with_state(name, true, None).map(|_| ())
14310 }
14311
14312 fn drop_table_with_state(
14313 &self,
14314 name: &str,
14315 building: bool,
14316 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14317 ) -> Result<Epoch> {
14318 use crate::wal::DdlOp;
14319 use std::sync::atomic::Ordering;
14320
14321 self.require(&crate::auth::Permission::Ddl)?;
14322 if self.poisoned.load(Ordering::Relaxed) {
14323 return Err(MongrelError::Other(
14324 "database poisoned by fsync error".into(),
14325 ));
14326 }
14327 let _operation = self.admit_operation()?;
14329 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14330 let _g = self.ddl_lock.lock();
14331 let _security_write = self.security_write()?;
14332 self.require(&crate::auth::Permission::Ddl)?;
14333 let table_id = {
14334 let cat = self.catalog.read();
14335 if building {
14336 cat.building(name)
14337 } else {
14338 cat.live(name)
14339 }
14340 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
14341 .table_id
14342 };
14343
14344 let commit_lock = Arc::clone(&self.commit_lock);
14345 let _c = commit_lock.lock();
14346 let epoch = self.epoch.bump_assigned();
14347 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14348 let txn_id = self.alloc_txn_id()?;
14349 let mut next_catalog = self.catalog.read().clone();
14350 if building {
14351 let entry = next_catalog
14354 .tables
14355 .iter_mut()
14356 .find(|t| t.table_id == table_id)
14357 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14358 entry.state = TableState::Dropped { at_epoch: epoch.0 };
14359 next_catalog.triggers.retain(|trigger| {
14360 !matches!(
14361 &trigger.trigger.target,
14362 TriggerTarget::Table(target) if target == name
14363 )
14364 });
14365 next_catalog
14366 .materialized_views
14367 .retain(|definition| definition.name != name);
14368 next_catalog
14369 .security
14370 .rls_tables
14371 .retain(|table| table != name);
14372 next_catalog
14373 .security
14374 .policies
14375 .retain(|policy| policy.table != name);
14376 next_catalog
14377 .security
14378 .masks
14379 .retain(|mask| mask.table != name);
14380 for role in &mut next_catalog.roles {
14381 role.permissions
14382 .retain(|permission| permission_table(permission) != Some(name));
14383 }
14384 advance_security_version(&mut next_catalog)?;
14385 } else {
14386 let command = crate::catalog_cmds::CatalogCommand::DropTable {
14390 name: name.to_string(),
14391 at_epoch: epoch.0,
14392 };
14393 self.apply_catalog_command_to(&mut next_catalog, command)?;
14394 }
14395 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14396 let commit_seq = {
14397 let mut wal = self.shared_wal.lock();
14398 if let Some(before_commit) = before_commit {
14399 before_commit()?;
14400 }
14401 let append: Result<u64> = (|| {
14402 wal.append(
14403 txn_id,
14404 table_id,
14405 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
14406 )?;
14407 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14408 wal.append_commit(txn_id, epoch, &[])
14409 })();
14410 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14411 };
14412 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14413
14414 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14415 self.tables.write().remove(&table_id);
14416 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14417 Ok(epoch)
14418 }
14419
14420 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
14429 self.rename_table_with_epoch(name, new_name).map(|_| ())
14430 }
14431
14432 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
14434 self.rename_table_with_epoch_inner(name, new_name, None)
14435 }
14436
14437 pub fn rename_table_with_epoch_controlled<F>(
14438 &self,
14439 name: &str,
14440 new_name: &str,
14441 mut before_commit: F,
14442 ) -> Result<Epoch>
14443 where
14444 F: FnMut() -> Result<()>,
14445 {
14446 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
14447 }
14448
14449 fn rename_table_with_epoch_inner(
14450 &self,
14451 name: &str,
14452 new_name: &str,
14453 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14454 ) -> Result<Epoch> {
14455 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14456 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14457 {
14458 return Err(MongrelError::InvalidArgument(
14459 "the CTAS building-table namespace is reserved".into(),
14460 ));
14461 }
14462 self.rename_table_with_state(name, new_name, false, None, before_commit)
14463 }
14464
14465 #[doc(hidden)]
14467 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14468 self.publish_building_table_inner(build_name, new_name, None)
14469 }
14470
14471 #[doc(hidden)]
14472 pub fn publish_building_table_controlled<F>(
14473 &self,
14474 build_name: &str,
14475 new_name: &str,
14476 mut before_commit: F,
14477 ) -> Result<Epoch>
14478 where
14479 F: FnMut() -> Result<()>,
14480 {
14481 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
14482 }
14483
14484 fn publish_building_table_inner(
14485 &self,
14486 build_name: &str,
14487 new_name: &str,
14488 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14489 ) -> Result<Epoch> {
14490 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14491 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14492 {
14493 return Err(MongrelError::InvalidArgument(
14494 "invalid CTAS publish identity".into(),
14495 ));
14496 }
14497 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
14498 }
14499
14500 #[doc(hidden)]
14502 pub fn publish_materialized_building_table(
14503 &self,
14504 build_name: &str,
14505 new_name: &str,
14506 definition: crate::catalog::MaterializedViewEntry,
14507 ) -> Result<Epoch> {
14508 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
14509 }
14510
14511 #[doc(hidden)]
14512 pub fn publish_materialized_building_table_controlled<F>(
14513 &self,
14514 build_name: &str,
14515 new_name: &str,
14516 definition: crate::catalog::MaterializedViewEntry,
14517 mut before_commit: F,
14518 ) -> Result<Epoch>
14519 where
14520 F: FnMut() -> Result<()>,
14521 {
14522 self.publish_materialized_building_table_inner(
14523 build_name,
14524 new_name,
14525 definition,
14526 Some(&mut before_commit),
14527 )
14528 }
14529
14530 fn publish_materialized_building_table_inner(
14531 &self,
14532 build_name: &str,
14533 new_name: &str,
14534 definition: crate::catalog::MaterializedViewEntry,
14535 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14536 ) -> Result<Epoch> {
14537 if definition.name != new_name || definition.query.trim().is_empty() {
14538 return Err(MongrelError::InvalidArgument(
14539 "invalid materialized-view publication".into(),
14540 ));
14541 }
14542 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
14543 }
14544
14545 #[doc(hidden)]
14547 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14548 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
14549 }
14550
14551 #[doc(hidden)]
14552 pub fn publish_rebuilding_table_controlled<F>(
14553 &self,
14554 build_name: &str,
14555 new_name: &str,
14556 mut before_commit: F,
14557 ) -> Result<Epoch>
14558 where
14559 F: FnMut() -> Result<()>,
14560 {
14561 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
14562 }
14563
14564 #[doc(hidden)]
14566 pub fn publish_materialized_rebuilding_table_controlled<F>(
14567 &self,
14568 build_name: &str,
14569 new_name: &str,
14570 definition: crate::catalog::MaterializedViewEntry,
14571 mut before_commit: F,
14572 ) -> Result<Epoch>
14573 where
14574 F: FnMut() -> Result<()>,
14575 {
14576 self.publish_rebuilding_table_inner(
14577 build_name,
14578 new_name,
14579 Some(definition),
14580 Some(&mut before_commit),
14581 )
14582 }
14583
14584 fn publish_rebuilding_table_inner(
14585 &self,
14586 build_name: &str,
14587 new_name: &str,
14588 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14589 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14590 ) -> Result<Epoch> {
14591 use crate::wal::DdlOp;
14592
14593 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14594 || new_name.is_empty()
14595 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14596 {
14597 return Err(MongrelError::InvalidArgument(
14598 "invalid rebuilding-table publish identity".into(),
14599 ));
14600 }
14601 if materialized_view.as_ref().is_some_and(|definition| {
14602 definition.name != new_name || definition.query.trim().is_empty()
14603 }) {
14604 return Err(MongrelError::InvalidArgument(
14605 "invalid materialized-view replacement".into(),
14606 ));
14607 }
14608 self.require(&crate::auth::Permission::Ddl)?;
14609 if self.poisoned.load(Ordering::Relaxed) {
14610 return Err(MongrelError::Other(
14611 "database poisoned by fsync error".into(),
14612 ));
14613 }
14614 let _operation = self.admit_operation()?;
14616 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14617 let _ddl = self.ddl_lock.lock();
14618 let _security_write = self.security_write()?;
14619 let (table_id, replaced_table_id) = {
14620 let catalog = self.catalog.read();
14621 let build = catalog.building(build_name).ok_or_else(|| {
14622 MongrelError::NotFound(format!("building table {build_name:?} not found"))
14623 })?;
14624 let replaced_table_id = match &build.state {
14625 TableState::Building {
14626 intended_name,
14627 replaces_table_id: Some(replaced_table_id),
14628 ..
14629 } if intended_name == new_name => *replaced_table_id,
14630 _ => {
14631 return Err(MongrelError::InvalidArgument(format!(
14632 "building table {build_name:?} is not a replacement for {new_name:?}"
14633 )))
14634 }
14635 };
14636 if catalog
14637 .live(new_name)
14638 .is_none_or(|entry| entry.table_id != replaced_table_id)
14639 {
14640 return Err(MongrelError::Conflict(format!(
14641 "table {new_name:?} changed while its replacement was built"
14642 )));
14643 }
14644 (build.table_id, replaced_table_id)
14645 };
14646
14647 let _commit = self.commit_lock.lock();
14648 let epoch = self.epoch.assigned().next();
14649 let txn_id = self.alloc_txn_id()?;
14650 let mut next_catalog = self.catalog.read().clone();
14651 apply_rebuilding_publish(
14652 &mut next_catalog,
14653 table_id,
14654 replaced_table_id,
14655 new_name,
14656 epoch.0,
14657 )?;
14658 if let Some(definition) = materialized_view.as_mut() {
14659 definition.last_refresh_epoch = epoch.0;
14660 }
14661 let materialized_view_json = materialized_view
14662 .as_ref()
14663 .map(DdlOp::encode_materialized_view)
14664 .transpose()?;
14665 if let Some(definition) = materialized_view {
14666 if let Some(existing) = next_catalog
14667 .materialized_views
14668 .iter_mut()
14669 .find(|existing| existing.name == definition.name)
14670 {
14671 *existing = definition;
14672 } else {
14673 next_catalog.materialized_views.push(definition);
14674 }
14675 }
14676 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14677 if let Some(before_commit) = before_commit {
14678 before_commit()?;
14679 }
14680 let assigned_epoch = self.epoch.bump_assigned();
14681 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
14682 if assigned_epoch != epoch {
14683 return Err(MongrelError::Conflict(
14684 "commit epoch changed while sequencer lock was held".into(),
14685 ));
14686 }
14687 let commit_seq = {
14688 let mut wal = self.shared_wal.lock();
14689 let append: Result<u64> = (|| {
14690 wal.append(
14691 txn_id,
14692 table_id,
14693 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
14694 table_id,
14695 replaced_table_id,
14696 new_name: new_name.to_string(),
14697 }),
14698 )?;
14699 if let Some(definition_json) = materialized_view_json {
14700 wal.append(
14701 txn_id,
14702 table_id,
14703 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14704 name: new_name.to_string(),
14705 definition_json,
14706 }),
14707 )?;
14708 }
14709 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14710 wal.append_commit(txn_id, epoch, &[])
14711 })();
14712 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14713 };
14714 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14715
14716 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14717 self.tables.write().remove(&replaced_table_id);
14718 if let Some(table) = self.tables.read().get(&table_id) {
14719 table.lock().set_catalog_name(new_name.to_string());
14720 }
14721 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
14722 Ok(epoch)
14723 }
14724
14725 fn rename_table_with_state(
14726 &self,
14727 name: &str,
14728 new_name: &str,
14729 building: bool,
14730 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14731 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14732 ) -> Result<Epoch> {
14733 use crate::wal::DdlOp;
14734 use std::sync::atomic::Ordering;
14735
14736 self.require(&crate::auth::Permission::Ddl)?;
14737 if self.poisoned.load(Ordering::Relaxed) {
14738 return Err(MongrelError::Other(
14739 "database poisoned by fsync error".into(),
14740 ));
14741 }
14742
14743 if name == new_name {
14746 return Ok(self.visible_epoch());
14747 }
14748 if new_name.is_empty() {
14749 return Err(MongrelError::InvalidArgument(
14750 "rename_table: new name must not be empty".into(),
14751 ));
14752 }
14753 let _operation = self.admit_operation()?;
14755 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14756 let _g = self.ddl_lock.lock();
14757 let _security_write = self.security_write()?;
14758 self.require(&crate::auth::Permission::Ddl)?;
14759 let table_id = {
14760 let cat = self.catalog.read();
14761 let src = if building {
14762 cat.building(name)
14763 } else {
14764 cat.live(name)
14765 }
14766 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14767 if building
14768 && !matches!(
14769 &src.state,
14770 TableState::Building { intended_name, .. } if intended_name == new_name
14771 )
14772 {
14773 return Err(MongrelError::InvalidArgument(format!(
14774 "building table {name:?} is not reserved for {new_name:?}"
14775 )));
14776 }
14777 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
14781 return Err(MongrelError::InvalidArgument(format!(
14782 "rename_table: a table named {new_name:?} already exists"
14783 )));
14784 }
14785 src.table_id
14786 };
14787
14788 let commit_lock = Arc::clone(&self.commit_lock);
14789 let _c = commit_lock.lock();
14790 let epoch = self.epoch.bump_assigned();
14791 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14792 let txn_id = self.alloc_txn_id()?;
14793 if let Some(definition) = materialized_view.as_mut() {
14794 definition.last_refresh_epoch = epoch.0;
14795 }
14796 let materialized_view_json = materialized_view
14797 .as_ref()
14798 .map(DdlOp::encode_materialized_view)
14799 .transpose()?;
14800 let mut next_catalog = self.catalog.read().clone();
14801 if building {
14802 let entry = next_catalog
14805 .tables
14806 .iter_mut()
14807 .find(|t| t.table_id == table_id)
14808 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14809 entry.name = new_name.to_string();
14810 entry.state = TableState::Live;
14811 for trigger in &mut next_catalog.triggers {
14812 if matches!(
14813 &trigger.trigger.target,
14814 TriggerTarget::Table(target) if target == name
14815 ) {
14816 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
14817 }
14818 }
14819 if let Some(definition) = next_catalog
14820 .materialized_views
14821 .iter_mut()
14822 .find(|definition| definition.name == name)
14823 {
14824 definition.name = new_name.to_string();
14825 }
14826 if let Some(definition) = materialized_view.take() {
14827 next_catalog.materialized_views.push(definition);
14828 }
14829 for table in &mut next_catalog.security.rls_tables {
14830 if table == name {
14831 *table = new_name.to_string();
14832 }
14833 }
14834 for policy in &mut next_catalog.security.policies {
14835 if policy.table == name {
14836 policy.table = new_name.to_string();
14837 }
14838 }
14839 for mask in &mut next_catalog.security.masks {
14840 if mask.table == name {
14841 mask.table = new_name.to_string();
14842 }
14843 }
14844 for role in &mut next_catalog.roles {
14845 for permission in &mut role.permissions {
14846 rename_permission_table(permission, name, new_name);
14847 }
14848 }
14849 advance_security_version(&mut next_catalog)?;
14850 } else {
14851 let command = crate::catalog_cmds::CatalogCommand::RenameTable {
14855 name: name.to_string(),
14856 new_name: new_name.to_string(),
14857 at_epoch: epoch.0,
14858 };
14859 self.apply_catalog_command_to(&mut next_catalog, command)?;
14860 }
14861 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14862 let ddl = if building {
14863 DdlOp::PublishBuildingTable {
14864 table_id,
14865 new_name: new_name.to_string(),
14866 }
14867 } else {
14868 DdlOp::RenameTable {
14869 table_id,
14870 new_name: new_name.to_string(),
14871 }
14872 };
14873 let commit_seq = {
14874 let mut wal = self.shared_wal.lock();
14875 if let Some(before_commit) = before_commit {
14876 before_commit()?;
14877 }
14878 let append: Result<u64> = (|| {
14879 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
14880 if let Some(definition_json) = materialized_view_json {
14881 wal.append(
14882 txn_id,
14883 table_id,
14884 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14885 name: new_name.to_string(),
14886 definition_json,
14887 }),
14888 )?;
14889 }
14890 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14891 wal.append_commit(txn_id, epoch, &[])
14892 })();
14893 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14894 };
14895 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14896
14897 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14898 if let Some(table) = self.tables.read().get(&table_id) {
14901 table.lock().set_catalog_name(new_name.to_string());
14902 }
14903 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14904 Ok(epoch)
14905 }
14906
14907 pub fn add_column(
14913 &self,
14914 table_name: &str,
14915 name: &str,
14916 ty: TypeId,
14917 flags: ColumnFlags,
14918 default_value: Option<crate::schema::DefaultExpr>,
14919 ) -> Result<ColumnDef> {
14920 self.add_column_with_id(table_name, name, ty, flags, default_value, None)
14921 }
14922
14923 pub fn add_column_with_id(
14925 &self,
14926 table_name: &str,
14927 name: &str,
14928 ty: TypeId,
14929 flags: ColumnFlags,
14930 default_value: Option<crate::schema::DefaultExpr>,
14931 requested_id: Option<u16>,
14932 ) -> Result<ColumnDef> {
14933 use crate::wal::DdlOp;
14934 use std::sync::atomic::Ordering;
14935
14936 self.require(&crate::auth::Permission::Ddl)?;
14937 if self.poisoned.load(Ordering::Relaxed) {
14938 return Err(MongrelError::Other(
14939 "database poisoned by fsync error".into(),
14940 ));
14941 }
14942 let _operation = self.admit_operation()?;
14943 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14944 let _ddl = self.ddl_lock.lock();
14945 let _security_write = self.security_write()?;
14946 self.require(&crate::auth::Permission::Ddl)?;
14947 let table_id = {
14948 let catalog = self.catalog.read();
14949 catalog
14950 .live(table_name)
14951 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
14952 .table_id
14953 };
14954 let handle =
14955 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
14956 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
14957 })?;
14958 let durable_epoch = std::cell::Cell::new(None);
14959 let result: Result<ColumnDef> = (|| {
14960 let mut table = handle.lock();
14961 let (column, prepared_schema) =
14962 table.prepare_add_column(name, ty, flags, default_value, requested_id)?;
14963 let command = crate::catalog_cmds::CatalogCommand::AddColumn {
14964 table: table_name.to_string(),
14965 column: column.clone(),
14966 };
14967 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
14968
14969 let commit_lock = Arc::clone(&self.commit_lock);
14970 let _commit = commit_lock.lock();
14971 let epoch = self.epoch.bump_assigned();
14972 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14973 let txn_id = self.alloc_txn_id()?;
14974 let column_json = DdlOp::encode_column(&column)?;
14975 let mut next_catalog = self.catalog.read().clone();
14976 let catalog_entry_index = next_catalog
14977 .tables
14978 .iter()
14979 .position(|entry| entry.table_id == table_id)
14980 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
14981 self.apply_catalog_command_to(&mut next_catalog, command)?;
14982 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
14983 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14984 let commit_seq = {
14985 let mut wal = self.shared_wal.lock();
14986 let append: Result<u64> = (|| {
14987 wal.append(
14988 txn_id,
14989 table_id,
14990 crate::wal::Op::Ddl(DdlOp::AlterTable {
14991 table_id,
14992 column_json,
14993 }),
14994 )?;
14995 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14996 wal.append_commit(txn_id, epoch, &[])
14997 })();
14998 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14999 };
15000 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15001 durable_epoch.set(Some(epoch));
15002
15003 table.apply_altered_schema_prepared(prepared_schema);
15004 let schema = table.schema().clone();
15005 let table_checkpoint = table.checkpoint_altered_schema();
15006 drop(table);
15007 next_catalog.tables[catalog_entry_index].schema = schema;
15008 let catalog_result =
15009 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15010 *self.catalog.write() = next_catalog;
15011 self.publish_committed(&receipt, epoch)?;
15012 epoch_guard.disarm();
15013 if let Err(error) = table_checkpoint.and(catalog_result) {
15014 self.poisoned.store(true, Ordering::Relaxed);
15015 self.lifecycle.poison();
15016 return Err(MongrelError::DurableCommit {
15017 epoch: epoch.0,
15018 message: error.to_string(),
15019 });
15020 }
15021 Ok(column)
15022 })();
15023 result.map_err(|error| match (durable_epoch.get(), error) {
15024 (_, error @ MongrelError::DurableCommit { .. }) => error,
15025 (Some(epoch), error) => MongrelError::DurableCommit {
15026 epoch: epoch.0,
15027 message: error.to_string(),
15028 },
15029 (None, error) => error,
15030 })
15031 }
15032
15033 pub fn alter_column(
15034 &self,
15035 table_name: &str,
15036 column_name: &str,
15037 change: AlterColumn,
15038 ) -> Result<ColumnDef> {
15039 self.alter_column_with_epoch(table_name, column_name, change)
15040 .map(|(column, _)| column)
15041 }
15042
15043 pub fn alter_column_with_epoch(
15044 &self,
15045 table_name: &str,
15046 column_name: &str,
15047 change: AlterColumn,
15048 ) -> Result<(ColumnDef, Option<Epoch>)> {
15049 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
15050 }
15051
15052 pub fn alter_column_with_epoch_controlled<B, A>(
15057 &self,
15058 table_name: &str,
15059 column_name: &str,
15060 change: AlterColumn,
15061 control: &crate::ExecutionControl,
15062 mut before_commit: B,
15063 mut after_commit: A,
15064 ) -> Result<(ColumnDef, Option<Epoch>)>
15065 where
15066 B: FnMut() -> Result<()>,
15067 A: FnMut(Option<Epoch>) -> Result<()>,
15068 {
15069 self.alter_column_with_epoch_inner(
15070 table_name,
15071 column_name,
15072 change,
15073 Some(control),
15074 Some(&mut before_commit),
15075 Some(&mut after_commit),
15076 )
15077 }
15078
15079 #[allow(clippy::too_many_arguments)]
15080 fn alter_column_with_epoch_inner(
15081 &self,
15082 table_name: &str,
15083 column_name: &str,
15084 change: AlterColumn,
15085 control: Option<&crate::ExecutionControl>,
15086 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
15087 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
15088 ) -> Result<(ColumnDef, Option<Epoch>)> {
15089 use crate::wal::DdlOp;
15090 use std::sync::atomic::Ordering;
15091
15092 self.require(&crate::auth::Permission::Ddl)?;
15093 commit_prepare_checkpoint(control, 0)?;
15094 if self.poisoned.load(Ordering::Relaxed) {
15095 return Err(MongrelError::Other(
15096 "database poisoned by fsync error".into(),
15097 ));
15098 }
15099 let _operation = self.admit_operation()?;
15101 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
15102 let _g = self.ddl_lock.lock();
15103 let table_id = {
15104 let cat = self.catalog.read();
15105 cat.live(table_name)
15106 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15107 .table_id
15108 };
15109 let handle =
15110 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15111 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15112 })?;
15113
15114 let backfill = {
15120 let table = handle.lock();
15121 let old = table
15122 .schema()
15123 .column(column_name)
15124 .cloned()
15125 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
15126 let next_flags = change.flags.unwrap_or(old.flags);
15127 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
15128 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
15129 && old.default_value.is_some()
15130 {
15131 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
15132 let mut updates = Vec::new();
15133 let rows = match control {
15134 Some(control) => table.visible_rows_controlled(snapshot, control)?,
15135 None => table.visible_rows(snapshot)?,
15136 };
15137 for (row_index, row) in rows.into_iter().enumerate() {
15138 commit_prepare_checkpoint(control, row_index)?;
15139 if row
15140 .columns
15141 .get(&old.id)
15142 .is_some_and(|value| !matches!(value, Value::Null))
15143 {
15144 continue;
15145 }
15146 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
15147 table.apply_defaults(&mut cells)?;
15148 updates.push((
15149 table_id,
15150 crate::txn::Staged::Update {
15151 row_id: row.row_id,
15152 new_row: cells,
15153 changed_columns: vec![old.id],
15154 },
15155 ));
15156 }
15157 updates
15158 } else {
15159 Vec::new()
15160 }
15161 };
15162 let durable_epoch = std::cell::Cell::new(None);
15163 let backfill_epoch = if backfill.is_empty() {
15164 None
15165 } else {
15166 let (principal, catalog_bound) = self.transaction_principal_snapshot();
15167 let txn_id = self.alloc_txn_id()?;
15168 let mut entered_fence = false;
15169 let commit_result = match (control, before_commit.as_deref_mut()) {
15170 (Some(control), Some(before_commit)) => self
15171 .commit_transaction_with_external_states_controlled(
15172 txn_id,
15173 self.epoch.visible(),
15174 backfill,
15175 Vec::new(),
15176 Vec::new(),
15177 principal.clone(),
15178 catalog_bound,
15179 None,
15180 crate::txn::TxnCommitContext::internal(),
15181 control,
15182 &mut || {
15183 before_commit()?;
15184 entered_fence = true;
15185 Ok(())
15186 },
15187 )
15188 .map(|(epoch, _)| epoch),
15189 _ => self
15190 .commit_transaction_with_external_states(
15191 txn_id,
15192 self.epoch.visible(),
15193 backfill,
15194 Vec::new(),
15195 Vec::new(),
15196 principal,
15197 catalog_bound,
15198 None,
15199 crate::txn::TxnCommitContext::internal(),
15200 )
15201 .map(|(epoch, _)| epoch),
15202 };
15203 let commit_result = if entered_fence {
15204 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15205 } else {
15206 commit_result
15207 };
15208 match &commit_result {
15209 Ok(epoch) => durable_epoch.set(Some(*epoch)),
15210 Err(MongrelError::DurableCommit { epoch, .. }) => {
15211 durable_epoch.set(Some(Epoch(*epoch)));
15212 }
15213 Err(_) => {}
15214 }
15215 Some(commit_result?)
15216 };
15217 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
15218 let _security_write = self.security_write()?;
15219 self.require(&crate::auth::Permission::Ddl)?;
15220 if self
15221 .catalog
15222 .read()
15223 .live(table_name)
15224 .is_none_or(|entry| entry.table_id != table_id)
15225 {
15226 return Err(MongrelError::Conflict(format!(
15227 "table {table_name:?} changed during ALTER"
15228 )));
15229 }
15230 let mut table = handle.lock();
15231 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
15232 let renamed_column = (column.name != column_name).then(|| column.name.clone());
15233 let Some(prepared_schema) = prepared_schema else {
15234 return Ok((column, backfill_epoch));
15235 };
15236 let command = crate::catalog_cmds::CatalogCommand::AlterColumn {
15241 table: table_name.to_string(),
15242 column: column.clone(),
15243 };
15244 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
15245
15246 let commit_lock = Arc::clone(&self.commit_lock);
15247 let _c = commit_lock.lock();
15248 let epoch = self.epoch.bump_assigned();
15249 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15250 let txn_id = self.alloc_txn_id()?;
15251 let column_json = DdlOp::encode_column(&column)?;
15252 let mut next_catalog = self.catalog.read().clone();
15253 let catalog_entry_index = next_catalog
15254 .tables
15255 .iter()
15256 .position(|entry| entry.table_id == table_id)
15257 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
15258 if let Some(new_column_name) = &renamed_column {
15259 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
15260 commit_prepare_checkpoint(control, trigger_index)?;
15261 if matches!(
15262 &trigger.trigger.target,
15263 TriggerTarget::Table(target) if target == table_name
15264 ) {
15265 trigger.trigger = trigger.trigger.renamed_update_column(
15266 column_name,
15267 new_column_name.clone(),
15268 epoch.0,
15269 )?;
15270 }
15271 }
15272 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
15273 commit_prepare_checkpoint(control, role_index)?;
15274 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
15275 commit_prepare_checkpoint(control, permission_index)?;
15276 rename_permission_column(
15277 permission,
15278 table_name,
15279 column_name,
15280 new_column_name,
15281 );
15282 }
15283 }
15284 advance_security_version(&mut next_catalog)?;
15285 }
15286 self.apply_catalog_command_to(&mut next_catalog, command)?;
15292 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
15293 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15294 commit_prepare_checkpoint(control, 0)?;
15295 let mut entered_fence = false;
15296 if let Some(before_commit) = before_commit.as_deref_mut() {
15297 before_commit()?;
15298 entered_fence = true;
15299 }
15300 let commit_result: Result<Epoch> = (|| {
15301 let commit_seq = {
15302 let mut wal = self.shared_wal.lock();
15303 let append: Result<u64> = (|| {
15304 wal.append(
15305 txn_id,
15306 table_id,
15307 crate::wal::Op::Ddl(DdlOp::AlterTable {
15308 table_id,
15309 column_json,
15310 }),
15311 )?;
15312 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15313 wal.append_commit(txn_id, epoch, &[])
15314 })();
15315 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15316 };
15317 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15318 durable_epoch.set(Some(epoch));
15319
15320 table.apply_altered_schema_prepared(prepared_schema);
15321 let schema = table.schema().clone();
15322 let table_checkpoint = table.checkpoint_altered_schema();
15323 drop(table);
15324 next_catalog.tables[catalog_entry_index].schema = schema;
15325 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15326 let catalog_result =
15327 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15328 let security_version = next_catalog.security_version;
15329 *self.catalog.write() = next_catalog;
15330 if renamed_column.is_some() {
15331 self.security_coordinator
15332 .version
15333 .store(security_version, Ordering::Release);
15334 }
15335 self.publish_committed(&receipt, epoch)?;
15336 _epoch_guard.disarm();
15337 if let Err(error) = table_checkpoint.and(catalog_result) {
15338 self.poisoned.store(true, Ordering::Relaxed);
15339 self.lifecycle.poison();
15340 return Err(MongrelError::DurableCommit {
15341 epoch: epoch.0,
15342 message: error.to_string(),
15343 });
15344 }
15345 Ok(epoch)
15346 })();
15347 let commit_result = if entered_fence {
15348 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15349 } else {
15350 commit_result
15351 };
15352 let epoch = commit_result?;
15353 Ok((column, Some(epoch)))
15354 })();
15355 result.map_err(|error| match (durable_epoch.get(), error) {
15356 (_, error @ MongrelError::DurableCommit { .. }) => error,
15357 (Some(epoch), error) => MongrelError::DurableCommit {
15358 epoch: epoch.0,
15359 message: error.to_string(),
15360 },
15361 (None, error) => error,
15362 })
15363 }
15364
15365 pub fn set_table_ttl(
15368 &self,
15369 table_name: &str,
15370 column_name: &str,
15371 duration_nanos: u64,
15372 ) -> Result<crate::manifest::TtlPolicy> {
15373 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
15374 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
15375 }
15376
15377 #[doc(hidden)]
15379 pub fn set_building_table_ttl(
15380 &self,
15381 table_name: &str,
15382 column_name: &str,
15383 duration_nanos: u64,
15384 ) -> Result<crate::manifest::TtlPolicy> {
15385 let policy = self.replace_table_ttl_with_state(
15386 table_name,
15387 Some((column_name, duration_nanos)),
15388 true,
15389 )?;
15390 policy
15391 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
15392 }
15393
15394 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
15395 self.replace_table_ttl(table_name, None)?;
15396 Ok(())
15397 }
15398
15399 fn replace_table_ttl(
15400 &self,
15401 table_name: &str,
15402 requested: Option<(&str, u64)>,
15403 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15404 self.replace_table_ttl_with_state(table_name, requested, false)
15405 }
15406
15407 fn replace_table_ttl_with_state(
15408 &self,
15409 table_name: &str,
15410 requested: Option<(&str, u64)>,
15411 building: bool,
15412 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15413 use crate::wal::DdlOp;
15414 use std::sync::atomic::Ordering;
15415
15416 self.require(&crate::auth::Permission::Ddl)?;
15417 if self.poisoned.load(Ordering::Relaxed) {
15418 return Err(MongrelError::Other(
15419 "database poisoned by fsync error".into(),
15420 ));
15421 }
15422
15423 let _g = self.ddl_lock.lock();
15424 let _security_write = self.security_write()?;
15425 self.require(&crate::auth::Permission::Ddl)?;
15426 let table_id = {
15427 let cat = self.catalog.read();
15428 if building {
15429 cat.building(table_name)
15430 } else {
15431 cat.live(table_name)
15432 }
15433 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15434 .table_id
15435 };
15436 let handle =
15437 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15438 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15439 })?;
15440 let mut table = handle.lock();
15441 let policy = match requested {
15442 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
15443 None => None,
15444 };
15445 if table.ttl() == policy {
15446 return Ok(policy);
15447 }
15448
15449 let commit_lock = Arc::clone(&self.commit_lock);
15450 let _c = commit_lock.lock();
15451 let epoch = self.epoch.bump_assigned();
15452 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15453 let txn_id = self.alloc_txn_id()?;
15454 let policy_json = DdlOp::encode_ttl(policy)?;
15455 let mut next_catalog = self.catalog.read().clone();
15456 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15457 let commit_seq = {
15458 let mut wal = self.shared_wal.lock();
15459 let append: Result<u64> = (|| {
15460 wal.append(
15461 txn_id,
15462 table_id,
15463 crate::wal::Op::Ddl(DdlOp::SetTtl {
15464 table_id,
15465 policy_json,
15466 }),
15467 )?;
15468 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15469 wal.append_commit(txn_id, epoch, &[])
15470 })();
15471 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15472 };
15473 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15474
15475 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
15476 drop(table);
15477 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
15478 publish_error.get_or_insert(error);
15479 }
15480 self.finish_durable_publish(
15481 epoch,
15482 &mut _epoch_guard,
15483 &receipt,
15484 publish_error.map_or(Ok(()), Err),
15485 )?;
15486 Ok(policy)
15487 }
15488
15489 pub fn gc(&self) -> Result<usize> {
15495 let control = crate::ExecutionControl::new(None);
15496 self.gc_controlled(&control, || true)
15497 }
15498
15499 #[doc(hidden)]
15502 pub fn gc_controlled<F>(
15503 &self,
15504 control: &crate::ExecutionControl,
15505 before_publish: F,
15506 ) -> Result<usize>
15507 where
15508 F: FnOnce() -> bool,
15509 {
15510 self.gc_controlled_with_receipt(control, before_publish)
15511 .map(|(reclaimed, _)| reclaimed)
15512 }
15513
15514 #[doc(hidden)]
15517 pub fn gc_controlled_with_receipt<F>(
15518 &self,
15519 control: &crate::ExecutionControl,
15520 before_publish: F,
15521 ) -> Result<(usize, Option<MaintenanceReceipt>)>
15522 where
15523 F: FnOnce() -> bool,
15524 {
15525 enum Candidate {
15526 Directory(PathBuf),
15527 File(PathBuf),
15528 }
15529
15530 self.require(&crate::auth::Permission::Ddl)?;
15531 let _operation = self.admit_operation()?;
15533 let _ddl = self.ddl_lock.lock();
15534 self.require(&crate::auth::Permission::Ddl)?;
15535 control.checkpoint()?;
15536 let maintenance_epoch = self.epoch.visible();
15537 let min_active = self.snapshots.min_active(maintenance_epoch).0;
15538 let mut candidates = Vec::new();
15539
15540 let cat = self.catalog.read();
15542 for (entry_index, entry) in cat.tables.iter().enumerate() {
15543 if entry_index % 256 == 0 {
15544 control.checkpoint()?;
15545 }
15546 if let TableState::Dropped { at_epoch } = entry.state {
15547 if at_epoch <= min_active {
15548 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
15549 if tdir.exists() {
15550 candidates.push(Candidate::Directory(tdir));
15551 }
15552 }
15553 }
15554 }
15555 drop(cat);
15556
15557 let cat = self.catalog.read();
15562 for (entry_index, entry) in cat.tables.iter().enumerate() {
15563 if entry_index % 256 == 0 {
15564 control.checkpoint()?;
15565 }
15566 if !matches!(entry.state, TableState::Live) {
15567 continue;
15568 }
15569 let txn_dir = self
15570 .root
15571 .join(TABLES_DIR)
15572 .join(entry.table_id.to_string())
15573 .join("_txn");
15574 if !txn_dir.exists() {
15575 continue;
15576 }
15577 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
15578 if sub_index % 256 == 0 {
15579 control.checkpoint()?;
15580 }
15581 let sub = sub?;
15582 let name = sub.file_name();
15583 let Some(name) = name.to_str() else { continue };
15584 let is_active = name
15586 .parse::<u64>()
15587 .map(|id| self.active_spills.is_active(id))
15588 .unwrap_or(false);
15589 if is_active {
15590 continue;
15591 }
15592 candidates.push(Candidate::Directory(sub.path()));
15593 }
15594 }
15595 drop(cat);
15596
15597 let external_names = {
15598 let cat = self.catalog.read();
15599 cat.external_tables
15600 .iter()
15601 .map(|entry| entry.name.clone())
15602 .collect::<std::collections::HashSet<_>>()
15603 };
15604 let vtab_dir = self.root.join(VTAB_DIR);
15605 if vtab_dir.exists() {
15606 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
15607 if entry_index % 256 == 0 {
15608 control.checkpoint()?;
15609 }
15610 let entry = entry?;
15611 let name = entry.file_name();
15612 let Some(name) = name.to_str() else { continue };
15613 if external_names.contains(name) {
15614 continue;
15615 }
15616 let path = entry.path();
15617 if path.is_dir() {
15618 candidates.push(Candidate::Directory(path));
15619 } else {
15620 candidates.push(Candidate::File(path));
15621 }
15622 }
15623 }
15624
15625 let tables = self
15629 .tables
15630 .read()
15631 .iter()
15632 .map(|(table_id, handle)| (*table_id, handle.clone()))
15633 .collect::<Vec<_>>();
15634 let mut retiring = Vec::new();
15635 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
15636 if table_index % 256 == 0 {
15637 control.checkpoint()?;
15638 }
15639 let backup_pinned: HashSet<u128> = self
15640 .backup_pins
15641 .lock()
15642 .keys()
15643 .filter_map(|(pinned_table, run_id)| {
15644 (*pinned_table == *table_id).then_some(*run_id)
15645 })
15646 .collect();
15647 if handle
15648 .lock()
15649 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
15650 {
15651 retiring.push((handle.clone(), backup_pinned));
15652 }
15653 }
15654
15655 let all_durable = self.active_spills.is_idle()
15663 && tables.iter().all(|(_, handle)| {
15664 let g = handle.lock();
15665 g.memtable_len() == 0 && g.mutable_run_len() == 0
15666 });
15667 let retain = self
15668 .replication_wal_retention_segments
15669 .load(std::sync::atomic::Ordering::Relaxed);
15670 let reap_wal = all_durable
15671 && self
15672 .shared_wal
15673 .lock()
15674 .has_gc_segments_retain_recent(retain)?;
15675
15676 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
15677 return Ok((0, None));
15678 }
15679 control.checkpoint()?;
15680 if !before_publish() {
15681 return Err(MongrelError::Cancelled);
15682 }
15683
15684 let mut reclaimed = 0;
15685 for candidate in candidates {
15686 match candidate {
15687 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
15688 Candidate::File(path) => std::fs::remove_file(path)?,
15689 }
15690 reclaimed += 1;
15691 }
15692 for (handle, backup_pinned) in retiring {
15693 reclaimed += handle
15694 .lock()
15695 .reap_retiring(Epoch(min_active), &backup_pinned)?;
15696 }
15697 if reap_wal {
15698 reclaimed += self
15699 .shared_wal
15700 .lock()
15701 .gc_segments_retain_recent(u64::MAX, retain)?;
15702 }
15703
15704 Ok((
15705 reclaimed,
15706 Some(MaintenanceReceipt {
15707 epoch: maintenance_epoch,
15708 }),
15709 ))
15710 }
15711
15712 pub fn checkpoint(&self) -> Result<()> {
15730 self.checkpoint_controlled(|| Ok(()))
15731 }
15732
15733 #[doc(hidden)]
15736 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
15737 where
15738 F: FnOnce() -> Result<()>,
15739 {
15740 self.require(&crate::auth::Permission::Ddl)?;
15741 let _operation = self.admit_operation()?;
15743 let _replication = self.replication_barrier.write();
15747 let _ddl = self.ddl_lock.lock();
15748 let _security = self.security_coordinator.gate.read();
15749 self.require(&crate::auth::Permission::Ddl)?;
15750
15751 let mut handles = self
15752 .tables
15753 .read()
15754 .iter()
15755 .map(|(table_id, handle)| (*table_id, handle.clone()))
15756 .collect::<Vec<_>>();
15757 handles.sort_by_key(|(table_id, _)| *table_id);
15758 let mut tables = handles
15759 .iter()
15760 .map(|(table_id, handle)| (*table_id, handle.lock()))
15761 .collect::<Vec<_>>();
15762
15763 for (_, table) in &mut tables {
15765 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
15766 {
15767 table.force_flush()?;
15768 }
15769 }
15770
15771 for (_, table) in &mut tables {
15774 if table.run_count() >= 2 || table.should_compact() {
15775 table.compact()?;
15776 }
15777 }
15778
15779 before_wal_reset()?;
15780
15781 let maintenance_epoch = self.epoch.visible();
15783 let min_active = self.snapshots.min_active(maintenance_epoch);
15784 for (table_id, table) in &mut tables {
15785 let backup_pinned: HashSet<u128> = self
15786 .backup_pins
15787 .lock()
15788 .keys()
15789 .filter_map(|(pinned_table, run_id)| {
15790 (*pinned_table == *table_id).then_some(*run_id)
15791 })
15792 .collect();
15793 table.reap_retiring(min_active, &backup_pinned)?;
15794 }
15795
15796 self.shared_wal.lock().reset_after_checkpoint()?;
15799
15800 let catalog_snapshot = self.catalog.read().clone();
15802 for entry in &catalog_snapshot.tables {
15803 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
15804 crate::durable_file::remove_directory_all(
15805 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
15806 )?;
15807 }
15808 if !matches!(entry.state, TableState::Live) {
15809 continue;
15810 }
15811 let transaction_dir = self
15812 .root
15813 .join(TABLES_DIR)
15814 .join(entry.table_id.to_string())
15815 .join("_txn");
15816 if transaction_dir.is_dir() {
15817 for child in std::fs::read_dir(&transaction_dir)? {
15818 let child = child?;
15819 let active = child
15820 .file_name()
15821 .to_str()
15822 .and_then(|name| name.parse::<u64>().ok())
15823 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
15824 if !active {
15825 crate::durable_file::remove_directory_all(&child.path())?;
15826 }
15827 }
15828 }
15829 }
15830 let external_names = catalog_snapshot
15831 .external_tables
15832 .iter()
15833 .map(|entry| entry.name.as_str())
15834 .collect::<HashSet<_>>();
15835 let external_root = self.root.join(VTAB_DIR);
15836 if external_root.is_dir() {
15837 for entry in std::fs::read_dir(&external_root)? {
15838 let entry = entry?;
15839 let name = entry.file_name();
15840 if name
15841 .to_str()
15842 .is_some_and(|name| external_names.contains(name))
15843 {
15844 continue;
15845 }
15846 if entry.file_type()?.is_dir() {
15847 crate::durable_file::remove_directory_all(&entry.path())?;
15848 } else {
15849 std::fs::remove_file(entry.path())?;
15850 crate::durable_file::sync_directory(&external_root)?;
15851 }
15852 }
15853 }
15854
15855 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
15858 let visible = self.epoch.visible();
15859 for (_, table) in &tables {
15860 table.persist_manifest(visible)?;
15861 }
15862
15863 Ok(())
15864 }
15865 fn alloc_txn_id(&self) -> Result<u64> {
15866 self.ensure_owner_process()?;
15867 crate::txn::allocate_txn_id(&self.next_txn_id)
15868 }
15869
15870 pub fn allocate_lock_txn_id(&self) -> Result<u64> {
15874 self.alloc_txn_id()
15875 }
15876
15877 pub fn set_spill_threshold(&self, bytes: u64) {
15881 self.spill_threshold
15882 .store(bytes, std::sync::atomic::Ordering::Relaxed);
15883 }
15884
15885 #[doc(hidden)]
15889 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15890 *self.spill_hook.lock() = Some(Box::new(f));
15891 }
15892
15893 #[doc(hidden)]
15896 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15897 *self.security_commit_hook.lock() = Some(Box::new(f));
15898 }
15899
15900 #[doc(hidden)]
15903 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15904 *self.catalog_commit_hook.lock() = Some(Box::new(f));
15905 }
15906
15907 #[doc(hidden)]
15910 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15911 *self.backup_hook.lock() = Some(Box::new(f));
15912 }
15913
15914 #[doc(hidden)]
15918 pub fn __set_fk_lock_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15919 *self.fk_lock_hook.lock() = Some(Arc::new(f));
15920 }
15921
15922 #[doc(hidden)]
15924 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
15925 *self.replication_hook.lock() = Some(Box::new(f));
15926 }
15927
15928 #[doc(hidden)]
15932 pub fn __wal_group_sync_count(&self) -> u64 {
15933 self.shared_wal.lock().group_sync_count()
15934 }
15935
15936 #[doc(hidden)]
15939 pub fn __poison(&self) {
15940 self.poisoned
15941 .store(true, std::sync::atomic::Ordering::Relaxed);
15942 }
15943
15944 pub fn check(&self) -> Vec<CheckIssue> {
15957 match self.check_inner(None) {
15958 Ok(issues) => issues,
15959 Err(error) => vec![CheckIssue {
15960 table_id: WAL_TABLE_ID,
15961 table_name: "shared WAL".into(),
15962 severity: "error".into(),
15963 description: error.to_string(),
15964 }],
15965 }
15966 }
15967
15968 #[doc(hidden)]
15970 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
15971 self.check_inner(Some(control))
15972 }
15973
15974 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
15975 let mut issues = Vec::new();
15976 let io_root = self.durable_root.io_path()?;
15977 let cat = self.catalog.read();
15978 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
15979 for (table_index, entry) in cat.tables.iter().enumerate() {
15980 if table_index % 256 == 0 {
15981 if let Some(control) = control {
15982 control.checkpoint()?;
15983 }
15984 }
15985 if !matches!(entry.state, TableState::Live) {
15986 continue;
15987 }
15988 let tdir = io_root.join(TABLES_DIR).join(entry.table_id.to_string());
15989 let mut err = |sev: &str, desc: String| {
15990 issues.push(CheckIssue {
15991 table_id: entry.table_id,
15992 table_name: entry.name.clone(),
15993 severity: sev.into(),
15994 description: desc,
15995 });
15996 };
15997 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
15998 Ok(m) => m,
15999 Err(e) => {
16000 err("error", format!("manifest read failed: {e}"));
16001 continue;
16002 }
16003 };
16004 if m.flushed_epoch > m.current_epoch {
16005 err(
16006 "error",
16007 format!(
16008 "flushed_epoch {} exceeds current_epoch {} (impossible)",
16009 m.flushed_epoch, m.current_epoch
16010 ),
16011 );
16012 }
16013
16014 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
16015 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
16016 for (run_index, rr) in m.runs.iter().enumerate() {
16017 if run_index % 256 == 0 {
16018 if let Some(control) = control {
16019 control.checkpoint()?;
16020 }
16021 }
16022 referenced.insert(rr.run_id);
16023 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
16024 if !run_path.exists() {
16025 err("error", format!("missing run file: r-{}.sr", rr.run_id));
16026 continue;
16027 }
16028 match crate::sorted_run::RunReader::open(
16029 &run_path,
16030 entry.schema.clone(),
16031 self.kek.clone(),
16032 ) {
16033 Ok(reader) => {
16034 if reader.row_count() as u64 != rr.row_count {
16035 err(
16036 "error",
16037 format!(
16038 "run r-{} row count mismatch: manifest {} vs run {}",
16039 rr.run_id,
16040 rr.row_count,
16041 reader.row_count()
16042 ),
16043 );
16044 }
16045 }
16046 Err(e) => {
16047 err(
16048 "error",
16049 format!("run r-{} integrity check failed: {e}", rr.run_id),
16050 );
16051 }
16052 }
16053 }
16054
16055 for r in &m.retiring {
16059 referenced.insert(r.run_id);
16060 }
16061
16062 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
16064 for (entry_index, ent) in rd.flatten().enumerate() {
16065 if entry_index % 256 == 0 {
16066 if let Some(control) = control {
16067 control.checkpoint()?;
16068 }
16069 }
16070 let p = ent.path();
16071 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
16072 continue;
16073 }
16074 let run_id = p
16075 .file_stem()
16076 .and_then(|s| s.to_str())
16077 .and_then(|s| s.strip_prefix("r-"))
16078 .and_then(|s| s.parse::<u128>().ok());
16079 if let Some(id) = run_id {
16080 if !referenced.contains(&id) {
16081 err(
16082 "warning",
16083 format!("orphan run file r-{id}.sr not referenced by the manifest"),
16084 );
16085 }
16086 }
16087 }
16088 }
16089 }
16090
16091 let external_names = cat
16092 .external_tables
16093 .iter()
16094 .map(|entry| entry.name.clone())
16095 .collect::<std::collections::HashSet<_>>();
16096 let vtab_dir = io_root.join(VTAB_DIR);
16097 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
16098 for (entry_index, entry) in entries.flatten().enumerate() {
16099 if entry_index % 256 == 0 {
16100 if let Some(control) = control {
16101 control.checkpoint()?;
16102 }
16103 }
16104 let name = entry.file_name();
16105 let Some(name) = name.to_str() else { continue };
16106 if !external_names.contains(name) {
16107 issues.push(CheckIssue {
16108 table_id: EXTERNAL_TABLE_ID,
16109 table_name: name.to_string(),
16110 severity: "warning".into(),
16111 description: format!(
16112 "orphan external table state entry {:?} not referenced by the catalog",
16113 entry.path()
16114 ),
16115 });
16116 }
16117 }
16118 }
16119
16120 if let Some(control) = control {
16127 control.checkpoint()?;
16128 }
16129 for (seg, msg) in self.shared_wal.lock().verify_segments() {
16130 issues.push(CheckIssue {
16131 table_id: WAL_TABLE_ID,
16132 table_name: "<wal>".into(),
16133 severity: "error".into(),
16134 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
16135 });
16136 }
16137 Ok(issues)
16138 }
16139
16140 pub fn doctor(&self) -> Result<Vec<u64>> {
16144 let control = crate::ExecutionControl::new(None);
16145 self.doctor_controlled(&control, || true)
16146 }
16147
16148 #[doc(hidden)]
16152 pub fn doctor_controlled<F>(
16153 &self,
16154 control: &crate::ExecutionControl,
16155 before_publish: F,
16156 ) -> Result<Vec<u64>>
16157 where
16158 F: FnOnce() -> bool,
16159 {
16160 self.doctor_controlled_with_receipt(control, before_publish)
16161 .map(|(quarantined, _)| quarantined)
16162 }
16163
16164 #[doc(hidden)]
16167 pub fn doctor_controlled_with_receipt<F>(
16168 &self,
16169 control: &crate::ExecutionControl,
16170 before_publish: F,
16171 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
16172 where
16173 F: FnOnce() -> bool,
16174 {
16175 let _ddl = self.ddl_lock.lock();
16178 let _security_write = self.security_write()?;
16179 let issues = self.check_inner(Some(control))?;
16180 let bad_tables: std::collections::HashSet<u64> = issues
16185 .iter()
16186 .filter(|i| {
16187 i.severity == "error"
16188 && i.table_id != WAL_TABLE_ID
16189 && i.table_id != EXTERNAL_TABLE_ID
16190 })
16191 .map(|i| i.table_id)
16192 .collect();
16193 if bad_tables.is_empty() {
16194 return Ok((Vec::new(), None));
16195 }
16196 let _commit = self.commit_lock.lock();
16197 control.checkpoint()?;
16198 if !before_publish() {
16199 return Err(MongrelError::Cancelled);
16200 }
16201 let maintenance_epoch = self.epoch.bump_assigned();
16202 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
16203
16204 let qdir = self.root.join("_quarantine");
16205 crate::durable_file::create_directory(&qdir)?;
16206 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
16207 bad_tables.sort_unstable();
16208
16209 let mut handles = self
16213 .tables
16214 .read()
16215 .iter()
16216 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
16217 .map(|(table_id, handle)| (*table_id, handle.clone()))
16218 .collect::<Vec<_>>();
16219 handles.sort_by_key(|(table_id, _)| *table_id);
16220 let mut table_guards = handles
16221 .iter()
16222 .map(|(table_id, handle)| (*table_id, handle.lock()))
16223 .collect::<Vec<_>>();
16224
16225 let mut next_catalog = self.catalog.read().clone();
16226 for table_id in &bad_tables {
16227 if let Some(entry) = next_catalog
16228 .tables
16229 .iter_mut()
16230 .find(|entry| entry.table_id == *table_id)
16231 {
16232 entry.state = TableState::Dropped {
16233 at_epoch: maintenance_epoch.0,
16234 };
16235 }
16236 }
16237 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
16238
16239 let txn_id = self.alloc_txn_id()?;
16240 let commit_seq = {
16241 let mut wal = self.shared_wal.lock();
16242 let append: Result<u64> = (|| {
16243 for table_id in &bad_tables {
16244 wal.append(
16245 txn_id,
16246 *table_id,
16247 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
16248 table_id: *table_id,
16249 }),
16250 )?;
16251 }
16252 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
16253 wal.append_commit(txn_id, maintenance_epoch, &[])
16254 })();
16255 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
16256 };
16257 let receipt = self.await_durable_commit(txn_id, commit_seq, maintenance_epoch)?;
16258 for (_, table) in &mut table_guards {
16259 table.mark_unavailable_after_quarantine();
16260 }
16261 {
16262 let mut live_tables = self.tables.write();
16263 for table_id in &bad_tables {
16264 live_tables.remove(table_id);
16265 }
16266 }
16267 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
16268 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, &receipt, checkpoint)?;
16269
16270 drop(table_guards);
16274 drop(handles);
16275
16276 for table_id in &bad_tables {
16280 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
16281 if source.exists() {
16282 let destination = qdir.join(table_id.to_string());
16283 if let Err(error) = crate::durable_file::rename(&source, &destination) {
16284 return Err(MongrelError::DurableCommit {
16285 epoch: maintenance_epoch.0,
16286 message: format!(
16287 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
16288 ),
16289 });
16290 }
16291 }
16292 }
16293 Ok((
16294 bad_tables,
16295 Some(MaintenanceReceipt {
16296 epoch: maintenance_epoch,
16297 }),
16298 ))
16299 }
16300
16301 #[allow(dead_code)]
16303 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
16304 self.kek.as_ref()
16305 }
16306
16307 #[allow(dead_code)]
16309 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
16310 &self.epoch
16311 }
16312
16313 #[allow(dead_code)]
16315 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
16316 &self.snapshots
16317 }
16318}
16319
16320fn external_state_dir(root: &Path, name: &str) -> PathBuf {
16321 root.join(VTAB_DIR).join(name)
16322}
16323
16324fn append_catalog_snapshot(
16325 wal: &mut crate::wal::SharedWal,
16326 txn_id: u64,
16327 catalog: &Catalog,
16328) -> Result<()> {
16329 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
16330 wal.append(
16331 txn_id,
16332 WAL_TABLE_ID,
16333 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
16334 )?;
16335 Ok(())
16336}
16337
16338fn filter_ignored_staging(
16339 staging: Vec<(u64, crate::txn::Staged)>,
16340 ignored_indices: &std::collections::BTreeSet<usize>,
16341) -> Vec<(u64, crate::txn::Staged)> {
16342 if ignored_indices.is_empty() {
16343 return staging;
16344 }
16345 staging
16346 .into_iter()
16347 .enumerate()
16348 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
16349 .collect()
16350}
16351
16352fn external_state_file(root: &Path, name: &str) -> PathBuf {
16353 external_state_dir(root, name).join("state.json")
16354}
16355
16356fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
16357 let path = external_state_file(root, name);
16358 match std::fs::read(path) {
16359 Ok(bytes) => Ok(bytes),
16360 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
16361 Err(e) => Err(e.into()),
16362 }
16363}
16364
16365fn current_external_state_bytes(
16366 root: &Path,
16367 external_states: &[(String, Vec<u8>)],
16368 name: &str,
16369) -> Result<Vec<u8>> {
16370 for (table, state) in external_states.iter().rev() {
16371 if table == name {
16372 return Ok(state.clone());
16373 }
16374 }
16375 read_external_state_file(root, name)
16376}
16377
16378fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
16379 let mut out = external_states;
16380 dedup_external_states_in_place(&mut out);
16381 out
16382}
16383
16384fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
16385 let mut seen = std::collections::HashSet::new();
16386 let mut out = Vec::with_capacity(external_states.len());
16387 for (name, state) in std::mem::take(external_states).into_iter().rev() {
16388 if seen.insert(name.clone()) {
16389 out.push((name, state));
16390 }
16391 }
16392 out.reverse();
16393 *external_states = out;
16394}
16395
16396fn prepare_external_state_file(
16397 root: &Path,
16398 name: &str,
16399 state: &[u8],
16400 txn_id: u64,
16401) -> Result<PathBuf> {
16402 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
16403 let dir = external_state_dir(root, name);
16404 crate::durable_file::create_directory(&dir)?;
16405 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
16406 {
16407 let mut file = std::fs::OpenOptions::new()
16408 .create_new(true)
16409 .write(true)
16410 .open(&pending)?;
16411 file.write_all(state)?;
16412 file.sync_all()?;
16413 }
16414 Ok(pending)
16415}
16416
16417fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
16418 let path = external_state_file(root, name);
16419 crate::durable_file::replace(pending, &path)?;
16420 Ok(())
16421}
16422
16423fn write_external_state_file(
16424 durable: &crate::durable_file::DurableRoot,
16425 name: &str,
16426 state: &[u8],
16427) -> Result<()> {
16428 let directory = Path::new(VTAB_DIR).join(name);
16429 durable.create_directory_all(&directory)?;
16430 durable.write_atomic(directory.join("state.json"), state)?;
16431 Ok(())
16432}
16433
16434fn validate_recovered_data_table(
16435 catalog: &Catalog,
16436 tables: &HashMap<u64, TableHandle>,
16437 table_id: u64,
16438 commit_epoch: u64,
16439 offset: u64,
16440) -> Result<bool> {
16441 let entry = catalog
16442 .tables
16443 .iter()
16444 .find(|entry| entry.table_id == table_id)
16445 .ok_or_else(|| MongrelError::CorruptWal {
16446 offset,
16447 reason: format!("committed record references unknown table {table_id}"),
16448 })?;
16449 if commit_epoch < entry.created_epoch {
16450 return Err(MongrelError::CorruptWal {
16451 offset,
16452 reason: format!(
16453 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16454 entry.created_epoch
16455 ),
16456 });
16457 }
16458 match entry.state {
16459 TableState::Dropped { at_epoch } => {
16460 let abandoned_build_boundary =
16465 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16466 if commit_epoch >= at_epoch && !abandoned_build_boundary {
16467 Err(MongrelError::CorruptWal {
16468 offset,
16469 reason: format!(
16470 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16471 ),
16472 })
16473 } else {
16474 Ok(false)
16475 }
16476 }
16477 TableState::Live | TableState::Building { .. } => {
16478 if tables.contains_key(&table_id) {
16479 Ok(true)
16480 } else {
16481 Err(MongrelError::CorruptWal {
16482 offset,
16483 reason: format!("live table {table_id} has no mounted recovery handle"),
16484 })
16485 }
16486 }
16487 }
16488}
16489
16490type RecoveryTableStage = (
16491 Vec<crate::memtable::Row>,
16492 Vec<(crate::rowid::RowId, Epoch)>,
16493 Option<Epoch>,
16494 Epoch,
16495);
16496
16497#[derive(Clone)]
16498struct RecoveryValidationTable {
16499 schema: Schema,
16500 flushed_epoch: u64,
16501}
16502
16503fn validate_shared_wal_recovery_plan(
16504 durable_root: &crate::durable_file::DurableRoot,
16505 catalog: &Catalog,
16506 recovered_table_ids: &HashSet<u64>,
16507 reconciled_table_ids: &HashSet<u64>,
16508 meta_dek: Option<&[u8; META_DEK_LEN]>,
16509 kek: Option<Arc<crate::encryption::Kek>>,
16510 records: &[crate::wal::Record],
16511) -> Result<()> {
16512 use crate::wal::{DdlOp, Op};
16513
16514 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
16515 for entry in &catalog.tables {
16516 if !matches!(entry.state, TableState::Live) {
16517 continue;
16518 }
16519 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
16520 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
16521 Ok(manifest) => Some(manifest),
16522 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
16523 Err(error) => return Err(error),
16524 };
16525 let flushed_epoch = if let Some(manifest) = manifest {
16526 if manifest.table_id != entry.table_id {
16527 return Err(MongrelError::Conflict(format!(
16528 "catalog table {} storage identity mismatch",
16529 entry.table_id
16530 )));
16531 }
16532 if (manifest.schema_id != entry.schema.schema_id
16533 && !reconciled_table_ids.contains(&entry.table_id))
16534 || manifest.flushed_epoch > manifest.current_epoch
16535 || manifest.global_idx_epoch > manifest.current_epoch
16536 || manifest.next_row_id == u64::MAX
16537 || manifest.auto_inc_next < 0
16538 || manifest.auto_inc_next == i64::MAX
16539 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
16540 {
16541 return Err(MongrelError::InvalidArgument(format!(
16542 "table {} manifest counters or schema identity are invalid",
16543 entry.table_id
16544 )));
16545 }
16546 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
16547 crate::global_idx::read_durable_for(
16548 durable_root,
16549 &relative_dir,
16550 entry.table_id,
16551 &entry.schema,
16552 idx_dek.as_deref(),
16553 )?;
16554 let mut run_ids = HashSet::new();
16555 let mut maximum_row_id = None::<u64>;
16556 for run in &manifest.runs {
16557 if run.run_id >= u64::MAX as u128
16558 || run.epoch_created > manifest.current_epoch
16559 || !run_ids.insert(run.run_id)
16560 {
16561 return Err(MongrelError::InvalidArgument(format!(
16562 "table {} manifest contains an invalid or duplicate run id",
16563 entry.table_id
16564 )));
16565 }
16566 let relative = relative_dir
16567 .join(crate::engine::RUNS_DIR)
16568 .join(format!("r-{}.sr", run.run_id as u64));
16569 let file = durable_root.open_regular(&relative)?;
16570 let mut reader = crate::sorted_run::RunReader::open_file(
16571 file,
16572 entry.schema.clone(),
16573 kek.clone(),
16574 )?;
16575 let header = reader.header();
16576 if header.run_id != run.run_id
16577 || header.level != run.level
16578 || header.row_count != run.row_count
16579 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
16580 || header.is_uniform_epoch() && header.epoch_created != 0
16581 || header.schema_id > entry.schema.schema_id
16582 {
16583 return Err(MongrelError::InvalidArgument(format!(
16584 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
16585 entry.table_id,
16586 run.run_id,
16587 header.run_id,
16588 header.level,
16589 header.row_count,
16590 header.epoch_created,
16591 header.schema_id,
16592 run.run_id,
16593 run.level,
16594 run.row_count,
16595 run.epoch_created,
16596 entry.schema.schema_id,
16597 )));
16598 }
16599 if header.row_count != 0 {
16600 maximum_row_id = Some(
16601 maximum_row_id
16602 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
16603 );
16604 }
16605 reader.validate_all_pages()?;
16606 }
16607 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
16608 return Err(MongrelError::InvalidArgument(format!(
16609 "table {} next_row_id does not advance beyond persisted rows",
16610 entry.table_id
16611 )));
16612 }
16613 for run in &manifest.retiring {
16614 if run.run_id >= u64::MAX as u128
16615 || run.retire_epoch > manifest.current_epoch
16616 || !run_ids.insert(run.run_id)
16617 {
16618 return Err(MongrelError::InvalidArgument(format!(
16619 "table {} manifest contains an invalid or aliased retired run",
16620 entry.table_id
16621 )));
16622 }
16623 }
16624 manifest.flushed_epoch
16625 } else {
16626 if !recovered_table_ids.contains(&entry.table_id) {
16627 return Err(MongrelError::NotFound(format!(
16628 "live table {} manifest is missing",
16629 entry.table_id
16630 )));
16631 }
16632 0
16633 };
16634 tables.insert(
16635 entry.table_id,
16636 RecoveryValidationTable {
16637 schema: entry.schema.clone(),
16638 flushed_epoch,
16639 },
16640 );
16641 }
16642
16643 let committed = records
16644 .iter()
16645 .filter_map(|record| match record.op {
16646 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
16647 _ => None,
16648 })
16649 .collect::<HashMap<_, _>>();
16650 let mut run_ids = HashSet::new();
16651 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
16652 for record in records {
16653 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
16654 continue;
16655 };
16656 match &record.op {
16657 Op::Put { table_id, rows } => {
16658 let table = validate_recovery_data_table_plan(
16659 catalog,
16660 &tables,
16661 *table_id,
16662 commit_epoch,
16663 record.seq.0,
16664 )?;
16665 let decoded: Vec<crate::memtable::Row> =
16666 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
16667 offset: record.seq.0,
16668 reason: format!(
16669 "committed Put payload for transaction {} could not be decoded: {error}",
16670 record.txn_id
16671 ),
16672 })?;
16673 if let Some(table) = table {
16674 for row in &decoded {
16675 if !recovered_row_ids
16676 .entry(*table_id)
16677 .or_default()
16678 .insert(row.row_id.0)
16679 {
16680 return Err(MongrelError::CorruptWal {
16681 offset: record.seq.0,
16682 reason: format!(
16683 "committed WAL repeats recovered row id {} for table {table_id}",
16684 row.row_id.0
16685 ),
16686 });
16687 }
16688 validate_recovered_row(&table.schema, row)?;
16689 }
16690 }
16691 }
16692 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
16693 validate_recovery_data_table_plan(
16694 catalog,
16695 &tables,
16696 *table_id,
16697 commit_epoch,
16698 record.seq.0,
16699 )?;
16700 }
16701 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
16702 Op::Ddl(DdlOp::ResetExternalTableState {
16703 name,
16704 generation_epoch,
16705 }) => {
16706 if *generation_epoch != commit_epoch {
16707 return Err(MongrelError::CorruptWal {
16708 offset: record.seq.0,
16709 reason: format!(
16710 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
16711 ),
16712 });
16713 }
16714 validate_recovered_external_name(name)?;
16715 }
16716 Op::TxnCommit { added_runs, .. } => {
16717 for added in added_runs {
16718 let Some(table) = validate_recovery_data_table_plan(
16719 catalog,
16720 &tables,
16721 added.table_id,
16722 commit_epoch,
16723 record.seq.0,
16724 )?
16725 else {
16726 continue;
16727 };
16728 if added.run_id >= u64::MAX as u128
16729 || !run_ids.insert((added.table_id, added.run_id))
16730 {
16731 return Err(MongrelError::CorruptWal {
16732 offset: record.seq.0,
16733 reason: format!(
16734 "duplicate or invalid recovered run {} for table {}",
16735 added.run_id, added.table_id
16736 ),
16737 });
16738 }
16739 if commit_epoch <= table.flushed_epoch {
16740 continue;
16741 }
16742 validate_planned_spilled_run(
16743 durable_root,
16744 record.txn_id,
16745 commit_epoch,
16746 added,
16747 &table.schema,
16748 kek.clone(),
16749 )?;
16750 }
16751 }
16752 _ => {}
16753 }
16754 }
16755 Ok(())
16756}
16757
16758fn validate_recovery_data_table_plan<'a>(
16759 catalog: &Catalog,
16760 tables: &'a HashMap<u64, RecoveryValidationTable>,
16761 table_id: u64,
16762 commit_epoch: u64,
16763 offset: u64,
16764) -> Result<Option<&'a RecoveryValidationTable>> {
16765 let entry = catalog
16766 .tables
16767 .iter()
16768 .find(|entry| entry.table_id == table_id)
16769 .ok_or_else(|| MongrelError::CorruptWal {
16770 offset,
16771 reason: format!("committed record references unknown table {table_id}"),
16772 })?;
16773 if commit_epoch < entry.created_epoch {
16774 return Err(MongrelError::CorruptWal {
16775 offset,
16776 reason: format!(
16777 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16778 entry.created_epoch
16779 ),
16780 });
16781 }
16782 match entry.state {
16783 TableState::Dropped { at_epoch } => {
16784 let abandoned =
16785 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16786 if commit_epoch >= at_epoch && !abandoned {
16787 return Err(MongrelError::CorruptWal {
16788 offset,
16789 reason: format!(
16790 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16791 ),
16792 });
16793 }
16794 Ok(None)
16795 }
16796 TableState::Live => {
16797 tables
16798 .get(&table_id)
16799 .map(Some)
16800 .ok_or_else(|| MongrelError::CorruptWal {
16801 offset,
16802 reason: format!("live table {table_id} has no recovery plan"),
16803 })
16804 }
16805 TableState::Building { .. } => Err(MongrelError::CorruptWal {
16806 offset,
16807 reason: format!("building table {table_id} was not normalized before recovery"),
16808 }),
16809 }
16810}
16811
16812fn validate_planned_spilled_run(
16813 root: &crate::durable_file::DurableRoot,
16814 txn_id: u64,
16815 commit_epoch: u64,
16816 added: &crate::wal::AddedRun,
16817 schema: &Schema,
16818 kek: Option<Arc<crate::encryption::Kek>>,
16819) -> Result<()> {
16820 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
16821 let destination = table
16822 .join(crate::engine::RUNS_DIR)
16823 .join(format!("r-{}.sr", added.run_id as u64));
16824 let pending = table
16825 .join("_txn")
16826 .join(txn_id.to_string())
16827 .join(format!("r-{}.sr", added.run_id as u64));
16828 let file = match root.open_regular(&destination) {
16829 Ok(file) => file,
16830 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
16831 root.open_regular(&pending).map_err(|pending_error| {
16832 if pending_error.kind() == std::io::ErrorKind::NotFound {
16833 MongrelError::CorruptWal {
16834 offset: commit_epoch,
16835 reason: format!(
16836 "committed spilled run {} for transaction {txn_id} is missing",
16837 added.run_id
16838 ),
16839 }
16840 } else {
16841 pending_error.into()
16842 }
16843 })?
16844 }
16845 Err(error) => return Err(error.into()),
16846 };
16847 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
16848 let header = reader.header();
16849 if header.run_id != added.run_id
16850 || header.content_hash != added.content_hash
16851 || header.row_count != added.row_count
16852 || header.level != added.level
16853 || header.min_row_id != added.min_row_id
16854 || header.max_row_id != added.max_row_id
16855 || header.schema_id != schema.schema_id
16856 || !header.is_uniform_epoch()
16857 || header.epoch_created != 0
16858 {
16859 return Err(MongrelError::CorruptWal {
16860 offset: commit_epoch,
16861 reason: format!(
16862 "committed spilled run {} metadata differs from WAL",
16863 added.run_id
16864 ),
16865 });
16866 }
16867 reader.validate_all_pages()?;
16868 Ok(())
16869}
16870
16871#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16891pub enum StagedTxnWrite {
16892 Put {
16897 table_id: u64,
16899 rows: Vec<u8>,
16901 },
16902 Delete {
16904 table_id: u64,
16906 row_ids: Vec<u64>,
16908 },
16909}
16910
16911impl StagedTxnWrite {
16912 pub fn encode(&self) -> Result<Vec<u8>> {
16914 Ok(bincode::serialize(self)?)
16915 }
16916
16917 pub fn decode(bytes: &[u8]) -> Result<Self> {
16919 Ok(bincode::deserialize(bytes)?)
16920 }
16921}
16922
16923pub fn translate_records_for_replication(
16960 records: &[crate::wal::Record],
16961) -> Result<Vec<crate::wal::Record>> {
16962 use crate::wal::Op;
16963
16964 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
16967 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
16968 })?;
16969 if records.iter().any(|record| record.txn_id != txn_id) {
16970 return Err(MongrelError::InvalidArgument(
16971 "replicated transaction payload mixes transaction ids".into(),
16972 ));
16973 }
16974 let commits = records
16975 .iter()
16976 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
16977 .count();
16978 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
16979 return Err(MongrelError::InvalidArgument(
16980 "replicated transaction payload must end in exactly one commit marker".into(),
16981 ));
16982 }
16983
16984 let mut spilled_rows: HashMap<u64, Vec<crate::memtable::Row>> = HashMap::new();
16988 for record in records {
16989 if let Op::SpilledRows { table_id, rows } = &record.op {
16990 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(rows).map_err(|error| {
16991 MongrelError::InvalidArgument(format!(
16992 "spilled row payload for table {table_id} cannot decode for replication: \
16993 {error}"
16994 ))
16995 })?;
16996 spilled_rows.entry(*table_id).or_default().extend(chunk);
16997 }
16998 }
16999
17000 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
17004 unreachable!("one trailing commit marker validated above");
17005 };
17006 for run in added_runs {
17007 let rows = spilled_rows.get(&run.table_id).ok_or_else(|| {
17008 MongrelError::InvalidArgument(format!(
17009 "commit links spilled run {} for table {} but carries no logical row records \
17010 for it",
17011 run.run_id, run.table_id
17012 ))
17013 })?;
17014 let covered = rows
17015 .iter()
17016 .filter(|row| row.row_id.0 >= run.min_row_id && row.row_id.0 <= run.max_row_id)
17017 .count() as u64;
17018 if covered != run.row_count {
17019 return Err(MongrelError::InvalidArgument(format!(
17020 "commit links spilled run {} for table {} ({} rows in [{}, {}]) but the logical \
17021 row records cover {} rows",
17022 run.run_id, run.table_id, run.row_count, run.min_row_id, run.max_row_id, covered
17023 )));
17024 }
17025 }
17026
17027 let translated = records
17030 .iter()
17031 .map(|record| {
17032 let op = match &record.op {
17033 Op::SpilledRows { table_id, rows } => Op::Put {
17034 table_id: *table_id,
17035 rows: rows.clone(),
17036 },
17037 Op::TxnCommit { epoch, .. } => Op::TxnCommit {
17038 epoch: *epoch,
17039 added_runs: Vec::new(),
17040 },
17041 op => op.clone(),
17042 };
17043 crate::wal::Record::new(record.seq, record.txn_id, op)
17044 })
17045 .collect();
17046 Ok(translated)
17047}
17048
17049fn recover_shared_wal(
17050 durable_root: &crate::durable_file::DurableRoot,
17051 tables: &HashMap<u64, TableHandle>,
17052 catalog: &Catalog,
17053 epoch: &EpochAuthority,
17054 records: &[crate::wal::Record],
17055) -> Result<()> {
17056 use crate::memtable::Row;
17057 use crate::wal::{DdlOp, Op};
17058
17059 let mut committed: HashMap<u64, u64> = HashMap::new();
17061 let mut commit_ts_by_txn: HashMap<u64, mongreldb_types::hlc::HlcTimestamp> = HashMap::new();
17063 let mut spilled_to_link: Vec<(
17064 u64, u64, Vec<crate::wal::AddedRun>,
17067 )> = Vec::new();
17068 for r in records {
17069 match &r.op {
17070 Op::CommitTimestamp { unix_nanos } => {
17071 commit_ts_by_txn.insert(
17072 r.txn_id,
17073 mongreldb_types::hlc::HlcTimestamp {
17074 physical_micros: unix_nanos / 1_000,
17075 logical: 0,
17076 node_tiebreaker: 0,
17077 },
17078 );
17079 }
17080 Op::TxnCommit {
17081 epoch: ce,
17082 ref added_runs,
17083 } => {
17084 committed.insert(r.txn_id, *ce);
17085 if !added_runs.is_empty() {
17086 spilled_to_link.push((r.txn_id, *ce, added_runs.clone()));
17087 }
17088 }
17089 _ => {}
17090 }
17091 }
17092 for record in records {
17093 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
17094 continue;
17095 };
17096 match &record.op {
17097 Op::Put { table_id, .. }
17098 | Op::Delete { table_id, .. }
17099 | Op::TruncateTable { table_id } => {
17100 validate_recovered_data_table(
17101 catalog,
17102 tables,
17103 *table_id,
17104 commit_epoch,
17105 record.seq.0,
17106 )?;
17107 }
17108 Op::TxnCommit { added_runs, .. } => {
17109 for run in added_runs {
17110 validate_recovered_data_table(
17111 catalog,
17112 tables,
17113 run.table_id,
17114 commit_epoch,
17115 record.seq.0,
17116 )?;
17117 }
17118 }
17119 _ => {}
17120 }
17121 }
17122 let truncated_transactions: HashSet<(u64, u64)> = records
17123 .iter()
17124 .filter_map(|record| {
17125 committed.get(&record.txn_id)?;
17126 match record.op {
17127 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
17128 _ => None,
17129 }
17130 })
17131 .collect();
17132
17133 enum ExternalRecoveryAction {
17135 Write { name: String, state: Vec<u8> },
17136 Reset { name: String },
17137 }
17138 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
17139 let mut external_actions = Vec::new();
17140 let mut max_epoch = epoch.visible().0;
17141 for r in records.iter().cloned() {
17142 let Some(&ce) = committed.get(&r.txn_id) else {
17143 continue; };
17145 let commit_epoch = Epoch(ce);
17146 max_epoch = max_epoch.max(ce);
17147 match r.op {
17148 Op::Put { table_id, rows } => {
17149 let skip = tables
17151 .get(&table_id)
17152 .map(|h| h.lock().flushed_epoch() >= ce)
17153 .unwrap_or(true);
17154 if skip {
17155 continue;
17156 }
17157 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
17158 MongrelError::CorruptWal {
17159 offset: r.seq.0,
17160 reason: format!(
17161 "committed Put payload for transaction {} could not be decoded: {error}",
17162 r.txn_id
17163 ),
17164 }
17165 })?;
17166 let txn_commit_ts = commit_ts_by_txn.get(&r.txn_id).copied();
17171 let rows: Vec<Row> = rows
17172 .into_iter()
17173 .map(|mut row| {
17174 row.committed_epoch = commit_epoch;
17175 if row.commit_ts.is_none() {
17176 row.commit_ts = txn_commit_ts;
17177 }
17178 row
17179 })
17180 .collect();
17181 let entry = stage
17182 .entry(table_id)
17183 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17184 entry.0.extend(rows);
17185 entry.3 = commit_epoch;
17186 }
17187 Op::Delete { table_id, row_ids } => {
17188 let skip = tables
17189 .get(&table_id)
17190 .map(|h| h.lock().flushed_epoch() >= ce)
17191 .unwrap_or(true);
17192 if skip {
17193 continue;
17194 }
17195 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
17196 let entry = stage
17197 .entry(table_id)
17198 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17199 entry.1.extend(dels);
17200 entry.3 = commit_epoch;
17201 }
17202 Op::TruncateTable { table_id } => {
17203 let skip = tables
17204 .get(&table_id)
17205 .map(|h| h.lock().flushed_epoch() >= ce)
17206 .unwrap_or(true);
17207 if skip {
17208 continue;
17209 }
17210 stage.insert(
17211 table_id,
17212 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
17213 );
17214 }
17215 Op::ExternalTableState { name, state } => {
17216 let current_generation = catalog
17217 .external_tables
17218 .iter()
17219 .find(|entry| entry.name == name)
17220 .map(|entry| entry.created_epoch);
17221 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
17222 validate_recovered_external_name(&name)?;
17223 external_actions.push(ExternalRecoveryAction::Write { name, state });
17224 }
17225 }
17226 Op::Ddl(DdlOp::ResetExternalTableState {
17227 name,
17228 generation_epoch,
17229 }) => {
17230 if generation_epoch != ce {
17231 return Err(MongrelError::CorruptWal {
17232 offset: r.seq.0,
17233 reason: format!(
17234 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
17235 ),
17236 });
17237 }
17238 validate_recovered_external_name(&name)?;
17239 external_actions.push(ExternalRecoveryAction::Reset { name });
17240 }
17241 Op::Flush { .. }
17242 | Op::TxnCommit { .. }
17243 | Op::TxnAbort
17244 | Op::Ddl(_)
17245 | Op::BeforeImage { .. }
17246 | Op::CommitTimestamp { .. }
17247 | Op::SpilledRows { .. } => {}
17248 }
17249 }
17250 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
17251 added_runs.retain(|added| {
17252 tables
17253 .get(&added.table_id)
17254 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
17255 });
17256 }
17257 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
17258 validate_recovery_table_stages(tables, &stage)?;
17259 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
17260
17261 for action in external_actions {
17265 match action {
17266 ExternalRecoveryAction::Write { name, state } => {
17267 write_external_state_file(durable_root, &name, &state)?;
17268 }
17269 ExternalRecoveryAction::Reset { name } => {
17270 durable_root.create_directory_all(VTAB_DIR)?;
17271 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
17272 }
17273 }
17274 }
17275 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
17276 let Some(handle) = tables.get(&table_id) else {
17277 continue;
17278 };
17279 let mut t = handle.lock();
17280 if let Some(epoch) = truncate_epoch {
17281 t.apply_truncate(epoch);
17282 }
17283 t.recover_apply(rows, deletes)?;
17284 let rows = t.visible_rows(Snapshot::unbounded())?;
17288 t.live_count = rows.len() as u64;
17289 t.persist_manifest(table_epoch.max(epoch.visible()))?;
17293 }
17294
17295 for (txn_id, ce, added_runs) in &spilled_to_link {
17299 for ar in added_runs {
17300 let Some(handle) = tables.get(&ar.table_id) else {
17301 continue;
17302 };
17303 let mut t = handle.lock();
17304 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
17305 let destination = table_dir
17306 .join(crate::engine::RUNS_DIR)
17307 .join(format!("r-{}.sr", ar.run_id));
17308 match durable_root.open_regular(&destination) {
17309 Ok(_) => {}
17310 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
17311 let pending = table_dir
17312 .join("_txn")
17313 .join(txn_id.to_string())
17314 .join(format!("r-{}.sr", ar.run_id));
17315 durable_root.rename_file_new(&pending, &destination)?;
17316 }
17317 Err(error) => return Err(error.into()),
17318 }
17319 let linked = t.recover_spilled_run(crate::manifest::RunRef {
17325 run_id: ar.run_id,
17326 level: ar.level,
17327 epoch_created: *ce,
17328 row_count: ar.row_count,
17329 });
17330 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
17331 if replaced {
17332 t.set_flushed_epoch(Epoch(*ce));
17333 }
17334 if linked || replaced {
17335 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
17336 }
17337 }
17338 }
17339
17340 epoch.advance_recovered(Epoch(max_epoch));
17341 Ok(())
17342}
17343
17344fn reconcile_recovered_table_metadata(
17345 tables: &HashMap<u64, TableHandle>,
17346 epoch: Epoch,
17347) -> Result<()> {
17348 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
17349 table_ids.sort_unstable();
17350 let mut plans = Vec::with_capacity(table_ids.len());
17351 for table_id in &table_ids {
17352 let handle = tables.get(table_id).ok_or_else(|| {
17353 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17354 })?;
17355 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
17356 }
17357 for (table_id, plan) in plans {
17360 let handle = tables.get(&table_id).ok_or_else(|| {
17361 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17362 })?;
17363 handle.lock().apply_recovered_metadata(plan, epoch)?;
17364 }
17365 Ok(())
17366}
17367
17368fn validate_recovered_external_name(name: &str) -> Result<()> {
17369 if name.is_empty()
17370 || !name.chars().all(|character| {
17371 character.is_ascii_alphanumeric() || character == '_' || character == '-'
17372 })
17373 {
17374 return Err(MongrelError::CorruptWal {
17375 offset: 0,
17376 reason: format!("unsafe recovered external-table name {name:?}"),
17377 });
17378 }
17379 Ok(())
17380}
17381
17382fn validate_recovery_table_stages(
17383 tables: &HashMap<u64, TableHandle>,
17384 stages: &HashMap<u64, RecoveryTableStage>,
17385) -> Result<()> {
17386 for (table_id, (rows, _, _, _)) in stages {
17387 let handle = tables
17388 .get(table_id)
17389 .ok_or_else(|| MongrelError::CorruptWal {
17390 offset: *table_id,
17391 reason: format!("recovery stage references unmounted table {table_id}"),
17392 })?;
17393 let table = handle.lock();
17394 table.visible_rows(Snapshot::unbounded())?;
17397 for row in rows {
17398 validate_recovered_row(table.schema(), row)?;
17399 }
17400 }
17401 Ok(())
17402}
17403
17404fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
17405 if row.deleted || row.row_id.0 == u64::MAX {
17406 return Err(MongrelError::CorruptWal {
17407 offset: row.row_id.0,
17408 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
17409 });
17410 }
17411 let cells = row
17412 .columns
17413 .iter()
17414 .map(|(column, value)| (*column, value.clone()))
17415 .collect::<Vec<_>>();
17416 schema
17417 .validate_persisted_values(&cells)
17418 .map_err(|error| MongrelError::CorruptWal {
17419 offset: row.row_id.0,
17420 reason: format!("recovered row violates table schema: {error}"),
17421 })?;
17422 if schema.auto_increment_column().is_some_and(|column| {
17423 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
17424 }) {
17425 return Err(MongrelError::CorruptWal {
17426 offset: row.row_id.0,
17427 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
17428 });
17429 }
17430 Ok(())
17431}
17432
17433fn validate_recovery_spilled_runs(
17434 root: &crate::durable_file::DurableRoot,
17435 tables: &HashMap<u64, TableHandle>,
17436 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
17437) -> Result<()> {
17438 let mut identities = HashSet::new();
17439 for (txn_id, commit_epoch, added_runs) in spilled {
17440 for added in added_runs {
17441 if added.run_id >= u64::MAX as u128 {
17442 return Err(MongrelError::CorruptWal {
17443 offset: *commit_epoch,
17444 reason: format!(
17445 "recovered run id {} exceeds the on-disk namespace",
17446 added.run_id
17447 ),
17448 });
17449 }
17450 let Some(handle) = tables.get(&added.table_id) else {
17451 continue;
17452 };
17453 if !identities.insert((added.table_id, added.run_id)) {
17454 return Err(MongrelError::CorruptWal {
17455 offset: *commit_epoch,
17456 reason: format!(
17457 "duplicate recovered run {} for table {}",
17458 added.run_id, added.table_id
17459 ),
17460 });
17461 }
17462 let table = handle.lock();
17463 validate_planned_spilled_run(
17464 root,
17465 *txn_id,
17466 *commit_epoch,
17467 added,
17468 table.schema(),
17469 table.kek(),
17470 )?;
17471 }
17472 }
17473 Ok(())
17474}
17475
17476fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
17477 match condition {
17478 ProcedureCondition::Pk { .. } => {
17479 if schema.primary_key().is_none() {
17480 return Err(MongrelError::InvalidArgument(
17481 "procedure condition Pk references a table without a primary key".into(),
17482 ));
17483 }
17484 }
17485 ProcedureCondition::BitmapEq { column_id, .. }
17486 | ProcedureCondition::BitmapIn { column_id, .. }
17487 | ProcedureCondition::Range { column_id, .. }
17488 | ProcedureCondition::RangeF64 { column_id, .. }
17489 | ProcedureCondition::IsNull { column_id }
17490 | ProcedureCondition::IsNotNull { column_id }
17491 | ProcedureCondition::FmContains { column_id, .. } => {
17492 validate_column_id(*column_id, schema)?;
17493 }
17494 }
17495 Ok(())
17496}
17497
17498fn bind_procedure_args(
17499 procedure: &StoredProcedure,
17500 mut args: HashMap<String, crate::Value>,
17501) -> Result<HashMap<String, crate::Value>> {
17502 let mut out = HashMap::new();
17503 for param in &procedure.params {
17504 let value = match args.remove(¶m.name) {
17505 Some(value) => value,
17506 None => param.default.clone().ok_or_else(|| {
17507 MongrelError::InvalidArgument(format!(
17508 "missing required procedure parameter {:?}",
17509 param.name
17510 ))
17511 })?,
17512 };
17513 if !param.nullable && matches!(value, crate::Value::Null) {
17514 return Err(MongrelError::InvalidArgument(format!(
17515 "procedure parameter {:?} must not be NULL",
17516 param.name
17517 )));
17518 }
17519 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
17520 return Err(MongrelError::InvalidArgument(format!(
17521 "procedure parameter {:?} has wrong type",
17522 param.name
17523 )));
17524 }
17525 out.insert(param.name.clone(), value);
17526 }
17527 if let Some(extra) = args.keys().next() {
17528 return Err(MongrelError::InvalidArgument(format!(
17529 "unknown procedure parameter {extra:?}"
17530 )));
17531 }
17532 Ok(out)
17533}
17534
17535fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
17536 matches!(
17537 (value, ty),
17538 (crate::Value::Bool(_), crate::TypeId::Bool)
17539 | (crate::Value::Int64(_), crate::TypeId::Int8)
17540 | (crate::Value::Int64(_), crate::TypeId::Int16)
17541 | (crate::Value::Int64(_), crate::TypeId::Int32)
17542 | (crate::Value::Int64(_), crate::TypeId::Int64)
17543 | (crate::Value::Int64(_), crate::TypeId::UInt8)
17544 | (crate::Value::Int64(_), crate::TypeId::UInt16)
17545 | (crate::Value::Int64(_), crate::TypeId::UInt32)
17546 | (crate::Value::Int64(_), crate::TypeId::UInt64)
17547 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
17548 | (crate::Value::Int64(_), crate::TypeId::Date32)
17549 | (crate::Value::Float64(_), crate::TypeId::Float32)
17550 | (crate::Value::Float64(_), crate::TypeId::Float64)
17551 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
17552 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
17553 | (
17554 crate::Value::GeneratedEmbedding(_),
17555 crate::TypeId::Embedding { .. }
17556 )
17557 )
17558}
17559
17560fn eval_cells(
17561 cells: &[crate::procedure::ProcedureCell],
17562 args: &HashMap<String, crate::Value>,
17563 outputs: &HashMap<String, ProcedureCallOutput>,
17564) -> Result<Vec<(u16, crate::Value)>> {
17565 cells
17566 .iter()
17567 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
17568 .collect()
17569}
17570
17571fn eval_condition(
17572 condition: &ProcedureCondition,
17573 args: &HashMap<String, crate::Value>,
17574 outputs: &HashMap<String, ProcedureCallOutput>,
17575) -> Result<crate::Condition> {
17576 Ok(match condition {
17577 ProcedureCondition::Pk { value } => {
17578 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
17579 }
17580 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
17581 column_id: *column_id,
17582 value: eval_value(value, args, outputs)?.encode_key(),
17583 },
17584 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
17585 column_id: *column_id,
17586 values: values
17587 .iter()
17588 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
17589 .collect::<Result<Vec<_>>>()?,
17590 },
17591 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
17592 column_id: *column_id,
17593 lo: expect_i64(eval_value(lo, args, outputs)?)?,
17594 hi: expect_i64(eval_value(hi, args, outputs)?)?,
17595 },
17596 ProcedureCondition::RangeF64 {
17597 column_id,
17598 lo,
17599 lo_inclusive,
17600 hi,
17601 hi_inclusive,
17602 } => crate::Condition::RangeF64 {
17603 column_id: *column_id,
17604 lo: expect_f64(eval_value(lo, args, outputs)?)?,
17605 lo_inclusive: *lo_inclusive,
17606 hi: expect_f64(eval_value(hi, args, outputs)?)?,
17607 hi_inclusive: *hi_inclusive,
17608 },
17609 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
17610 column_id: *column_id,
17611 },
17612 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
17613 column_id: *column_id,
17614 },
17615 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
17616 column_id: *column_id,
17617 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
17618 },
17619 })
17620}
17621
17622fn eval_value(
17623 value: &ProcedureValue,
17624 args: &HashMap<String, crate::Value>,
17625 outputs: &HashMap<String, ProcedureCallOutput>,
17626) -> Result<crate::Value> {
17627 match value {
17628 ProcedureValue::Literal(value) => Ok(value.clone()),
17629 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
17630 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17631 }),
17632 ProcedureValue::StepScalar(id) => match outputs.get(id) {
17633 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
17634 _ => Err(MongrelError::InvalidArgument(format!(
17635 "procedure step {id:?} did not return a scalar"
17636 ))),
17637 },
17638 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
17639 Err(MongrelError::InvalidArgument(
17640 "row-valued procedure reference cannot be used as a scalar".into(),
17641 ))
17642 }
17643 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
17644 "structured procedure value cannot be used as a scalar cell".into(),
17645 )),
17646 }
17647}
17648
17649fn eval_return_output(
17650 value: &ProcedureValue,
17651 args: &HashMap<String, crate::Value>,
17652 outputs: &HashMap<String, ProcedureCallOutput>,
17653) -> Result<ProcedureCallOutput> {
17654 match value {
17655 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
17656 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
17657 args.get(name).cloned().ok_or_else(|| {
17658 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17659 })?,
17660 )),
17661 ProcedureValue::StepRows(id)
17662 | ProcedureValue::StepRow(id)
17663 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
17664 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
17665 }),
17666 ProcedureValue::Object(fields) => {
17667 let mut out = Vec::with_capacity(fields.len());
17668 for (name, value) in fields {
17669 out.push((name.clone(), eval_return_output(value, args, outputs)?));
17670 }
17671 Ok(ProcedureCallOutput::Object(out))
17672 }
17673 ProcedureValue::Array(values) => {
17674 let mut out = Vec::with_capacity(values.len());
17675 for value in values {
17676 out.push(eval_return_output(value, args, outputs)?);
17677 }
17678 Ok(ProcedureCallOutput::Array(out))
17679 }
17680 }
17681}
17682
17683fn expect_i64(value: crate::Value) -> Result<i64> {
17684 match value {
17685 crate::Value::Int64(value) => Ok(value),
17686 _ => Err(MongrelError::InvalidArgument(
17687 "procedure value must be Int64".into(),
17688 )),
17689 }
17690}
17691
17692fn expect_f64(value: crate::Value) -> Result<f64> {
17693 match value {
17694 crate::Value::Float64(value) => Ok(value),
17695 _ => Err(MongrelError::InvalidArgument(
17696 "procedure value must be Float64".into(),
17697 )),
17698 }
17699}
17700
17701fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
17702 match value {
17703 crate::Value::Bytes(value) => Ok(value),
17704 _ => Err(MongrelError::InvalidArgument(
17705 "procedure value must be Bytes".into(),
17706 )),
17707 }
17708}
17709
17710fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
17711 if schema.columns.iter().any(|c| c.id == column_id) {
17712 Ok(())
17713 } else {
17714 Err(MongrelError::InvalidArgument(format!(
17715 "unknown column id {column_id}"
17716 )))
17717 }
17718}
17719
17720fn trigger_matches_event(
17721 trigger: &StoredTrigger,
17722 event: &WriteEvent,
17723 cat: &Catalog,
17724) -> Result<bool> {
17725 if trigger.event != event.kind {
17726 return Ok(false);
17727 }
17728 let TriggerTarget::Table(target) = &trigger.target else {
17729 return Ok(false);
17730 };
17731 if target != &event.table {
17732 return Ok(false);
17733 }
17734 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
17735 let schema = &cat
17736 .live(target)
17737 .ok_or_else(|| {
17738 MongrelError::InvalidArgument(format!(
17739 "trigger {:?} references unknown table {target:?}",
17740 trigger.name
17741 ))
17742 })?
17743 .schema;
17744 let mut watched = Vec::with_capacity(trigger.update_of.len());
17745 for name in &trigger.update_of {
17746 let col = schema.column(name).ok_or_else(|| {
17747 MongrelError::InvalidArgument(format!(
17748 "trigger {:?} references unknown UPDATE OF column {name:?}",
17749 trigger.name
17750 ))
17751 })?;
17752 watched.push(col.id);
17753 }
17754 if !event
17755 .changed_columns
17756 .iter()
17757 .any(|column_id| watched.contains(column_id))
17758 {
17759 return Ok(false);
17760 }
17761 }
17762 Ok(true)
17763}
17764
17765fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
17766 let mut ids = std::collections::BTreeSet::new();
17767 if let Some(old) = old {
17768 ids.extend(old.columns.keys().copied());
17769 }
17770 if let Some(new) = new {
17771 ids.extend(new.columns.keys().copied());
17772 }
17773 ids.into_iter()
17774 .filter(|id| {
17775 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
17776 })
17777 .collect()
17778}
17779
17780fn eval_trigger_cells(
17781 cells: &[crate::trigger::TriggerCell],
17782 event: &WriteEvent,
17783 selected: Option<&TriggerRowImage>,
17784) -> Result<Vec<(u16, Value)>> {
17785 cells
17786 .iter()
17787 .map(|cell| {
17788 Ok((
17789 cell.column_id,
17790 eval_trigger_value(&cell.value, event, selected)?,
17791 ))
17792 })
17793 .collect()
17794}
17795
17796fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
17797 match expr {
17798 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
17799 Value::Bool(value) => Ok(value),
17800 Value::Null => Ok(false),
17801 other => Err(MongrelError::InvalidArgument(format!(
17802 "trigger WHEN value must be boolean, got {other:?}"
17803 ))),
17804 },
17805 TriggerExpr::Eq { left, right } => Ok(values_equal(
17806 &eval_trigger_value(left, event, None)?,
17807 &eval_trigger_value(right, event, None)?,
17808 )),
17809 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
17810 &eval_trigger_value(left, event, None)?,
17811 &eval_trigger_value(right, event, None)?,
17812 )),
17813 TriggerExpr::Lt { left, right } => match value_order(
17814 &eval_trigger_value(left, event, None)?,
17815 &eval_trigger_value(right, event, None)?,
17816 ) {
17817 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
17818 None => Ok(false),
17819 },
17820 TriggerExpr::Lte { left, right } => match value_order(
17821 &eval_trigger_value(left, event, None)?,
17822 &eval_trigger_value(right, event, None)?,
17823 ) {
17824 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
17825 None => Ok(false),
17826 },
17827 TriggerExpr::Gt { left, right } => match value_order(
17828 &eval_trigger_value(left, event, None)?,
17829 &eval_trigger_value(right, event, None)?,
17830 ) {
17831 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
17832 None => Ok(false),
17833 },
17834 TriggerExpr::Gte { left, right } => match value_order(
17835 &eval_trigger_value(left, event, None)?,
17836 &eval_trigger_value(right, event, None)?,
17837 ) {
17838 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
17839 None => Ok(false),
17840 },
17841 TriggerExpr::IsNull(value) => Ok(matches!(
17842 eval_trigger_value(value, event, None)?,
17843 Value::Null
17844 )),
17845 TriggerExpr::IsNotNull(value) => Ok(!matches!(
17846 eval_trigger_value(value, event, None)?,
17847 Value::Null
17848 )),
17849 TriggerExpr::And { left, right } => {
17850 if !eval_trigger_expr(left, event)? {
17851 Ok(false)
17852 } else {
17853 Ok(eval_trigger_expr(right, event)?)
17854 }
17855 }
17856 TriggerExpr::Or { left, right } => {
17857 if eval_trigger_expr(left, event)? {
17858 Ok(true)
17859 } else {
17860 Ok(eval_trigger_expr(right, event)?)
17861 }
17862 }
17863 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
17864 }
17865}
17866
17867fn eval_trigger_condition(
17868 condition: &TriggerCondition,
17869 event: &WriteEvent,
17870 selected: &TriggerRowImage,
17871 schema: &Schema,
17872) -> Result<bool> {
17873 match condition {
17874 TriggerCondition::Pk { value } => {
17875 let pk = schema.primary_key().ok_or_else(|| {
17876 MongrelError::InvalidArgument(
17877 "trigger condition Pk references a table without a primary key".into(),
17878 )
17879 })?;
17880 let lhs = eval_trigger_value(value, event, Some(selected))?;
17881 Ok(values_equal(
17882 &lhs,
17883 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
17884 ))
17885 }
17886 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
17887 selected.columns.get(column_id).unwrap_or(&Value::Null),
17888 &eval_trigger_value(value, event, Some(selected))?,
17889 )),
17890 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
17891 selected.columns.get(column_id).unwrap_or(&Value::Null),
17892 &eval_trigger_value(value, event, Some(selected))?,
17893 )),
17894 TriggerCondition::Lt { column_id, value } => match value_order(
17895 selected.columns.get(column_id).unwrap_or(&Value::Null),
17896 &eval_trigger_value(value, event, Some(selected))?,
17897 ) {
17898 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
17899 None => Ok(false),
17900 },
17901 TriggerCondition::Lte { column_id, value } => match value_order(
17902 selected.columns.get(column_id).unwrap_or(&Value::Null),
17903 &eval_trigger_value(value, event, Some(selected))?,
17904 ) {
17905 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
17906 None => Ok(false),
17907 },
17908 TriggerCondition::Gt { column_id, value } => match value_order(
17909 selected.columns.get(column_id).unwrap_or(&Value::Null),
17910 &eval_trigger_value(value, event, Some(selected))?,
17911 ) {
17912 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
17913 None => Ok(false),
17914 },
17915 TriggerCondition::Gte { column_id, value } => match value_order(
17916 selected.columns.get(column_id).unwrap_or(&Value::Null),
17917 &eval_trigger_value(value, event, Some(selected))?,
17918 ) {
17919 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
17920 None => Ok(false),
17921 },
17922 TriggerCondition::IsNull { column_id } => Ok(matches!(
17923 selected.columns.get(column_id),
17924 None | Some(Value::Null)
17925 )),
17926 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
17927 selected.columns.get(column_id),
17928 None | Some(Value::Null)
17929 )),
17930 TriggerCondition::And { left, right } => {
17931 if !eval_trigger_condition(left, event, selected, schema)? {
17932 Ok(false)
17933 } else {
17934 Ok(eval_trigger_condition(right, event, selected, schema)?)
17935 }
17936 }
17937 TriggerCondition::Or { left, right } => {
17938 if eval_trigger_condition(left, event, selected, schema)? {
17939 Ok(true)
17940 } else {
17941 Ok(eval_trigger_condition(right, event, selected, schema)?)
17942 }
17943 }
17944 TriggerCondition::Not(condition) => {
17945 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
17946 }
17947 }
17948}
17949
17950fn eval_trigger_value(
17951 value: &TriggerValue,
17952 event: &WriteEvent,
17953 selected: Option<&TriggerRowImage>,
17954) -> Result<Value> {
17955 match value {
17956 TriggerValue::Literal(value) => Ok(value.clone()),
17957 TriggerValue::NewColumn(column_id) => event
17958 .new
17959 .as_ref()
17960 .and_then(|row| row.columns.get(column_id))
17961 .cloned()
17962 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
17963 TriggerValue::OldColumn(column_id) => event
17964 .old
17965 .as_ref()
17966 .and_then(|row| row.columns.get(column_id))
17967 .cloned()
17968 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
17969 TriggerValue::SelectedColumn(column_id) => selected
17970 .and_then(|row| row.columns.get(column_id))
17971 .cloned()
17972 .ok_or_else(|| {
17973 MongrelError::InvalidArgument("SELECTED column is not available".into())
17974 }),
17975 }
17976}
17977
17978fn values_equal(left: &Value, right: &Value) -> bool {
17979 match (left, right) {
17980 (Value::Null, Value::Null) => true,
17981 (Value::Bool(a), Value::Bool(b)) => a == b,
17982 (Value::Int64(a), Value::Int64(b)) => a == b,
17983 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
17984 (Value::Bytes(a), Value::Bytes(b)) => a == b,
17985 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => {
17986 let a = a.as_embedding().unwrap();
17987 let b = b.as_embedding().unwrap();
17988 a.len() == b.len()
17989 && a.iter()
17990 .zip(b.iter())
17991 .all(|(a, b)| a.to_bits() == b.to_bits())
17992 }
17993 _ => false,
17994 }
17995}
17996
17997fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
17998 match (left, right) {
17999 (Value::Null, _) | (_, Value::Null) => None,
18000 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
18001 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
18002 (Value::Int64(a), Value::Float64(b)) => {
18005 let af = *a as f64;
18006 Some(af.total_cmp(b))
18007 }
18008 (Value::Float64(a), Value::Int64(b)) => {
18011 let bf = *b as f64;
18012 Some(a.total_cmp(&bf))
18013 }
18014 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
18015 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
18016 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => None,
18017 _ => None,
18018 }
18019}
18020
18021fn trigger_message(value: Value) -> String {
18022 match value {
18023 Value::Null => "NULL".into(),
18024 Value::Bool(value) => value.to_string(),
18025 Value::Int64(value) => value.to_string(),
18026 Value::Float64(value) => value.to_string(),
18027 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
18028 Value::Embedding(value) => format!("{value:?}"),
18029 Value::GeneratedEmbedding(value) => format!("{:?}", value.vector),
18030 Value::Decimal(value) => value.to_string(),
18031 Value::Interval {
18032 months,
18033 days,
18034 nanos,
18035 } => format!("{months}m {days}d {nanos}ns"),
18036 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
18037 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
18038 }
18039}
18040
18041fn validate_trigger_step<'a>(
18042 step: &TriggerStep,
18043 cat: &'a Catalog,
18044 target_schema: &Schema,
18045 event: TriggerEvent,
18046 select_schemas: &mut HashMap<String, &'a Schema>,
18047) -> Result<()> {
18048 match step {
18049 TriggerStep::SetNew { cells } => {
18050 if event == TriggerEvent::Delete {
18051 return Err(MongrelError::InvalidArgument(
18052 "SetNew trigger step is not valid for DELETE triggers".into(),
18053 ));
18054 }
18055 for cell in cells {
18056 validate_column_id(cell.column_id, target_schema)?;
18057 validate_trigger_value(&cell.value, target_schema, event)?;
18058 }
18059 }
18060 TriggerStep::Insert { table, cells } => {
18061 let schema = trigger_write_schema(cat, table, "insert")?;
18062 for cell in cells {
18063 validate_column_id(cell.column_id, schema)?;
18064 validate_trigger_value(&cell.value, target_schema, event)?;
18065 }
18066 }
18067 TriggerStep::UpdateByPk { table, pk, cells } => {
18068 let schema = trigger_write_schema(cat, table, "update")?;
18069 if schema.primary_key().is_none() {
18070 return Err(MongrelError::InvalidArgument(format!(
18071 "trigger update_by_pk references table {table:?} without a primary key"
18072 )));
18073 }
18074 validate_trigger_value(pk, target_schema, event)?;
18075 for cell in cells {
18076 validate_column_id(cell.column_id, schema)?;
18077 validate_trigger_value(&cell.value, target_schema, event)?;
18078 }
18079 }
18080 TriggerStep::DeleteByPk { table, pk } => {
18081 let schema = trigger_write_schema(cat, table, "delete")?;
18082 if schema.primary_key().is_none() {
18083 return Err(MongrelError::InvalidArgument(format!(
18084 "trigger delete_by_pk references table {table:?} without a primary key"
18085 )));
18086 }
18087 validate_trigger_value(pk, target_schema, event)?;
18088 }
18089 TriggerStep::Select {
18090 id,
18091 table,
18092 conditions,
18093 } => {
18094 let schema = trigger_read_schema(cat, table)?;
18095 for condition in conditions {
18096 validate_trigger_condition(condition, schema, target_schema, event)?;
18097 }
18098 if select_schemas.contains_key(id) {
18099 return Err(MongrelError::InvalidArgument(format!(
18100 "duplicate select id {id:?} in trigger program"
18101 )));
18102 }
18103 select_schemas.insert(id.clone(), schema);
18104 }
18105 TriggerStep::Foreach { id, steps } => {
18106 if !select_schemas.contains_key(id) {
18107 return Err(MongrelError::InvalidArgument(format!(
18108 "foreach references unknown select id {id:?}"
18109 )));
18110 }
18111 let mut inner_select_schemas = select_schemas.clone();
18112 for step in steps {
18113 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
18114 }
18115 }
18116 TriggerStep::DeleteWhere { table, conditions } => {
18117 let schema = trigger_write_schema(cat, table, "delete")?;
18118 for condition in conditions {
18119 validate_trigger_condition(condition, schema, target_schema, event)?;
18120 }
18121 }
18122 TriggerStep::UpdateWhere {
18123 table,
18124 conditions,
18125 cells,
18126 } => {
18127 let schema = trigger_write_schema(cat, table, "update")?;
18128 for condition in conditions {
18129 validate_trigger_condition(condition, schema, target_schema, event)?;
18130 }
18131 for cell in cells {
18132 validate_column_id(cell.column_id, schema)?;
18133 validate_trigger_value(&cell.value, target_schema, event)?;
18134 }
18135 }
18136 TriggerStep::Raise { message, .. } => {
18137 validate_trigger_value(message, target_schema, event)?
18138 }
18139 }
18140 Ok(())
18141}
18142
18143fn trigger_validation_error(error: MongrelError) -> MongrelError {
18144 match error {
18145 MongrelError::TriggerValidation(_) => error,
18146 MongrelError::InvalidArgument(message)
18147 | MongrelError::Conflict(message)
18148 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
18149 error => error,
18150 }
18151}
18152
18153fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
18154 if let Some(entry) = cat.live(table) {
18155 return Ok(&entry.schema);
18156 }
18157 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18158 let allowed = match op {
18159 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
18160 "update" | "delete" => entry.capabilities.writable,
18161 _ => false,
18162 };
18163 if !allowed {
18164 return Err(MongrelError::InvalidArgument(format!(
18165 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
18166 entry.module
18167 )));
18168 }
18169 if !entry.capabilities.transaction_safe {
18170 return Err(MongrelError::InvalidArgument(format!(
18171 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
18172 entry.module
18173 )));
18174 }
18175 return Ok(&entry.declared_schema);
18176 }
18177 Err(MongrelError::InvalidArgument(format!(
18178 "trigger references unknown table {table:?}"
18179 )))
18180}
18181
18182fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
18183 if let Some(entry) = cat.live(table) {
18184 return Ok(&entry.schema);
18185 }
18186 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18187 if entry.capabilities.trigger_safe {
18188 return Ok(&entry.declared_schema);
18189 }
18190 return Err(MongrelError::InvalidArgument(format!(
18191 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
18192 entry.module
18193 )));
18194 }
18195 Err(MongrelError::InvalidArgument(format!(
18196 "trigger references unknown table {table:?}"
18197 )))
18198}
18199
18200fn validate_trigger_condition(
18201 condition: &TriggerCondition,
18202 schema: &Schema,
18203 target_schema: &Schema,
18204 event: TriggerEvent,
18205) -> Result<()> {
18206 match condition {
18207 TriggerCondition::Pk { value } => {
18208 if schema.primary_key().is_none() {
18209 return Err(MongrelError::InvalidArgument(
18210 "trigger condition Pk references a table without a primary key".into(),
18211 ));
18212 }
18213 validate_trigger_value(value, target_schema, event)
18214 }
18215 TriggerCondition::Eq { column_id, value }
18216 | TriggerCondition::NotEq { column_id, value }
18217 | TriggerCondition::Lt { column_id, value }
18218 | TriggerCondition::Lte { column_id, value }
18219 | TriggerCondition::Gt { column_id, value }
18220 | TriggerCondition::Gte { column_id, value } => {
18221 validate_column_id(*column_id, schema)?;
18222 validate_trigger_value(value, target_schema, event)
18223 }
18224 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
18225 validate_column_id(*column_id, schema)
18226 }
18227 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
18228 validate_trigger_condition(left, schema, target_schema, event)?;
18229 validate_trigger_condition(right, schema, target_schema, event)
18230 }
18231 TriggerCondition::Not(condition) => {
18232 validate_trigger_condition(condition, schema, target_schema, event)
18233 }
18234 }
18235}
18236
18237fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
18238 match expr {
18239 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
18240 validate_trigger_value(value, schema, event)
18241 }
18242 TriggerExpr::Eq { left, right }
18243 | TriggerExpr::NotEq { left, right }
18244 | TriggerExpr::Lt { left, right }
18245 | TriggerExpr::Lte { left, right }
18246 | TriggerExpr::Gt { left, right }
18247 | TriggerExpr::Gte { left, right } => {
18248 validate_trigger_value(left, schema, event)?;
18249 validate_trigger_value(right, schema, event)
18250 }
18251 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
18252 validate_trigger_expr(left, schema, event)?;
18253 validate_trigger_expr(right, schema, event)
18254 }
18255 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
18256 }
18257}
18258
18259fn validate_trigger_value(
18260 value: &TriggerValue,
18261 schema: &Schema,
18262 event: TriggerEvent,
18263) -> Result<()> {
18264 match value {
18265 TriggerValue::Literal(_) => Ok(()),
18266 TriggerValue::NewColumn(id) => {
18267 if event == TriggerEvent::Delete {
18268 return Err(MongrelError::InvalidArgument(
18269 "DELETE triggers cannot reference NEW".into(),
18270 ));
18271 }
18272 validate_column_id(*id, schema)
18273 }
18274 TriggerValue::OldColumn(id) => {
18275 if event == TriggerEvent::Insert {
18276 return Err(MongrelError::InvalidArgument(
18277 "INSERT triggers cannot reference OLD".into(),
18278 ));
18279 }
18280 validate_column_id(*id, schema)
18281 }
18282 TriggerValue::SelectedColumn(_) => Ok(()),
18286 }
18287}
18288
18289const COMMIT_TS_LEDGER_CAP: usize = 10_000;
18294
18295fn commit_ts_ledger_from_recovery(
18303 records: &[crate::wal::Record],
18304) -> std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp> {
18305 use crate::wal::Op;
18306 let mut commits = HashMap::new();
18307 let mut timestamps = HashMap::new();
18308 for record in records {
18309 match &record.op {
18310 Op::TxnCommit { epoch, .. } => {
18311 commits.insert(record.txn_id, *epoch);
18312 }
18313 Op::CommitTimestamp { unix_nanos } => {
18314 timestamps.insert(record.txn_id, *unix_nanos);
18315 }
18316 _ => {}
18317 }
18318 }
18319 let mut ledger = std::collections::BTreeMap::new();
18320 for (txn_id, epoch) in commits {
18321 let Some(unix_nanos) = timestamps.get(&txn_id) else {
18322 continue;
18323 };
18324 ledger.insert(
18325 epoch,
18326 mongreldb_types::hlc::HlcTimestamp {
18327 physical_micros: unix_nanos / 1_000,
18328 logical: 0,
18329 node_tiebreaker: 0,
18330 },
18331 );
18332 }
18333 while ledger.len() > COMMIT_TS_LEDGER_CAP {
18334 ledger.pop_first();
18335 }
18336 ledger
18337}
18338
18339fn recover_ddl_from_wal(
18345 root: &Path,
18346 durable_root: Option<&crate::durable_file::DurableRoot>,
18347 target_catalog: &mut Catalog,
18348 meta_dek: Option<&[u8; META_DEK_LEN]>,
18349 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
18350 apply: bool,
18351 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18352) -> Result<()> {
18353 use crate::wal::SharedWal;
18354 let records = match durable_root {
18355 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
18356 None => SharedWal::replay_with_dek(root, wal_dek)?,
18357 };
18358 recover_ddl_from_records(
18359 root,
18360 durable_root,
18361 target_catalog,
18362 meta_dek,
18363 apply,
18364 table_roots,
18365 &records,
18366 )
18367}
18368
18369fn recover_ddl_from_records(
18370 root: &Path,
18371 durable_root: Option<&crate::durable_file::DurableRoot>,
18372 target_catalog: &mut Catalog,
18373 meta_dek: Option<&[u8; META_DEK_LEN]>,
18374 apply: bool,
18375 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18376 records: &[crate::wal::Record],
18377) -> Result<()> {
18378 use crate::wal::{DdlOp, Op};
18379
18380 let original_catalog = target_catalog.clone();
18381 let mut recovered_catalog = original_catalog.clone();
18382 let cat = &mut recovered_catalog;
18383 let mut created_table_ids = HashSet::<u64>::new();
18384 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
18385
18386 let mut committed: HashMap<u64, u64> = HashMap::new();
18387 for r in records {
18388 if let Op::TxnCommit { epoch: ce, .. } = r.op {
18389 committed.insert(r.txn_id, ce);
18390 }
18391 }
18392 let catalog_snapshot_txns = records
18393 .iter()
18394 .filter_map(|record| {
18395 (committed.contains_key(&record.txn_id)
18396 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
18397 .then_some(record.txn_id)
18398 })
18399 .collect::<HashSet<_>>();
18400
18401 let mut changed = false;
18402 let mut applied_catalog_epoch = cat.db_epoch;
18403 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
18404 for r in records.iter().cloned() {
18405 let Some(&ce) = committed.get(&r.txn_id) else {
18406 continue;
18407 };
18408 let txn_id = r.txn_id;
18409 match r.op {
18410 Op::Ddl(DdlOp::CreateTable {
18411 table_id,
18412 ref name,
18413 ref schema_json,
18414 }) => {
18415 if cat.tables.iter().any(|t| t.table_id == table_id) {
18416 continue;
18417 }
18418 let schema = DdlOp::decode_schema(schema_json)?;
18419 validate_recovered_schema(&schema)?;
18420 created_table_ids.insert(table_id);
18421 cat.tables.push(CatalogEntry {
18422 table_id,
18423 name: name.clone(),
18424 schema,
18425 state: TableState::Live,
18426 created_epoch: ce,
18427 });
18428 cat.next_table_id =
18429 cat.next_table_id
18430 .max(table_id.checked_add(1).ok_or_else(|| {
18431 MongrelError::Full("table id namespace exhausted".into())
18432 })?);
18433 changed = true;
18434 }
18435 Op::Ddl(DdlOp::CreateBuildingTable {
18436 table_id,
18437 ref build_name,
18438 ref intended_name,
18439 ref query_id,
18440 created_at_unix_nanos,
18441 ref schema_json,
18442 }) => {
18443 if cat.tables.iter().any(|table| table.table_id == table_id) {
18444 continue;
18445 }
18446 let schema = DdlOp::decode_schema(schema_json)?;
18447 validate_recovered_schema(&schema)?;
18448 created_table_ids.insert(table_id);
18449 cat.tables.push(CatalogEntry {
18450 table_id,
18451 name: build_name.clone(),
18452 schema,
18453 state: TableState::Building {
18454 intended_name: intended_name.clone(),
18455 query_id: query_id.clone(),
18456 created_at_unix_nanos,
18457 replaces_table_id: None,
18458 },
18459 created_epoch: ce,
18460 });
18461 cat.next_table_id =
18462 cat.next_table_id
18463 .max(table_id.checked_add(1).ok_or_else(|| {
18464 MongrelError::Full("table id namespace exhausted".into())
18465 })?);
18466 changed = true;
18467 }
18468 Op::Ddl(DdlOp::CreateRebuildingTable {
18469 table_id,
18470 ref build_name,
18471 ref intended_name,
18472 ref query_id,
18473 created_at_unix_nanos,
18474 replaces_table_id,
18475 ref schema_json,
18476 }) => {
18477 if cat.tables.iter().any(|table| table.table_id == table_id) {
18478 continue;
18479 }
18480 let schema = DdlOp::decode_schema(schema_json)?;
18481 validate_recovered_schema(&schema)?;
18482 created_table_ids.insert(table_id);
18483 cat.tables.push(CatalogEntry {
18484 table_id,
18485 name: build_name.clone(),
18486 schema,
18487 state: TableState::Building {
18488 intended_name: intended_name.clone(),
18489 query_id: query_id.clone(),
18490 created_at_unix_nanos,
18491 replaces_table_id: Some(replaces_table_id),
18492 },
18493 created_epoch: ce,
18494 });
18495 cat.next_table_id =
18496 cat.next_table_id
18497 .max(table_id.checked_add(1).ok_or_else(|| {
18498 MongrelError::Full("table id namespace exhausted".into())
18499 })?);
18500 changed = true;
18501 }
18502 Op::Ddl(DdlOp::DropTable { table_id }) => {
18503 let mut dropped_name = None;
18504 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18505 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18506 dropped_name = Some(entry.name.clone());
18507 entry.state = TableState::Dropped { at_epoch: ce };
18508 changed = true;
18509 }
18510 }
18511 if let Some(name) = dropped_name {
18512 let before = cat.materialized_views.len();
18513 cat.materialized_views
18514 .retain(|definition| definition.name != name);
18515 changed |= before != cat.materialized_views.len();
18516 cat.security.rls_tables.retain(|table| table != &name);
18517 cat.security.policies.retain(|policy| policy.table != name);
18518 cat.security.masks.retain(|mask| mask.table != name);
18519 for role in &mut cat.roles {
18520 role.permissions
18521 .retain(|permission| permission_table(permission) != Some(&name));
18522 }
18523 if !catalog_snapshot_txns.contains(&txn_id) {
18524 advance_security_version(cat)?;
18525 }
18526 }
18527 }
18528 Op::Ddl(DdlOp::PublishBuildingTable {
18529 table_id,
18530 ref new_name,
18531 }) => {
18532 if let Some(entry) = cat
18533 .tables
18534 .iter_mut()
18535 .find(|table| table.table_id == table_id)
18536 {
18537 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
18538 entry.name = new_name.clone();
18539 entry.state = TableState::Live;
18540 changed = true;
18541 }
18542 }
18543 }
18544 Op::Ddl(DdlOp::ReplaceBuildingTable {
18545 table_id,
18546 replaced_table_id,
18547 ref new_name,
18548 }) => {
18549 changed |=
18550 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
18551 }
18552 Op::Ddl(DdlOp::RenameTable {
18553 table_id,
18554 ref new_name,
18555 }) => {
18556 let mut old_name = None;
18557 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18558 if entry.name != *new_name {
18559 old_name = Some(entry.name.clone());
18560 entry.name = new_name.clone();
18561 changed = true;
18562 }
18563 }
18564 if let Some(old_name) = old_name {
18565 if let Some(definition) = cat
18566 .materialized_views
18567 .iter_mut()
18568 .find(|definition| definition.name == old_name)
18569 {
18570 definition.name = new_name.clone();
18571 }
18572 for table in &mut cat.security.rls_tables {
18573 if *table == old_name {
18574 *table = new_name.clone();
18575 }
18576 }
18577 for policy in &mut cat.security.policies {
18578 if policy.table == old_name {
18579 policy.table = new_name.clone();
18580 }
18581 }
18582 for mask in &mut cat.security.masks {
18583 if mask.table == old_name {
18584 mask.table = new_name.clone();
18585 }
18586 }
18587 for role in &mut cat.roles {
18588 for permission in &mut role.permissions {
18589 rename_permission_table(permission, &old_name, new_name);
18590 }
18591 }
18592 if !catalog_snapshot_txns.contains(&txn_id) {
18593 advance_security_version(cat)?;
18594 }
18595 }
18596 }
18600 Op::Ddl(DdlOp::AlterTable {
18601 table_id,
18602 ref column_json,
18603 }) => {
18604 let column = DdlOp::decode_column(column_json)?;
18605 let mut renamed = None;
18606 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18607 renamed = entry
18608 .schema
18609 .columns
18610 .iter()
18611 .find(|existing| existing.id == column.id && existing.name != column.name)
18612 .map(|existing| {
18613 (
18614 entry.name.clone(),
18615 existing.name.clone(),
18616 column.name.clone(),
18617 )
18618 });
18619 if apply_recovered_column_def(&mut entry.schema, column)? {
18620 validate_recovered_schema(&entry.schema)?;
18621 changed = true;
18622 }
18623 }
18624 if let Some((table, old_name, new_name)) = renamed {
18625 for role in &mut cat.roles {
18626 for permission in &mut role.permissions {
18627 rename_permission_column(permission, &table, &old_name, &new_name);
18628 }
18629 }
18630 if !catalog_snapshot_txns.contains(&txn_id) {
18631 advance_security_version(cat)?;
18632 }
18633 }
18634 }
18635 Op::Ddl(DdlOp::SetTtl {
18636 table_id,
18637 ref policy_json,
18638 }) => {
18639 let policy = DdlOp::decode_ttl(policy_json)?;
18640 let entry = cat
18641 .tables
18642 .iter()
18643 .find(|entry| entry.table_id == table_id)
18644 .ok_or_else(|| {
18645 MongrelError::Schema(format!(
18646 "recovered TTL references unknown table id {table_id}"
18647 ))
18648 })?;
18649 if let Some(policy) = policy {
18650 let valid = entry
18651 .schema
18652 .columns
18653 .iter()
18654 .find(|column| column.id == policy.column_id)
18655 .is_some_and(|column| {
18656 column.ty == TypeId::TimestampNanos
18657 && policy.duration_nanos > 0
18658 && policy.duration_nanos <= i64::MAX as u64
18659 });
18660 if !valid {
18661 return Err(MongrelError::Schema(format!(
18662 "invalid recovered TTL policy for table id {table_id}"
18663 )));
18664 }
18665 }
18666 ttl_updates.insert(table_id, (policy, ce));
18667 }
18668 Op::Ddl(DdlOp::SetMaterializedView {
18669 ref name,
18670 ref definition_json,
18671 }) => {
18672 let definition = DdlOp::decode_materialized_view(definition_json)?;
18673 if definition.name != *name {
18674 return Err(MongrelError::Schema(format!(
18675 "materialized view WAL name mismatch: {name:?}"
18676 )));
18677 }
18678 if cat.live(name).is_some() {
18679 if let Some(existing) = cat
18680 .materialized_views
18681 .iter_mut()
18682 .find(|existing| existing.name == *name)
18683 {
18684 if *existing != definition {
18685 *existing = definition;
18686 changed = true;
18687 }
18688 } else {
18689 cat.materialized_views.push(definition);
18690 changed = true;
18691 }
18692 }
18693 }
18694 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
18695 let security = DdlOp::decode_security(security_json)?;
18696 validate_security_catalog(cat, &security)?;
18697 if cat.security != security {
18698 cat.security = security;
18699 if !catalog_snapshot_txns.contains(&txn_id) {
18700 advance_security_version(cat)?;
18701 }
18702 changed = true;
18703 }
18704 }
18705 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
18706 let target = match key.as_str() {
18707 "user_version" => &mut cat.user_version,
18708 "application_id" => &mut cat.application_id,
18709 _ => {
18710 return Err(MongrelError::InvalidArgument(format!(
18711 "unsupported recovered SQL pragma {key:?}"
18712 )))
18713 }
18714 };
18715 if *target != Some(value) {
18716 *target = Some(value);
18717 cat.db_epoch = cat.db_epoch.max(ce);
18718 changed = true;
18719 }
18720 }
18721 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
18722 if ce <= applied_catalog_epoch {
18723 continue;
18724 }
18725 let snapshot = DdlOp::decode_catalog(catalog_json)?;
18726 if snapshot.db_epoch != ce {
18727 return Err(MongrelError::Schema(format!(
18728 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
18729 snapshot.db_epoch
18730 )));
18731 }
18732 validate_recovered_catalog(&snapshot)?;
18733 validate_catalog_transition(cat, &snapshot)?;
18734 *cat = snapshot;
18735 applied_catalog_epoch = ce;
18736 changed = true;
18737 }
18738 _ => {}
18739 }
18740 }
18741
18742 if cat.db_epoch < max_committed_epoch {
18743 cat.db_epoch = max_committed_epoch;
18744 changed = true;
18745 }
18746 changed |= repair_catalog_allocator_counters(cat)?;
18747
18748 validate_recovered_catalog(cat)?;
18749 let storage_reconciliation = validate_recovered_storage_plan(
18750 root,
18751 durable_root,
18752 cat,
18753 &created_table_ids,
18754 &ttl_updates,
18755 meta_dek,
18756 )?;
18757
18758 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
18759 if apply && (changed || needs_storage_apply) {
18760 for table_id in storage_reconciliation {
18761 let entry = cat
18762 .tables
18763 .iter()
18764 .find(|entry| entry.table_id == table_id)
18765 .ok_or_else(|| MongrelError::CorruptWal {
18766 offset: table_id,
18767 reason: "recovery storage plan lost its catalog table".into(),
18768 })?;
18769 ensure_recovered_table_storage(
18770 table_roots
18771 .and_then(|roots| roots.get(&table_id))
18772 .map(Arc::as_ref),
18773 durable_root,
18774 &root.join(TABLES_DIR).join(table_id.to_string()),
18775 table_id,
18776 &entry.schema,
18777 meta_dek,
18778 )?;
18779 }
18780 for (table_id, (policy, ttl_epoch)) in ttl_updates {
18781 let Some(entry) = cat.tables.iter().find(|entry| {
18782 entry.table_id == table_id
18783 && matches!(entry.state, TableState::Live | TableState::Building { .. })
18784 }) else {
18785 continue;
18786 };
18787 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
18788 {
18789 root.try_clone()?
18790 } else if let Some(root) = durable_root {
18791 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
18792 } else {
18793 crate::durable_file::DurableRoot::open(
18794 root.join(TABLES_DIR).join(table_id.to_string()),
18795 )?
18796 };
18797 let table_dir = table_root.io_path()?;
18798 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
18799 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
18800 manifest.ttl = policy;
18801 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
18802 manifest.schema_id = entry.schema.schema_id;
18803 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18804 }
18805 }
18806 if changed {
18807 match durable_root {
18808 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
18809 None => catalog::write_atomic(root, cat, meta_dek)?,
18810 }
18811 }
18812 }
18813 *target_catalog = recovered_catalog;
18814 Ok(())
18815}
18816
18817fn ensure_recovered_table_storage(
18818 pinned_table: Option<&crate::durable_file::DurableRoot>,
18819 durable_root: Option<&crate::durable_file::DurableRoot>,
18820 fallback_table_dir: &Path,
18821 table_id: u64,
18822 schema: &Schema,
18823 meta_dek: Option<&[u8; META_DEK_LEN]>,
18824) -> Result<()> {
18825 let table_root = if let Some(root) = pinned_table {
18826 root.try_clone()?
18827 } else if let Some(root) = durable_root {
18828 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
18829 match root.open_directory(&relative) {
18830 Ok(table) => table,
18831 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
18832 root.create_directory_all_pinned(relative)?
18833 }
18834 Err(error) => return Err(error.into()),
18835 }
18836 } else {
18837 crate::durable_file::create_directory_all(fallback_table_dir)?;
18838 crate::durable_file::DurableRoot::open(fallback_table_dir)?
18839 };
18840 let table_dir = table_root.io_path()?;
18841 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
18842 Ok(manifest) => {
18843 if manifest.table_id != table_id {
18844 return Err(MongrelError::Conflict(format!(
18845 "recovered table directory id mismatch: expected {table_id}, found {}",
18846 manifest.table_id
18847 )));
18848 }
18849 Some(manifest)
18850 }
18851 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
18852 Err(error) => return Err(error),
18853 };
18854
18855 table_root.create_directory_all(crate::engine::WAL_DIR)?;
18856 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
18857 crate::engine::write_schema(&table_dir, schema)?;
18858
18859 if let Some(mut manifest) = existing_manifest.take() {
18860 if manifest.schema_id != schema.schema_id {
18861 manifest.schema_id = schema.schema_id;
18862 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18863 }
18864 } else {
18865 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
18867 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18868 }
18869 Ok(())
18870}
18871
18872fn validate_recovered_schema(schema: &Schema) -> Result<()> {
18873 schema.validate_auto_increment()?;
18874 schema.validate_defaults()?;
18875 schema.validate_ai()?;
18876 let mut column_ids = HashSet::new();
18877 let mut column_names = HashSet::new();
18878 for column in &schema.columns {
18879 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
18880 return Err(MongrelError::Schema(
18881 "recovered schema contains duplicate columns".into(),
18882 ));
18883 }
18884 match &column.ty {
18885 TypeId::Decimal128 { precision, scale }
18886 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
18887 {
18888 return Err(MongrelError::Schema(format!(
18889 "column {:?} has invalid decimal precision or scale",
18890 column.name
18891 )));
18892 }
18893 TypeId::Enum { variants }
18894 if variants.is_empty()
18895 || variants.iter().any(String::is_empty)
18896 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
18897 {
18898 return Err(MongrelError::Schema(format!(
18899 "column {:?} has invalid enum variants",
18900 column.name
18901 )));
18902 }
18903 _ => {}
18904 }
18905 }
18906 let mut index_names = HashSet::new();
18907 for index in &schema.indexes {
18908 index.validate_options()?;
18909 if index.name.is_empty()
18910 || !index_names.insert(index.name.as_str())
18911 || schema
18912 .columns
18913 .iter()
18914 .all(|column| column.id != index.column_id)
18915 {
18916 return Err(MongrelError::Schema(format!(
18917 "recovered index {:?} references missing column {}",
18918 index.name, index.column_id
18919 )));
18920 }
18921 }
18922 let mut colocated = HashSet::new();
18923 for group in &schema.colocation {
18924 if group.is_empty()
18925 || group.iter().any(|id| !column_ids.contains(id))
18926 || group.iter().any(|id| !colocated.insert(*id))
18927 {
18928 return Err(MongrelError::Schema(
18929 "recovered schema contains invalid column co-location groups".into(),
18930 ));
18931 }
18932 }
18933
18934 let mut constraint_ids = HashSet::new();
18935 let mut constraint_names = HashSet::<String>::new();
18936 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
18937 if name.is_empty()
18938 || !constraint_ids.insert(id)
18939 || !constraint_names.insert(name.to_owned())
18940 {
18941 return Err(MongrelError::Schema(
18942 "recovered schema contains duplicate or empty constraint identities".into(),
18943 ));
18944 }
18945 Ok(())
18946 };
18947 for unique in &schema.constraints.uniques {
18948 validate_constraint_identity(unique.id, &unique.name)?;
18949 if unique.columns.is_empty()
18950 || unique.columns.iter().any(|id| !column_ids.contains(id))
18951 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
18952 {
18953 return Err(MongrelError::Schema(format!(
18954 "unique constraint {:?} has invalid columns",
18955 unique.name
18956 )));
18957 }
18958 }
18959 for foreign_key in &schema.constraints.foreign_keys {
18960 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
18961 if foreign_key.ref_table.is_empty()
18962 || foreign_key.columns.is_empty()
18963 || foreign_key.columns.len() != foreign_key.ref_columns.len()
18964 || foreign_key
18965 .columns
18966 .iter()
18967 .any(|id| !column_ids.contains(id))
18968 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
18969 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
18970 != foreign_key.ref_columns.len()
18971 {
18972 return Err(MongrelError::Schema(format!(
18973 "foreign key {:?} has invalid columns",
18974 foreign_key.name
18975 )));
18976 }
18977 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
18978 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
18979 && foreign_key.columns.iter().any(|id| {
18980 schema
18981 .columns
18982 .iter()
18983 .find(|column| column.id == *id)
18984 .is_none_or(|column| {
18985 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
18986 })
18987 })
18988 {
18989 return Err(MongrelError::Schema(format!(
18990 "foreign key {:?} uses SET NULL on a non-nullable column",
18991 foreign_key.name
18992 )));
18993 }
18994 }
18995 for check in &schema.constraints.checks {
18996 validate_constraint_identity(check.id, &check.name)?;
18997 check.expr.validate()?;
18998 validate_check_columns(&check.expr, &column_ids)?;
18999 }
19000 Ok(())
19001}
19002
19003fn validate_check_columns(
19004 expression: &crate::constraint::CheckExpr,
19005 column_ids: &HashSet<u16>,
19006) -> Result<()> {
19007 use crate::constraint::CheckExpr;
19008 match expression {
19009 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
19010 if column_ids.contains(id) {
19011 Ok(())
19012 } else {
19013 Err(MongrelError::Schema(format!(
19014 "check constraint references unknown column {id}"
19015 )))
19016 }
19017 }
19018 CheckExpr::Regex { col, .. } => {
19019 if column_ids.contains(col) {
19020 Ok(())
19021 } else {
19022 Err(MongrelError::Schema(format!(
19023 "check constraint references unknown column {col}"
19024 )))
19025 }
19026 }
19027 CheckExpr::Add(left, right)
19028 | CheckExpr::Sub(left, right)
19029 | CheckExpr::Mul(left, right)
19030 | CheckExpr::Div(left, right)
19031 | CheckExpr::Mod(left, right)
19032 | CheckExpr::Eq(left, right)
19033 | CheckExpr::Ne(left, right)
19034 | CheckExpr::Lt(left, right)
19035 | CheckExpr::Le(left, right)
19036 | CheckExpr::Gt(left, right)
19037 | CheckExpr::Ge(left, right)
19038 | CheckExpr::And(left, right)
19039 | CheckExpr::Or(left, right) => {
19040 validate_check_columns(left, column_ids)?;
19041 validate_check_columns(right, column_ids)
19042 }
19043 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
19044 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
19045 }
19046}
19047
19048fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
19049 for (name, prior, candidate) in [
19050 ("db_epoch", current.db_epoch, next.db_epoch),
19051 ("next_table_id", current.next_table_id, next.next_table_id),
19052 (
19053 "next_segment_no",
19054 current.next_segment_no,
19055 next.next_segment_no,
19056 ),
19057 ("next_user_id", current.next_user_id, next.next_user_id),
19058 (
19059 "security_version",
19060 current.security_version,
19061 next.security_version,
19062 ),
19063 ] {
19064 if candidate < prior {
19065 return Err(MongrelError::Schema(format!(
19066 "catalog snapshot rolls back {name} from {prior} to {candidate}"
19067 )));
19068 }
19069 }
19070 for prior in ¤t.tables {
19071 let Some(candidate) = next
19072 .tables
19073 .iter()
19074 .find(|entry| entry.table_id == prior.table_id)
19075 else {
19076 return Err(MongrelError::Schema(format!(
19077 "catalog snapshot removes table identity {}",
19078 prior.table_id
19079 )));
19080 };
19081 if candidate.created_epoch != prior.created_epoch
19082 || candidate.schema.schema_id < prior.schema.schema_id
19083 || matches!(prior.state, TableState::Dropped { .. })
19084 && !matches!(candidate.state, TableState::Dropped { .. })
19085 {
19086 return Err(MongrelError::Schema(format!(
19087 "catalog snapshot rolls back table identity {}",
19088 prior.table_id
19089 )));
19090 }
19091 }
19092 for prior in ¤t.users {
19093 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
19094 if candidate.username != prior.username
19095 || candidate.created_epoch != prior.created_epoch
19096 {
19097 return Err(MongrelError::Schema(format!(
19098 "catalog snapshot reuses user identity {}",
19099 prior.id
19100 )));
19101 }
19102 }
19103 }
19104 Ok(())
19105}
19106
19107fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
19108 let mut table_ids = HashSet::new();
19109 let mut active_names = HashSet::new();
19110 let mut max_table_id = None::<u64>;
19111 for entry in &catalog.tables {
19112 if !table_ids.insert(entry.table_id) {
19113 return Err(MongrelError::Schema(format!(
19114 "catalog contains duplicate table id {}",
19115 entry.table_id
19116 )));
19117 }
19118 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
19119 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
19120 return Err(MongrelError::Schema(format!(
19121 "catalog table {} has invalid name or creation epoch",
19122 entry.table_id
19123 )));
19124 }
19125 validate_recovered_schema(&entry.schema)?;
19126 match &entry.state {
19127 TableState::Live => {
19128 if !active_names.insert(entry.name.as_str()) {
19129 return Err(MongrelError::Schema(format!(
19130 "catalog contains duplicate active table name {:?}",
19131 entry.name
19132 )));
19133 }
19134 }
19135 TableState::Dropped { at_epoch } => {
19136 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
19137 return Err(MongrelError::Schema(format!(
19138 "catalog table {} has invalid drop epoch {at_epoch}",
19139 entry.table_id
19140 )));
19141 }
19142 }
19143 TableState::Building {
19144 intended_name,
19145 query_id,
19146 replaces_table_id,
19147 ..
19148 } => {
19149 if intended_name.is_empty() || query_id.is_empty() {
19150 return Err(MongrelError::Schema(format!(
19151 "building table {} has empty identity fields",
19152 entry.table_id
19153 )));
19154 }
19155 if !active_names.insert(entry.name.as_str()) {
19156 return Err(MongrelError::Schema(format!(
19157 "catalog contains duplicate active/building table name {:?}",
19158 entry.name
19159 )));
19160 }
19161 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
19162 return Err(MongrelError::Schema(
19163 "building table cannot replace itself".into(),
19164 ));
19165 }
19166 }
19167 }
19168 }
19169 if let Some(maximum) = max_table_id {
19170 let required = maximum
19171 .checked_add(1)
19172 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19173 if catalog.next_table_id < required {
19174 return Err(MongrelError::Schema(format!(
19175 "catalog next_table_id {} precedes required {required}",
19176 catalog.next_table_id
19177 )));
19178 }
19179 }
19180 for entry in &catalog.tables {
19181 if let TableState::Building {
19182 replaces_table_id: Some(replaced),
19183 ..
19184 } = entry.state
19185 {
19186 if !table_ids.contains(&replaced) {
19187 return Err(MongrelError::Schema(format!(
19188 "building table {} replaces unknown table {replaced}",
19189 entry.table_id
19190 )));
19191 }
19192 }
19193 }
19194 for entry in &catalog.tables {
19195 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19196 validate_foreign_key_targets(catalog, &entry.schema)?;
19197 }
19198 }
19199
19200 let mut external_names = HashSet::new();
19201 for entry in &catalog.external_tables {
19202 entry.validate()?;
19203 validate_recovered_schema(&entry.declared_schema)?;
19204 if !entry.declared_schema.constraints.is_empty() {
19205 return Err(MongrelError::Schema(format!(
19206 "external table {:?} cannot carry engine-enforced constraints",
19207 entry.name
19208 )));
19209 }
19210 if entry.created_epoch > catalog.db_epoch
19211 || !external_names.insert(entry.name.as_str())
19212 || active_names.contains(entry.name.as_str())
19213 {
19214 return Err(MongrelError::Schema(format!(
19215 "invalid or duplicate external table {:?}",
19216 entry.name
19217 )));
19218 }
19219 }
19220
19221 let mut procedure_names = HashSet::new();
19222 for entry in &catalog.procedures {
19223 entry.procedure.validate()?;
19224 if entry.procedure.created_epoch > entry.procedure.updated_epoch
19225 || entry.procedure.updated_epoch > catalog.db_epoch
19226 || !procedure_names.insert(entry.procedure.name.as_str())
19227 {
19228 return Err(MongrelError::Schema(format!(
19229 "invalid or duplicate procedure {:?}",
19230 entry.procedure.name
19231 )));
19232 }
19233 validate_recovered_procedure_references(catalog, &entry.procedure)?;
19234 }
19235
19236 let mut trigger_names = HashSet::new();
19237 for entry in &catalog.triggers {
19238 entry.trigger.validate()?;
19239 if entry.trigger.created_epoch > entry.trigger.updated_epoch
19240 || entry.trigger.updated_epoch > catalog.db_epoch
19241 || !trigger_names.insert(entry.trigger.name.as_str())
19242 {
19243 return Err(MongrelError::Schema(format!(
19244 "invalid or duplicate trigger {:?}",
19245 entry.trigger.name
19246 )));
19247 }
19248 validate_recovered_trigger_references(catalog, &entry.trigger)?;
19249 }
19250
19251 let mut views = HashSet::new();
19252 for view in &catalog.materialized_views {
19253 let target = catalog.live(&view.name).ok_or_else(|| {
19254 MongrelError::Schema(format!(
19255 "materialized view {:?} has no live table",
19256 view.name
19257 ))
19258 })?;
19259 if view.name.is_empty()
19260 || view.query.trim().is_empty()
19261 || view.last_refresh_epoch > catalog.db_epoch
19262 || !views.insert(view.name.as_str())
19263 {
19264 return Err(MongrelError::Schema(format!(
19265 "materialized view {:?} has no unique live table",
19266 view.name
19267 )));
19268 }
19269 if let Some(incremental) = &view.incremental {
19270 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
19271 MongrelError::Schema(format!(
19272 "materialized view {:?} references missing source {:?}",
19273 view.name, incremental.source_table
19274 ))
19275 })?;
19276 if source.table_id != incremental.source_table_id
19277 || source
19278 .schema
19279 .columns
19280 .iter()
19281 .all(|column| column.id != incremental.group_column)
19282 {
19283 return Err(MongrelError::Schema(format!(
19284 "materialized view {:?} has invalid incremental source",
19285 view.name
19286 )));
19287 }
19288 let target_ids = target
19289 .schema
19290 .columns
19291 .iter()
19292 .map(|column| column.id)
19293 .collect::<HashSet<_>>();
19294 let mut output_ids = HashSet::new();
19295 let count_outputs = incremental
19296 .outputs
19297 .iter()
19298 .filter(|output| {
19299 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19300 })
19301 .count();
19302 if incremental.checkpoint_event_id.is_empty()
19303 || !target_ids.contains(&incremental.group_output_column)
19304 || !target_ids.contains(&incremental.count_output_column)
19305 || incremental.outputs.is_empty()
19306 || count_outputs != 1
19307 || incremental.outputs.iter().any(|output| {
19308 !target_ids.contains(&output.output_column)
19309 || output.output_column == incremental.group_output_column
19310 || !output_ids.insert(output.output_column)
19311 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19312 && output.output_column != incremental.count_output_column
19313 || match output.kind {
19314 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
19315 source
19316 .schema
19317 .columns
19318 .iter()
19319 .all(|column| column.id != source_column)
19320 }
19321 crate::catalog::IncrementalAggregateKind::Count => false,
19322 }
19323 })
19324 {
19325 return Err(MongrelError::Schema(format!(
19326 "materialized view {:?} has invalid incremental outputs",
19327 view.name
19328 )));
19329 }
19330 }
19331 }
19332
19333 validate_security_catalog(catalog, &catalog.security)?;
19334 validate_recovered_auth_catalog(catalog)?;
19335 Ok(())
19336}
19337
19338fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
19339 let mut changed = false;
19340 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
19341 let required = maximum
19342 .checked_add(1)
19343 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19344 if catalog.next_table_id < required {
19345 catalog.next_table_id = required;
19346 changed = true;
19347 }
19348 }
19349 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
19350 let required = maximum
19351 .checked_add(1)
19352 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
19353 if catalog.next_user_id < required {
19354 catalog.next_user_id = required;
19355 changed = true;
19356 }
19357 }
19358 Ok(changed)
19359}
19360
19361fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
19362 for foreign_key in &schema.constraints.foreign_keys {
19363 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
19364 MongrelError::Schema(format!(
19365 "foreign key {:?} references unknown live table {:?}",
19366 foreign_key.name, foreign_key.ref_table
19367 ))
19368 })?;
19369 let referenced_unique = parent
19370 .schema
19371 .constraints
19372 .uniques
19373 .iter()
19374 .any(|unique| unique.columns == foreign_key.ref_columns)
19375 || foreign_key.ref_columns.len() == 1
19376 && parent
19377 .schema
19378 .primary_key()
19379 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
19380 if !referenced_unique {
19381 return Err(MongrelError::Schema(format!(
19382 "foreign key {:?} does not reference a unique key",
19383 foreign_key.name
19384 )));
19385 }
19386 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
19387 let local = schema.columns.iter().find(|column| column.id == *local_id);
19388 let referenced = parent
19389 .schema
19390 .columns
19391 .iter()
19392 .find(|column| column.id == *parent_id);
19393 if local
19394 .zip(referenced)
19395 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
19396 {
19397 return Err(MongrelError::Schema(format!(
19398 "foreign key {:?} has missing or incompatible columns",
19399 foreign_key.name
19400 )));
19401 }
19402 }
19403 }
19404 Ok(())
19405}
19406
19407fn validate_recovered_procedure_references(
19408 catalog: &Catalog,
19409 procedure: &StoredProcedure,
19410) -> Result<()> {
19411 for step in &procedure.body.steps {
19412 let Some(table_name) = step.table() else {
19413 continue;
19414 };
19415 let schema = &catalog
19416 .live(table_name)
19417 .ok_or_else(|| {
19418 MongrelError::Schema(format!(
19419 "procedure {:?} references unknown table {table_name:?}",
19420 procedure.name
19421 ))
19422 })?
19423 .schema;
19424 match step {
19425 ProcedureStep::NativeQuery {
19426 conditions,
19427 projection,
19428 ..
19429 } => {
19430 for condition in conditions {
19431 validate_condition_columns(condition, schema)?;
19432 }
19433 for id in projection.iter().flatten() {
19434 validate_column_id(*id, schema)?;
19435 }
19436 }
19437 ProcedureStep::Put { cells, .. } => {
19438 for cell in cells {
19439 validate_column_id(cell.column_id, schema)?;
19440 }
19441 }
19442 ProcedureStep::Upsert {
19443 cells,
19444 update_cells,
19445 ..
19446 } => {
19447 for cell in cells.iter().chain(update_cells.iter().flatten()) {
19448 validate_column_id(cell.column_id, schema)?;
19449 }
19450 }
19451 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
19452 return Err(MongrelError::Schema(format!(
19453 "procedure {:?} deletes by primary key on table without one",
19454 procedure.name
19455 )));
19456 }
19457 ProcedureStep::DeleteByPk { .. }
19458 | ProcedureStep::DeleteRows { .. }
19459 | ProcedureStep::SqlQuery { .. } => {}
19460 }
19461 }
19462 Ok(())
19463}
19464
19465fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
19466 let target_schema = match &trigger.target {
19467 TriggerTarget::Table(name) => catalog
19468 .live(name)
19469 .ok_or_else(|| {
19470 MongrelError::Schema(format!(
19471 "trigger {:?} references unknown table {name:?}",
19472 trigger.name
19473 ))
19474 })?
19475 .schema
19476 .clone(),
19477 TriggerTarget::View(_) => Schema {
19478 columns: trigger.target_columns.clone(),
19479 ..Schema::default()
19480 },
19481 };
19482 for column in &trigger.update_of {
19483 if target_schema.column(column).is_none() {
19484 return Err(MongrelError::Schema(format!(
19485 "trigger {:?} references unknown UPDATE OF column {column:?}",
19486 trigger.name
19487 )));
19488 }
19489 }
19490 if let Some(expr) = &trigger.when {
19491 validate_trigger_expr(expr, &target_schema, trigger.event)?;
19492 }
19493 let mut selects = HashMap::new();
19494 for step in &trigger.program.steps {
19495 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
19496 return Err(MongrelError::Schema(
19497 "SetNew is only valid in BEFORE triggers".into(),
19498 ));
19499 }
19500 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
19501 }
19502 Ok(())
19503}
19504
19505fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
19506 let mut role_names = HashSet::new();
19507 for role in &catalog.roles {
19508 if role.name.is_empty()
19509 || role.created_epoch > catalog.db_epoch
19510 || !role_names.insert(role.name.as_str())
19511 {
19512 return Err(MongrelError::Schema(format!(
19513 "invalid or duplicate role {:?}",
19514 role.name
19515 )));
19516 }
19517 for permission in &role.permissions {
19518 if let Some(table) = permission_table(permission) {
19519 let schema = catalog
19520 .live(table)
19521 .map(|entry| &entry.schema)
19522 .or_else(|| {
19523 catalog
19524 .external_tables
19525 .iter()
19526 .find(|entry| entry.name == table)
19527 .map(|entry| &entry.declared_schema)
19528 })
19529 .ok_or_else(|| {
19530 MongrelError::Schema(format!(
19531 "role {:?} references unknown table {table:?}",
19532 role.name
19533 ))
19534 })?;
19535 let columns = match permission {
19536 crate::auth::Permission::SelectColumns { columns, .. }
19537 | crate::auth::Permission::InsertColumns { columns, .. }
19538 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
19539 _ => None,
19540 };
19541 if columns.is_some_and(|columns| {
19542 columns.is_empty()
19543 || columns.iter().any(|column| schema.column(column).is_none())
19544 }) {
19545 return Err(MongrelError::Schema(format!(
19546 "role {:?} contains invalid column permissions",
19547 role.name
19548 )));
19549 }
19550 }
19551 }
19552 }
19553 let mut user_ids = HashSet::new();
19554 let mut usernames = HashSet::new();
19555 let mut maximum_user_id = 0;
19556 for user in &catalog.users {
19557 maximum_user_id = maximum_user_id.max(user.id);
19558 if user.id == 0
19559 || user.username.is_empty()
19560 || user.password_hash.is_empty()
19561 || user.created_epoch > catalog.db_epoch
19562 || !user_ids.insert(user.id)
19563 || !usernames.insert(user.username.as_str())
19564 || user
19565 .roles
19566 .iter()
19567 .any(|role| !role_names.contains(role.as_str()))
19568 {
19569 return Err(MongrelError::Schema(format!(
19570 "invalid or duplicate user {:?}",
19571 user.username
19572 )));
19573 }
19574 }
19575 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
19576 return Err(MongrelError::Schema(
19577 "catalog next_user_id does not advance beyond existing user ids".into(),
19578 ));
19579 }
19580 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
19581 return Err(MongrelError::Schema(
19582 "authenticated catalog has no administrator".into(),
19583 ));
19584 }
19585 Ok(())
19586}
19587
19588fn validate_recovered_storage_plan(
19589 root: &Path,
19590 durable_root: Option<&crate::durable_file::DurableRoot>,
19591 catalog: &Catalog,
19592 created_table_ids: &HashSet<u64>,
19593 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
19594 meta_dek: Option<&[u8; META_DEK_LEN]>,
19595) -> Result<Vec<u64>> {
19596 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
19597 let mut reconcile = Vec::new();
19598 for entry in &catalog.tables {
19599 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19600 continue;
19601 }
19602 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19603 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
19604 let table_exists = match durable_root {
19605 Some(root) => match root.open_directory(&relative_dir) {
19606 Ok(_) => true,
19607 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19608 Err(error) => return Err(error.into()),
19609 },
19610 None => table_dir.is_dir(),
19611 };
19612 if !table_exists {
19613 if created_table_ids.contains(&entry.table_id) {
19614 reconcile.push(entry.table_id);
19615 continue;
19616 }
19617 return Err(MongrelError::NotFound(format!(
19618 "catalog table {} storage is missing",
19619 entry.table_id
19620 )));
19621 }
19622 let manifest_result = match durable_root {
19623 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
19624 None => crate::manifest::read(&table_dir, meta_dek),
19625 };
19626 let manifest = match manifest_result {
19627 Ok(manifest) => manifest,
19628 Err(MongrelError::Io(error))
19629 if created_table_ids.contains(&entry.table_id)
19630 && error.kind() == std::io::ErrorKind::NotFound =>
19631 {
19632 reconcile.push(entry.table_id);
19633 continue;
19634 }
19635 Err(error) => return Err(error),
19636 };
19637 if manifest.table_id != entry.table_id {
19638 return Err(MongrelError::Conflict(format!(
19639 "catalog table {} storage identity mismatch",
19640 entry.table_id
19641 )));
19642 }
19643 let schema_result = match durable_root {
19644 Some(root) => root
19645 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
19646 .map_err(MongrelError::from),
19647 None => crate::durable_file::open_regular_nofollow(
19648 &table_dir.join(crate::engine::SCHEMA_FILENAME),
19649 ),
19650 };
19651 let file = match schema_result {
19652 Ok(file) => file,
19653 Err(MongrelError::Io(error))
19654 if created_table_ids.contains(&entry.table_id)
19655 && error.kind() == std::io::ErrorKind::NotFound =>
19656 {
19657 reconcile.push(entry.table_id);
19658 continue;
19659 }
19660 Err(error) => return Err(error),
19661 };
19662 let length = file.metadata()?.len();
19663 if length > MAX_SCHEMA_BYTES {
19664 return Err(MongrelError::ResourceLimitExceeded {
19665 resource: "recovered schema bytes",
19666 requested: usize::try_from(length).unwrap_or(usize::MAX),
19667 limit: MAX_SCHEMA_BYTES as usize,
19668 });
19669 }
19670 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
19671 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
19672 if manifest.schema_id != entry.schema.schema_id
19673 || crate::wal::DdlOp::encode_schema(&disk_schema)?
19674 != crate::wal::DdlOp::encode_schema(&entry.schema)?
19675 {
19676 reconcile.push(entry.table_id);
19677 }
19678 }
19679 for table_id in ttl_updates.keys() {
19680 if !catalog.tables.iter().any(|entry| {
19681 entry.table_id == *table_id
19682 && matches!(entry.state, TableState::Live | TableState::Building { .. })
19683 }) {
19684 continue;
19685 }
19686 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
19687 let table_exists = match durable_root {
19688 Some(root) => match root.open_directory(&relative_dir) {
19689 Ok(_) => true,
19690 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19691 Err(error) => return Err(error.into()),
19692 },
19693 None => root.join(&relative_dir).is_dir(),
19694 };
19695 if !table_exists && !created_table_ids.contains(table_id) {
19696 return Err(MongrelError::NotFound(format!(
19697 "TTL recovery table {table_id} storage is missing"
19698 )));
19699 }
19700 }
19701 reconcile.sort_unstable();
19702 reconcile.dedup();
19703 Ok(reconcile)
19704}
19705
19706fn validate_catalog_table_storage(
19707 root: &crate::durable_file::DurableRoot,
19708 catalog: &Catalog,
19709 meta_dek: Option<&[u8; META_DEK_LEN]>,
19710) -> Result<()> {
19711 for entry in &catalog.tables {
19712 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19713 continue;
19714 }
19715 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19716 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
19717 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
19718 return Err(MongrelError::Conflict(format!(
19719 "catalog table {} storage identity mismatch",
19720 entry.table_id
19721 )));
19722 }
19723 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
19724 }
19725 Ok(())
19726}
19727
19728fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
19729 match schema.columns.iter_mut().find(|c| c.id == column.id) {
19730 Some(existing) if *existing == column => Ok(false),
19731 Some(existing) => {
19732 *existing = column;
19733 schema.schema_id = schema
19734 .schema_id
19735 .checked_add(1)
19736 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19737 Ok(true)
19738 }
19739 None => {
19740 schema.columns.push(column);
19741 schema.schema_id = schema
19742 .schema_id
19743 .checked_add(1)
19744 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19745 Ok(true)
19746 }
19747 }
19748}
19749
19750fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
19751 use crate::auth::Permission;
19752 match permission {
19753 Permission::Select { table }
19754 | Permission::Insert { table }
19755 | Permission::Update { table }
19756 | Permission::Delete { table }
19757 | Permission::SelectColumns { table, .. }
19758 | Permission::InsertColumns { table, .. }
19759 | Permission::UpdateColumns { table, .. } => Some(table),
19760 Permission::All | Permission::Ddl | Permission::Admin => None,
19761 }
19762}
19763
19764fn apply_rebuilding_publish(
19765 catalog: &mut Catalog,
19766 table_id: u64,
19767 replaced_table_id: u64,
19768 new_name: &str,
19769 epoch: u64,
19770) -> Result<bool> {
19771 let already_published = catalog.tables.iter().any(|entry| {
19772 entry.table_id == table_id
19773 && entry.name == new_name
19774 && matches!(entry.state, TableState::Live)
19775 }) && catalog.tables.iter().any(|entry| {
19776 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
19777 });
19778 if already_published {
19779 return Ok(false);
19780 }
19781 let schema = catalog
19782 .tables
19783 .iter()
19784 .find(|entry| entry.table_id == table_id)
19785 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
19786 .schema
19787 .clone();
19788 let replaced = catalog
19789 .tables
19790 .iter_mut()
19791 .find(|entry| entry.table_id == replaced_table_id)
19792 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
19793 replaced.state = TableState::Dropped { at_epoch: epoch };
19794 let replacement = catalog
19795 .tables
19796 .iter_mut()
19797 .find(|entry| entry.table_id == table_id)
19798 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
19799 replacement.name = new_name.to_string();
19800 replacement.state = TableState::Live;
19801
19802 for role in &mut catalog.roles {
19803 role.permissions.retain_mut(|permission| {
19804 retain_rebuilt_permission_columns(permission, new_name, &schema)
19805 });
19806 }
19807 for definition in &mut catalog.materialized_views {
19808 if let Some(incremental) = definition.incremental.as_mut() {
19809 if incremental.source_table == new_name
19810 && incremental.source_table_id == replaced_table_id
19811 {
19812 incremental.source_table_id = table_id;
19813 }
19814 }
19815 }
19816 advance_security_version(catalog)?;
19817 Ok(true)
19818}
19819
19820fn retain_rebuilt_permission_columns(
19821 permission: &mut crate::auth::Permission,
19822 target_table: &str,
19823 schema: &Schema,
19824) -> bool {
19825 use crate::auth::Permission;
19826 let columns = match permission {
19827 Permission::SelectColumns { table, columns }
19828 | Permission::InsertColumns { table, columns }
19829 | Permission::UpdateColumns { table, columns }
19830 if table == target_table =>
19831 {
19832 Some(columns)
19833 }
19834 _ => None,
19835 };
19836 if let Some(columns) = columns {
19837 columns.retain(|column| schema.column(column).is_some());
19838 !columns.is_empty()
19839 } else {
19840 true
19841 }
19842}
19843
19844fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
19845 use crate::auth::Permission;
19846 let table = match permission {
19847 Permission::Select { table }
19848 | Permission::Insert { table }
19849 | Permission::Update { table }
19850 | Permission::Delete { table }
19851 | Permission::SelectColumns { table, .. }
19852 | Permission::InsertColumns { table, .. }
19853 | Permission::UpdateColumns { table, .. } => Some(table),
19854 Permission::All | Permission::Ddl | Permission::Admin => None,
19855 };
19856 if let Some(table) = table.filter(|table| table.as_str() == old) {
19857 *table = new.to_string();
19858 }
19859}
19860
19861fn rename_permission_column(
19862 permission: &mut crate::auth::Permission,
19863 target_table: &str,
19864 old: &str,
19865 new: &str,
19866) {
19867 use crate::auth::Permission;
19868 let columns = match permission {
19869 Permission::SelectColumns { table, columns }
19870 | Permission::InsertColumns { table, columns }
19871 | Permission::UpdateColumns { table, columns }
19872 if table == target_table =>
19873 {
19874 Some(columns)
19875 }
19876 _ => None,
19877 };
19878 if let Some(column) = columns
19879 .into_iter()
19880 .flatten()
19881 .find(|column| column.as_str() == old)
19882 {
19883 *column = new.to_string();
19884 }
19885}
19886
19887pub(crate) fn validate_security_catalog(
19888 catalog: &Catalog,
19889 security: &crate::security::SecurityCatalog,
19890) -> Result<()> {
19891 let mut policy_names = HashSet::new();
19892 for table in &security.rls_tables {
19893 if catalog.live(table).is_none() {
19894 return Err(MongrelError::NotFound(format!(
19895 "RLS table {table:?} not found"
19896 )));
19897 }
19898 }
19899 for policy in &security.policies {
19900 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
19901 return Err(MongrelError::InvalidArgument(format!(
19902 "duplicate policy {:?} on {:?}",
19903 policy.name, policy.table
19904 )));
19905 }
19906 let schema = &catalog
19907 .live(&policy.table)
19908 .ok_or_else(|| {
19909 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
19910 })?
19911 .schema;
19912 if let Some(expression) = &policy.using {
19913 validate_security_expression(expression, schema)?;
19914 }
19915 if let Some(expression) = &policy.with_check {
19916 validate_security_expression(expression, schema)?;
19917 }
19918 }
19919 let mut mask_names = HashSet::new();
19920 for mask in &security.masks {
19921 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
19922 return Err(MongrelError::InvalidArgument(format!(
19923 "duplicate mask {:?} on {:?}",
19924 mask.name, mask.table
19925 )));
19926 }
19927 let column = catalog
19928 .live(&mask.table)
19929 .and_then(|entry| {
19930 entry
19931 .schema
19932 .columns
19933 .iter()
19934 .find(|column| column.id == mask.column)
19935 })
19936 .ok_or_else(|| {
19937 MongrelError::NotFound(format!(
19938 "mask column {} on {:?} not found",
19939 mask.column, mask.table
19940 ))
19941 })?;
19942 if matches!(
19943 mask.strategy,
19944 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
19945 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
19946 {
19947 return Err(MongrelError::InvalidArgument(format!(
19948 "mask {:?} requires a string/bytes column",
19949 mask.name
19950 )));
19951 }
19952 }
19953 Ok(())
19954}
19955
19956fn validate_security_expression(
19957 expression: &crate::security::SecurityExpr,
19958 schema: &Schema,
19959) -> Result<()> {
19960 use crate::security::SecurityExpr;
19961 match expression {
19962 SecurityExpr::True => Ok(()),
19963 SecurityExpr::ColumnEqCurrentUser { column }
19964 | SecurityExpr::ColumnEqValue { column, .. } => {
19965 if schema
19966 .columns
19967 .iter()
19968 .any(|candidate| candidate.id == *column)
19969 {
19970 Ok(())
19971 } else {
19972 Err(MongrelError::InvalidArgument(format!(
19973 "security expression references unknown column id {column}"
19974 )))
19975 }
19976 }
19977 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
19978 validate_security_expression(left, schema)?;
19979 validate_security_expression(right, schema)
19980 }
19981 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
19982 }
19983}
19984
19985fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
19987 let referenced = cat
19988 .tables
19989 .iter()
19990 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
19991 .map(|entry| entry.table_id)
19992 .collect::<HashSet<_>>();
19993 let tables_dir = root.join(TABLES_DIR);
19994 let entries = match std::fs::read_dir(&tables_dir) {
19995 Ok(entries) => entries,
19996 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
19997 Err(error) => return Err(error.into()),
19998 };
19999 for entry in entries {
20000 let entry = entry?;
20001 if !entry.file_type()?.is_dir() {
20002 continue;
20003 }
20004 let file_name = entry.file_name();
20005 let Some(name) = file_name.to_str() else {
20006 continue;
20007 };
20008 let Ok(table_id) = name.parse::<u64>() else {
20009 continue;
20010 };
20011 if name != table_id.to_string() {
20012 continue;
20013 }
20014 if !referenced.contains(&table_id) {
20015 crate::durable_file::remove_directory_all(&entry.path())?;
20016 }
20017 }
20018 Ok(())
20019}
20020
20021fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
20026 for entry in &cat.tables {
20027 let txn_dir = root
20028 .join(TABLES_DIR)
20029 .join(entry.table_id.to_string())
20030 .join("_txn");
20031 if txn_dir.exists() {
20032 let _ = std::fs::remove_dir_all(&txn_dir);
20033 }
20034 }
20035}
20036
20037#[cfg(test)]
20038mod write_permission_tests {
20039 use super::*;
20040 use crate::txn::Staged;
20041
20042 struct NoopExternalBridge;
20043
20044 impl ExternalTriggerBridge for NoopExternalBridge {
20045 fn apply_trigger_external_write(
20046 &self,
20047 _entry: &ExternalTableEntry,
20048 base_state: Vec<u8>,
20049 _op: ExternalTriggerWrite,
20050 ) -> Result<ExternalTriggerWriteResult> {
20051 Ok(ExternalTriggerWriteResult::new(base_state))
20052 }
20053 }
20054
20055 fn assert_txn_namespace_full<T>(result: Result<T>) {
20056 assert!(matches!(result, Err(MongrelError::Full(_))));
20057 }
20058
20059 #[test]
20060 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
20061 let directory = tempfile::tempdir().unwrap();
20062 let database = Database::create(directory.path()).unwrap();
20063 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
20064 *database.next_txn_id.lock() = generation << 32;
20065 let before = crate::wal::SharedWal::replay(directory.path())
20066 .unwrap()
20067 .len();
20068 let bridge = NoopExternalBridge;
20069
20070 assert_txn_namespace_full(database.begin().commit());
20071 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
20072 assert_txn_namespace_full(
20073 database
20074 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
20075 .commit(),
20076 );
20077 assert_txn_namespace_full(
20078 database
20079 .begin_with_external_trigger_bridge(&bridge)
20080 .commit(),
20081 );
20082 assert_txn_namespace_full(
20083 database
20084 .begin_with_external_trigger_bridge_as(&bridge, None)
20085 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
20086 );
20087
20088 assert_eq!(
20089 crate::wal::SharedWal::replay(directory.path())
20090 .unwrap()
20091 .len(),
20092 before
20093 );
20094 drop(database);
20095 Database::open(directory.path()).unwrap();
20096 }
20097
20098 #[test]
20099 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
20100 let directory = tempfile::tempdir().unwrap();
20101 let table_dir = directory.path().join("7");
20102 crate::durable_file::create_directory_all(&table_dir).unwrap();
20103 let original_schema = test_schema();
20104 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
20105 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
20106 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
20107 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
20108 let original_bytes = std::fs::read(&schema_path).unwrap();
20109
20110 let mut replacement_schema = original_schema;
20111 replacement_schema.schema_id += 1;
20112 assert!(matches!(
20113 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
20114 Err(MongrelError::Conflict(_))
20115 ));
20116
20117 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
20118 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
20119 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
20120 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
20121 }
20122
20123 #[test]
20124 fn catalog_table_missing_storage_fails_without_recreating_it() {
20125 let directory = tempfile::tempdir().unwrap();
20126 let table_dir = {
20127 let database = Database::create(directory.path()).unwrap();
20128 database.create_table("docs", test_schema()).unwrap();
20129 directory
20130 .path()
20131 .join(TABLES_DIR)
20132 .join(database.table_id("docs").unwrap().to_string())
20133 };
20134 std::fs::remove_dir_all(&table_dir).unwrap();
20135
20136 assert!(matches!(
20137 Database::open(directory.path()),
20138 Err(MongrelError::NotFound(_))
20139 ));
20140 assert!(!table_dir.exists());
20141 }
20142
20143 #[test]
20144 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
20145 let directory = tempfile::tempdir().unwrap();
20146 let database = std::sync::Arc::new(
20147 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
20148 );
20149 database.create_user("alice", "old-password").unwrap();
20150 let old_identity = database.user_identity("alice").unwrap();
20151 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
20152 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
20153 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
20154 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
20155
20156 std::thread::scope(|scope| {
20157 let authenticate = {
20158 let database = std::sync::Arc::clone(&database);
20159 scope.spawn(move || {
20160 database.authenticate_principal_inner("alice", "old-password", || {
20161 verified_tx.send(()).unwrap();
20162 resume_rx.recv().unwrap();
20163 })
20164 })
20165 };
20166 verified_rx.recv().unwrap();
20167 let mutate = {
20168 let database = std::sync::Arc::clone(&database);
20169 scope.spawn(move || {
20170 mutation_started_tx.send(()).unwrap();
20171 database.drop_user("alice").unwrap();
20172 database.create_user("alice", "new-password").unwrap();
20173 mutation_done_tx.send(()).unwrap();
20174 })
20175 };
20176 mutation_started_rx.recv().unwrap();
20177 assert!(mutation_done_rx
20178 .recv_timeout(std::time::Duration::from_millis(50))
20179 .is_err());
20180 resume_tx.send(()).unwrap();
20181 let principal = authenticate.join().unwrap().unwrap().unwrap();
20182 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
20183 mutate.join().unwrap();
20184 });
20185
20186 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
20187 assert!(database
20188 .authenticate_principal("alice", "old-password")
20189 .unwrap()
20190 .is_none());
20191 assert!(database
20192 .authenticate_principal("alice", "new-password")
20193 .unwrap()
20194 .is_some());
20195 }
20196
20197 #[test]
20198 fn homogeneous_batch_summarizes_to_one_permission_decision() {
20199 let staging = (0..10_050)
20200 .map(|_| {
20201 (
20202 7,
20203 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
20204 )
20205 })
20206 .collect::<Vec<_>>();
20207
20208 let needs = summarize_write_permissions(&staging);
20209 let table = needs.get(&7).unwrap();
20210 assert_eq!(needs.len(), 1);
20211 assert!(table.insert);
20212 assert_eq!(table.insert_columns, [1, 2]);
20213 assert!(!table.update);
20214 assert!(!table.delete);
20215 assert!(!table.truncate);
20216 }
20217
20218 #[test]
20219 fn mixed_writes_union_columns_and_preserve_empty_operations() {
20220 let staging = vec![
20221 (7, Staged::Put(vec![(2, Value::Int64(2))])),
20222 (7, Staged::Put(vec![(1, Value::Int64(1))])),
20223 (
20224 7,
20225 Staged::Update {
20226 row_id: RowId(1),
20227 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
20228 changed_columns: vec![2],
20229 },
20230 ),
20231 (7, Staged::Delete(RowId(2))),
20232 (8, Staged::Truncate),
20233 ];
20234
20235 let needs = summarize_write_permissions(&staging);
20236 let table = needs.get(&7).unwrap();
20237 assert_eq!(table.insert_columns, [1, 2]);
20238 assert!(table.update);
20239 assert_eq!(table.update_columns, [2]);
20240 assert!(table.delete);
20241 assert!(needs.get(&8).unwrap().truncate);
20242 }
20243
20244 #[test]
20245 fn final_permission_decisions_do_not_scale_with_rows() {
20246 let credentialless_dir = tempfile::tempdir().unwrap();
20247 let credentialless = Database::create(credentialless_dir.path()).unwrap();
20248 credentialless.create_table("docs", test_schema()).unwrap();
20249 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20250 credentialless
20251 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
20252 .unwrap();
20253 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
20254
20255 let authenticated_dir = tempfile::tempdir().unwrap();
20256 let authenticated =
20257 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
20258 .unwrap();
20259 authenticated.create_table("docs", test_schema()).unwrap();
20260 let admin = authenticated.resolve_principal("admin").unwrap();
20261 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20262 authenticated
20263 .validate_write_permissions(
20264 &puts(authenticated.table_id("docs").unwrap()),
20265 Some(&admin),
20266 None,
20267 )
20268 .unwrap();
20269 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20270 }
20271
20272 #[test]
20273 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
20274 let dir = tempfile::tempdir().unwrap();
20275 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20276 db.create_table("docs", test_schema()).unwrap();
20277 let admin = db.resolve_principal("admin").unwrap();
20278 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20279
20280 let mut transaction = db.begin_as(Some(admin));
20281 transaction
20282 .delete_batch("docs", (0..100).map(RowId).collect())
20283 .unwrap();
20284 transaction.commit().unwrap();
20285
20286 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
20287 }
20288
20289 #[test]
20290 fn truncate_validation_checks_admin_once_for_all_tables() {
20291 let dir = tempfile::tempdir().unwrap();
20292 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20293 db.create_table("first", test_schema()).unwrap();
20294 db.create_table("second", test_schema()).unwrap();
20295 let admin = db.resolve_principal("admin").unwrap();
20296 let staging = vec![
20297 (db.table_id("first").unwrap(), Staged::Truncate),
20298 (db.table_id("second").unwrap(), Staged::Truncate),
20299 ];
20300
20301 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20302 db.validate_write_permissions(&staging, Some(&admin), None)
20303 .unwrap();
20304 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20305 }
20306
20307 #[test]
20308 fn one_table_commit_batches_structural_work() {
20309 let dir = tempfile::tempdir().unwrap();
20310 let db = Database::create(dir.path()).unwrap();
20311 db.create_table("docs", test_schema()).unwrap();
20312 let table_id = db.table_id("docs").unwrap();
20313
20314 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
20315 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20316 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20317 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20318 db.transaction(|transaction| {
20319 for id in 0..100 {
20320 transaction.put("docs", vec![(1, Value::Int64(id))])?;
20321 }
20322 Ok(())
20323 })
20324 .unwrap();
20325
20326 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
20327 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20328 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20329 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20330
20331 let puts = crate::wal::SharedWal::replay(dir.path())
20332 .unwrap()
20333 .into_iter()
20334 .filter_map(|record| match record.op {
20335 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
20336 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
20337 .unwrap()
20338 .len(),
20339 ),
20340 _ => None,
20341 })
20342 .collect::<Vec<_>>();
20343 assert_eq!(puts, [100]);
20344
20345 let row_ids = db
20346 .table("docs")
20347 .unwrap()
20348 .lock()
20349 .visible_rows(db.snapshot().0)
20350 .unwrap()
20351 .into_iter()
20352 .take(2)
20353 .map(|row| row.row_id)
20354 .collect::<Vec<_>>();
20355 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20356 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20357 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20358 db.transaction(|transaction| {
20359 for row_id in row_ids {
20360 transaction.delete("docs", row_id)?;
20361 }
20362 Ok(())
20363 })
20364 .unwrap();
20365 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20366 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20367 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20368
20369 let deletes = crate::wal::SharedWal::replay(dir.path())
20370 .unwrap()
20371 .into_iter()
20372 .filter_map(|record| match record.op {
20373 crate::wal::Op::Delete {
20374 table_id: id,
20375 row_ids,
20376 } if id == table_id => Some(row_ids.len()),
20377 _ => None,
20378 })
20379 .collect::<Vec<_>>();
20380 assert_eq!(deletes, [2]);
20381 }
20382
20383 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
20384 (0..10_050)
20385 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
20386 .collect()
20387 }
20388
20389 fn test_schema() -> Schema {
20390 Schema {
20391 columns: vec![ColumnDef {
20392 id: 1,
20393 name: "id".into(),
20394 ty: TypeId::Int64,
20395 flags: crate::schema::ColumnFlags::empty()
20396 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20397 default_value: None,
20398 embedding_source: None,
20399 }],
20400 ..Schema::default()
20401 }
20402 }
20403}
20404
20405#[cfg(test)]
20406mod cdc_bounds_tests {
20407 use super::*;
20408
20409 #[test]
20410 fn retained_byte_limit_rejects_without_allocating_payload() {
20411 let mut retained = 0;
20412 let error = charge_cdc_bytes(
20413 &mut retained,
20414 CDC_MAX_RETAINED_BYTES.saturating_add(1),
20415 "CDC retained bytes",
20416 )
20417 .unwrap_err();
20418 assert!(matches!(
20419 error,
20420 MongrelError::ResourceLimitExceeded {
20421 resource: "CDC retained bytes",
20422 ..
20423 }
20424 ));
20425 }
20426
20427 #[test]
20428 fn row_json_estimate_accounts_for_byte_array_expansion() {
20429 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
20430 .with_column(1, Value::Bytes(vec![0; 1024]));
20431 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
20432 }
20433}
20434
20435#[cfg(test)]
20436mod generation_metrics_tests {
20437 use super::*;
20438 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20439
20440 #[test]
20441 fn legacy_cow_fallback_is_measured() {
20442 let dir = tempfile::tempdir().unwrap();
20443 let table = Table::create(
20444 dir.path(),
20445 Schema {
20446 columns: vec![ColumnDef {
20447 id: 1,
20448 name: "id".into(),
20449 ty: TypeId::Int64,
20450 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20451 default_value: None,
20452 embedding_source: None,
20453 }],
20454 ..Schema::default()
20455 },
20456 1,
20457 )
20458 .unwrap();
20459 let handle = TableHandle::from_table(table);
20460 let held = match &handle.inner {
20461 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
20462 TableHandleInner::Direct(_) => unreachable!(),
20463 };
20464
20465 handle.lock().set_sync_byte_threshold(1);
20466
20467 let stats = handle.generation_stats();
20468 assert_eq!(stats.cow_clone_count, 1);
20469 assert!(stats.estimated_cow_clone_bytes > 0);
20470 drop(held);
20471 }
20472}
20473
20474#[cfg(test)]
20475mod trigger_engine_tests {
20476 use super::*;
20477
20478 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
20479 WriteEvent {
20480 table: "test".into(),
20481 kind: TriggerEvent::Insert,
20482 new: Some(TriggerRowImage {
20483 columns: new_cells.iter().cloned().collect(),
20484 }),
20485 old: Some(TriggerRowImage {
20486 columns: old_cells.iter().cloned().collect(),
20487 }),
20488 changed_columns: Vec::new(),
20489 op_indices: Vec::new(),
20490 put_idx: None,
20491 trigger_stack: Vec::new(),
20492 }
20493 }
20494
20495 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
20496 WriteEvent {
20497 table: "test".into(),
20498 kind: TriggerEvent::Insert,
20499 new: Some(TriggerRowImage {
20500 columns: new_cells.iter().cloned().collect(),
20501 }),
20502 old: None,
20503 changed_columns: Vec::new(),
20504 op_indices: Vec::new(),
20505 put_idx: None,
20506 trigger_stack: Vec::new(),
20507 }
20508 }
20509
20510 #[test]
20511 fn value_order_int64_vs_float64() {
20512 assert_eq!(
20513 value_order(&Value::Int64(5), &Value::Float64(5.0)),
20514 Some(std::cmp::Ordering::Equal)
20515 );
20516 assert_eq!(
20517 value_order(&Value::Int64(5), &Value::Float64(3.0)),
20518 Some(std::cmp::Ordering::Greater)
20519 );
20520 assert_eq!(
20521 value_order(&Value::Int64(2), &Value::Float64(3.0)),
20522 Some(std::cmp::Ordering::Less)
20523 );
20524 }
20525
20526 #[test]
20527 fn value_order_null_returns_none() {
20528 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
20529 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
20530 assert_eq!(value_order(&Value::Null, &Value::Null), None);
20531 }
20532
20533 #[test]
20534 fn value_order_cross_group_returns_none() {
20535 assert_eq!(
20536 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
20537 None
20538 );
20539 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
20540 assert_eq!(
20541 value_order(
20542 &Value::Embedding(vec![1.0, 2.0]),
20543 &Value::Embedding(vec![1.0, 2.0])
20544 ),
20545 None
20546 );
20547 }
20548
20549 #[test]
20550 fn eval_trigger_expr_ranges_and_booleans() {
20551 let expr = TriggerExpr::And {
20552 left: Box::new(TriggerExpr::Gt {
20553 left: TriggerValue::NewColumn(1),
20554 right: TriggerValue::Literal(Value::Int64(0)),
20555 }),
20556 right: Box::new(TriggerExpr::Lte {
20557 left: TriggerValue::NewColumn(1),
20558 right: TriggerValue::Literal(Value::Int64(100)),
20559 }),
20560 };
20561 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
20562 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
20563 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
20564
20565 let or_expr = TriggerExpr::Or {
20566 left: Box::new(TriggerExpr::Lt {
20567 left: TriggerValue::NewColumn(1),
20568 right: TriggerValue::Literal(Value::Int64(0)),
20569 }),
20570 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
20571 TriggerValue::OldColumn(2),
20572 )))),
20573 };
20574 assert!(eval_trigger_expr(
20575 &or_expr,
20576 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
20577 )
20578 .unwrap());
20579 assert!(!eval_trigger_expr(
20580 &or_expr,
20581 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
20582 )
20583 .unwrap());
20584
20585 assert!(eval_trigger_expr(
20586 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
20587 &event_insert(&[])
20588 )
20589 .unwrap());
20590 assert!(!eval_trigger_expr(
20591 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
20592 &event_insert(&[])
20593 )
20594 .unwrap());
20595 assert!(!eval_trigger_expr(
20596 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
20597 &event_insert(&[])
20598 )
20599 .unwrap());
20600 }
20601}
20602
20603#[cfg(test)]
20604mod core_resource_tests {
20605 use super::*;
20606
20607 fn int_pk_schema() -> Schema {
20608 Schema {
20609 columns: vec![ColumnDef {
20610 id: 1,
20611 name: "id".into(),
20612 ty: TypeId::Int64,
20613 flags: crate::schema::ColumnFlags::empty()
20614 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20615 default_value: None,
20616 embedding_source: None,
20617 }],
20618 ..Schema::default()
20619 }
20620 }
20621
20622 #[test]
20623 fn open_constructs_governor_spill_and_jobs_with_documented_defaults() {
20624 let dir = tempfile::tempdir().unwrap();
20625 let db = Database::create(dir.path()).unwrap();
20626 assert_eq!(
20627 db.memory_governor().max_bytes(),
20628 DEFAULT_MEMORY_BUDGET_BYTES
20629 );
20630 assert_eq!(
20631 db.spill_manager().config().global_bytes,
20632 DEFAULT_TEMP_DISK_BUDGET_BYTES
20633 );
20634 assert!(db.job_registry().list().is_empty());
20635 assert_eq!(
20637 db.resource_groups().len(),
20638 crate::resource::WorkloadClass::ALL.len()
20639 );
20640 assert!(db.resource_groups().get("control").is_some());
20641 assert!(db.embedding_providers().list_ids().is_empty());
20642 let err = db
20644 .embedding_providers()
20645 .embed(
20646 &crate::embedding::EmbeddingSource::SuppliedByApplication,
20647 &["text"],
20648 4,
20649 )
20650 .unwrap_err();
20651 assert!(matches!(
20652 err,
20653 crate::embedding::EmbeddingError::SuppliedByApplication
20654 ));
20655 }
20656
20657 #[test]
20658 fn lock_rows_for_update_acquires_exclusive_row_locks() {
20659 use crate::locks::LockKey;
20660 use crate::rowid::RowId;
20661
20662 let dir = tempfile::tempdir().unwrap();
20663 let db = Database::create(dir.path()).unwrap();
20664 let txn_id = db.allocate_lock_txn_id().unwrap();
20665 let rid = RowId(42);
20666 db.lock_rows_for_update(txn_id, 7, &[rid], None).unwrap();
20667 assert!(db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20668 db.release_txn_locks(txn_id);
20669 assert!(!db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20670 }
20671
20672 #[test]
20673 fn open_with_options_sizes_the_core_budgets() {
20674 let dir = tempfile::tempdir().unwrap();
20675 let db = Database::create(dir.path()).unwrap();
20676 drop(db);
20677 let db = Database::open_with_options(
20678 dir.path(),
20679 OpenOptions::default()
20680 .with_memory_budget_bytes(256 * 1024 * 1024)
20681 .with_temp_disk_budget_bytes(16 * 1024 * 1024),
20682 )
20683 .unwrap();
20684 assert_eq!(db.memory_governor().max_bytes(), 256 * 1024 * 1024);
20685 assert_eq!(db.spill_manager().config().global_bytes, 16 * 1024 * 1024);
20686 }
20687
20688 #[test]
20689 fn zero_budgets_are_rejected() {
20690 let dir = tempfile::tempdir().unwrap();
20691 let db = Database::create(dir.path()).unwrap();
20692 drop(db);
20693 let result = Database::open_with_options(
20694 dir.path(),
20695 OpenOptions::default().with_memory_budget_bytes(0),
20696 );
20697 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20698 let result = Database::open_with_options(
20699 dir.path(),
20700 OpenOptions::default().with_temp_disk_budget_bytes(0),
20701 );
20702 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20703 }
20704
20705 #[test]
20706 fn page_caches_reserve_under_the_governor() {
20707 let dir = tempfile::tempdir().unwrap();
20708 let db = Database::create(dir.path()).unwrap();
20709 db.create_table("t", int_pk_schema()).unwrap();
20710 let mut txn = db.begin();
20711 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20712 txn.commit().unwrap();
20713 let stats = db.memory_governor().stats();
20717 assert_eq!(
20718 stats.usage_for(crate::memory::MemoryClass::PageCache),
20719 db.page_cache.used_bytes()
20720 );
20721 assert_eq!(
20722 stats.usage_for(crate::memory::MemoryClass::DecodedCache),
20723 db.decoded_cache.used_bytes()
20724 );
20725 assert_eq!(
20726 db.memory_governor().reclaimable_bytes(),
20727 db.page_cache.used_bytes() + db.decoded_cache.used_bytes()
20728 );
20729 let _ = db.memory_governor().evict_reclaimable(1024 * 1024);
20731 }
20732
20733 #[test]
20734 fn job_registry_persists_across_reopen() {
20735 let dir = tempfile::tempdir().unwrap();
20736 let db = Database::create(dir.path()).unwrap();
20737 db.create_table("t", int_pk_schema()).unwrap();
20738 let job_id = db
20739 .job_registry()
20740 .submit(
20741 crate::jobs::JobKind::IndexBuild,
20742 crate::jobs::JobTarget {
20743 table: "t".to_string(),
20744 index: Some("idx".to_string()),
20745 },
20746 )
20747 .unwrap();
20748 drop(db);
20749 let db = Database::open(dir.path()).unwrap();
20750 let record = db.job_registry().get(job_id).expect("job survives reopen");
20751 assert_eq!(record.state, crate::jobs::JobState::Pending);
20752 }
20753
20754 #[test]
20755 fn spill_manager_open_sweeps_stale_temp_tree() {
20756 let dir = tempfile::tempdir().unwrap();
20757 let db = Database::create(dir.path()).unwrap();
20758 let stale = dir.path().join("temp").join("spill").join("q-deadbeef");
20759 std::fs::create_dir_all(&stale).unwrap();
20760 std::fs::write(stale.join("chunk-0"), b"stale").unwrap();
20761 drop(db);
20762 let db = Database::open(dir.path()).unwrap();
20763 assert!(
20764 !stale.exists(),
20765 "the startup sweep removes stale spill files (S1E-004)"
20766 );
20767 let session = db
20769 .spill_manager()
20770 .begin_query(
20771 mongreldb_types::ids::QueryId::from_bytes([7u8; 16]),
20772 1024 * 1024,
20773 )
20774 .unwrap();
20775 assert_eq!(session.used(), 0);
20776 }
20777}
20778
20779#[cfg(test)]
20780mod version_pin_tests {
20781 use super::*;
20782
20783 fn int_pk_schema() -> Schema {
20784 Schema {
20785 columns: vec![ColumnDef {
20786 id: 1,
20787 name: "id".into(),
20788 ty: TypeId::Int64,
20789 flags: crate::schema::ColumnFlags::empty()
20790 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20791 default_value: None,
20792 embedding_source: None,
20793 }],
20794 ..Schema::default()
20795 }
20796 }
20797
20798 fn pins_for(
20799 report: &[TablePinsReport],
20800 table: &str,
20801 source: crate::retention::PinSource,
20802 ) -> Option<crate::retention::PinInfo> {
20803 report
20804 .iter()
20805 .find(|entry| entry.table == table)
20806 .and_then(|entry| entry.pins.get(source).cloned())
20807 }
20808
20809 #[test]
20810 fn backup_boundary_registers_backup_pitr_pin() {
20811 let source = tempfile::tempdir().unwrap();
20812 let destination_parent = tempfile::tempdir().unwrap();
20813 let destination = destination_parent.path().join("backup");
20814 let db = Arc::new(Database::create(source.path()).unwrap());
20815 db.create_table("t", int_pk_schema()).unwrap();
20816 let mut txn = db.begin();
20817 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
20818 let boundary_epoch = txn.commit().unwrap();
20819
20820 let hold = Arc::new(std::sync::Barrier::new(2));
20821 let resume = Arc::new(std::sync::Barrier::new(2));
20822 db.__set_backup_hook({
20823 let hold = Arc::clone(&hold);
20824 let resume = Arc::clone(&resume);
20825 move || {
20826 hold.wait();
20827 resume.wait();
20828 }
20829 });
20830
20831 let backup = {
20832 let db = Arc::clone(&db);
20833 let destination = destination.clone();
20834 std::thread::spawn(move || db.hot_backup(destination))
20835 };
20836 hold.wait();
20837 let report = db.version_pins_report();
20840 let pin = pins_for(&report, "t", crate::retention::PinSource::BackupPitr)
20841 .expect("backup boundary must register a BackupPitr pin");
20842 assert_eq!(pin.oldest_epoch, boundary_epoch);
20843 assert!(pin.pin_count >= 1);
20844 resume.wait();
20845 backup.join().unwrap().unwrap();
20846
20847 let report = db.version_pins_report();
20848 assert!(
20849 pins_for(&report, "t", crate::retention::PinSource::BackupPitr).is_none(),
20850 "the BackupPitr pin releases when the backup finishes"
20851 );
20852 }
20853
20854 #[test]
20855 fn snapshot_and_read_generation_pins_surface_in_report() {
20856 let dir = tempfile::tempdir().unwrap();
20857 let db = Database::create(dir.path()).unwrap();
20858 db.create_table("t", int_pk_schema()).unwrap();
20859 let mut txn = db.begin();
20860 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20861 txn.commit().unwrap();
20862
20863 let (_snapshot, guard) = db.snapshot();
20864 let report = db.version_pins_report();
20865 assert!(
20866 pins_for(
20867 &report,
20868 "t",
20869 crate::retention::PinSource::TransactionSnapshot
20870 )
20871 .is_some(),
20872 "a database snapshot projects the TransactionSnapshot source"
20873 );
20874 drop(guard);
20875
20876 let handle = db.table("t").unwrap();
20877 let (generation, _snapshot) = handle.read_generation_with_context(None).unwrap();
20878 let report = db.version_pins_report();
20879 assert!(
20880 pins_for(&report, "t", crate::retention::PinSource::ReadGeneration).is_some(),
20881 "a cloned read generation registers a ReadGeneration pin"
20882 );
20883 drop(generation);
20884
20885 let report = db.version_pins_report();
20886 let entry = report.iter().find(|entry| entry.table == "t").unwrap();
20887 assert!(
20888 entry
20889 .pins
20890 .get(crate::retention::PinSource::BackupPitr)
20891 .is_none()
20892 && entry
20893 .pins
20894 .get(crate::retention::PinSource::Replication)
20895 .is_none()
20896 && entry
20897 .pins
20898 .get(crate::retention::PinSource::OnlineIndexBuild)
20899 .is_none(),
20900 "untaken sources stay absent from the report"
20901 );
20902 }
20903}
20904
20905#[cfg(test)]
20906mod lock_manager_tests {
20907 use super::*;
20908 use crate::locks::LockKey;
20909
20910 fn col(id: u16, name: &str, ty: TypeId, flags: crate::schema::ColumnFlags) -> ColumnDef {
20911 ColumnDef {
20912 id,
20913 name: name.into(),
20914 ty,
20915 flags,
20916 default_value: None,
20917 embedding_source: None,
20918 }
20919 }
20920
20921 fn unique_schema() -> Schema {
20922 let mut constraints = crate::constraint::TableConstraints::default();
20923 constraints
20924 .uniques
20925 .push(crate::constraint::UniqueConstraint {
20926 id: 1,
20927 name: "users_email_unique".into(),
20928 columns: vec![1],
20929 });
20930 Schema {
20931 columns: vec![
20932 col(
20933 0,
20934 "id",
20935 TypeId::Int64,
20936 crate::schema::ColumnFlags::empty()
20937 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20938 ),
20939 col(
20940 1,
20941 "email",
20942 TypeId::Bytes,
20943 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
20944 ),
20945 ],
20946 constraints,
20947 ..Schema::default()
20948 }
20949 }
20950
20951 fn parent_schema() -> Schema {
20952 Schema {
20953 columns: vec![col(
20954 0,
20955 "id",
20956 TypeId::Int64,
20957 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::PRIMARY_KEY),
20958 )],
20959 ..Schema::default()
20960 }
20961 }
20962
20963 fn child_schema() -> Schema {
20964 let mut constraints = crate::constraint::TableConstraints::default();
20965 constraints
20966 .foreign_keys
20967 .push(crate::constraint::ForeignKey {
20968 id: 1,
20969 name: "child_parent_fk".into(),
20970 columns: vec![1],
20971 ref_table: "parent".into(),
20972 ref_columns: vec![0],
20973 on_delete: crate::constraint::FkAction::Restrict,
20974 on_update: crate::constraint::FkAction::Restrict,
20975 });
20976 Schema {
20977 columns: vec![
20978 col(
20979 0,
20980 "id",
20981 TypeId::Int64,
20982 crate::schema::ColumnFlags::empty()
20983 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20984 ),
20985 col(
20986 1,
20987 "pid",
20988 TypeId::Int64,
20989 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
20990 ),
20991 ],
20992 constraints,
20993 ..Schema::default()
20994 }
20995 }
20996
20997 fn auto_inc_schema() -> Schema {
20998 Schema {
20999 columns: vec![col(
21000 0,
21001 "id",
21002 TypeId::Int64,
21003 crate::schema::ColumnFlags::empty()
21004 .with(crate::schema::ColumnFlags::PRIMARY_KEY)
21005 .with(crate::schema::ColumnFlags::AUTO_INCREMENT),
21006 )],
21007 ..Schema::default()
21008 }
21009 }
21010
21011 fn pk_lock_key(table_id: u64, value: i64) -> LockKey {
21012 let mut key = b"pk:".to_vec();
21013 key.extend_from_slice(&Value::Int64(value).encode_key());
21014 LockKey::key(table_id, key)
21015 }
21016
21017 #[test]
21018 fn unique_claims_serialize_concurrent_commits() {
21019 let dir = tempfile::tempdir().unwrap();
21020 let db = Arc::new(Database::create(dir.path()).unwrap());
21021 let table_id = db.create_table("users", unique_schema()).unwrap();
21022 let pk_key = pk_lock_key(table_id, 1);
21023 let entered = Arc::new(std::sync::Barrier::new(2));
21024 let resume = Arc::new(std::sync::Barrier::new(2));
21025 let parked = Arc::new(AtomicBool::new(false));
21026 db.__set_catalog_commit_hook({
21027 let entered = Arc::clone(&entered);
21028 let resume = Arc::clone(&resume);
21029 let parked = Arc::clone(&parked);
21030 move || {
21031 if !parked.swap(true, Ordering::SeqCst) {
21034 entered.wait();
21035 resume.wait();
21036 }
21037 }
21038 });
21039
21040 let mut txn_a = db.begin();
21041 txn_a
21042 .put(
21043 "users",
21044 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21045 )
21046 .unwrap();
21047 let a_id = txn_a.txn_id();
21048 let (a_tx, a_rx) = std::sync::mpsc::channel();
21049 let (b_tx, b_rx) = std::sync::mpsc::channel();
21050 std::thread::scope(|scope| {
21051 scope.spawn(|| {
21052 a_tx.send(txn_a.commit()).unwrap();
21053 });
21054 entered.wait();
21055 assert!(
21057 db.lock_manager().holds(a_id, &pk_key),
21058 "primary-key claim must be held until the commit ends"
21059 );
21060 let mut uq_key = format!("uq{}:", 1).into_bytes();
21061 let cells_map: HashMap<u16, Value> = [(1u16, Value::Bytes(b"a@x".to_vec()))]
21062 .into_iter()
21063 .collect();
21064 uq_key.extend_from_slice(
21065 &crate::constraint::encode_composite_key(&[1], &cells_map).unwrap(),
21066 );
21067 assert!(
21068 db.lock_manager()
21069 .holds(a_id, &LockKey::key(table_id, uq_key)),
21070 "declared-unique claim must be held until the commit ends"
21071 );
21072
21073 let mut txn_b = db.begin();
21074 txn_b
21075 .put(
21076 "users",
21077 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"b@x".to_vec()))],
21078 )
21079 .unwrap();
21080 scope.spawn(|| {
21081 b_tx.send(txn_b.commit()).unwrap();
21082 });
21083 std::thread::sleep(std::time::Duration::from_millis(100));
21084 assert!(
21085 b_rx.try_recv().is_err(),
21086 "the concurrent claim must block until A ends its transaction"
21087 );
21088 resume.wait();
21089 assert!(a_rx.recv().unwrap().is_ok());
21090 let b_result = b_rx.recv().unwrap();
21091 assert!(
21092 matches!(b_result, Err(MongrelError::Conflict(_))),
21093 "the loser surfaces a conflict after serializing: {b_result:?}"
21094 );
21095 });
21096 assert!(
21097 !db.lock_manager().holds(a_id, &pk_key),
21098 "no phantom holds remain after the commit"
21099 );
21100 }
21101
21102 #[test]
21103 fn ddl_waits_for_inflight_dml_commit_on_schema_barrier() {
21104 let dir = tempfile::tempdir().unwrap();
21105 let db = Arc::new(Database::create(dir.path()).unwrap());
21106 db.create_table("parent", parent_schema()).unwrap();
21107 db.create_table("child", child_schema()).unwrap();
21108 let mut seed = db.begin();
21109 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21110 seed.commit().unwrap();
21111
21112 let entered = Arc::new(std::sync::Barrier::new(2));
21113 let resume = Arc::new(std::sync::Barrier::new(2));
21114 db.__set_fk_lock_hook({
21115 let entered = Arc::clone(&entered);
21116 let resume = Arc::clone(&resume);
21117 move || {
21118 entered.wait();
21119 resume.wait();
21120 }
21121 });
21122
21123 let mut txn_a = db.begin();
21124 txn_a
21125 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(1))])
21126 .unwrap();
21127 let a_id = txn_a.txn_id();
21128 let (a_tx, a_rx) = std::sync::mpsc::channel();
21129 let (ddl_tx, ddl_rx) = std::sync::mpsc::channel();
21130 std::thread::scope(|scope| {
21131 scope.spawn(|| {
21132 a_tx.send(txn_a.commit()).unwrap();
21133 });
21134 entered.wait();
21135 assert!(
21137 db.lock_manager().holds(a_id, &LockKey::schema_barrier()),
21138 "DML holds the schema barrier Shared for its commit"
21139 );
21140 let db = Arc::clone(&db);
21141 scope.spawn(move || {
21142 ddl_tx.send(db.drop_table("parent")).unwrap();
21143 });
21144 std::thread::sleep(std::time::Duration::from_millis(100));
21145 assert!(
21146 ddl_rx.try_recv().is_err(),
21147 "DDL must wait on the Exclusive schema barrier while DML is in flight"
21148 );
21149 resume.wait();
21150 let a_result = a_rx.recv().unwrap();
21156 match &a_result {
21157 Ok(_) => {}
21158 Err(MongrelError::Conflict(message)) => {
21159 assert!(
21160 message.contains("security policy changed during write"),
21161 "unexpected commit conflict: {message}"
21162 );
21163 }
21164 other => panic!("unexpected commit outcome: {other:?}"),
21165 }
21166 assert!(ddl_rx.recv().unwrap().is_ok());
21167 });
21168 assert!(!db.lock_manager().holds(a_id, &LockKey::schema_barrier()));
21169 }
21170
21171 #[test]
21172 fn auto_increment_sequence_barrier_held_until_commit() {
21173 let dir = tempfile::tempdir().unwrap();
21174 let db = Database::create(dir.path()).unwrap();
21175 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
21176 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
21177 let entered = Arc::new(std::sync::Barrier::new(2));
21178 let resume = Arc::new(std::sync::Barrier::new(2));
21179 let parked = Arc::new(AtomicBool::new(false));
21180 db.__set_catalog_commit_hook({
21181 let entered = Arc::clone(&entered);
21182 let resume = Arc::clone(&resume);
21183 let parked = Arc::clone(&parked);
21184 move || {
21185 if !parked.swap(true, Ordering::SeqCst) {
21186 entered.wait();
21187 resume.wait();
21188 }
21189 }
21190 });
21191
21192 let mut txn_a = db.begin();
21193 txn_a.put("seq_t", vec![(0, Value::Null)]).unwrap();
21194 let a_id = txn_a.txn_id();
21195 assert!(
21197 db.lock_manager().holds(a_id, &barrier_key),
21198 "sequence allocation takes the barrier at stage time"
21199 );
21200 let (a_tx, a_rx) = std::sync::mpsc::channel();
21201 std::thread::scope(|scope| {
21202 scope.spawn(|| {
21203 a_tx.send(txn_a.commit()).unwrap();
21204 });
21205 entered.wait();
21206 assert!(
21207 db.lock_manager().holds(a_id, &barrier_key),
21208 "the barrier is held through the commit"
21209 );
21210 resume.wait();
21211 assert!(a_rx.recv().unwrap().is_ok());
21212 });
21213 assert!(
21214 !db.lock_manager().holds(a_id, &barrier_key),
21215 "the barrier releases when the commit ends"
21216 );
21217 }
21218
21219 #[test]
21220 fn fk_wait_for_cycle_surfaces_deadlock_victim() {
21221 let dir = tempfile::tempdir().unwrap();
21222 let db = Database::create(dir.path()).unwrap();
21223 db.create_table("parent", parent_schema()).unwrap();
21224 db.create_table("child", child_schema()).unwrap();
21225 let mut seed = db.begin();
21226 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21227 seed.put("parent", vec![(0, Value::Int64(2))]).unwrap();
21228 seed.commit().unwrap();
21229 let (rid1, rid2) = {
21230 let handle = db.table("parent").unwrap();
21231 let table = handle.lock();
21232 let rid = |pk: i64| {
21233 table
21234 .lookup_pk(&Value::Int64(pk).encode_key())
21235 .expect("seeded parent row")
21236 };
21237 (rid(1), rid(2))
21238 };
21239
21240 let rendezvous = Arc::new(std::sync::Barrier::new(2));
21245 let calls = Arc::new(AtomicUsize::new(0));
21246 db.__set_fk_lock_hook({
21247 let rendezvous = Arc::clone(&rendezvous);
21248 let calls = Arc::clone(&calls);
21249 move || {
21250 if calls.fetch_add(1, Ordering::SeqCst) < 2 {
21251 rendezvous.wait();
21252 }
21253 }
21254 });
21255
21256 let mut txn_a = db.begin();
21259 txn_a.delete("parent", rid1).unwrap();
21260 txn_a
21261 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(2))])
21262 .unwrap();
21263 let mut txn_b = db.begin();
21264 txn_b.delete("parent", rid2).unwrap();
21265 txn_b
21266 .put("child", vec![(0, Value::Int64(101)), (1, Value::Int64(1))])
21267 .unwrap();
21268 let b_id = txn_b.txn_id();
21269
21270 let (a_tx, a_rx) = std::sync::mpsc::channel();
21271 let (b_tx, b_rx) = std::sync::mpsc::channel();
21272 std::thread::scope(|scope| {
21273 scope.spawn(|| {
21274 a_tx.send(txn_a.commit()).unwrap();
21275 });
21276 scope.spawn(|| {
21277 b_tx.send(txn_b.commit()).unwrap();
21278 });
21279 let a_result = a_rx.recv().unwrap();
21280 let b_result = b_rx.recv().unwrap();
21281 assert!(
21282 a_result.is_ok(),
21283 "the survivor commits once the victim releases: {a_result:?}"
21284 );
21285 match b_result {
21286 Err(MongrelError::Deadlock { victim, .. }) => {
21287 assert_eq!(victim, b_id, "the youngest transaction is the victim");
21288 }
21289 other => panic!("the victim must surface a deadlock, got {other:?}"),
21290 }
21291 });
21292 let fk_key = |table: &str, pk: i64| {
21294 let table_id = db.table_id(table).unwrap();
21295 let mut key = b"fk:".to_vec();
21296 key.extend_from_slice(&Value::Int64(pk).encode_key());
21297 LockKey::key(table_id, key)
21298 };
21299 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 2)));
21300 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 1)));
21301 }
21302
21303 #[test]
21304 fn locks_release_after_commit_rollback_and_failed_commit() {
21305 let dir = tempfile::tempdir().unwrap();
21306 let db = Database::create(dir.path()).unwrap();
21307 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
21308 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
21309
21310 let mut txn = db.begin();
21312 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21313 let committed_id = txn.txn_id();
21314 txn.commit().unwrap();
21315 assert!(!db.lock_manager().holds(committed_id, &barrier_key));
21316
21317 let mut txn = db.begin();
21319 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21320 let rolled_back_id = txn.txn_id();
21321 assert!(db.lock_manager().holds(rolled_back_id, &barrier_key));
21322 txn.rollback();
21323 assert!(
21324 !db.lock_manager().holds(rolled_back_id, &barrier_key),
21325 "rollback must not leave phantom holds"
21326 );
21327
21328 db.create_table("users", unique_schema()).unwrap();
21331 let users_id = db.table_id("users").unwrap();
21332 let mut seed = db.begin();
21333 seed.put(
21334 "users",
21335 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21336 )
21337 .unwrap();
21338 seed.commit().unwrap();
21339 let mut txn = db.begin();
21340 txn.put(
21343 "users",
21344 vec![(0, Value::Int64(2)), (1, Value::Bytes(b"a@x".to_vec()))],
21345 )
21346 .unwrap();
21347 let failed_id = txn.txn_id();
21348 let result = txn.commit();
21349 assert!(matches!(result, Err(MongrelError::Conflict(_))));
21350 assert!(
21351 !db.lock_manager()
21352 .holds(failed_id, &pk_lock_key(users_id, 2)),
21353 "a failed commit must not leave phantom holds"
21354 );
21355 }
21356}
21357
21358#[cfg(test)]
21359mod lifecycle_tests {
21360 use super::*;
21361
21362 fn int_pk_schema() -> Schema {
21363 Schema {
21364 columns: vec![ColumnDef {
21365 id: 1,
21366 name: "id".into(),
21367 ty: TypeId::Int64,
21368 flags: crate::schema::ColumnFlags::empty()
21369 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21370 default_value: None,
21371 embedding_source: None,
21372 }],
21373 ..Schema::default()
21374 }
21375 }
21376
21377 #[test]
21378 fn poisoned_core_rejects_operations_with_typed_errors() {
21379 let dir = tempfile::tempdir().unwrap();
21380 let db = Database::create(dir.path()).unwrap();
21381 db.create_table("t", int_pk_schema()).unwrap();
21382 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Open);
21383
21384 db.poisoned.store(true, Ordering::Relaxed);
21389 db.lifecycle.poison();
21390 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Poisoned);
21391
21392 let error = db.gc().unwrap_err();
21395 assert!(
21396 matches!(error, MongrelError::Conflict(_)),
21397 "gc must reject on a poisoned core: {error:?}"
21398 );
21399 let error = db.compact().unwrap_err();
21400 assert!(
21401 matches!(error, MongrelError::Conflict(_)),
21402 "compact must reject on a poisoned core: {error:?}"
21403 );
21404 assert!(db.operation_guard().is_err());
21405 let error = db.create_table("t2", int_pk_schema()).unwrap_err();
21408 assert!(
21409 error.to_string().contains("database poisoned"),
21410 "the legacy poison error still wins where it existed: {error:?}"
21411 );
21412 let mut txn = db.begin();
21413 txn.put("t", vec![(1, Value::Int64(2))]).unwrap();
21414 assert!(txn
21415 .commit()
21416 .unwrap_err()
21417 .to_string()
21418 .contains("database poisoned"));
21419 }
21420
21421 #[test]
21422 fn shutdown_waits_for_operation_guards_to_drain() {
21423 let dir = tempfile::tempdir().unwrap();
21424 let db = Arc::new(Database::create(dir.path()).unwrap());
21425 db.create_table("t", int_pk_schema()).unwrap();
21426 let guard = db.operation_guard().unwrap();
21429 let (started_tx, started_rx) = std::sync::mpsc::channel();
21430 let (done_tx, done_rx) = std::sync::mpsc::channel();
21431 let shutdown_thread = std::thread::spawn(move || {
21432 started_tx.send(()).unwrap();
21433 let result = db.shutdown();
21434 let _ = done_tx.send(result);
21435 });
21436 started_rx.recv().unwrap();
21437 std::thread::sleep(std::time::Duration::from_millis(100));
21438 assert!(
21439 done_rx.try_recv().is_err(),
21440 "shutdown must wait for the outstanding guard to drain"
21441 );
21442 drop(guard);
21443 shutdown_thread.join().unwrap();
21444 assert!(
21445 done_rx.recv().unwrap().is_ok(),
21446 "shutdown completes once the guard drops"
21447 );
21448 }
21449}
21450
21451#[cfg(test)]
21452mod commit_ts_ledger_tests {
21453 use super::*;
21454 use crate::memtable::Row;
21455
21456 fn int_pk_schema() -> Schema {
21457 Schema {
21458 columns: vec![ColumnDef {
21459 id: 1,
21460 name: "id".into(),
21461 ty: TypeId::Int64,
21462 flags: crate::schema::ColumnFlags::empty()
21463 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21464 default_value: None,
21465 embedding_source: None,
21466 }],
21467 ..Schema::default()
21468 }
21469 }
21470
21471 fn commit_one(db: &Database) -> (Epoch, mongreldb_types::hlc::HlcTimestamp) {
21472 let mut txn = db.begin();
21473 let handle = txn.state_handle();
21474 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
21475 let epoch = txn.commit().unwrap();
21476 let crate::txn::TransactionState::Committed(receipt) = handle.state() else {
21477 panic!("expected Committed, got {:?}", handle.state());
21478 };
21479 (epoch, receipt.commit_ts)
21480 }
21481
21482 #[test]
21483 fn commit_ts_for_epoch_returns_the_exact_receipt_within_one_open() {
21484 let dir = tempfile::tempdir().unwrap();
21485 let db = Database::create(dir.path()).unwrap();
21486 db.create_table("t", int_pk_schema()).unwrap();
21487
21488 let (epoch, commit_ts) = commit_one(&db);
21489 assert_eq!(db.commit_ts_for_epoch(epoch), Some(commit_ts));
21490 assert_eq!(db.commit_ts_for_epoch(Epoch(epoch.0 + 100)), None);
21492 }
21493
21494 #[test]
21495 fn commit_ts_for_epoch_survives_reopen_with_the_physical_component() {
21496 let dir = tempfile::tempdir().unwrap();
21497 let (epoch, commit_ts) = {
21498 let db = Database::create(dir.path()).unwrap();
21499 db.create_table("t", int_pk_schema()).unwrap();
21500 commit_one(&db)
21501 };
21502
21503 let db = Database::open(dir.path()).unwrap();
21504 let reconstructed = db
21505 .commit_ts_for_epoch(epoch)
21506 .expect("the durable WAL CommitTimestamp ledger reconstructs the epoch");
21507 assert_eq!(reconstructed.physical_micros, commit_ts.physical_micros);
21508 assert_eq!(reconstructed.logical, 0);
21511 assert_eq!(reconstructed.node_tiebreaker, 0);
21512 }
21513
21514 #[test]
21515 fn recovery_ledger_keeps_only_newest_epochs_and_ignores_aborted_txns() {
21516 use crate::wal::Op;
21517 let records = vec![
21518 crate::wal::Record::new(Epoch(1), 7, Op::CommitTimestamp { unix_nanos: 1_000 }),
21519 crate::wal::Record::new(
21520 Epoch(2),
21521 7,
21522 Op::TxnCommit {
21523 epoch: 41,
21524 added_runs: vec![],
21525 },
21526 ),
21527 crate::wal::Record::new(
21529 Epoch(3),
21530 8,
21531 Op::TxnCommit {
21532 epoch: 42,
21533 added_runs: vec![],
21534 },
21535 ),
21536 crate::wal::Record::new(Epoch(4), 9, Op::CommitTimestamp { unix_nanos: 9_000 }),
21538 ];
21539 let ledger = commit_ts_ledger_from_recovery(&records);
21540 assert_eq!(ledger.len(), 1);
21541 assert_eq!(
21542 ledger.get(&41),
21543 Some(&mongreldb_types::hlc::HlcTimestamp {
21544 physical_micros: 1,
21545 logical: 0,
21546 node_tiebreaker: 0,
21547 })
21548 );
21549 }
21550
21551 #[test]
21552 fn new_writes_always_have_some_commit_ts() {
21553 let dir = tempfile::tempdir().unwrap();
21554 let db = Database::create(dir.path()).unwrap();
21555 db.create_table("t", int_pk_schema()).unwrap();
21556 let mut txn = db.begin();
21557 let state = txn.state_handle();
21558 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
21559 let epoch = txn.commit().unwrap();
21560 let crate::txn::TransactionState::Committed(receipt) = state.state() else {
21561 panic!("expected Committed receipt");
21562 };
21563 let handle = db.table("t").unwrap();
21564 let table = handle.lock();
21565 let (snap, _g) = db.snapshot();
21568 let rows = table.visible_rows(snap).expect("visible rows");
21569 assert_eq!(
21570 rows.len(),
21571 1,
21572 "committed put must be visible under HLC snapshot"
21573 );
21574 assert_eq!(rows[0].commit_ts, Some(receipt.commit_ts));
21575 assert_eq!(db.commit_ts_for_epoch(epoch), Some(receipt.commit_ts));
21576 }
21577
21578 #[test]
21579 fn same_transaction_identical_hlc_on_apply() {
21580 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21581
21582 let dir = tempfile::tempdir().unwrap();
21583 let db = Database::create_cluster_replica(
21584 dir.path(),
21585 ClusterId::from_bytes([1; 16]),
21586 NodeId::from_bytes([2; 16]),
21587 DatabaseId::from_bytes([3; 16]),
21588 )
21589 .unwrap();
21590 db.apply_replicated_catalog_command(&crate::catalog_cmds::CatalogCommandRecord {
21591 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21592 catalog_version: 1,
21593 command: crate::catalog_cmds::CatalogCommand::CreateTable {
21594 name: "t".into(),
21595 schema: int_pk_schema(),
21596 created_epoch: 1,
21597 },
21598 })
21599 .unwrap();
21600 let table_id = db.table_id("t").unwrap();
21601 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
21602 physical_micros: 42_000,
21603 logical: 3,
21604 node_tiebreaker: 9,
21605 };
21606 let staged = vec![StagedTxnWrite::Put {
21609 table_id,
21610 rows: bincode::serialize(&vec![
21611 Row::new(crate::RowId(1), Epoch(0)).with_column(1, Value::Int64(1))
21612 ])
21613 .unwrap(),
21614 }
21615 .encode()
21616 .unwrap()];
21617 assert!(db
21618 .apply_committed_transaction(1 << 63, commit_ts, &staged)
21619 .unwrap());
21620 let staged2 = vec![StagedTxnWrite::Put {
21621 table_id,
21622 rows: bincode::serialize(&vec![
21623 Row::new(crate::RowId(2), Epoch(0)).with_column(1, Value::Int64(2))
21624 ])
21625 .unwrap(),
21626 }
21627 .encode()
21628 .unwrap()];
21629 assert!(db
21630 .apply_committed_transaction((1 << 63) + 1, commit_ts, &staged2)
21631 .unwrap());
21632 let handle = db.table("t").unwrap();
21633 let table = handle.lock();
21634 let row1 = table
21635 .get(crate::RowId(1), Snapshot::unbounded())
21636 .expect("applied row 1");
21637 let row2 = table
21638 .get(crate::RowId(2), Snapshot::unbounded())
21639 .expect("applied row 2");
21640 let recovery_form = mongreldb_types::hlc::HlcTimestamp {
21645 physical_micros: commit_ts.physical_micros,
21646 logical: 0,
21647 node_tiebreaker: 0,
21648 };
21649 assert_eq!(row1.commit_ts, Some(recovery_form));
21650 assert_eq!(row2.commit_ts, Some(recovery_form));
21651 assert_eq!(row1.commit_ts, row2.commit_ts);
21652 drop(table);
21653 let epoch = db.visible_epoch();
21656 assert_eq!(
21657 db.commit_ts_for_epoch(epoch).map(|ts| ts.physical_micros),
21658 Some(commit_ts.physical_micros)
21659 );
21660 }
21661
21662 #[test]
21663 fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
21664 use mongreldb_types::hlc::HlcTimestamp;
21665 let early = HlcTimestamp {
21666 physical_micros: 100,
21667 logical: 0,
21668 node_tiebreaker: 1,
21669 };
21670 let late = HlcTimestamp {
21671 physical_micros: 200,
21672 logical: 0,
21673 node_tiebreaker: 1,
21674 };
21675 let snap = Snapshot::at_hlc(Epoch(99), early);
21677 assert!(!snap.observes_version(Epoch(1), Some(late)));
21678 assert!(snap.observes_version(Epoch(1), Some(early)));
21679 let dir = tempfile::tempdir().unwrap();
21681 let db = Database::create(dir.path()).unwrap();
21682 let (snap, _g) = db.snapshot();
21683 assert_ne!(
21684 snap.commit_ts,
21685 HlcTimestamp::ZERO,
21686 "Database::snapshot must pin live HLC via at_hlc"
21687 );
21688 }
21689
21690 #[test]
21691 fn hlc_stamped_row_visible_at_hlc_snapshot_not_epoch_only() {
21692 let dir = tempfile::tempdir().unwrap();
21693 let db = Database::create(dir.path()).unwrap();
21694 db.create_table("t", int_pk_schema()).unwrap();
21695 let (_epoch, commit_ts) = commit_one(&db);
21696 assert_ne!(commit_ts, mongreldb_types::hlc::HlcTimestamp::ZERO);
21697
21698 let handle = db.table("t").unwrap();
21699 let table = handle.lock();
21700 let hlc_snap = Snapshot::at_hlc(Epoch(u64::MAX), commit_ts);
21702 let rows = table.visible_rows(hlc_snap).expect("visible");
21703 assert_eq!(rows.len(), 1);
21704 assert_eq!(rows[0].commit_ts, Some(commit_ts));
21705 assert!(table.get(rows[0].row_id, hlc_snap).is_some());
21706
21707 let legacy = Snapshot::at(Epoch(u64::MAX));
21709 assert!(!legacy.uses_hlc_authority());
21710 assert_eq!(
21711 table.visible_rows(legacy).expect("visible").len(),
21712 1,
21713 "epoch pin sees HLC-stamped rows by epoch during dual-model migration"
21714 );
21715 assert!(table.get(rows[0].row_id, legacy).is_some());
21716 }
21717
21718 #[test]
21719 fn hlc_gc_floor_reports_named_sources() {
21720 let dir = tempfile::tempdir().unwrap();
21721 let db = Database::create(dir.path()).unwrap();
21722 db.create_table("t", int_pk_schema()).unwrap();
21723 let (epoch, commit_ts) = commit_one(&db);
21724
21725 let empty = db.hlc_gc_floor();
21727 assert_eq!(empty.floor(), mongreldb_types::hlc::HlcTimestamp::ZERO);
21728 assert_eq!(empty.sources().len(), 6);
21729
21730 let (_snap, guard) = db.snapshot();
21732 let with_pin = db.hlc_gc_floor();
21733 let projected = db.commit_ts_for_epoch(epoch);
21735 if let Some(ts) = projected {
21736 assert_eq!(with_pin.transaction_snapshot, ts);
21738 assert_eq!(with_pin.floor(), ts);
21739 assert_eq!(ts.physical_micros, commit_ts.physical_micros);
21740 } else {
21741 assert_eq!(
21742 with_pin.transaction_snapshot,
21743 mongreldb_types::hlc::HlcTimestamp::ZERO
21744 );
21745 }
21746 drop(guard);
21747 }
21748}
21749
21750#[cfg(test)]
21751mod stage2e_storage_mode_tests {
21752 use super::*;
21753 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21754 use crate::storage_mode::{StorageMode, STORAGE_MODE_FILENAME};
21755 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21756
21757 fn identity(seed: u8) -> (ClusterId, NodeId, DatabaseId) {
21758 (
21759 ClusterId::from_bytes([seed; 16]),
21760 NodeId::from_bytes([seed + 1; 16]),
21761 DatabaseId::from_bytes([seed + 2; 16]),
21762 )
21763 }
21764
21765 fn marker(root: &Path) -> Option<StorageMode> {
21766 let durable = crate::durable_file::DurableRoot::open(root).unwrap();
21767 crate::storage_mode::read(&durable).unwrap()
21768 }
21769
21770 fn simple_schema() -> Schema {
21771 Schema {
21772 columns: vec![ColumnDef {
21773 id: 1,
21774 name: "id".into(),
21775 ty: TypeId::Int64,
21776 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21777 default_value: None,
21778 embedding_source: None,
21779 }],
21780 ..Schema::default()
21781 }
21782 }
21783
21784 #[test]
21785 fn standalone_create_writes_marker_and_reopens() {
21786 let dir = tempfile::tempdir().unwrap();
21787 let root = dir.path().join("db");
21788 let db = Database::create(&root).unwrap();
21789 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21790 assert_eq!(db.storage_mode().unwrap(), Some(StorageMode::Standalone));
21791 drop(db);
21792 let db = Database::open(&root).unwrap();
21793 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21794 drop(db);
21795 }
21796
21797 #[test]
21798 fn legacy_database_without_marker_opens_and_gains_marker() {
21799 let dir = tempfile::tempdir().unwrap();
21800 let root = dir.path().join("db");
21801 let db = Database::create(&root).unwrap();
21802 drop(db);
21803 std::fs::remove_file(root.join(META_DIR).join(STORAGE_MODE_FILENAME)).unwrap();
21805 assert_eq!(marker(&root), None);
21806 let db = Database::open(&root).unwrap();
21807 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21808 drop(db);
21809 }
21810
21811 #[test]
21812 fn server_owned_standalone_opens_embedded() {
21813 let dir = tempfile::tempdir().unwrap();
21814 let root = dir.path().join("db");
21815 let db = Database::create(&root).unwrap();
21816 drop(db);
21817 let durable = crate::durable_file::DurableRoot::open(&root).unwrap();
21818 crate::storage_mode::rewrite(&durable, &StorageMode::ServerOwnedStandalone).unwrap();
21819 let db = Database::open(&root).unwrap();
21820 assert_eq!(marker(&root), Some(StorageMode::ServerOwnedStandalone));
21821 drop(db);
21822 }
21823
21824 #[test]
21825 fn cluster_replica_is_rejected_by_normal_opens() {
21826 let dir = tempfile::tempdir().unwrap();
21827 let root = dir.path().join("db");
21828 let (cluster_id, node_id, database_id) = identity(10);
21829 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21830 assert_eq!(
21831 marker(&root),
21832 Some(StorageMode::ClusterReplica {
21833 cluster_id,
21834 node_id,
21835 database_id,
21836 })
21837 );
21838 drop(db);
21839
21840 let error = Database::open(&root).unwrap_err();
21841 let message = error.to_string();
21842 assert!(
21843 matches!(error, MongrelError::InvalidArgument(_)),
21844 "unexpected error: {message}"
21845 );
21846 assert!(message.contains("cluster node runtime"), "{message}");
21847 assert!(message.contains(&cluster_id.to_hex()), "{message}");
21848 assert!(message.contains(&node_id.to_hex()), "{message}");
21849 assert!(message.contains(&database_id.to_hex()), "{message}");
21850
21851 let error = Database::open_with_options(&root, OpenOptions::default()).unwrap_err();
21852 assert!(error.to_string().contains("cluster node runtime"));
21853 assert_eq!(
21855 marker(&root),
21856 Some(StorageMode::ClusterReplica {
21857 cluster_id,
21858 node_id,
21859 database_id,
21860 })
21861 );
21862 }
21863
21864 #[test]
21865 fn offline_validation_opens_cluster_replica_read_only() {
21866 let dir = tempfile::tempdir().unwrap();
21867 let root = dir.path().join("db");
21868 let (cluster_id, node_id, database_id) = identity(20);
21869 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21870 drop(db);
21871
21872 let options = OpenOptions::default().with_offline_validation(true);
21873 let db = Database::open_with_options(&root, options).unwrap();
21874 assert!(db.is_read_only_replica());
21875 let error = db.create_table("t", simple_schema()).unwrap_err();
21876 assert!(matches!(error, MongrelError::ReadOnlyReplica));
21877 drop(db);
21878 assert_eq!(
21880 marker(&root),
21881 Some(StorageMode::ClusterReplica {
21882 cluster_id,
21883 node_id,
21884 database_id,
21885 })
21886 );
21887 }
21888
21889 #[test]
21890 fn cluster_runtime_open_requires_exact_identity() {
21891 let dir = tempfile::tempdir().unwrap();
21892 let root = dir.path().join("db");
21893 let (cluster_id, node_id, database_id) = identity(30);
21894 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21895 drop(db);
21896
21897 let error = Database::open_cluster_replica(&root, &StorageMode::Standalone).unwrap_err();
21899 assert!(matches!(error, MongrelError::InvalidArgument(_)));
21900 let wrong = StorageMode::ClusterReplica {
21902 cluster_id,
21903 node_id,
21904 database_id: DatabaseId::from_bytes([99; 16]),
21905 };
21906 let error = Database::open_cluster_replica(&root, &wrong).unwrap_err();
21907 assert!(error.to_string().contains("identity mismatch"), "{error}");
21908 let legacy = dir.path().join("legacy");
21910 let legacy_db = Database::create(&legacy).unwrap();
21911 drop(legacy_db);
21912 let expected = StorageMode::ClusterReplica {
21913 cluster_id,
21914 node_id,
21915 database_id,
21916 };
21917 let error = Database::open_cluster_replica(&legacy, &expected).unwrap_err();
21918 assert!(error.to_string().contains("identity mismatch"), "{error}");
21919
21920 let db = Database::open_cluster_replica(&root, &expected).unwrap();
21923 assert!(db.is_read_only_replica());
21924 let error = db.create_table("t", simple_schema()).unwrap_err();
21925 assert!(matches!(error, MongrelError::ReadOnlyReplica));
21926 drop(db);
21927 }
21928}
21929
21930#[cfg(test)]
21931mod stage2e_replicated_apply_tests {
21932 use super::*;
21933 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord, CatalogDelta};
21934 use crate::memtable::{Row, Value};
21935 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21936 use crate::wal::{Op, Record};
21937 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21938 use std::sync::Arc;
21939
21940 fn ids() -> (ClusterId, NodeId, DatabaseId) {
21941 (
21942 ClusterId::from_bytes([1; 16]),
21943 NodeId::from_bytes([2; 16]),
21944 DatabaseId::from_bytes([3; 16]),
21945 )
21946 }
21947
21948 fn expected_mode() -> crate::storage_mode::StorageMode {
21949 let (cluster_id, node_id, database_id) = ids();
21950 crate::storage_mode::StorageMode::ClusterReplica {
21951 cluster_id,
21952 node_id,
21953 database_id,
21954 }
21955 }
21956
21957 fn simple_schema() -> Schema {
21958 Schema {
21959 columns: vec![ColumnDef {
21960 id: 1,
21961 name: "id".into(),
21962 ty: TypeId::Int64,
21963 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21964 default_value: None,
21965 embedding_source: None,
21966 }],
21967 ..Schema::default()
21968 }
21969 }
21970
21971 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
21972 CatalogCommandRecord {
21973 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21974 catalog_version,
21975 command: CatalogCommand::CreateTable {
21976 name: name.to_string(),
21977 schema: simple_schema(),
21978 created_epoch: 1,
21979 },
21980 }
21981 }
21982
21983 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
21984 let rows: Vec<Row> = values
21985 .iter()
21986 .map(|value| {
21987 Row::new(crate::RowId(*value as u64), Epoch(epoch))
21990 .with_column(1, Value::Int64(*value))
21991 })
21992 .collect();
21993 vec![
21994 Record::new(
21995 Epoch(0),
21996 txn_id,
21997 Op::Put {
21998 table_id,
21999 rows: bincode::serialize(&rows).unwrap(),
22000 },
22001 ),
22002 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
22003 Record::new(
22004 Epoch(0),
22005 txn_id,
22006 Op::TxnCommit {
22007 epoch,
22008 added_runs: Vec::new(),
22009 },
22010 ),
22011 ]
22012 }
22013
22014 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22015 let handle = db.table(table).unwrap();
22016 let snap = db.visible_snapshot();
22018 let rows = handle.lock().visible_rows(snap).unwrap();
22019 let mut values: Vec<i64> = rows
22020 .iter()
22021 .map(|row| match row.columns.get(&1) {
22022 Some(Value::Int64(value)) => *value,
22023 other => panic!("unexpected column: {other:?}"),
22024 })
22025 .collect();
22026 values.sort_unstable();
22027 values
22028 }
22029
22030 #[test]
22031 fn catalog_command_mounts_table_and_replays_as_noop() {
22032 let dir = tempfile::tempdir().unwrap();
22033 let (cluster_id, node_id, database_id) = ids();
22034 let db =
22035 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22036
22037 let record = create_table_record("items", 1);
22038 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22039 assert!(matches!(delta, CatalogDelta::TableCreated { .. }));
22040 assert_eq!(db.table_names(), vec!["items".to_string()]);
22041 assert_eq!(db.catalog_version(), 1);
22042
22043 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22045 assert!(matches!(delta, CatalogDelta::NoOp));
22046 assert_eq!(db.table_names().len(), 1);
22047 drop(db);
22048
22049 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22051 assert_eq!(db.table_names(), vec!["items".to_string()]);
22052 assert_eq!(db.catalog_version(), 1);
22053 }
22054
22055 #[test]
22056 fn records_apply_rows_and_skip_replays_across_restart() {
22057 let dir = tempfile::tempdir().unwrap();
22058 let (cluster_id, node_id, database_id) = ids();
22059 let db =
22060 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22061 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22062 .unwrap();
22063
22064 let records = put_records(1, 0, 2, &[10, 20, 30]);
22065 assert!(db.apply_replicated_records(&records).unwrap());
22066 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22067 assert_eq!(db.visible_epoch(), Epoch(2));
22068
22069 assert!(!db.apply_replicated_records(&records).unwrap());
22072 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22073
22074 let later = put_records(2, 0, 3, &[40]);
22076 assert!(db.apply_replicated_records(&later).unwrap());
22077 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22078 let db = Arc::new(db);
22079 db.shutdown().unwrap();
22080
22081 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22084 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22085 assert!(!db.apply_replicated_records(&later).unwrap());
22086 assert!(!db.apply_replicated_records(&records).unwrap());
22087 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22088 }
22089
22090 #[test]
22091 fn spilled_run_commits_fail_closed_this_wave() {
22092 let dir = tempfile::tempdir().unwrap();
22093 let (cluster_id, node_id, database_id) = ids();
22094 let db =
22095 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22096 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22097 .unwrap();
22098 let mut records = put_records(1, 0, 2, &[10]);
22099 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22100 panic!("put_records ends in TxnCommit");
22101 };
22102 added_runs.push(crate::wal::AddedRun {
22103 table_id: 0,
22104 run_id: 7,
22105 row_count: 1,
22106 level: 0,
22107 min_row_id: 1,
22108 max_row_id: 1,
22109 content_hash: [0; 32],
22110 });
22111 let error = db.apply_replicated_records(&records).unwrap_err();
22112 assert!(
22113 error.to_string().contains("spilled-run"),
22114 "unexpected error: {error}"
22115 );
22116 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22117 }
22118
22119 #[test]
22120 fn records_without_commit_marker_fail_closed() {
22121 let dir = tempfile::tempdir().unwrap();
22122 let (cluster_id, node_id, database_id) = ids();
22123 let db =
22124 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22125 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22126 .unwrap();
22127 let mut records = put_records(1, 0, 2, &[10]);
22128 records.pop(); let error = db.apply_replicated_records(&records).unwrap_err();
22130 assert!(matches!(error, MongrelError::InvalidArgument(_)));
22131 assert!(db.apply_replicated_records(&[]).is_err());
22132 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22133 }
22134}
22135
22136#[cfg(test)]
22137mod stage2c_spill_translation_tests {
22138 use super::*;
22139 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord};
22140 use crate::memtable::{Row, Value};
22141 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
22142 use crate::wal::{Op, Record};
22143 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
22144
22145 fn simple_schema() -> Schema {
22146 Schema {
22147 columns: vec![ColumnDef {
22148 id: 1,
22149 name: "id".into(),
22150 ty: TypeId::Int64,
22151 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
22152 default_value: None,
22153 embedding_source: None,
22154 }],
22155 ..Schema::default()
22156 }
22157 }
22158
22159 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
22160 CatalogCommandRecord {
22161 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
22162 catalog_version,
22163 command: CatalogCommand::CreateTable {
22164 name: name.to_string(),
22165 schema: simple_schema(),
22166 created_epoch: 1,
22167 },
22168 }
22169 }
22170
22171 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22172 let handle = db.table(table).unwrap();
22173 let snap = db.visible_snapshot();
22175 let rows = handle.lock().visible_rows(snap).unwrap();
22176 let mut values: Vec<i64> = rows
22177 .iter()
22178 .map(|row| match row.columns.get(&1) {
22179 Some(Value::Int64(value)) => *value,
22180 other => panic!("unexpected column: {other:?}"),
22181 })
22182 .collect();
22183 values.sort_unstable();
22184 values
22185 }
22186
22187 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
22188 let rows: Vec<Row> = values
22189 .iter()
22190 .map(|value| {
22191 Row::new(crate::RowId(*value as u64), Epoch(epoch))
22192 .with_column(1, Value::Int64(*value))
22193 })
22194 .collect();
22195 vec![
22196 Record::new(
22197 Epoch(0),
22198 txn_id,
22199 Op::Put {
22200 table_id,
22201 rows: bincode::serialize(&rows).unwrap(),
22202 },
22203 ),
22204 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
22205 Record::new(
22206 Epoch(0),
22207 txn_id,
22208 Op::TxnCommit {
22209 epoch,
22210 added_runs: Vec::new(),
22211 },
22212 ),
22213 ]
22214 }
22215
22216 fn added_run(
22217 table_id: u64,
22218 row_count: u64,
22219 min_row_id: u64,
22220 max_row_id: u64,
22221 ) -> crate::wal::AddedRun {
22222 crate::wal::AddedRun {
22223 table_id,
22224 run_id: 7,
22225 row_count,
22226 level: 0,
22227 min_row_id,
22228 max_row_id,
22229 content_hash: [0; 32],
22230 }
22231 }
22232
22233 fn spilled_commit_records(db: &Database) -> Vec<Record> {
22236 let records = crate::wal::SharedWal::replay_with_dek(&db.root, None).unwrap();
22237 let txn_id = records
22238 .iter()
22239 .find_map(|record| match &record.op {
22240 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty() => Some(record.txn_id),
22241 _ => None,
22242 })
22243 .expect("a spilled commit is present in the WAL");
22244 records
22245 .into_iter()
22246 .filter(|record| record.txn_id == txn_id)
22247 .collect()
22248 }
22249
22250 #[test]
22251 fn non_spilled_records_translate_byte_identical() {
22252 let records = put_records(1, 0, 2, &[10, 20, 30]);
22253 let translated = translate_records_for_replication(&records).unwrap();
22254 assert_eq!(
22255 bincode::serialize(&translated).unwrap(),
22256 bincode::serialize(&records).unwrap(),
22257 "a commit without spill links must pass through byte-identical"
22258 );
22259 }
22260
22261 #[test]
22262 fn translation_rejects_uncovered_or_malformed_spills() {
22263 let mut records = put_records(1, 0, 2, &[10]);
22265 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22266 panic!("put_records ends in TxnCommit");
22267 };
22268 added_runs.push(added_run(0, 1, 10, 10));
22269 let error = translate_records_for_replication(&records).unwrap_err();
22270 assert!(
22271 error.to_string().contains("no logical row records"),
22272 "unexpected error: {error}"
22273 );
22274
22275 let mut records = put_records(1, 0, 2, &[10]);
22277 let spilled: Vec<Row> = (0..3_u64)
22278 .map(|value| {
22279 Row::new(crate::RowId(value), Epoch(2)).with_column(1, Value::Int64(value as i64))
22280 })
22281 .collect();
22282 records.insert(
22283 0,
22284 Record::new(
22285 Epoch(0),
22286 1,
22287 Op::SpilledRows {
22288 table_id: 0,
22289 rows: bincode::serialize(&spilled).unwrap(),
22290 },
22291 ),
22292 );
22293 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22294 panic!("put_records ends in TxnCommit");
22295 };
22296 added_runs.push(added_run(0, 4, 0, 3));
22297 let error = translate_records_for_replication(&records).unwrap_err();
22298 assert!(
22299 error.to_string().contains("cover 3 rows"),
22300 "unexpected error: {error}"
22301 );
22302
22303 let mut records = put_records(1, 0, 2, &[10]);
22305 records.insert(
22306 0,
22307 Record::new(
22308 Epoch(0),
22309 1,
22310 Op::SpilledRows {
22311 table_id: 0,
22312 rows: vec![0xFF, 0x01, 0x02],
22313 },
22314 ),
22315 );
22316 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22317 panic!("put_records ends in TxnCommit");
22318 };
22319 added_runs.push(added_run(0, 1, 0, 0));
22320 assert!(translate_records_for_replication(&records).is_err());
22321
22322 assert!(translate_records_for_replication(&[]).is_err());
22324 let mut mixed = put_records(1, 0, 2, &[10]);
22325 mixed[0].txn_id = 99;
22326 assert!(translate_records_for_replication(&mixed).is_err());
22327 let mut no_commit = put_records(1, 0, 2, &[10]);
22328 no_commit.pop();
22329 assert!(translate_records_for_replication(&no_commit).is_err());
22330 }
22331
22332 #[test]
22333 fn spilled_commit_translates_to_logical_rows_and_applies_on_replica() {
22334 let leader_dir = tempfile::tempdir().unwrap();
22336 let leader = Database::create(leader_dir.path()).unwrap();
22337 leader.create_table("t", simple_schema()).unwrap();
22338 leader.set_spill_threshold(1);
22339 let table_id = leader.table_id("t").unwrap();
22340 let values: Vec<i64> = (0..60).collect();
22341 leader
22342 .transaction(|txn| {
22343 for value in &values {
22344 txn.put("t", vec![(1, Value::Int64(*value))])?;
22345 }
22346 Ok(())
22347 })
22348 .unwrap();
22349 assert_eq!(visible_ids(&leader, "t"), values);
22350
22351 let records = spilled_commit_records(&leader);
22354 assert!(records
22355 .iter()
22356 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22357 let Some(Op::TxnCommit { added_runs, epoch }) = records.last().map(|r| &r.op) else {
22358 panic!("a commit sequence ends in TxnCommit");
22359 };
22360 assert!(!added_runs.is_empty());
22361 let commit_epoch = *epoch;
22362
22363 let translated = translate_records_for_replication(&records).unwrap();
22366 assert!(records
22367 .iter()
22368 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22369 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
22370 panic!("a commit sequence ends in TxnCommit");
22371 };
22372 assert!(!added_runs.is_empty(), "input records must be unchanged");
22373 assert_eq!(translated.len(), records.len());
22374 assert!(translated
22375 .iter()
22376 .all(|record| !matches!(record.op, Op::SpilledRows { .. })));
22377 assert!(translated
22378 .iter()
22379 .any(|record| matches!(record.op, Op::Put { .. })));
22380 let Some(Op::TxnCommit { added_runs, epoch }) = translated.last().map(|r| &r.op) else {
22381 panic!("a commit sequence ends in TxnCommit");
22382 };
22383 assert!(added_runs.is_empty(), "no added_runs may reach a replica");
22384 assert_eq!(*epoch, commit_epoch);
22385
22386 let replica_dir = tempfile::tempdir().unwrap();
22388 let replica = Database::create_cluster_replica(
22389 replica_dir.path(),
22390 ClusterId::from_bytes([1; 16]),
22391 NodeId::from_bytes([2; 16]),
22392 DatabaseId::from_bytes([3; 16]),
22393 )
22394 .unwrap();
22395 replica
22396 .apply_replicated_catalog_command(&create_table_record("t", 1))
22397 .unwrap();
22398 assert_eq!(replica.table_id("t").unwrap(), table_id);
22399 assert!(replica.apply_replicated_records(&translated).unwrap());
22400 assert_eq!(visible_ids(&replica, "t"), values);
22401 assert_eq!(visible_ids(&replica, "t"), visible_ids(&leader, "t"));
22402
22403 drop(leader);
22406 let leader = Database::open(leader_dir.path()).unwrap();
22407 assert_eq!(visible_ids(&leader, "t"), values);
22408 }
22409
22410 #[test]
22411 fn staged_txn_writes_validate_and_apply() {
22412 let dir = tempfile::tempdir().unwrap();
22413 let db = Database::create_cluster_replica(
22414 dir.path(),
22415 ClusterId::from_bytes([1; 16]),
22416 NodeId::from_bytes([2; 16]),
22417 DatabaseId::from_bytes([3; 16]),
22418 )
22419 .unwrap();
22420 db.apply_replicated_catalog_command(&create_table_record("t", 1))
22421 .unwrap();
22422 let table_id = db.table_id("t").unwrap();
22423
22424 assert!(db.validate_staged_txn_writes(&[vec![0xFF]]).is_err());
22426 let unknown_table = StagedTxnWrite::Put {
22427 table_id: 99,
22428 rows: bincode::serialize(&Vec::<Row>::new()).unwrap(),
22429 }
22430 .encode()
22431 .unwrap();
22432 assert!(db.validate_staged_txn_writes(&[unknown_table]).is_err());
22433 let good: Vec<Vec<u8>> = [10_i64, 20, 30]
22434 .iter()
22435 .map(|value| {
22436 let rows = vec![Row::new(crate::RowId(*value as u64), Epoch(0))
22437 .with_column(1, Value::Int64(*value))];
22438 StagedTxnWrite::Put {
22439 table_id,
22440 rows: bincode::serialize(&rows).unwrap(),
22441 }
22442 .encode()
22443 .unwrap()
22444 })
22445 .collect();
22446 db.validate_staged_txn_writes(&good).unwrap();
22447
22448 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
22451 physical_micros: 5_000,
22452 logical: 0,
22453 node_tiebreaker: 0,
22454 };
22455 assert!(db
22456 .apply_staged_txn_writes(1 << 63, &good, commit_ts)
22457 .unwrap());
22458 assert_eq!(visible_ids(&db, "t"), vec![10, 20, 30]);
22459 let delete = StagedTxnWrite::Delete {
22460 table_id,
22461 row_ids: vec![20],
22462 }
22463 .encode()
22464 .unwrap();
22465 assert!(db
22466 .apply_staged_txn_writes((1 << 63) + 1, &[delete], commit_ts)
22467 .unwrap());
22468 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22469
22470 let db = Arc::new(db);
22473 db.shutdown().unwrap();
22474 let expected = crate::storage_mode::StorageMode::ClusterReplica {
22475 cluster_id: ClusterId::from_bytes([1; 16]),
22476 node_id: NodeId::from_bytes([2; 16]),
22477 database_id: DatabaseId::from_bytes([3; 16]),
22478 };
22479 let db = Database::open_cluster_replica(dir.path(), &expected).unwrap();
22480 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22481 }
22482}