1#[path = "index_ddl.rs"]
13mod index_ddl;
14
15use crate::catalog::{self, Catalog, CatalogEntry, TableState, META_DEK_LEN};
16use crate::engine::{SharedCtx, Table};
17use crate::epoch::{Epoch, EpochAuthority, EpochGuard, MaintenanceReceipt, Snapshot};
18use crate::error::{MongrelError, Result};
19use crate::external_table::ExternalTableEntry;
20use crate::memtable::Value;
21use crate::procedure::{
22 ProcedureCallOutput, ProcedureCallResult, ProcedureCallRow, ProcedureCondition, ProcedureStep,
23 ProcedureValue, StoredProcedure,
24};
25use crate::retention::{OwnedSnapshotGuard, SnapshotGuard, SnapshotRegistry};
26use crate::rowid::RowId;
27use crate::schema::{AlterColumn, ColumnDef, ColumnFlags, Schema, TypeId};
28use crate::trigger::{
29 StoredTrigger, TriggerCondition, TriggerConfig, TriggerEntry, TriggerEvent, TriggerExpr,
30 TriggerRaiseAction, TriggerStep, TriggerTarget, TriggerTiming, TriggerValue,
31};
32use parking_lot::{Mutex, RwLock};
33use sha2::{Digest, Sha256};
34use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
35use std::io::{Read, Write};
36use std::path::{Path, PathBuf};
37use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
38use std::sync::Arc;
39use subtle::ConstantTimeEq;
40
41pub const TABLES_DIR: &str = "tables";
42pub const VTAB_DIR: &str = "_vtab";
43pub const META_DIR: &str = "_meta";
44pub const KEYS_FILENAME: &str = "keys";
45pub const KMS_KEY_FILENAME: &str = "kms_key.json";
46const MAX_KMS_KEY_ENVELOPE_BYTES: usize = 1024 * 1024;
47pub const HISTORY_RETENTION_FILENAME: &str = "history_retention";
48pub const CTAS_BUILD_TABLE_PREFIX: &str = "__mongreldb_ctas_build_";
49
50pub const WAL_TABLE_ID: u64 = u64::MAX;
54pub const EXTERNAL_TABLE_ID: u64 = u64::MAX - 1;
57
58fn advance_security_version(catalog: &mut Catalog) -> Result<()> {
59 catalog.security_version = catalog.security_version.checked_add(1).ok_or_else(|| {
60 MongrelError::Conflict("security catalog version space is exhausted".into())
61 })?;
62 Ok(())
63}
64
65type OpenLeaseId = u64;
66
67static DATABASE_OPEN_WAIT_COUNT: AtomicU64 = AtomicU64::new(0);
68static DATABASE_OPEN_FAILURE_COUNT: AtomicU64 = AtomicU64::new(0);
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct DatabaseOpenMetrics {
72 pub lock_waits: u64,
73 pub failures: u64,
74}
75
76#[derive(Clone, Debug, Eq, Hash, PartialEq)]
77enum DatabaseOpenKey {
78 IntendedPath(PathBuf),
79 FileIdentity(crate::durable_file::DurableFileIdentity),
80}
81
82#[derive(Debug)]
83enum ProcessOpenState {
84 Opening { lease_id: OpenLeaseId },
85 Open { lease_id: OpenLeaseId },
86 Closing { lease_id: OpenLeaseId },
87}
88
89impl ProcessOpenState {
90 fn lease_id(&self) -> OpenLeaseId {
91 match self {
92 Self::Opening { lease_id } | Self::Open { lease_id } | Self::Closing { lease_id } => {
93 *lease_id
94 }
95 }
96 }
97}
98
99fn embedding_error(error: crate::embedding::EmbeddingError) -> MongrelError {
100 match error {
101 crate::embedding::EmbeddingError::LimitExceeded {
102 resource,
103 requested,
104 limit,
105 } => MongrelError::ResourceLimitExceeded {
106 resource,
107 requested,
108 limit,
109 },
110 crate::embedding::EmbeddingError::Execution(message) => {
111 if message.contains("deadline") {
112 MongrelError::DeadlineExceeded
113 } else {
114 MongrelError::Cancelled
115 }
116 }
117 error => MongrelError::Other(error.to_string()),
118 }
119}
120
121fn render_embedding_input(
122 schema: &Schema,
123 spec: &crate::embedding::GeneratedEmbeddingSpec,
124 cells: &[(u16, crate::memtable::Value)],
125) -> Result<String> {
126 let mut values = Vec::with_capacity(spec.source_columns.len());
127 for source_id in &spec.source_columns {
128 let column = schema
129 .columns
130 .iter()
131 .find(|column| column.id == *source_id)
132 .ok_or_else(|| MongrelError::Schema(format!("unknown embedding source {source_id}")))?;
133 let value = cells
134 .iter()
135 .find(|(id, _)| id == source_id)
136 .map(|(_, value)| embedding_input_value(value))
137 .transpose()?
138 .unwrap_or_default();
139 values.push((column.name.as_str(), value));
140 }
141 if spec.input_template.is_empty() {
142 return Ok(values
143 .into_iter()
144 .map(|(_, value)| value)
145 .collect::<Vec<_>>()
146 .join("\n"));
147 }
148 let mut rendered = spec.input_template.clone();
149 for (name, value) in values {
150 rendered = rendered.replace(&format!("{{{name}}}"), &value);
151 }
152 if rendered.contains('{') || rendered.contains('}') {
153 return Err(MongrelError::Schema(
154 "embedding input template contains an unknown placeholder".into(),
155 ));
156 }
157 Ok(rendered)
158}
159
160fn embedding_input_value(value: &crate::memtable::Value) -> Result<String> {
161 use crate::memtable::Value;
162 match value {
163 Value::Null => Ok(String::new()),
164 Value::Bool(value) => Ok(value.to_string()),
165 Value::Int64(value) => Ok(value.to_string()),
166 Value::Float64(value) => Ok(value.to_string()),
167 Value::Bytes(value) | Value::Json(value) => String::from_utf8(value.clone())
168 .map_err(|_| MongrelError::InvalidArgument("embedding source is not UTF-8".into())),
169 Value::Decimal(value) => Ok(value.to_string()),
170 Value::Interval {
171 months,
172 days,
173 nanos,
174 } => Ok(format!("{months}:{days}:{nanos}")),
175 Value::Uuid(value) => Ok(value.iter().map(|byte| format!("{byte:02x}")).collect()),
176 Value::Embedding(_) | Value::GeneratedEmbedding(_) => Err(MongrelError::InvalidArgument(
177 "embedding columns cannot be embedding text sources".into(),
178 )),
179 }
180}
181
182#[derive(Default)]
183struct ProcessOpenRegistry {
184 next_lease_id: OpenLeaseId,
185 entries: HashMap<DatabaseOpenKey, ProcessOpenState>,
186}
187
188fn process_open_registry() -> &'static Mutex<ProcessOpenRegistry> {
189 static REGISTRY: std::sync::OnceLock<Mutex<ProcessOpenRegistry>> = std::sync::OnceLock::new();
190 REGISTRY.get_or_init(|| Mutex::new(ProcessOpenRegistry::default()))
191}
192
193fn same_process_locked(path: &Path) -> MongrelError {
194 MongrelError::DatabaseLocked {
195 path: path.to_path_buf(),
196 message: "database is already open in this process; reuse the existing Arc<Database>"
197 .into(),
198 }
199}
200
201struct OpenReservation {
202 lease_id: OpenLeaseId,
203 keys: Vec<DatabaseOpenKey>,
204 committed: bool,
205}
206
207impl OpenReservation {
208 fn acquire(key: DatabaseOpenKey, display_path: &Path) -> Result<Self> {
209 let mut registry = process_open_registry().lock();
210 if registry.entries.contains_key(&key) {
211 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
212 return Err(same_process_locked(display_path));
213 }
214 registry.next_lease_id = registry.next_lease_id.checked_add(1).ok_or_else(|| {
215 MongrelError::Full("process database-open lease namespace exhausted".into())
216 })?;
217 let lease_id = registry.next_lease_id;
218 registry
219 .entries
220 .insert(key.clone(), ProcessOpenState::Opening { lease_id });
221 Ok(Self {
222 lease_id,
223 keys: vec![key],
224 committed: false,
225 })
226 }
227
228 fn into_lease(
229 mut self,
230 bootstrap_file: std::fs::File,
231 canonical_path: PathBuf,
232 ) -> ExclusiveDatabaseLease {
233 self.committed = true;
234 ExclusiveDatabaseLease {
235 lease_id: self.lease_id,
236 keys: std::mem::take(&mut self.keys),
237 bootstrap_file,
238 legacy_file: None,
239 canonical_path,
240 durable_root: None,
241 owner_pid: std::process::id(),
242 opened: false,
243 }
244 }
245}
246
247impl Drop for OpenReservation {
248 fn drop(&mut self) {
249 if self.committed {
250 return;
251 }
252 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
253 let mut registry = process_open_registry().lock();
254 for key in &self.keys {
255 if registry
256 .entries
257 .get(key)
258 .is_some_and(|state| state.lease_id() == self.lease_id)
259 {
260 registry.entries.remove(key);
261 }
262 }
263 }
264}
265
266struct ExclusiveDatabaseLease {
267 lease_id: OpenLeaseId,
268 keys: Vec<DatabaseOpenKey>,
269 bootstrap_file: std::fs::File,
270 legacy_file: Option<std::fs::File>,
271 canonical_path: PathBuf,
272 durable_root: Option<Arc<crate::durable_file::DurableRoot>>,
273 owner_pid: u32,
274 opened: bool,
275}
276
277impl ExclusiveDatabaseLease {
278 fn claim_root_identity(&mut self, root: &crate::durable_file::DurableRoot) -> Result<()> {
279 let key = DatabaseOpenKey::FileIdentity(root.file_identity()?);
280 if self.keys.contains(&key) {
281 return Ok(());
282 }
283 let mut registry = process_open_registry().lock();
284 if registry.entries.contains_key(&key) {
285 return Err(same_process_locked(&self.canonical_path));
286 }
287 registry.entries.insert(
288 key.clone(),
289 ProcessOpenState::Opening {
290 lease_id: self.lease_id,
291 },
292 );
293 self.keys.push(key);
294 Ok(())
295 }
296
297 fn mark_open(&mut self) -> Result<()> {
298 let mut registry = process_open_registry().lock();
299 if self.keys.iter().any(|key| {
300 registry
301 .entries
302 .get(key)
303 .is_none_or(|state| state.lease_id() != self.lease_id)
304 }) {
305 return Err(MongrelError::Conflict(
306 "database-open reservation changed during initialization".into(),
307 ));
308 }
309 for key in &self.keys {
310 registry.entries.insert(
311 key.clone(),
312 ProcessOpenState::Open {
313 lease_id: self.lease_id,
314 },
315 );
316 }
317 self.opened = true;
318 Ok(())
319 }
320}
321
322impl Drop for ExclusiveDatabaseLease {
323 fn drop(&mut self) {
324 if std::process::id() != self.owner_pid {
325 return;
326 }
327 if !self.opened {
328 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
329 }
330 {
331 let mut registry = process_open_registry().lock();
332 for key in &self.keys {
333 if registry
334 .entries
335 .get(key)
336 .is_some_and(|state| state.lease_id() == self.lease_id)
337 {
338 registry.entries.insert(
339 key.clone(),
340 ProcessOpenState::Closing {
341 lease_id: self.lease_id,
342 },
343 );
344 }
345 }
346 }
347 if let Some(file) = &self.legacy_file {
348 let _ = fs2::FileExt::unlock(file);
349 }
350 let _ = fs2::FileExt::unlock(&self.bootstrap_file);
351 let mut registry = process_open_registry().lock();
352 for key in &self.keys {
353 if registry
354 .entries
355 .get(key)
356 .is_some_and(|state| state.lease_id() == self.lease_id)
357 {
358 registry.entries.remove(key);
359 }
360 }
361 }
362}
363
364fn commit_prepare_checkpoint(
365 control: Option<&crate::ExecutionControl>,
366 index: usize,
367) -> Result<()> {
368 if index.is_multiple_of(256) {
369 if let Some(control) = control {
370 control.checkpoint()?;
371 }
372 }
373 Ok(())
374}
375
376fn finish_controlled_commit_attempt(
377 result: Result<Epoch>,
378 after_commit: &mut Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
379) -> Result<Epoch> {
380 let Some(after_commit) = after_commit.as_mut() else {
381 return result;
382 };
383 match result {
384 Ok(epoch) => match (**after_commit)(Some(epoch)) {
385 Ok(()) => Ok(epoch),
386 Err(error) => Err(MongrelError::DurableCommit {
387 epoch: epoch.0,
388 message: error.to_string(),
389 }),
390 },
391 Err(MongrelError::DurableCommit { epoch, message }) => {
392 let callback_error = (**after_commit)(Some(Epoch(epoch))).err();
393 Err(MongrelError::DurableCommit {
394 epoch,
395 message: callback_error
396 .map(|error| format!("{message}; commit callback: {error}"))
397 .unwrap_or(message),
398 })
399 }
400 Err(error) => match (**after_commit)(None) {
401 Ok(()) => Err(error),
402 Err(callback_error) => Err(MongrelError::Other(format!(
403 "{error}; commit callback: {callback_error}"
404 ))),
405 },
406 }
407}
408
409fn current_unix_nanos() -> u64 {
410 std::time::SystemTime::now()
411 .duration_since(std::time::UNIX_EPOCH)
412 .unwrap_or_default()
413 .as_nanos() as u64
414}
415
416fn read_encryption_salt(
417 root: &crate::durable_file::DurableRoot,
418) -> Result<[u8; crate::encryption::SALT_LEN]> {
419 let mut file = root
420 .open_regular(Path::new(META_DIR).join(KEYS_FILENAME))
421 .map_err(|error| MongrelError::NotFound(format!("encryption salt file: {error}")))?;
422 let length = file.metadata()?.len();
423 if length != crate::encryption::SALT_LEN as u64 {
424 return Err(MongrelError::Encryption(format!(
425 "invalid encryption salt length: got {length}, expected {}",
426 crate::encryption::SALT_LEN
427 )));
428 }
429 let mut salt = [0_u8; crate::encryption::SALT_LEN];
430 file.read_exact(&mut salt)?;
431 Ok(salt)
432}
433
434fn read_kms_key_envelope(
435 root: &crate::durable_file::DurableRoot,
436) -> Result<crate::security_hardening::KmsDatabaseKeyEnvelope> {
437 let file = root
438 .open_regular(Path::new(META_DIR).join(KMS_KEY_FILENAME))
439 .map_err(|error| MongrelError::NotFound(format!("KMS key envelope: {error}")))?;
440 let mut bytes = Vec::new();
441 file.take((MAX_KMS_KEY_ENVELOPE_BYTES + 1) as u64)
442 .read_to_end(&mut bytes)?;
443 if bytes.len() > MAX_KMS_KEY_ENVELOPE_BYTES {
444 return Err(MongrelError::Encryption(
445 "KMS key envelope exceeds 1 MiB".into(),
446 ));
447 }
448 serde_json::from_slice(&bytes)
449 .map_err(|error| MongrelError::Encryption(format!("invalid KMS key envelope: {error}")))
450}
451
452fn write_kms_key_envelope(
453 root: &Path,
454 envelope: &crate::security_hardening::KmsDatabaseKeyEnvelope,
455) -> Result<()> {
456 let bytes = serde_json::to_vec(envelope)
457 .map_err(|error| MongrelError::Encryption(format!("encode KMS key envelope: {error}")))?;
458 if bytes.len() > MAX_KMS_KEY_ENVELOPE_BYTES {
459 return Err(MongrelError::Encryption(
460 "KMS key envelope exceeds 1 MiB".into(),
461 ));
462 }
463 Ok(crate::durable_file::write_atomic(
464 &root.join(META_DIR).join(KMS_KEY_FILENAME),
465 &bytes,
466 )?)
467}
468
469fn unwrap_kms_database_key(
470 provider: &dyn crate::security_hardening::KeyManagementProvider,
471 envelope: &crate::security_hardening::KmsDatabaseKeyEnvelope,
472) -> Result<zeroize::Zeroizing<Vec<u8>>> {
473 if provider.provider_id() != envelope.provider_id {
474 return Err(MongrelError::Encryption(format!(
475 "KMS provider mismatch: database requires {:?}, got {:?}",
476 envelope.provider_id,
477 provider.provider_id()
478 )));
479 }
480 let key = provider
481 .unwrap_key(&envelope.wrapped_key)
482 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
483 if key.len() != crate::encryption::DEK_LEN {
484 return Err(MongrelError::Encryption(format!(
485 "KMS returned {} key bytes, expected {}",
486 key.len(),
487 crate::encryption::DEK_LEN
488 )));
489 }
490 Ok(key)
491}
492
493fn create_kms_kek(
494 root: &Path,
495 provider: &dyn crate::security_hardening::KeyManagementProvider,
496 kms_key_id: &str,
497) -> Result<Arc<crate::encryption::Kek>> {
498 if kms_key_id.is_empty() {
499 return Err(MongrelError::InvalidArgument(
500 "KMS key id must not be empty".into(),
501 ));
502 }
503 let salt = crate::encryption::random_salt()?;
504 let mut raw_key = zeroize::Zeroizing::new([0_u8; crate::encryption::DEK_LEN]);
505 crate::encryption::fill_random(raw_key.as_mut())?;
506 let wrapped_key = provider
507 .wrap_key(kms_key_id, raw_key.as_ref())
508 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
509 let envelope = crate::security_hardening::KmsDatabaseKeyEnvelope {
510 provider_id: provider.provider_id().to_owned(),
511 wrapped_key,
512 };
513 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
514 write_kms_key_envelope(root, &envelope)?;
515 Ok(Arc::new(crate::encryption::Kek::from_raw_key(
516 raw_key.as_ref(),
517 &salt,
518 )?))
519}
520
521fn open_kms_kek(
522 root: &crate::durable_file::DurableRoot,
523 provider: &dyn crate::security_hardening::KeyManagementProvider,
524) -> Result<Arc<crate::encryption::Kek>> {
525 let salt = read_encryption_salt(root)?;
526 let envelope = read_kms_key_envelope(root)?;
527 let raw_key = unwrap_kms_database_key(provider, &envelope)?;
528 Ok(Arc::new(crate::encryption::Kek::from_raw_key(
529 raw_key.as_ref(),
530 &salt,
531 )?))
532}
533
534fn persist_key_rotation(
535 journal: &crate::security_hardening::KeyRotationJournal,
536 record: &crate::security_hardening::KeyRotationRecord,
537) -> Result<()> {
538 journal
539 .persist(record)
540 .map_err(|error| MongrelError::Encryption(error.to_string()))
541}
542
543fn fail_key_rotation<T>(
544 journal: &crate::security_hardening::KeyRotationJournal,
545 record: &mut crate::security_hardening::KeyRotationRecord,
546 error: impl ToString,
547) -> Result<T> {
548 let message = error.to_string();
549 record.fail(&message);
550 persist_key_rotation(journal, record)?;
551 Err(MongrelError::Encryption(message))
552}
553
554fn advance_key_rotation(
555 journal: &crate::security_hardening::KeyRotationJournal,
556 record: &mut crate::security_hardening::KeyRotationRecord,
557) -> Result<()> {
558 let now = std::time::SystemTime::now()
559 .duration_since(std::time::UNIX_EPOCH)
560 .unwrap_or_default()
561 .as_micros() as u64;
562 record
563 .advance(now)
564 .map_err(|error| MongrelError::Encryption(error.to_string()))?;
565 persist_key_rotation(journal, record)?;
566 let hook = match record.phase {
567 crate::security_hardening::KeyRotationPhase::WrappingNewKey => "kms.rotation.phase.1",
568 crate::security_hardening::KeyRotationPhase::DualRead => "kms.rotation.phase.2",
569 crate::security_hardening::KeyRotationPhase::Reencrypting => "kms.rotation.phase.3",
570 crate::security_hardening::KeyRotationPhase::Validating => "kms.rotation.phase.4",
571 crate::security_hardening::KeyRotationPhase::Published => "kms.rotation.phase.5",
572 crate::security_hardening::KeyRotationPhase::RetiringOldKey => "kms.rotation.phase.6",
573 crate::security_hardening::KeyRotationPhase::Succeeded => "kms.rotation.phase.7",
574 crate::security_hardening::KeyRotationPhase::Pending
575 | crate::security_hardening::KeyRotationPhase::Failed => return Ok(()),
576 };
577 mongreldb_fault::inject(hook).map_err(|error| MongrelError::Encryption(error.to_string()))
578}
579
580fn incremental_aggregate_cache_key(
581 table: &str,
582 conditions: &[crate::query::Condition],
583 column: Option<u16>,
584 agg: crate::engine::NativeAgg,
585 principal: Option<&crate::auth::Principal>,
586 security_version: u64,
587) -> u64 {
588 use std::hash::{Hash, Hasher};
589 let projection = column.as_ref().map(std::slice::from_ref);
590 let query_key = crate::query::canonical_query_key(conditions, projection, security_version);
591 let mut hasher = std::collections::hash_map::DefaultHasher::new();
592 table.hash(&mut hasher);
593 query_key.hash(&mut hasher);
594 match agg {
595 crate::engine::NativeAgg::Count => 0u8,
596 crate::engine::NativeAgg::Sum => 1,
597 crate::engine::NativeAgg::Min => 2,
598 crate::engine::NativeAgg::Max => 3,
599 crate::engine::NativeAgg::Avg => 4,
600 }
601 .hash(&mut hasher);
602 if let Some(principal) = principal {
603 principal.user_id.hash(&mut hasher);
604 principal.created_epoch.hash(&mut hasher);
605 principal.username.hash(&mut hasher);
606 principal.is_admin.hash(&mut hasher);
607 let mut roles = principal.roles.clone();
608 roles.sort_unstable();
609 roles.hash(&mut hasher);
610 }
611 hasher.finish()
612}
613
614fn read_history_retention(
615 root: &crate::durable_file::DurableRoot,
616 current_epoch: Epoch,
617) -> Result<(u64, Epoch)> {
618 const MAX_HISTORY_RETENTION_BYTES: u64 = 128;
619 let file = match root.open_regular(Path::new(META_DIR).join(HISTORY_RETENTION_FILENAME)) {
620 Ok(file) => file,
621 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
622 return Ok((0, current_epoch));
623 }
624 Err(error) => return Err(error.into()),
625 };
626 let length = file.metadata()?.len();
627 if length > MAX_HISTORY_RETENTION_BYTES {
628 return Err(MongrelError::ResourceLimitExceeded {
629 resource: "history retention bytes",
630 requested: usize::try_from(length).unwrap_or(usize::MAX),
631 limit: MAX_HISTORY_RETENTION_BYTES as usize,
632 });
633 }
634 let mut bytes = Vec::with_capacity(length as usize);
635 file.take(MAX_HISTORY_RETENTION_BYTES + 1)
636 .read_to_end(&mut bytes)?;
637 if bytes.len() as u64 != length {
638 return Err(MongrelError::Other(
639 "history retention length changed while reading".into(),
640 ));
641 }
642 let text = std::str::from_utf8(&bytes)
643 .map_err(|error| MongrelError::Other(format!("history retention encoding: {error}")))?;
644 let mut fields = text.split_whitespace();
645 let epochs = fields
646 .next()
647 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
648 .parse::<u64>()
649 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
650 let start = fields
651 .next()
652 .ok_or_else(|| MongrelError::Other("history retention start is missing".into()))?
653 .parse::<u64>()
654 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
655 if fields.next().is_some() || start > current_epoch.0 {
656 return Err(MongrelError::Other(
657 "history retention file has trailing fields or a future start epoch".into(),
658 ));
659 }
660 Ok((epochs, Epoch(start)))
661}
662
663fn write_history_retention<F>(
664 root: &Path,
665 epochs: u64,
666 start: Epoch,
667 after_publish: F,
668) -> Result<()>
669where
670 F: FnOnce(),
671{
672 let meta = root.join(META_DIR);
673 let path = meta.join(HISTORY_RETENTION_FILENAME);
674 let bytes = format!("{epochs} {}\n", start.0);
675 crate::durable_file::write_atomic_with_after(&path, bytes.as_bytes(), after_publish)?;
676 Ok(())
677}
678
679struct PreparedBackupDestination {
680 parent: crate::durable_file::DurableRoot,
681 destination_name: std::ffi::OsString,
682 destination_path: PathBuf,
683 stage_name: std::ffi::OsString,
684 stage: Option<Box<crate::durable_file::DurableRoot>>,
685}
686
687fn prepare_backup_destination(
688 source: &Path,
689 destination: &Path,
690) -> Result<PreparedBackupDestination> {
691 let destination_name = destination
692 .file_name()
693 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?
694 .to_os_string();
695 let requested_parent = destination
696 .parent()
697 .filter(|path| !path.as_os_str().is_empty())
698 .unwrap_or_else(|| Path::new("."));
699 crate::durable_file::create_directory_all(requested_parent)?;
700 let parent = crate::durable_file::DurableRoot::open(requested_parent)?;
701 prepare_backup_destination_in(source, &parent, &destination_name)
702}
703
704fn prepare_backup_destination_in(
705 source: &Path,
706 parent: &crate::durable_file::DurableRoot,
707 destination_name: &std::ffi::OsStr,
708) -> Result<PreparedBackupDestination> {
709 let source = source.canonicalize()?;
710 if parent.canonical_path().starts_with(&source) {
711 return Err(MongrelError::InvalidArgument(
712 "backup destination must not be inside the source database".into(),
713 ));
714 }
715 if parent.entry_exists(Path::new(&destination_name))? {
716 return Err(MongrelError::Conflict(format!(
717 "backup destination already exists: {}",
718 parent.canonical_path().join(destination_name).display()
719 )));
720 }
721 let mut stage_name = None;
722 for _ in 0..128 {
723 let mut nonce = [0_u8; 8];
724 crate::encryption::fill_random(&mut nonce)?;
725 let suffix = nonce
726 .iter()
727 .map(|byte| format!("{byte:02x}"))
728 .collect::<String>();
729 let name = std::ffi::OsString::from(format!(
730 ".{}.backup-stage-{}-{suffix}",
731 destination_name.to_string_lossy(),
732 std::process::id()
733 ));
734 match parent.create_directory_new(Path::new(&name)) {
735 Ok(()) => {
736 stage_name = Some(name);
737 break;
738 }
739 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
740 Err(error) => return Err(error.into()),
741 }
742 }
743 let stage_name = stage_name
744 .ok_or_else(|| MongrelError::Conflict("could not allocate backup staging path".into()))?;
745 let stage = parent.open_directory(Path::new(&stage_name))?;
746 Ok(PreparedBackupDestination {
747 destination_path: parent.canonical_path().join(destination_name),
748 destination_name: destination_name.to_os_string(),
749 stage_name,
750 stage: Some(Box::new(stage)),
751 parent: parent.try_clone()?,
752 })
753}
754
755fn copy_backup_boundary(
756 source_root: &Path,
757 destination_root: &crate::durable_file::DurableRoot,
758 deferred_runs: &HashSet<PathBuf>,
759 copied: &mut Vec<PathBuf>,
760 control: Option<&crate::ExecutionControl>,
761) -> Result<()> {
762 let mut visited = 0;
763 crate::durable_file::walk_regular_files_nofollow(
764 source_root,
765 |relative, is_directory| {
766 if visited % 256 == 0 {
767 if let Some(control) = control {
768 control.checkpoint()?;
769 }
770 }
771 visited += 1;
772 if backup_path_excluded(relative) {
773 return Ok(false);
774 }
775 if is_directory {
776 return Ok(true);
777 }
778 if deferred_runs.contains(relative) {
779 return Ok(false);
780 }
781 Ok(!(relative
782 .parent()
783 .and_then(Path::file_name)
784 .is_some_and(|parent| parent == "_runs")
785 && relative
786 .extension()
787 .is_some_and(|extension| extension == "sr")))
788 },
789 |relative| {
790 destination_root.create_directory_all(relative)?;
791 Ok(())
792 },
793 |relative, source| {
794 destination_root.copy_new_from(relative, source)?;
795 copied.push(relative.to_path_buf());
796 Ok(())
797 },
798 )
799}
800
801fn backup_path_excluded(relative: &Path) -> bool {
802 relative == Path::new("_meta/.lock")
803 || relative == Path::new("_meta/replica")
804 || relative == Path::new("_meta/repl_epoch")
805 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
806 || relative.components().any(|component| {
807 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
808 })
809}
810
811#[derive(Debug, Clone)]
812pub enum ExternalTriggerWrite {
813 Insert {
814 table: String,
815 cells: Vec<(u16, Value)>,
816 },
817 UpdateByPk {
818 table: String,
819 pk: Value,
820 cells: Vec<(u16, Value)>,
821 },
822 DeleteByPk {
823 table: String,
824 pk: Value,
825 },
826}
827
828impl ExternalTriggerWrite {
829 fn table(&self) -> &str {
830 match self {
831 Self::Insert { table, .. }
832 | Self::UpdateByPk { table, .. }
833 | Self::DeleteByPk { table, .. } => table,
834 }
835 }
836}
837
838#[derive(Debug, Clone, PartialEq)]
839pub enum ExternalTriggerBaseWrite {
840 Put {
841 table: String,
842 cells: Vec<(u16, Value)>,
843 },
844 Delete {
845 table: String,
846 row_id: RowId,
847 },
848}
849
850#[derive(Debug, Clone, PartialEq)]
851pub struct ExternalTriggerWriteResult {
852 pub state: Vec<u8>,
853 pub base_writes: Vec<ExternalTriggerBaseWrite>,
854}
855
856impl ExternalTriggerWriteResult {
857 pub fn new(state: Vec<u8>) -> Self {
858 Self {
859 state,
860 base_writes: Vec::new(),
861 }
862 }
863}
864
865pub trait ExternalTriggerBridge: Send + Sync {
866 fn apply_trigger_external_write(
867 &self,
868 entry: &ExternalTableEntry,
869 base_state: Vec<u8>,
870 op: ExternalTriggerWrite,
871 ) -> Result<ExternalTriggerWriteResult>;
872}
873
874struct SpilledRun {
876 table_id: u64,
877 run_id: u128,
878 pending_path: PathBuf,
879 final_path: PathBuf,
880 rows: Vec<crate::memtable::Row>,
881 row_count: u64,
882 min_rid: u64,
883 max_rid: u64,
884 content_hash: [u8; 32],
885}
886
887const SPILLED_WAL_PAYLOAD_MAX_BYTES: usize = 24 * 1024 * 1024;
888const SPILLED_WAL_TOTAL_MAX_BYTES: usize = 256 * 1024 * 1024;
889
890fn encode_spilled_row_chunks(
891 rows: &[crate::memtable::Row],
892 total_bytes: &mut usize,
893 total_limit: usize,
894 control: Option<&crate::ExecutionControl>,
895) -> Result<Vec<Vec<u8>>> {
896 let mut output = Vec::new();
897 let mut start = 0;
898 while start < rows.len() {
899 let mut estimated_bytes = std::mem::size_of::<u64>();
903 let mut end = start;
904 while end < rows.len() {
905 if end % 256 == 0 {
906 if let Some(control) = control {
907 control.checkpoint()?;
908 }
909 }
910 let row_bytes =
911 usize::try_from(bincode::serialized_size(&rows[end])?).map_err(|_| {
912 MongrelError::ResourceLimitExceeded {
913 resource: "spilled WAL row bytes",
914 requested: usize::MAX,
915 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
916 }
917 })?;
918 let next_bytes = estimated_bytes.checked_add(row_bytes).ok_or(
919 MongrelError::ResourceLimitExceeded {
920 resource: "spilled WAL row bytes",
921 requested: usize::MAX,
922 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
923 },
924 )?;
925 if next_bytes > SPILLED_WAL_PAYLOAD_MAX_BYTES {
926 break;
927 }
928 estimated_bytes = next_bytes;
929 end += 1;
930 }
931 if end == start {
932 return Err(MongrelError::ResourceLimitExceeded {
933 resource: "spilled WAL row bytes",
934 requested: estimated_bytes.saturating_add(1),
935 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
936 });
937 }
938 let payload = bincode::serialize(&rows[start..end])?;
939 if payload.len() > SPILLED_WAL_PAYLOAD_MAX_BYTES {
940 return Err(MongrelError::ResourceLimitExceeded {
941 resource: "spilled WAL row bytes",
942 requested: payload.len(),
943 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
944 });
945 }
946 let requested = total_bytes.checked_add(payload.len()).unwrap_or(usize::MAX);
947 if requested > total_limit {
948 return Err(MongrelError::ResourceLimitExceeded {
949 resource: "spilled WAL transaction bytes",
950 requested,
951 limit: total_limit,
952 });
953 }
954 *total_bytes = requested;
955 output.push(payload);
956 start = end;
957 }
958 Ok(output)
959}
960
961#[cfg(test)]
962mod spilled_wal_encoding_tests {
963 use super::*;
964
965 #[test]
966 fn logical_spill_payload_has_a_total_bound() {
967 let rows = (0..4)
968 .map(|row_id| crate::memtable::Row {
969 row_id: crate::rowid::RowId(row_id),
970 committed_epoch: Epoch::ZERO,
971 columns: [(1, Value::Bytes(vec![0; 64]))].into_iter().collect(),
972 deleted: false,
973 commit_ts: None,
974 })
975 .collect::<Vec<_>>();
976 let mut total = 0;
977 let error = encode_spilled_row_chunks(&rows, &mut total, 32, None).unwrap_err();
978 assert!(matches!(
979 error,
980 MongrelError::ResourceLimitExceeded {
981 resource: "spilled WAL transaction bytes",
982 ..
983 }
984 ));
985 }
986}
987
988struct PreparedRunLinks {
993 links: Vec<(PathBuf, PathBuf)>,
994 armed: bool,
995}
996
997impl PreparedRunLinks {
998 fn prepare(spilled: &[SpilledRun]) -> Result<Self> {
999 let mut guard = Self {
1000 links: Vec::with_capacity(spilled.len()),
1001 armed: true,
1002 };
1003 for run in spilled {
1004 crate::durable_file::rename(&run.pending_path, &run.final_path)?;
1005 guard
1006 .links
1007 .push((run.pending_path.clone(), run.final_path.clone()));
1008 }
1009 Ok(guard)
1010 }
1011
1012 fn disarm(&mut self) {
1013 self.armed = false;
1014 for (pending, _) in &self.links {
1015 if let Some(parent) = pending.parent() {
1016 let _ = std::fs::remove_dir_all(parent);
1017 }
1018 }
1019 }
1020}
1021
1022impl Drop for PreparedRunLinks {
1023 fn drop(&mut self) {
1024 if !self.armed {
1025 return;
1026 }
1027 for (pending, final_path) in self.links.iter().rev() {
1028 let _ = std::fs::rename(final_path, pending);
1029 }
1030 }
1031}
1032
1033struct TableApplyBatch {
1034 table_id: u64,
1035 handle: TableHandle,
1036 ops: Vec<crate::txn::StagedOp>,
1037}
1038
1039#[derive(Debug, Clone)]
1040struct TriggerRowImage {
1041 columns: HashMap<u16, Value>,
1042}
1043
1044impl TriggerRowImage {
1045 fn from_row(row: crate::memtable::Row) -> Self {
1046 Self {
1047 columns: row.columns,
1048 }
1049 }
1050
1051 fn from_cells(cells: &[(u16, Value)]) -> Self {
1052 Self {
1053 columns: cells.iter().cloned().collect(),
1054 }
1055 }
1056}
1057
1058#[derive(Debug, Clone)]
1059struct WriteEvent {
1060 table: String,
1061 kind: TriggerEvent,
1062 old: Option<TriggerRowImage>,
1063 new: Option<TriggerRowImage>,
1064 changed_columns: Vec<u16>,
1065 op_indices: Vec<usize>,
1066 put_idx: Option<usize>,
1067 trigger_stack: Vec<String>,
1068}
1069
1070#[derive(Default)]
1071struct TriggerExpansion {
1072 before: Vec<(u64, crate::txn::Staged)>,
1073 before_stacks: Vec<Vec<String>>,
1074 before_external: Vec<ExternalTriggerWrite>,
1075 after: Vec<(u64, crate::txn::Staged)>,
1076 after_stacks: Vec<Vec<String>>,
1077 after_external: Vec<ExternalTriggerWrite>,
1078 ignored_indices: std::collections::BTreeSet<usize>,
1079}
1080
1081#[derive(Clone, PartialEq)]
1082struct TriggerCatalogBinding {
1083 triggers: Vec<TriggerEntry>,
1084 tables: Vec<(String, u64, u64)>,
1085 external_tables: Vec<(String, u64, u64)>,
1086}
1087
1088fn trigger_catalog_binding(catalog: &Catalog) -> Option<TriggerCatalogBinding> {
1089 let mut triggers = catalog
1090 .triggers
1091 .iter()
1092 .filter(|entry| entry.trigger.enabled)
1093 .cloned()
1094 .collect::<Vec<_>>();
1095 if triggers.is_empty() {
1096 return None;
1097 }
1098 triggers.sort_by(|left, right| left.trigger.name.cmp(&right.trigger.name));
1099 let mut tables = catalog
1100 .tables
1101 .iter()
1102 .filter(|entry| matches!(entry.state, TableState::Live))
1103 .map(|entry| (entry.name.clone(), entry.table_id, entry.schema.schema_id))
1104 .collect::<Vec<_>>();
1105 tables.sort_unstable();
1106 let mut external_tables = catalog
1107 .external_tables
1108 .iter()
1109 .map(|entry| {
1110 (
1111 entry.name.clone(),
1112 entry.created_epoch,
1113 entry.declared_schema.schema_id,
1114 )
1115 })
1116 .collect::<Vec<_>>();
1117 external_tables.sort_unstable();
1118 Some(TriggerCatalogBinding {
1119 triggers,
1120 tables,
1121 external_tables,
1122 })
1123}
1124
1125struct TriggerProgramOutput<'a> {
1126 added: &'a mut Vec<(u64, crate::txn::Staged)>,
1127 added_stacks: &'a mut Vec<Vec<String>>,
1128 added_external: &'a mut Vec<ExternalTriggerWrite>,
1129 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
1130}
1131
1132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1133enum TriggerProgramOutcome {
1134 Continue,
1135 Ignore,
1136}
1137
1138#[derive(Debug, Clone)]
1140pub struct CheckIssue {
1141 pub table_id: u64,
1142 pub table_name: String,
1143 pub severity: String,
1144 pub description: String,
1145}
1146
1147#[derive(Debug, Clone)]
1149pub struct AuthorizedReadSnapshot {
1150 pub table: String,
1151 pub table_snapshot: Snapshot,
1152 pub data_generation: u64,
1153 pub security_version: u64,
1154 pub allowed_row_ids: Option<HashSet<RowId>>,
1155}
1156
1157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1159pub struct AuthorizedReadStamp {
1160 pub table_id: u64,
1161 pub schema_id: u64,
1162 pub data_generation: u64,
1163 pub security_version: u64,
1164 pub snapshot: Snapshot,
1165}
1166
1167type RlsCacheKey = (String, u64, u64, String);
1168
1169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1171pub struct RlsCacheStats {
1172 pub entries: usize,
1173 pub bytes: usize,
1174 pub hits: u64,
1175 pub misses: u64,
1176 pub evictions: u64,
1177 pub build_nanos: u64,
1178 pub rows_evaluated: u64,
1179}
1180
1181const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
1182const CDC_MAX_WAL_RECORDS: usize = 1_000_000;
1183const CDC_MAX_WAL_REPLAY_BYTES: usize = 256 * 1024 * 1024;
1184const CDC_MAX_EVENTS: usize = 100_000;
1185const CDC_MAX_ROWS: usize = 1_000_000;
1186const CDC_MAX_INLINE_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
1187const CDC_MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
1188
1189fn charge_cdc_bytes(total: &mut usize, amount: usize, resource: &'static str) -> Result<()> {
1190 let requested = total.saturating_add(amount);
1191 if requested > CDC_MAX_RETAINED_BYTES {
1192 return Err(MongrelError::ResourceLimitExceeded {
1193 resource,
1194 requested,
1195 limit: CDC_MAX_RETAINED_BYTES,
1196 });
1197 }
1198 *total = requested;
1199 Ok(())
1200}
1201
1202fn cdc_row_storage_bytes(row: &crate::memtable::Row) -> usize {
1203 usize::try_from(row.estimated_bytes())
1204 .unwrap_or(usize::MAX)
1205 .saturating_add(std::mem::size_of::<crate::memtable::Row>())
1206}
1207
1208fn cdc_row_json_bytes(row: &crate::memtable::Row) -> usize {
1209 let value_slot = std::mem::size_of::<serde_json::Value>();
1210 row.columns.values().fold(512_usize, |bytes, value| {
1211 let values = match value {
1212 Value::Bytes(values) => values.len(),
1213 Value::Json(values) => values.len(),
1214 Value::Embedding(values) => values.len(),
1215 Value::GeneratedEmbedding(value) => value.vector.len(),
1216 _ => 1,
1217 };
1218 bytes.saturating_add(values.saturating_mul(value_slot))
1219 })
1220}
1221
1222fn cdc_rows_json_bytes(rows: &[crate::memtable::Row]) -> usize {
1223 rows.iter().fold(0_usize, |bytes, row| {
1224 bytes.saturating_add(cdc_row_json_bytes(row))
1225 })
1226}
1227
1228struct RlsCacheEntry {
1229 value: Arc<HashSet<RowId>>,
1230 bytes: usize,
1231 generation: u64,
1232}
1233
1234#[derive(Default)]
1235struct RlsCache {
1236 entries: HashMap<RlsCacheKey, RlsCacheEntry>,
1237 lru: BTreeSet<(u64, RlsCacheKey)>,
1238 next_generation: u64,
1239 bytes: usize,
1240 hits: u64,
1241 misses: u64,
1242 evictions: u64,
1243 build_nanos: u64,
1244 rows_evaluated: u64,
1245}
1246
1247impl RlsCache {
1248 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
1249 let value = self.entries.get(key).map(|entry| Arc::clone(&entry.value));
1250 if value.is_some() {
1251 self.hits = self.hits.saturating_add(1);
1252 self.touch(key);
1253 } else {
1254 self.misses = self.misses.saturating_add(1);
1255 }
1256 value
1257 }
1258
1259 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
1260 let bytes = key
1261 .0
1262 .len()
1263 .saturating_add(key.3.len())
1264 .saturating_add(
1265 value
1266 .capacity()
1267 .saturating_mul(std::mem::size_of::<RowId>() * 3),
1268 )
1269 .saturating_add(std::mem::size_of::<RlsCacheKey>());
1270 if bytes > RLS_CACHE_MAX_BYTES {
1271 return;
1272 }
1273 if let Some(old) = self.entries.remove(&key) {
1274 self.bytes = self.bytes.saturating_sub(old.bytes);
1275 self.lru.remove(&(old.generation, key.clone()));
1276 }
1277 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
1278 let Some((_, oldest)) = self.lru.pop_first() else {
1279 break;
1280 };
1281 if let Some(old) = self.entries.remove(&oldest) {
1282 self.bytes = self.bytes.saturating_sub(old.bytes);
1283 self.evictions = self.evictions.saturating_add(1);
1284 }
1285 }
1286 let generation = self.allocate_generation();
1287 self.bytes = self.bytes.saturating_add(bytes);
1288 self.lru.insert((generation, key.clone()));
1289 self.entries.insert(
1290 key,
1291 RlsCacheEntry {
1292 value,
1293 bytes,
1294 generation,
1295 },
1296 );
1297 }
1298
1299 fn touch(&mut self, key: &RlsCacheKey) {
1300 let Some(previous) = self.entries.get(key).map(|entry| entry.generation) else {
1301 return;
1302 };
1303 self.lru.remove(&(previous, key.clone()));
1304 let generation = self.allocate_generation();
1305 if let Some(entry) = self.entries.get_mut(key) {
1306 entry.generation = generation;
1307 }
1308 self.lru.insert((generation, key.clone()));
1309 }
1310
1311 fn allocate_generation(&mut self) -> u64 {
1312 if self.next_generation == u64::MAX {
1313 self.rebase_generations();
1314 }
1315 let generation = self.next_generation;
1316 self.next_generation += 1;
1317 generation
1318 }
1319
1320 fn rebase_generations(&mut self) {
1321 let ordered = std::mem::take(&mut self.lru);
1322 for (generation, (_, key)) in ordered.into_iter().enumerate() {
1323 let generation = generation as u64;
1324 if let Some(entry) = self.entries.get_mut(&key) {
1325 entry.generation = generation;
1326 self.lru.insert((generation, key));
1327 }
1328 }
1329 self.next_generation = self.lru.len() as u64;
1330 }
1331
1332 fn stats(&self) -> RlsCacheStats {
1333 RlsCacheStats {
1334 entries: self.entries.len(),
1335 bytes: self.bytes,
1336 hits: self.hits,
1337 misses: self.misses,
1338 evictions: self.evictions,
1339 build_nanos: self.build_nanos,
1340 rows_evaluated: self.rows_evaluated,
1341 }
1342 }
1343}
1344
1345#[cfg(test)]
1346mod rls_cache_tests {
1347 use super::*;
1348
1349 fn key(principal: &str) -> RlsCacheKey {
1350 ("table".into(), 1, 1, principal.into())
1351 }
1352
1353 fn rows(row_id: u64) -> Arc<HashSet<RowId>> {
1354 Arc::new(std::iter::once(RowId(row_id)).collect())
1355 }
1356
1357 #[test]
1358 fn hits_update_recency_without_growing_the_order_index() {
1359 let mut cache = RlsCache::default();
1360 let first = key("first");
1361 let second = key("second");
1362 cache.insert(first.clone(), rows(1));
1363 cache.insert(second.clone(), rows(2));
1364 assert_eq!(cache.lru.len(), cache.entries.len());
1365
1366 for _ in 0..1_000 {
1367 assert!(cache.get(&first).is_some());
1368 }
1369
1370 assert_eq!(cache.lru.len(), cache.entries.len());
1371 assert_eq!(
1372 cache.lru.first().map(|(_, candidate)| candidate),
1373 Some(&second)
1374 );
1375
1376 cache.insert(first.clone(), rows(3));
1377 assert_eq!(cache.lru.len(), cache.entries.len());
1378 let replacement = cache.get(&first).unwrap();
1379 assert_eq!(replacement.len(), 1);
1380 assert!(replacement.contains(&RowId(3)));
1381 }
1382}
1383
1384#[derive(Clone)]
1386pub struct TableHandle {
1387 inner: TableHandleInner,
1388 generation_metrics: Arc<TableGenerationMetrics>,
1389}
1390
1391#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1392pub struct TableGenerationStats {
1393 pub active_read_generations: usize,
1394 pub max_live_read_generations: usize,
1395 pub cow_clone_count: u64,
1396 pub cow_clone_nanos: u64,
1397 pub estimated_cow_clone_bytes: u64,
1398 pub writer_wait_nanos: u64,
1399}
1400
1401#[derive(Default)]
1402#[doc(hidden)]
1403pub struct TableGenerationMetrics {
1404 active_read_generations: AtomicUsize,
1405 max_live_read_generations: AtomicUsize,
1406 cow_clone_count: AtomicU64,
1407 cow_clone_nanos: AtomicU64,
1408 estimated_cow_clone_bytes: AtomicU64,
1409 writer_wait_nanos: AtomicU64,
1410}
1411
1412impl TableGenerationMetrics {
1413 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
1414 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
1415 self.max_live_read_generations
1416 .fetch_max(active, Ordering::Relaxed);
1417 Arc::new(TableReadGeneration {
1418 table,
1419 metrics: Arc::clone(self),
1420 })
1421 }
1422
1423 fn stats(&self) -> TableGenerationStats {
1424 TableGenerationStats {
1425 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
1426 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
1427 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
1428 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
1429 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
1430 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
1431 }
1432 }
1433}
1434
1435pub struct TableReadGeneration {
1437 table: Table,
1438 metrics: Arc<TableGenerationMetrics>,
1439}
1440
1441impl std::ops::Deref for TableReadGeneration {
1442 type Target = Table;
1443
1444 fn deref(&self) -> &Self::Target {
1445 &self.table
1446 }
1447}
1448
1449impl Drop for TableReadGeneration {
1450 fn drop(&mut self) {
1451 self.metrics
1452 .active_read_generations
1453 .fetch_sub(1, Ordering::Relaxed);
1454 }
1455}
1456
1457#[derive(Clone)]
1458enum TableHandleInner {
1459 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
1460 Direct(Arc<Mutex<Table>>),
1461}
1462
1463pub enum TableGuard<'a> {
1464 CopyOnWrite {
1465 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
1466 metrics: Arc<TableGenerationMetrics>,
1467 },
1468 Direct {
1469 table: parking_lot::MutexGuard<'a, Table>,
1470 },
1471}
1472
1473pub enum TableReadGuard<'a> {
1479 CopyOnWrite {
1480 table: parking_lot::RwLockReadGuard<'a, Arc<Table>>,
1481 },
1482 Direct {
1483 table: parking_lot::MutexGuard<'a, Table>,
1484 },
1485}
1486
1487impl TableHandle {
1488 fn new(table: Table) -> Self {
1489 Self {
1490 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
1491 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1492 }
1493 }
1494
1495 pub fn from_table(table: Table) -> Self {
1496 Self::new(table)
1497 }
1498
1499 pub fn read(&self) -> TableReadGuard<'_> {
1502 match &self.inner {
1503 TableHandleInner::CopyOnWrite(table) => TableReadGuard::CopyOnWrite {
1504 table: table.read(),
1505 },
1506 TableHandleInner::Direct(table) => TableReadGuard::Direct {
1507 table: table.lock(),
1508 },
1509 }
1510 }
1511
1512 pub fn lock(&self) -> TableGuard<'_> {
1513 let started = std::time::Instant::now();
1514 let guard = match &self.inner {
1515 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
1516 table: table.write(),
1517 metrics: Arc::clone(&self.generation_metrics),
1518 },
1519 TableHandleInner::Direct(table) => TableGuard::Direct {
1520 table: table.lock(),
1521 },
1522 };
1523 self.generation_metrics.writer_wait_nanos.fetch_add(
1524 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1525 Ordering::Relaxed,
1526 );
1527 guard
1528 }
1529
1530 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
1531 let started = std::time::Instant::now();
1532 let guard = match &self.inner {
1533 TableHandleInner::CopyOnWrite(table) => {
1534 table
1535 .try_write_for(timeout)
1536 .map(|table| TableGuard::CopyOnWrite {
1537 table,
1538 metrics: Arc::clone(&self.generation_metrics),
1539 })
1540 }
1541 TableHandleInner::Direct(table) => table
1542 .try_lock_for(timeout)
1543 .map(|table| TableGuard::Direct { table }),
1544 };
1545 self.generation_metrics.writer_wait_nanos.fetch_add(
1546 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1547 Ordering::Relaxed,
1548 );
1549 guard
1550 }
1551
1552 pub fn ptr_eq(&self, other: &Self) -> bool {
1553 match (&self.inner, &other.inner) {
1554 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1555 Arc::ptr_eq(left, right)
1556 }
1557 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1558 Arc::ptr_eq(left, right)
1559 }
1560 _ => false,
1561 }
1562 }
1563
1564 pub fn read_generation_with_context(
1565 &self,
1566 context: Option<&crate::query::AiExecutionContext>,
1567 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1568 let mut table = if let Some(context) = context {
1569 loop {
1570 context.checkpoint()?;
1571 let wait = context
1572 .remaining_duration()
1573 .unwrap_or(std::time::Duration::from_millis(5))
1574 .min(std::time::Duration::from_millis(5));
1575 if let Some(table) = self.try_lock_for(wait) {
1576 break table;
1577 }
1578 }
1579 } else {
1580 self.lock()
1581 };
1582 let snapshot = table.snapshot();
1583 let generation = table.clone_read_generation()?;
1584 Ok((self.generation_metrics.activate(generation), snapshot))
1585 }
1586
1587 pub fn generation_stats(&self) -> TableGenerationStats {
1588 self.generation_metrics.stats()
1589 }
1590}
1591
1592impl From<Arc<Mutex<Table>>> for TableHandle {
1593 fn from(table: Arc<Mutex<Table>>) -> Self {
1594 Self {
1595 inner: TableHandleInner::Direct(table),
1596 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1597 }
1598 }
1599}
1600
1601impl std::ops::Deref for TableGuard<'_> {
1602 type Target = Table;
1603
1604 fn deref(&self) -> &Self::Target {
1605 match self {
1606 Self::CopyOnWrite { table, .. } => table.as_ref(),
1607 Self::Direct { table } => table,
1608 }
1609 }
1610}
1611
1612impl std::ops::Deref for TableReadGuard<'_> {
1613 type Target = Table;
1614
1615 fn deref(&self) -> &Self::Target {
1616 match self {
1617 Self::CopyOnWrite { table } => table.as_ref(),
1618 Self::Direct { table } => table,
1619 }
1620 }
1621}
1622
1623impl std::ops::DerefMut for TableGuard<'_> {
1624 fn deref_mut(&mut self) -> &mut Self::Target {
1625 match self {
1626 Self::CopyOnWrite { table, metrics } => {
1627 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1628 let estimated_bytes = table.estimated_clone_bytes();
1629 let started = std::time::Instant::now();
1630 let table = Arc::make_mut(table);
1631 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1632 metrics.cow_clone_nanos.fetch_add(
1633 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1634 Ordering::Relaxed,
1635 );
1636 metrics
1637 .estimated_cow_clone_bytes
1638 .fetch_add(estimated_bytes, Ordering::Relaxed);
1639 table
1640 } else {
1641 Arc::make_mut(table)
1642 }
1643 }
1644 Self::Direct { table } => table,
1645 }
1646 }
1647}
1648
1649#[derive(Clone, Debug)]
1650pub struct ReadAuthorization {
1651 pub operation: crate::auth::ColumnOperation,
1652 pub columns: Vec<u16>,
1653 pub permissions: Vec<crate::auth::Permission>,
1654}
1655
1656#[derive(Default, Debug)]
1657struct TableWritePermissionNeeds {
1658 insert: bool,
1659 insert_columns: Vec<u16>,
1660 update: bool,
1661 update_columns: Vec<u16>,
1662 delete: bool,
1663 truncate: bool,
1664}
1665
1666#[cfg(test)]
1667thread_local! {
1668 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1669 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1670 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1671 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1672 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1673 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1674}
1675
1676fn summarize_write_permissions(
1677 staging: &[(u64, crate::txn::Staged)],
1678) -> HashMap<u64, TableWritePermissionNeeds> {
1679 use crate::txn::Staged;
1680
1681 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1682 for (table_id, operation) in staging {
1683 let table = needs.entry(*table_id).or_default();
1684 match operation {
1685 Staged::Put(cells) => {
1686 table.insert = true;
1687 table
1688 .insert_columns
1689 .extend(cells.iter().map(|(column, _)| *column));
1690 }
1691 Staged::Update {
1692 changed_columns, ..
1693 } => {
1694 table.update = true;
1695 table.update_columns.extend(changed_columns);
1696 }
1697 Staged::Delete(_) => table.delete = true,
1698 Staged::Truncate => table.truncate = true,
1699 }
1700 }
1701 for table in needs.values_mut() {
1702 table.insert_columns.sort_unstable();
1703 table.insert_columns.dedup();
1704 table.update_columns.sort_unstable();
1705 table.update_columns.dedup();
1706 }
1707 needs
1708}
1709
1710struct SecurityCoordinator {
1711 gate: RwLock<()>,
1713 version: AtomicU64,
1714}
1715
1716fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1717 static COORDINATORS: std::sync::OnceLock<
1718 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1719 > = std::sync::OnceLock::new();
1720
1721 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1722 let mut coordinators = COORDINATORS
1723 .get_or_init(|| Mutex::new(HashMap::new()))
1724 .lock();
1725 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1726 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1727 return coordinator;
1728 }
1729 let coordinator = Arc::new(SecurityCoordinator {
1730 gate: RwLock::new(()),
1731 version: AtomicU64::new(version),
1732 });
1733 coordinators.insert(root, Arc::downgrade(&coordinator));
1734 coordinator
1735}
1736
1737pub fn lock_table_with_context<'a>(
1738 handle: &'a TableHandle,
1739 context: Option<&crate::query::AiExecutionContext>,
1740) -> Result<TableGuard<'a>> {
1741 let Some(context) = context else {
1742 return Ok(handle.lock());
1743 };
1744 loop {
1745 context.checkpoint()?;
1746 let wait = context
1747 .remaining_duration()
1748 .unwrap_or(std::time::Duration::from_millis(5))
1749 .min(std::time::Duration::from_millis(5));
1750 if let Some(guard) = handle.try_lock_for(wait) {
1751 return Ok(guard);
1752 }
1753 }
1754}
1755
1756#[derive(Clone, Debug, Default)]
1762pub struct OpenOptions {
1763 pub lock_timeout_ms: u32,
1779 pub memory_budget_bytes: Option<u64>,
1784 pub temp_disk_budget_bytes: Option<u64>,
1789 pub offline_validation: bool,
1796}
1797
1798impl OpenOptions {
1799 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1802 self.lock_timeout_ms = ms;
1803 self
1804 }
1805
1806 pub fn with_memory_budget_bytes(mut self, bytes: u64) -> Self {
1808 self.memory_budget_bytes = Some(bytes);
1809 self
1810 }
1811
1812 pub fn with_temp_disk_budget_bytes(mut self, bytes: u64) -> Self {
1814 self.temp_disk_budget_bytes = Some(bytes);
1815 self
1816 }
1817
1818 pub fn with_offline_validation(mut self, offline_validation: bool) -> Self {
1821 self.offline_validation = offline_validation;
1822 self
1823 }
1824}
1825
1826#[derive(Debug, Clone)]
1830pub(crate) enum OpenModeGate {
1831 Normal,
1834 SharedCore,
1837 OfflineValidation,
1839 ClusterRuntime {
1843 cluster_id: mongreldb_types::ids::ClusterId,
1845 node_id: mongreldb_types::ids::NodeId,
1847 database_id: mongreldb_types::ids::DatabaseId,
1849 },
1850 Create(crate::storage_mode::StorageMode),
1852}
1853
1854pub const DEFAULT_MEMORY_BUDGET_BYTES: u64 = 1024 * 1024 * 1024;
1858
1859pub const DEFAULT_TEMP_DISK_BUDGET_BYTES: u64 = 4 * 1024 * 1024 * 1024;
1862
1863#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1867pub(crate) struct CoreResourceConfig {
1868 pub memory_budget_bytes: u64,
1869 pub temp_disk_budget_bytes: u64,
1870}
1871
1872impl Default for CoreResourceConfig {
1873 fn default() -> Self {
1874 Self {
1875 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
1876 temp_disk_budget_bytes: DEFAULT_TEMP_DISK_BUDGET_BYTES,
1877 }
1878 }
1879}
1880
1881impl CoreResourceConfig {
1882 fn from_options(options: &OpenOptions) -> Result<Self> {
1883 let config = Self {
1884 memory_budget_bytes: options
1885 .memory_budget_bytes
1886 .unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES),
1887 temp_disk_budget_bytes: options
1888 .temp_disk_budget_bytes
1889 .unwrap_or(DEFAULT_TEMP_DISK_BUDGET_BYTES),
1890 };
1891 if config.memory_budget_bytes == 0 {
1892 return Err(MongrelError::InvalidArgument(
1893 "memory_budget_bytes must be nonzero".into(),
1894 ));
1895 }
1896 if config.temp_disk_budget_bytes == 0 {
1897 return Err(MongrelError::InvalidArgument(
1898 "temp_disk_budget_bytes must be nonzero".into(),
1899 ));
1900 }
1901 Ok(config)
1902 }
1903}
1904
1905#[derive(Debug, Clone)]
1908pub struct TablePinsReport {
1909 pub table_id: u64,
1911 pub table: String,
1913 pub pins: crate::retention::PinsReport,
1915}
1916
1917pub struct DatabaseCore {
1930 root: PathBuf,
1931 durable_root: Arc<crate::durable_file::DurableRoot>,
1932 lifecycle: Arc<crate::core::LifecycleController>,
1935 registry: std::sync::OnceLock<crate::manager::CoreRegistration>,
1939 read_only: bool,
1941 catalog: RwLock<Catalog>,
1942 security_coordinator: Arc<SecurityCoordinator>,
1943 security_catalog_disk_reads: AtomicU64,
1944 rls_cache: Mutex<RlsCache>,
1945 epoch: Arc<EpochAuthority>,
1946 snapshots: Arc<SnapshotRegistry>,
1947 memory_governor: crate::memory::MemoryGovernor,
1952 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1953 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1954 spill_manager: crate::spill::SpillManager,
1958 resource_groups: crate::resource::ResourceGroupRegistry,
1962 embedding_providers: crate::embedding::EmbeddingProviderRegistry,
1966 pub(crate) service_principals: crate::service_principal::ServicePrincipalStore,
1970 job_registry: Arc<crate::jobs::JobRegistry>,
1973 commit_lock: Arc<Mutex<()>>,
1974 kms_rotation_lock: Mutex<()>,
1975 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1980 next_txn_id: Arc<Mutex<u64>>,
1984 tables: RwLock<HashMap<u64, TableHandle>>,
1985 kek: Option<Arc<crate::encryption::Kek>>,
1986 ddl_lock: Mutex<()>,
1989 meta_dek: Option<[u8; META_DEK_LEN]>,
1990 spill_threshold: std::sync::atomic::AtomicU64,
1993 conflicts: crate::txn::ConflictIndex,
1996 active_txns: crate::txn::ActiveTxns,
1999 lock_manager: Arc<crate::locks::LockManager>,
2005 poisoned: Arc<std::sync::atomic::AtomicBool>,
2008 group: Arc<crate::txn::GroupCommit>,
2012 commit_log: RwLock<Arc<dyn mongreldb_log::CommitLog>>,
2016 standalone_commit_log: Arc<crate::commit_log::StandaloneCommitLog>,
2020 hlc: Arc<mongreldb_types::hlc::HlcClock>,
2026 idempotency: crate::txn::IdempotencyLedger,
2029 commit_ts_ledger: Mutex<std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp>>,
2037 active_spills: Arc<crate::retention::ActiveSpills>,
2040 replication_barrier: parking_lot::RwLock<()>,
2043 replication_wal_retention_segments: AtomicUsize,
2045 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
2048 #[doc(hidden)]
2052 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2053 #[doc(hidden)]
2055 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2056 #[doc(hidden)]
2059 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2060 #[doc(hidden)]
2063 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2064 #[doc(hidden)]
2070 fk_lock_hook: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
2071 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
2072 trigger_recursive: AtomicBool,
2073 trigger_max_depth: AtomicU32,
2074 trigger_max_loop_iterations: AtomicU32,
2075 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
2078 change_wake: tokio::sync::broadcast::Sender<()>,
2081 _lock: Mutex<Option<ExclusiveDatabaseLease>>,
2085}
2086
2087pub struct Database {
2097 core: Arc<DatabaseCore>,
2098 principal: RwLock<Option<crate::auth::Principal>>,
2108 auth_state: crate::auth_state::AuthState,
2114 shared: bool,
2121}
2122
2123impl std::ops::Deref for Database {
2124 type Target = DatabaseCore;
2125
2126 fn deref(&self) -> &DatabaseCore {
2127 &self.core
2128 }
2129}
2130
2131struct RunPins {
2132 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
2133 runs: Vec<(u64, u128)>,
2134}
2135
2136struct TxnLockGuard<'a> {
2141 locks: &'a crate::locks::LockManager,
2142 txn_id: u64,
2143}
2144
2145impl Drop for TxnLockGuard<'_> {
2146 fn drop(&mut self) {
2147 self.locks.release_all(self.txn_id);
2148 }
2149}
2150
2151struct BackupFilePins {
2152 root: PathBuf,
2153}
2154
2155struct PendingTableDir {
2156 path: PathBuf,
2157 armed: bool,
2158}
2159
2160impl PendingTableDir {
2161 fn new(path: PathBuf) -> Self {
2162 Self { path, armed: true }
2163 }
2164
2165 fn disarm(&mut self) {
2166 self.armed = false;
2167 }
2168}
2169
2170impl Drop for PendingTableDir {
2171 fn drop(&mut self) {
2172 if self.armed {
2173 let _ = std::fs::remove_dir_all(&self.path);
2174 }
2175 }
2176}
2177
2178impl Drop for BackupFilePins {
2179 fn drop(&mut self) {
2180 let _ = std::fs::remove_dir_all(&self.root);
2181 }
2182}
2183
2184impl Drop for RunPins {
2185 fn drop(&mut self) {
2186 let mut pins = self.pins.lock();
2187 for run in &self.runs {
2188 if let Some(count) = pins.get_mut(run) {
2189 *count -= 1;
2190 if *count == 0 {
2191 pins.remove(run);
2192 }
2193 }
2194 }
2195 }
2196}
2197
2198#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2201pub struct ChangeEvent {
2202 pub id: Option<String>,
2203 pub channel: String,
2204 pub table_id: Option<u64>,
2205 pub table: String,
2206 pub op: String,
2207 pub epoch: u64,
2208 pub txn_id: Option<u64>,
2209 pub message: Option<String>,
2210 pub data: Option<serde_json::Value>,
2211}
2212
2213#[derive(Debug, Clone)]
2214pub struct CdcBatch {
2215 pub events: Vec<ChangeEvent>,
2216 pub current_epoch: u64,
2217 pub earliest_epoch: Option<u64>,
2218 pub gap: bool,
2219}
2220
2221impl std::fmt::Debug for Database {
2228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2229 let cat = self.catalog.read();
2230 let principal_guard = self.principal.read();
2231 let principal: &str = principal_guard
2232 .as_ref()
2233 .map(|p| p.username.as_str())
2234 .unwrap_or("<none>");
2235 f.debug_struct("Database")
2236 .field("root", &self.root)
2237 .field("db_epoch", &cat.db_epoch)
2238 .field("open_generation", &"sidecar")
2239 .field("tables", &cat.tables.len())
2240 .field("visible_epoch", &self.epoch.visible().0)
2241 .field("encrypted", &self.kek.is_some())
2242 .field("require_auth", &cat.require_auth)
2243 .field("principal", &principal)
2244 .finish()
2245 }
2246}
2247
2248impl std::fmt::Debug for DatabaseCore {
2251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2252 let cat = self.catalog.read();
2253 f.debug_struct("DatabaseCore")
2254 .field("root", &self.root)
2255 .field("lifecycle", &self.lifecycle.state())
2256 .field("db_epoch", &cat.db_epoch)
2257 .field("tables", &cat.tables.len())
2258 .field("visible_epoch", &self.epoch.visible().0)
2259 .field("encrypted", &self.kek.is_some())
2260 .field("require_auth", &cat.require_auth)
2261 .finish()
2262 }
2263}
2264
2265impl DatabaseCore {
2266 fn ensure_owner_process(&self) -> Result<()> {
2267 let current_pid = std::process::id();
2268 let owner_pid = self
2269 ._lock
2270 .lock()
2271 .as_ref()
2272 .map(|lease| lease.owner_pid)
2273 .unwrap_or(current_pid);
2274 if current_pid == owner_pid {
2275 Ok(())
2276 } else {
2277 Err(MongrelError::ForkedProcess {
2278 owner_pid,
2279 current_pid,
2280 })
2281 }
2282 }
2283
2284 pub fn root(&self) -> &Path {
2286 self.durable_root.canonical_path()
2287 }
2288
2289 pub fn file_identity(&self) -> Result<crate::core::DatabaseFileIdentity> {
2291 crate::core::DatabaseFileIdentity::from_durable_root(&self.durable_root)
2292 }
2293
2294 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2296 self.lifecycle.state()
2297 }
2298
2299 pub fn is_open(&self) -> bool {
2301 self.lifecycle.is_open()
2302 }
2303
2304 pub fn operation_guard(self: &Arc<Self>) -> Result<crate::core::OperationGuard> {
2308 self.lifecycle.begin_operation()
2309 }
2310
2311 pub(crate) fn set_registry(
2315 &self,
2316 registration: crate::manager::CoreRegistration,
2317 ) -> Result<()> {
2318 self.registry
2319 .set(registration)
2320 .map_err(|_| MongrelError::Conflict("database core is already registry-bound".into()))
2321 }
2322
2323 pub fn shutdown(self: &Arc<Self>, drain_deadline: std::time::Duration) -> Result<()> {
2343 self.ensure_owner_process()?;
2344 let initiated = self.lifecycle.begin_shutdown();
2345 if !initiated {
2346 match self.lifecycle.state() {
2347 crate::core::LifecycleState::Closed => return Ok(()),
2348 crate::core::LifecycleState::Draining => {}
2351 state => {
2352 return Err(MongrelError::Conflict(format!(
2353 "database core cannot shut down from lifecycle state {state}"
2354 )))
2355 }
2356 }
2357 }
2358 if let Some(registration) = self.registry.get() {
2359 registration
2360 .manager
2361 .mark_closing(®istration.identity, self);
2362 }
2363 self.lifecycle.wait_drained(drain_deadline)?;
2364 let sync = self.shared_wal.lock().group_sync().map(|_| ());
2365 self.lifecycle.mark_closing();
2366 let lease = self._lock.lock().take();
2367 drop(lease);
2368 if let Some(registration) = self.registry.get() {
2369 registration
2370 .manager
2371 .entry_closed(®istration.identity, self);
2372 }
2373 self.lifecycle.mark_closed();
2374 sync
2375 }
2376}
2377
2378impl Database {
2379 pub fn open_metrics() -> DatabaseOpenMetrics {
2380 DatabaseOpenMetrics {
2381 lock_waits: DATABASE_OPEN_WAIT_COUNT.load(Ordering::Relaxed),
2382 failures: DATABASE_OPEN_FAILURE_COUNT.load(Ordering::Relaxed),
2383 }
2384 }
2385
2386 pub fn core(&self) -> Arc<DatabaseCore> {
2388 Arc::clone(&self.core)
2389 }
2390
2391 pub fn identity(&self) -> crate::handle::HandleIdentity {
2394 match self.principal.read().as_ref() {
2395 Some(principal) => crate::handle::HandleIdentity::CatalogUser {
2396 username: principal.username.clone(),
2397 user_id: principal.user_id,
2398 created_version: principal.created_epoch,
2399 },
2400 None => crate::handle::HandleIdentity::Credentialless,
2401 }
2402 }
2403
2404 pub fn lifecycle_state(&self) -> crate::core::LifecycleState {
2406 self.core.lifecycle_state()
2407 }
2408
2409 pub fn operation_guard(&self) -> Result<crate::core::OperationGuard> {
2413 self.core.operation_guard()
2414 }
2415
2416 pub(crate) fn from_core(
2420 core: Arc<DatabaseCore>,
2421 principal: Option<crate::auth::Principal>,
2422 shared: bool,
2423 ) -> Self {
2424 let require_auth = core.catalog.read().require_auth;
2425 Self {
2426 core,
2427 principal: RwLock::new(principal.clone()),
2428 auth_state: crate::auth_state::AuthState::new(require_auth, principal),
2429 shared,
2430 }
2431 }
2432
2433 pub(crate) fn rebind_service_principal(
2437 &self,
2438 def: &crate::service_principal::ServicePrincipalDefinition,
2439 ) {
2440 let principal = crate::auth::Principal {
2441 user_id: 0,
2442 created_epoch: def.creation_version,
2443 username: format!("service:{}", def.token_id),
2444 is_admin: false,
2445 roles: Vec::new(),
2446 permissions: def.permissions.clone(),
2447 };
2448 *self.principal.write() = Some(principal.clone());
2449 self.auth_state.set_principal(Some(principal));
2450 }
2451
2452 pub(crate) fn into_core(self) -> Arc<DatabaseCore> {
2454 self.core
2455 }
2456
2457 pub(crate) fn open_for_shared_core(root: impl AsRef<Path>) -> Result<Self> {
2462 Self::open_inner_with_lock_timeout(
2463 root,
2464 None,
2465 None,
2466 0,
2467 CoreResourceConfig::default(),
2468 OpenModeGate::SharedCore,
2469 )
2470 }
2471
2472 pub fn shutdown(self: Arc<Self>) -> Result<()> {
2478 if self.shared {
2479 return Err(MongrelError::Conflict(
2480 "shared-core facades do not own storage; use DatabaseHandle::shutdown".into(),
2481 ));
2482 }
2483 match Arc::try_unwrap(self) {
2484 Ok(database) => {
2485 database.ensure_owner_process()?;
2486 database.core.shutdown(std::time::Duration::from_secs(30))?;
2490 drop(database);
2491 Ok(())
2492 }
2493 Err(database) => Err(MongrelError::DatabaseBusy {
2494 strong_handles: Arc::strong_count(&database),
2495 }),
2496 }
2497 }
2498
2499 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
2500 if let Ok(canonical) = root.canonicalize() {
2501 let lock_dir = canonical.parent().ok_or_else(|| {
2502 std::io::Error::new(
2503 std::io::ErrorKind::InvalidInput,
2504 "database root must have a parent directory",
2505 )
2506 })?;
2507 return Ok((canonical.clone(), lock_dir.to_path_buf()));
2508 }
2509
2510 let absolute = if root.is_absolute() {
2511 root.to_path_buf()
2512 } else {
2513 std::env::current_dir()?.join(root)
2514 };
2515 let mut cursor = absolute.as_path();
2516 let mut suffix = Vec::new();
2517 while !cursor.exists() {
2518 let name = cursor.file_name().ok_or_else(|| {
2519 std::io::Error::new(
2520 std::io::ErrorKind::NotFound,
2521 format!("no existing ancestor for database root {}", root.display()),
2522 )
2523 })?;
2524 suffix.push(name.to_os_string());
2525 cursor = cursor.parent().ok_or_else(|| {
2526 std::io::Error::new(
2527 std::io::ErrorKind::NotFound,
2528 format!("no existing ancestor for database root {}", root.display()),
2529 )
2530 })?;
2531 }
2532 let lock_dir = cursor.canonicalize()?;
2533 let mut canonical = lock_dir.clone();
2534 for component in suffix.iter().rev() {
2535 canonical.push(component);
2536 }
2537 Ok((canonical, lock_dir))
2538 }
2539
2540 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<ExclusiveDatabaseLease> {
2541 use std::hash::{Hash, Hasher};
2542
2543 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
2544 let reservation =
2545 OpenReservation::acquire(DatabaseOpenKey::IntendedPath(canonical_path.clone()), root)?;
2546
2547 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2548 canonical_path.hash(&mut hasher);
2549 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
2550 let file = std::fs::OpenOptions::new()
2551 .create(true)
2552 .truncate(false)
2553 .write(true)
2554 .open(lock_path)?;
2555 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2556 return Err(MongrelError::DatabaseLocked {
2557 path: root.to_path_buf(),
2558 message: error.to_string(),
2559 });
2560 }
2561 Ok(reservation.into_lease(file, canonical_path))
2562 }
2563
2564 fn acquire_legacy_database_lock(
2565 lock: &mut ExclusiveDatabaseLease,
2566 root: &Path,
2567 timeout_ms: u32,
2568 ) -> Result<()> {
2569 let durable_root = lock
2570 .durable_root
2571 .as_ref()
2572 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
2573 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
2574 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
2575 return Err(MongrelError::DatabaseLocked {
2576 path: root.to_path_buf(),
2577 message: error.to_string(),
2578 });
2579 }
2580 lock.legacy_file = Some(file);
2581 Ok(())
2582 }
2583
2584 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
2585 if path.exists() {
2586 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
2587 }
2588 let mut ancestor = path;
2589 while !ancestor.exists() {
2590 ancestor = ancestor.parent().ok_or_else(|| {
2591 MongrelError::NotFound(format!(
2592 "no existing ancestor for database root {}",
2593 path.display()
2594 ))
2595 })?;
2596 }
2597 let relative = path.strip_prefix(ancestor).map_err(|error| {
2598 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
2599 })?;
2600 crate::durable_file::DurableRoot::open(ancestor)?
2601 .create_directory_all_pinned(relative)
2602 .map_err(Into::into)
2603 }
2604
2605 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2606 let requested_root = root.as_ref();
2607 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
2608 let root = lock.canonical_path.clone();
2609 Self::reject_existing_database(&root)?;
2610 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
2611 if durable_root.canonical_path() != lock.canonical_path {
2612 return Err(MongrelError::Conflict(
2613 "database root changed while it was being created".into(),
2614 ));
2615 }
2616 lock.claim_root_identity(&durable_root)?;
2617 durable_root.create_directory_all(META_DIR)?;
2618 lock.durable_root = Some(durable_root);
2619 let io_root = lock
2620 .durable_root
2621 .as_ref()
2622 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2623 .io_path()?;
2624 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
2625 Self::reject_existing_database(&io_root)?;
2626 Ok((io_root, lock))
2627 }
2628
2629 fn begin_open(
2630 root: impl AsRef<Path>,
2631 lock_timeout_ms: u32,
2632 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2633 let root = root.as_ref();
2634 let canonical_root = root.canonicalize().map_err(|error| {
2635 if error.kind() == std::io::ErrorKind::NotFound {
2636 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
2637 } else {
2638 error.into()
2639 }
2640 })?;
2641 let durable_root = crate::durable_file::DurableRoot::open(&canonical_root)?;
2642 Self::begin_open_durable(durable_root, lock_timeout_ms)
2643 }
2644
2645 fn begin_open_durable(
2646 durable_root: crate::durable_file::DurableRoot,
2647 lock_timeout_ms: u32,
2648 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
2649 let io_root = durable_root.io_path()?;
2650 let current_root = io_root.canonicalize()?;
2651 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
2652 lock.claim_root_identity(&durable_root)?;
2653 lock.durable_root = Some(Arc::new(durable_root));
2654 let io_root = lock
2655 .durable_root
2656 .as_ref()
2657 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2658 .io_path()?;
2659 if lock
2660 .durable_root
2661 .as_ref()
2662 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
2663 .open_directory(META_DIR)
2664 .is_err()
2665 {
2666 return Err(MongrelError::NotFound(format!(
2667 "no database metadata found at {:?}",
2668 current_root
2669 )));
2670 }
2671 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
2672 Ok((io_root, lock))
2673 }
2674
2675 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
2677 let (root, lock) = Self::begin_create(root)?;
2678 Self::create_inner(
2679 root,
2680 None,
2681 lock,
2682 crate::storage_mode::StorageMode::Standalone,
2683 )
2684 }
2685
2686 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2689 let (root, lock) = Self::begin_create(root)?;
2690 let salt = crate::encryption::random_salt()?;
2691 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2692 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2693 Self::create_inner(
2694 root,
2695 Some(kek),
2696 lock,
2697 crate::storage_mode::StorageMode::Standalone,
2698 )
2699 }
2700
2701 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2704 let (root, lock) = Self::begin_create(root)?;
2705 let salt = crate::encryption::random_salt()?;
2706 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2707 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2708 Self::create_inner(
2709 root,
2710 Some(kek),
2711 lock,
2712 crate::storage_mode::StorageMode::Standalone,
2713 )
2714 }
2715
2716 pub fn create_with_kms(
2719 root: impl AsRef<Path>,
2720 provider: &dyn crate::security_hardening::KeyManagementProvider,
2721 kms_key_id: &str,
2722 ) -> Result<Self> {
2723 let (root, lock) = Self::begin_create(root)?;
2724 let kek = create_kms_kek(&root, provider, kms_key_id)?;
2725 Self::create_inner(
2726 root,
2727 Some(kek),
2728 lock,
2729 crate::storage_mode::StorageMode::Standalone,
2730 )
2731 }
2732
2733 pub fn create_cluster_replica(
2741 root: impl AsRef<Path>,
2742 cluster_id: mongreldb_types::ids::ClusterId,
2743 node_id: mongreldb_types::ids::NodeId,
2744 database_id: mongreldb_types::ids::DatabaseId,
2745 ) -> Result<Self> {
2746 let (root, lock) = Self::begin_create(root)?;
2747 Self::create_inner(
2748 root,
2749 None,
2750 lock,
2751 crate::storage_mode::StorageMode::ClusterReplica {
2752 cluster_id,
2753 node_id,
2754 database_id,
2755 },
2756 )
2757 }
2758
2759 pub fn open_cluster_replica(
2766 root: impl AsRef<Path>,
2767 expected: &crate::storage_mode::StorageMode,
2768 ) -> Result<Self> {
2769 let Some((cluster_id, node_id, database_id)) = expected.cluster_identity() else {
2770 return Err(MongrelError::InvalidArgument(format!(
2771 "open_cluster_replica requires a ClusterReplica identity, got {expected:?}"
2772 )));
2773 };
2774 let (root, lock) = Self::begin_open(root, 0)?;
2775 Self::open_inner_locked(
2776 root,
2777 None,
2778 lock,
2779 CoreResourceConfig::default(),
2780 OpenModeGate::ClusterRuntime {
2781 cluster_id,
2782 node_id,
2783 database_id,
2784 },
2785 )
2786 }
2787
2788 fn create_inner(
2789 root: PathBuf,
2790 kek: Option<Arc<crate::encryption::Kek>>,
2791 lock: ExclusiveDatabaseLease,
2792 storage_mode: crate::storage_mode::StorageMode,
2793 ) -> Result<Self> {
2794 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2795 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2796 let cat = Catalog::empty();
2797 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2798 Self::finish_open(
2799 root,
2800 cat,
2801 kek,
2802 meta_dek,
2803 false,
2804 None,
2805 None,
2806 None,
2807 lock,
2808 CoreResourceConfig::default(),
2809 OpenModeGate::Create(storage_mode),
2810 )
2811 }
2812
2813 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
2815 Self::open_inner(root, None, None)
2816 }
2817
2818 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
2820 let (root, lock) = Self::begin_open(root, 0)?;
2821 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2822 MongrelError::Other("database root descriptor was not pinned".into())
2823 })?)?;
2824 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2825 Self::open_inner_locked(
2826 root,
2827 Some(kek),
2828 lock,
2829 CoreResourceConfig::default(),
2830 OpenModeGate::Normal,
2831 )
2832 }
2833
2834 pub fn open_encrypted_with_options(
2837 root: impl AsRef<Path>,
2838 passphrase: &str,
2839 options: OpenOptions,
2840 ) -> Result<Self> {
2841 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2842 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2843 MongrelError::Other("database root descriptor was not pinned".into())
2844 })?)?;
2845 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2846 let gate = if options.offline_validation {
2847 OpenModeGate::OfflineValidation
2848 } else {
2849 OpenModeGate::Normal
2850 };
2851 Self::open_inner_locked(
2852 root,
2853 Some(kek),
2854 lock,
2855 CoreResourceConfig::from_options(&options)?,
2856 gate,
2857 )
2858 }
2859
2860 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
2862 let (root, lock) = Self::begin_open(root, 0)?;
2863 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2864 MongrelError::Other("database root descriptor was not pinned".into())
2865 })?)?;
2866 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
2867 Self::open_inner_locked(
2868 root,
2869 Some(kek),
2870 lock,
2871 CoreResourceConfig::default(),
2872 OpenModeGate::Normal,
2873 )
2874 }
2875
2876 pub fn open_with_kms(
2879 root: impl AsRef<Path>,
2880 provider: &dyn crate::security_hardening::KeyManagementProvider,
2881 ) -> Result<Self> {
2882 Self::open_with_kms_and_options(root, provider, OpenOptions::default())
2883 }
2884
2885 pub fn open_with_kms_and_options(
2887 root: impl AsRef<Path>,
2888 provider: &dyn crate::security_hardening::KeyManagementProvider,
2889 options: OpenOptions,
2890 ) -> Result<Self> {
2891 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2892 let kek = open_kms_kek(
2893 lock.durable_root.as_deref().ok_or_else(|| {
2894 MongrelError::Other("database root descriptor was not pinned".into())
2895 })?,
2896 provider,
2897 )?;
2898 let gate = if options.offline_validation {
2899 OpenModeGate::OfflineValidation
2900 } else {
2901 OpenModeGate::Normal
2902 };
2903 Self::open_inner_locked(
2904 root,
2905 Some(kek),
2906 lock,
2907 CoreResourceConfig::from_options(&options)?,
2908 gate,
2909 )
2910 }
2911
2912 pub fn rotate_kms_key(
2916 &self,
2917 provider: &dyn crate::security_hardening::KeyManagementProvider,
2918 new_kms_key_id: &str,
2919 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2920 let _rotation_guard = self.core.kms_rotation_lock.lock();
2921 if new_kms_key_id.is_empty() {
2922 return Err(MongrelError::InvalidArgument(
2923 "KMS key id must not be empty".into(),
2924 ));
2925 }
2926 let durable_root = Arc::clone(&self.core.durable_root);
2927 let root = durable_root.io_path()?;
2928 let envelope = read_kms_key_envelope(&durable_root)?;
2929 if envelope.provider_id != provider.provider_id() {
2930 return Err(MongrelError::Encryption(format!(
2931 "KMS provider mismatch: database requires {:?}, got {:?}",
2932 envelope.provider_id,
2933 provider.provider_id()
2934 )));
2935 }
2936 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2937 if let Some(record) = journal
2938 .load()
2939 .map_err(|error| MongrelError::Encryption(error.to_string()))?
2940 .filter(|record| record.phase != crate::security_hardening::KeyRotationPhase::Succeeded)
2941 {
2942 if record.to_key_id != new_kms_key_id {
2943 return Err(MongrelError::Conflict(format!(
2944 "KMS rotation to {:?} is already in progress",
2945 record.to_key_id
2946 )));
2947 }
2948 return self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record);
2949 }
2950 let now_micros = std::time::SystemTime::now()
2951 .duration_since(std::time::UNIX_EPOCH)
2952 .unwrap_or_default()
2953 .as_micros() as u64;
2954 let mut record = crate::security_hardening::KeyRotationRecord::new(
2955 envelope.wrapped_key.key_version.clone(),
2956 String::new(),
2957 now_micros,
2958 );
2959 record.from_key_id = envelope.wrapped_key.kms_key_id.clone();
2960 record.to_key_id = new_kms_key_id.to_owned();
2961 persist_key_rotation(&journal, &record)?;
2962 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
2963 }
2964
2965 pub fn retry_kms_key_rotation(
2967 &self,
2968 provider: &dyn crate::security_hardening::KeyManagementProvider,
2969 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2970 let _rotation_guard = self.core.kms_rotation_lock.lock();
2971 let durable_root = Arc::clone(&self.core.durable_root);
2972 let root = durable_root.io_path()?;
2973 let journal = crate::security_hardening::KeyRotationJournal::new(root.join(META_DIR));
2974 let mut record = journal
2975 .load()
2976 .map_err(|error| MongrelError::Encryption(error.to_string()))?
2977 .ok_or_else(|| MongrelError::NotFound("KMS rotation journal".into()))?;
2978 if record.phase != crate::security_hardening::KeyRotationPhase::Failed {
2979 return Err(MongrelError::Conflict(
2980 "KMS rotation is not in Failed state".into(),
2981 ));
2982 }
2983 record.retry();
2984 persist_key_rotation(&journal, &record)?;
2985 self.run_kms_key_rotation(provider, &durable_root, &root, &journal, record)
2986 }
2987
2988 fn run_kms_key_rotation(
2989 &self,
2990 provider: &dyn crate::security_hardening::KeyManagementProvider,
2991 durable_root: &crate::durable_file::DurableRoot,
2992 root: &Path,
2993 journal: &crate::security_hardening::KeyRotationJournal,
2994 mut record: crate::security_hardening::KeyRotationRecord,
2995 ) -> Result<crate::security_hardening::KeyRotationRecord> {
2996 use crate::security_hardening::{KeyRotationPhase, KmsDatabaseKeyEnvelope};
2997 loop {
2998 match record.phase {
2999 KeyRotationPhase::Pending => advance_key_rotation(journal, &mut record)?,
3000 KeyRotationPhase::WrappingNewKey => {
3001 if record.staged_wrapped_key.is_none() {
3002 let current = read_kms_key_envelope(durable_root)?;
3003 let wrapped =
3004 match provider.rewrap_key(¤t.wrapped_key, &record.to_key_id) {
3005 Ok(wrapped) => wrapped,
3006 Err(error) => {
3007 return fail_key_rotation(journal, &mut record, error);
3008 }
3009 };
3010 record.to_version = wrapped.key_version.clone();
3011 record.staged_wrapped_key = Some(wrapped);
3012 persist_key_rotation(journal, &record)?;
3013 }
3014 advance_key_rotation(journal, &mut record)?;
3015 }
3016 KeyRotationPhase::DualRead => {
3017 let old_envelope = read_kms_key_envelope(durable_root)?;
3018 let new_envelope = KmsDatabaseKeyEnvelope {
3019 provider_id: provider.provider_id().to_owned(),
3020 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
3021 MongrelError::Encryption(
3022 "KMS rotation has no staged wrapped key".into(),
3023 )
3024 })?,
3025 };
3026 let old_raw = match unwrap_kms_database_key(provider, &old_envelope) {
3027 Ok(key) => key,
3028 Err(error) => {
3029 return fail_key_rotation(journal, &mut record, error);
3030 }
3031 };
3032 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
3033 Ok(key) => key,
3034 Err(error) => {
3035 return fail_key_rotation(journal, &mut record, error);
3036 }
3037 };
3038 if !bool::from(old_raw.as_slice().ct_eq(new_raw.as_slice())) {
3039 return fail_key_rotation(
3040 journal,
3041 &mut record,
3042 "KMS rewrap changed the database root key",
3043 );
3044 }
3045 advance_key_rotation(journal, &mut record)?;
3046 }
3047 KeyRotationPhase::Reencrypting => {
3048 advance_key_rotation(journal, &mut record)?;
3051 }
3052 KeyRotationPhase::Validating => {
3053 let new_envelope = KmsDatabaseKeyEnvelope {
3054 provider_id: provider.provider_id().to_owned(),
3055 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
3056 MongrelError::Encryption(
3057 "KMS rotation has no staged wrapped key".into(),
3058 )
3059 })?,
3060 };
3061 let new_raw = match unwrap_kms_database_key(provider, &new_envelope) {
3062 Ok(key) => key,
3063 Err(error) => {
3064 return fail_key_rotation(journal, &mut record, error);
3065 }
3066 };
3067 let salt = read_encryption_salt(durable_root)?;
3068 let candidate = crate::encryption::Kek::from_raw_key(new_raw.as_ref(), &salt)?;
3069 let candidate_meta = candidate.derive_meta_key();
3070 let Some(active_meta) = self.core.meta_dek.as_ref() else {
3071 return fail_key_rotation(
3072 journal,
3073 &mut record,
3074 "KMS metadata exists on a plaintext database",
3075 );
3076 };
3077 if !bool::from(candidate_meta.as_ref().ct_eq(active_meta.as_slice())) {
3078 return fail_key_rotation(
3079 journal,
3080 &mut record,
3081 "KMS rewrap validation failed",
3082 );
3083 }
3084 advance_key_rotation(journal, &mut record)?;
3085 }
3086 KeyRotationPhase::Published => {
3087 let envelope = KmsDatabaseKeyEnvelope {
3088 provider_id: provider.provider_id().to_owned(),
3089 wrapped_key: record.staged_wrapped_key.clone().ok_or_else(|| {
3090 MongrelError::Encryption(
3091 "KMS rotation has no staged wrapped key".into(),
3092 )
3093 })?,
3094 };
3095 write_kms_key_envelope(root, &envelope)?;
3096 advance_key_rotation(journal, &mut record)?;
3097 }
3098 KeyRotationPhase::RetiringOldKey => {
3099 advance_key_rotation(journal, &mut record)?;
3100 }
3101 KeyRotationPhase::Succeeded => return Ok(record),
3102 KeyRotationPhase::Failed => {
3103 return Err(MongrelError::Encryption(
3104 "failed KMS rotation must be explicitly retried".into(),
3105 ));
3106 }
3107 }
3108 }
3109 }
3110
3111 pub fn open_with_credentials(
3123 root: impl AsRef<Path>,
3124 username: &str,
3125 password: &str,
3126 ) -> Result<Self> {
3127 Self::open_inner_with_credentials(root, None, username, password)
3128 }
3129
3130 pub fn open_with_credentials_and_options(
3134 root: impl AsRef<Path>,
3135 username: &str,
3136 password: &str,
3137 options: OpenOptions,
3138 ) -> Result<Self> {
3139 let gate = if options.offline_validation {
3140 OpenModeGate::OfflineValidation
3141 } else {
3142 OpenModeGate::Normal
3143 };
3144 Self::open_inner_with_credentials_and_lock_timeout(
3145 root,
3146 None,
3147 username,
3148 password,
3149 options.lock_timeout_ms,
3150 CoreResourceConfig::from_options(&options)?,
3151 gate,
3152 )
3153 }
3154
3155 pub fn open_encrypted_with_credentials(
3158 root: impl AsRef<Path>,
3159 passphrase: &str,
3160 username: &str,
3161 password: &str,
3162 ) -> Result<Self> {
3163 let (root, lock) = Self::begin_open(root, 0)?;
3164 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3165 MongrelError::Other("database root descriptor was not pinned".into())
3166 })?)?;
3167 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3168 Self::open_inner_with_credentials_locked(
3169 root,
3170 Some(kek),
3171 username,
3172 password,
3173 lock,
3174 CoreResourceConfig::default(),
3175 OpenModeGate::Normal,
3176 )
3177 }
3178
3179 pub fn open_encrypted_with_credentials_and_options(
3183 root: impl AsRef<Path>,
3184 passphrase: &str,
3185 username: &str,
3186 password: &str,
3187 options: OpenOptions,
3188 ) -> Result<Self> {
3189 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
3190 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
3191 MongrelError::Other("database root descriptor was not pinned".into())
3192 })?)?;
3193 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3194 let gate = if options.offline_validation {
3195 OpenModeGate::OfflineValidation
3196 } else {
3197 OpenModeGate::Normal
3198 };
3199 Self::open_inner_with_credentials_locked(
3200 root,
3201 Some(kek),
3202 username,
3203 password,
3204 lock,
3205 CoreResourceConfig::from_options(&options)?,
3206 gate,
3207 )
3208 }
3209
3210 pub fn open_with_kms_and_credentials(
3212 root: impl AsRef<Path>,
3213 provider: &dyn crate::security_hardening::KeyManagementProvider,
3214 username: &str,
3215 password: &str,
3216 ) -> Result<Self> {
3217 let (root, lock) = Self::begin_open(root, 0)?;
3218 let kek = open_kms_kek(
3219 lock.durable_root.as_deref().ok_or_else(|| {
3220 MongrelError::Other("database root descriptor was not pinned".into())
3221 })?,
3222 provider,
3223 )?;
3224 Self::open_inner_with_credentials_locked(
3225 root,
3226 Some(kek),
3227 username,
3228 password,
3229 lock,
3230 CoreResourceConfig::default(),
3231 OpenModeGate::Normal,
3232 )
3233 }
3234
3235 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
3242 let gate = if options.offline_validation {
3245 OpenModeGate::OfflineValidation
3246 } else {
3247 OpenModeGate::Normal
3248 };
3249 Self::open_inner_with_lock_timeout(
3250 root,
3251 None,
3252 None,
3253 options.lock_timeout_ms,
3254 CoreResourceConfig::from_options(&options)?,
3255 gate,
3256 )
3257 }
3258
3259 fn open_inner_with_lock_timeout(
3260 root: impl AsRef<Path>,
3261 kek: Option<Arc<crate::encryption::Kek>>,
3262 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3263 lock_timeout_ms: u32,
3264 resources: CoreResourceConfig,
3265 mode_gate: OpenModeGate,
3266 ) -> Result<Self> {
3267 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3268 Self::open_inner_locked(root, kek, lock, resources, mode_gate)
3269 }
3270
3271 fn open_inner_locked(
3272 root: PathBuf,
3273 kek: Option<Arc<crate::encryption::Kek>>,
3274 lock: ExclusiveDatabaseLease,
3275 resources: CoreResourceConfig,
3276 mode_gate: OpenModeGate,
3277 ) -> Result<Self> {
3278 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3279 let mut cat = catalog::read_durable(
3280 lock.durable_root.as_deref().ok_or_else(|| {
3281 MongrelError::Other("database root descriptor was not pinned".into())
3282 })?,
3283 meta_dek.as_ref(),
3284 )?
3285 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3286 let recovery_checkpoint = cat.clone();
3287
3288 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3291 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3292 lock.durable_root.as_deref().ok_or_else(|| {
3293 MongrelError::Other("database root descriptor was not pinned".into())
3294 })?,
3295 wal_dek.as_ref(),
3296 )?;
3297 recover_ddl_from_records(
3298 &root,
3299 Some(lock.durable_root.as_deref().ok_or_else(|| {
3300 MongrelError::Other("database root descriptor was not pinned".into())
3301 })?),
3302 &mut cat,
3303 meta_dek.as_ref(),
3304 false,
3305 None,
3306 &recovery_records,
3307 )?;
3308 Self::finish_open(
3309 root,
3310 cat,
3311 kek,
3312 meta_dek,
3313 true,
3314 Some(recovery_checkpoint),
3315 Some(recovery_records),
3316 None,
3317 lock,
3318 resources,
3319 mode_gate,
3320 )
3321 }
3322
3323 fn open_inner_with_credentials(
3330 root: impl AsRef<Path>,
3331 kek: Option<Arc<crate::encryption::Kek>>,
3332 username: &str,
3333 password: &str,
3334 ) -> Result<Self> {
3335 Self::open_inner_with_credentials_and_lock_timeout(
3336 root,
3337 kek,
3338 username,
3339 password,
3340 0,
3341 CoreResourceConfig::default(),
3342 OpenModeGate::Normal,
3343 )
3344 }
3345
3346 #[allow(clippy::too_many_arguments)]
3350 fn open_inner_with_credentials_and_lock_timeout(
3351 root: impl AsRef<Path>,
3352 kek: Option<Arc<crate::encryption::Kek>>,
3353 username: &str,
3354 password: &str,
3355 lock_timeout_ms: u32,
3356 resources: CoreResourceConfig,
3357 mode_gate: OpenModeGate,
3358 ) -> Result<Self> {
3359 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
3360 Self::open_inner_with_credentials_locked(
3361 root, kek, username, password, lock, resources, mode_gate,
3362 )
3363 }
3364
3365 #[allow(clippy::too_many_arguments)]
3366 fn open_inner_with_credentials_locked(
3367 root: PathBuf,
3368 kek: Option<Arc<crate::encryption::Kek>>,
3369 username: &str,
3370 password: &str,
3371 lock: ExclusiveDatabaseLease,
3372 resources: CoreResourceConfig,
3373 mode_gate: OpenModeGate,
3374 ) -> Result<Self> {
3375 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3376 let mut cat = catalog::read_durable(
3377 lock.durable_root.as_deref().ok_or_else(|| {
3378 MongrelError::Other("database root descriptor was not pinned".into())
3379 })?,
3380 meta_dek.as_ref(),
3381 )?
3382 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3383 let recovery_checkpoint = cat.clone();
3384
3385 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3388 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3389 lock.durable_root.as_deref().ok_or_else(|| {
3390 MongrelError::Other("database root descriptor was not pinned".into())
3391 })?,
3392 wal_dek.as_ref(),
3393 )?;
3394 recover_ddl_from_records(
3395 &root,
3396 Some(lock.durable_root.as_deref().ok_or_else(|| {
3397 MongrelError::Other("database root descriptor was not pinned".into())
3398 })?),
3399 &mut cat,
3400 meta_dek.as_ref(),
3401 false,
3402 None,
3403 &recovery_records,
3404 )?;
3405
3406 if !cat.require_auth {
3409 return Err(MongrelError::AuthNotRequired);
3410 }
3411
3412 let user = cat
3417 .users
3418 .iter()
3419 .find(|u| u.username == username)
3420 .filter(|u| !u.password_hash.is_empty())
3421 .ok_or_else(|| MongrelError::InvalidCredentials {
3422 username: username.to_string(),
3423 })?;
3424 let password_ok = crate::auth::verify_password(password, &user.password_hash)
3425 .map_err(MongrelError::Other)?;
3426 if !password_ok {
3427 return Err(MongrelError::InvalidCredentials {
3428 username: username.to_string(),
3429 });
3430 }
3431
3432 let principal =
3434 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
3435 MongrelError::InvalidCredentials {
3436 username: username.to_string(),
3437 }
3438 })?;
3439
3440 Self::finish_open(
3441 root,
3442 cat,
3443 kek,
3444 meta_dek,
3445 true,
3446 Some(recovery_checkpoint),
3447 Some(recovery_records),
3448 Some(principal),
3449 lock,
3450 resources,
3451 mode_gate,
3452 )
3453 }
3454
3455 pub fn create_with_credentials(
3465 root: impl AsRef<Path>,
3466 admin_username: &str,
3467 admin_password: &str,
3468 ) -> Result<Self> {
3469 let (root, lock) = Self::begin_create(root)?;
3470 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
3471 }
3472
3473 pub fn create_encrypted_with_credentials(
3477 root: impl AsRef<Path>,
3478 passphrase: &str,
3479 admin_username: &str,
3480 admin_password: &str,
3481 ) -> Result<Self> {
3482 let (root, lock) = Self::begin_create(root)?;
3483 let salt = crate::encryption::random_salt()?;
3484 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
3485 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3486 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3487 }
3488
3489 pub fn create_with_kms_and_credentials(
3491 root: impl AsRef<Path>,
3492 provider: &dyn crate::security_hardening::KeyManagementProvider,
3493 kms_key_id: &str,
3494 admin_username: &str,
3495 admin_password: &str,
3496 ) -> Result<Self> {
3497 let (root, lock) = Self::begin_create(root)?;
3498 let kek = create_kms_kek(&root, provider, kms_key_id)?;
3499 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
3500 }
3501
3502 fn create_inner_with_credentials(
3503 root: PathBuf,
3504 kek: Option<Arc<crate::encryption::Kek>>,
3505 admin_username: &str,
3506 admin_password: &str,
3507 lock: ExclusiveDatabaseLease,
3508 ) -> Result<Self> {
3509 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
3510 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3511
3512 let password_hash =
3514 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
3515 let scram_sha_256 =
3516 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
3517 let mysql_caching_sha2 =
3518 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
3519 let mut cat = Catalog::empty();
3520 cat.require_auth = true;
3521 cat.next_user_id = 2;
3522 cat.users.push(crate::auth::UserEntry {
3523 id: 1,
3524 username: admin_username.to_string(),
3525 password_hash,
3526 scram_sha_256: Some(scram_sha_256),
3527 mysql_caching_sha2: Some(mysql_caching_sha2),
3528 roles: Vec::new(),
3529 is_admin: true,
3530 created_epoch: 0,
3531 });
3532 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3533
3534 let admin_principal = crate::auth::Principal {
3537 user_id: 1,
3538 created_epoch: 0,
3539 username: admin_username.to_string(),
3540 is_admin: true,
3541 roles: Vec::new(),
3542 permissions: Vec::new(),
3543 };
3544 Self::finish_open(
3545 root,
3546 cat,
3547 kek,
3548 meta_dek,
3549 false,
3550 None,
3551 None,
3552 Some(admin_principal),
3553 lock,
3554 CoreResourceConfig::default(),
3555 OpenModeGate::Create(crate::storage_mode::StorageMode::Standalone),
3556 )
3557 }
3558
3559 fn reject_existing_database(root: &Path) -> Result<()> {
3560 if root.join(catalog::CATALOG_FILENAME).exists() {
3563 return Err(MongrelError::InvalidArgument(format!(
3564 "database already exists at {}; use Database::open() to open it, \
3565 or remove the directory first",
3566 root.display()
3567 )));
3568 }
3569 Ok(())
3570 }
3571
3572 fn open_inner(
3573 root: impl AsRef<Path>,
3574 kek: Option<Arc<crate::encryption::Kek>>,
3575 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
3576 ) -> Result<Self> {
3577 Self::open_inner_with_lock_timeout(
3578 root,
3579 kek,
3580 None,
3581 0,
3582 CoreResourceConfig::default(),
3583 OpenModeGate::Normal,
3584 )
3585 }
3586
3587 pub(crate) fn open_offline_validation(root: impl AsRef<Path>) -> Result<Self> {
3590 Self::open_inner_with_lock_timeout(
3591 root,
3592 None,
3593 None,
3594 0,
3595 CoreResourceConfig::default(),
3596 OpenModeGate::OfflineValidation,
3597 )
3598 }
3599
3600 pub(crate) fn open_replica_recovery_durable(
3604 root: &crate::durable_file::DurableRoot,
3605 ) -> Result<Self> {
3606 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3607 Self::open_replica_recovery_inner(root, None, lock)
3608 }
3609
3610 pub(crate) fn open_encrypted_replica_recovery_durable(
3611 root: &crate::durable_file::DurableRoot,
3612 passphrase: &str,
3613 ) -> Result<Self> {
3614 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
3615 let salt = read_encryption_salt(root)?;
3616 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
3617 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
3618 }
3619
3620 fn open_replica_recovery_inner(
3621 root: PathBuf,
3622 kek: Option<Arc<crate::encryption::Kek>>,
3623 lock: ExclusiveDatabaseLease,
3624 ) -> Result<Self> {
3625 if !root.join(META_DIR).join("replica").is_file() {
3626 return Err(MongrelError::InvalidArgument(
3627 "recovery auth bypass requires a marked replica staging directory".into(),
3628 ));
3629 }
3630 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
3631 let mut cat = catalog::read_durable(
3632 lock.durable_root.as_deref().ok_or_else(|| {
3633 MongrelError::Other("database root descriptor was not pinned".into())
3634 })?,
3635 meta_dek.as_ref(),
3636 )?
3637 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
3638 let recovery_checkpoint = cat.clone();
3639 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3640 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
3641 lock.durable_root.as_deref().ok_or_else(|| {
3642 MongrelError::Other("database root descriptor was not pinned".into())
3643 })?,
3644 wal_dek.as_ref(),
3645 )?;
3646 recover_ddl_from_records(
3647 &root,
3648 Some(lock.durable_root.as_deref().ok_or_else(|| {
3649 MongrelError::Other("database root descriptor was not pinned".into())
3650 })?),
3651 &mut cat,
3652 meta_dek.as_ref(),
3653 false,
3654 None,
3655 &recovery_records,
3656 )?;
3657 let principal = if cat.require_auth {
3658 cat.users
3659 .iter()
3660 .find(|user| user.is_admin)
3661 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
3662 .ok_or_else(|| {
3663 MongrelError::Schema(
3664 "authenticated replica catalog has no recoverable admin".into(),
3665 )
3666 })?
3667 .into()
3668 } else {
3669 None
3670 };
3671 Self::finish_open(
3672 root,
3673 cat,
3674 kek,
3675 meta_dek,
3676 true,
3677 Some(recovery_checkpoint),
3678 Some(recovery_records),
3679 principal,
3680 lock,
3681 CoreResourceConfig::default(),
3682 OpenModeGate::Normal,
3683 )
3684 }
3685
3686 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
3698 use fs2::FileExt;
3699 if timeout_ms == 0 {
3700 return f.try_lock_exclusive();
3701 }
3702 let deadline =
3704 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
3705 let mut next_sleep = std::time::Duration::from_millis(1);
3706 let mut recorded_wait = false;
3707 loop {
3708 match f.try_lock_exclusive() {
3709 Ok(()) => return Ok(()),
3710 Err(e) if Self::is_file_lock_contended(&e) => {
3711 if !recorded_wait {
3712 DATABASE_OPEN_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
3713 recorded_wait = true;
3714 }
3715 let now = std::time::Instant::now();
3716 if now >= deadline {
3717 return Err(std::io::Error::new(
3718 std::io::ErrorKind::WouldBlock,
3719 format!("could not acquire database lock within {timeout_ms}ms"),
3720 ));
3721 }
3722 let remaining = deadline - now;
3723 let sleep = next_sleep.min(remaining);
3724 std::thread::sleep(sleep);
3725 next_sleep = next_sleep
3728 .saturating_mul(10)
3729 .min(std::time::Duration::from_millis(50));
3730 }
3731 Err(e) => return Err(e),
3732 }
3733 }
3734 }
3735
3736 fn is_file_lock_contended(error: &std::io::Error) -> bool {
3737 if error.kind() == std::io::ErrorKind::WouldBlock {
3738 return true;
3739 }
3740 #[cfg(windows)]
3741 {
3742 return error.raw_os_error() == Some(33);
3745 }
3746 #[cfg(not(windows))]
3747 false
3748 }
3749
3750 #[allow(clippy::too_many_arguments)]
3751 fn finish_open(
3752 root: PathBuf,
3753 cat: Catalog,
3754 kek: Option<Arc<crate::encryption::Kek>>,
3755 meta_dek: Option<[u8; META_DEK_LEN]>,
3756 existing: bool,
3757 recovery_checkpoint: Option<Catalog>,
3758 recovery_records: Option<Vec<crate::wal::Record>>,
3759 principal: Option<crate::auth::Principal>,
3760 lock: ExclusiveDatabaseLease,
3761 resources: CoreResourceConfig,
3762 mode_gate: OpenModeGate,
3763 ) -> Result<Self> {
3764 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
3765 MongrelError::Other("database root descriptor was not pinned".into())
3766 })?);
3767 let storage_mode = if existing {
3772 crate::storage_mode::read(&durable_root)?
3773 } else {
3774 None
3775 };
3776 match (&mode_gate, &storage_mode) {
3777 (OpenModeGate::Create(_), _) => {}
3778 (OpenModeGate::Normal | OpenModeGate::SharedCore, mode) => {
3779 crate::storage_mode::check_open(mode.as_ref(), false)?
3780 }
3781 (OpenModeGate::OfflineValidation, mode) => {
3782 crate::storage_mode::check_open(mode.as_ref(), true)?
3783 }
3784 (
3785 OpenModeGate::ClusterRuntime {
3786 cluster_id,
3787 node_id,
3788 database_id,
3789 },
3790 Some(actual),
3791 ) => {
3792 let expected = crate::storage_mode::StorageMode::ClusterReplica {
3793 cluster_id: *cluster_id,
3794 node_id: *node_id,
3795 database_id: *database_id,
3796 };
3797 if *actual != expected {
3798 return Err(
3799 crate::storage_mode::StorageModeError::IdentityMismatch(format!(
3800 "expected {expected:?}, marker holds {actual:?}"
3801 ))
3802 .into(),
3803 );
3804 }
3805 }
3806 (OpenModeGate::ClusterRuntime { .. }, None) => {
3807 return Err(crate::storage_mode::StorageModeError::IdentityMismatch(
3808 "expected a ClusterReplica marker; directory has none".into(),
3809 )
3810 .into());
3811 }
3812 }
3813 let read_only = match &mode_gate {
3814 OpenModeGate::OfflineValidation | OpenModeGate::ClusterRuntime { .. } => true,
3815 OpenModeGate::Create(mode) => mode.cluster_identity().is_some(),
3818 OpenModeGate::Normal | OpenModeGate::SharedCore => false,
3819 } || if existing {
3820 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
3821 Ok(_) => true,
3822 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
3823 Err(error) => return Err(error.into()),
3824 }
3825 } else {
3826 false
3827 };
3828 let recovered_catalog = cat;
3829 let mut cat = recovered_catalog.clone();
3830 let abandoned = if existing && !read_only {
3831 let abandoned = cat
3832 .tables
3833 .iter()
3834 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
3835 .map(|entry| entry.table_id)
3836 .collect::<Vec<_>>();
3837 for entry in &mut cat.tables {
3838 if abandoned.contains(&entry.table_id) {
3839 entry.state = TableState::Dropped {
3840 at_epoch: cat.db_epoch,
3841 };
3842 }
3843 }
3844 abandoned
3845 } else {
3846 Vec::new()
3847 };
3848 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
3849 let recovery_records = match (existing, recovery_records) {
3850 (true, Some(records)) => records,
3851 (true, None) => {
3852 return Err(MongrelError::Other(
3853 "existing open has no validated WAL recovery plan".into(),
3854 ))
3855 }
3856 (false, _) => Vec::new(),
3857 };
3858 let (history_epochs, history_start) =
3859 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
3860 let open_generation = if existing {
3861 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
3862 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3863 })?;
3864 let recovered_table_ids = cat
3865 .tables
3866 .iter()
3867 .filter(|entry| {
3868 checkpoint
3869 .tables
3870 .iter()
3871 .all(|checkpoint| checkpoint.table_id != entry.table_id)
3872 })
3873 .map(|entry| entry.table_id)
3874 .collect::<HashSet<_>>();
3875 let reconciled_table_ids = cat
3876 .tables
3877 .iter()
3878 .filter(|entry| {
3879 checkpoint
3880 .tables
3881 .iter()
3882 .find(|checkpoint| checkpoint.table_id == entry.table_id)
3883 .is_some_and(|checkpoint| {
3884 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
3885 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
3886 })
3887 })
3888 .map(|entry| entry.table_id)
3889 .collect::<HashSet<_>>();
3890 validate_shared_wal_recovery_plan(
3891 &durable_root,
3892 &cat,
3893 &recovered_table_ids,
3894 &reconciled_table_ids,
3895 meta_dek.as_ref(),
3896 kek.clone(),
3897 &recovery_records,
3898 )?;
3899 let retained_generation = recovery_records
3900 .iter()
3901 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
3902 .map(|record| record.txn_id >> 32)
3903 .max()
3904 .unwrap_or(0);
3905 let head_generation =
3906 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
3907 let durable_floor = match head_generation {
3908 Some(head) if retained_generation > head => {
3909 return Err(MongrelError::CorruptWal {
3910 offset: retained_generation,
3911 reason: format!(
3912 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
3913 ),
3914 })
3915 }
3916 Some(head) => head,
3917 None => retained_generation,
3918 };
3919 let stored = catalog::read_generation(&durable_root)?;
3920 if stored.is_some_and(|generation| generation < durable_floor) {
3921 return Err(MongrelError::Other(format!(
3922 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
3923 )));
3924 }
3925 let bumped = stored
3926 .unwrap_or(durable_floor)
3927 .max(durable_floor)
3928 .checked_add(1)
3929 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
3930 if bumped > u32::MAX as u64 {
3931 return Err(MongrelError::Full(
3932 "open-generation namespace exhausted".into(),
3933 ));
3934 }
3935 bumped
3936 } else {
3937 0
3938 };
3939 let principal = if cat.require_auth && !matches!(&mode_gate, OpenModeGate::SharedCore) {
3940 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3941 Some(
3942 Self::resolve_bound_principal_from_catalog(&cat, supplied)
3943 .ok_or(MongrelError::AuthRequired)?,
3944 )
3945 } else {
3946 principal
3947 };
3948 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
3949 if existing {
3950 for entry in &cat.tables {
3951 if !matches!(entry.state, TableState::Live) {
3952 continue;
3953 }
3954 match durable_root
3955 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
3956 {
3957 Ok(root) => {
3958 table_roots.insert(entry.table_id, Arc::new(root));
3959 }
3960 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
3961 Err(error) => return Err(error.into()),
3962 }
3963 }
3964 }
3965
3966 if existing {
3970 let mut applied = recovery_checkpoint.ok_or_else(|| {
3971 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
3972 })?;
3973 recover_ddl_from_records(
3974 &root,
3975 Some(&durable_root),
3976 &mut applied,
3977 meta_dek.as_ref(),
3978 true,
3979 Some(&table_roots),
3980 &recovery_records,
3981 )?;
3982 let catalog_value = |catalog: &Catalog| {
3983 serde_json::to_value(catalog)
3984 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
3985 };
3986 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
3987 return Err(MongrelError::CorruptWal {
3988 offset: 0,
3989 reason: "validated and applied DDL recovery plans differ".into(),
3990 });
3991 }
3992 if catalog_value(&cat)? != catalog_value(&applied)? {
3993 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
3994 }
3995 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
3996 if !read_only {
3997 sweep_unreferenced_table_dirs(&root, &cat)?;
3998 }
3999 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
4000 Ok(()) => {}
4001 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
4002 Err(error) => return Err(error.into()),
4003 }
4004 }
4005
4006 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
4007 let snapshots = Arc::new(SnapshotRegistry::new());
4008 snapshots.configure_history(history_epochs, history_start);
4009 let memory_governor = crate::memory::MemoryGovernor::new(
4014 crate::memory::GovernorConfig::new(resources.memory_budget_bytes),
4015 )
4016 .map_err(|error| MongrelError::InvalidArgument(format!("memory governor: {error}")))?;
4017 let spill_manager = crate::spill::SpillManager::open(
4021 &durable_root,
4022 crate::spill::SpillConfig::new(resources.temp_disk_budget_bytes),
4023 meta_dek,
4024 )?;
4025 let job_registry = Arc::new(crate::jobs::JobRegistry::open(&root, meta_dek.as_ref())?);
4029 let page_cache = Arc::new(crate::cache::Sharded::new(
4030 crate::cache::CACHE_SHARDS,
4031 || {
4032 crate::cache::PageCache::new(
4033 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
4034 )
4035 .with_governor(
4036 memory_governor.clone(),
4037 crate::memory::MemoryClass::PageCache,
4038 )
4039 },
4040 ));
4041 memory_governor.register_reclaimable(&page_cache);
4042 let decoded_cache = Arc::new(crate::cache::Sharded::new(
4043 crate::cache::CACHE_SHARDS,
4044 || {
4045 crate::cache::DecodedPageCache::new(
4046 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
4047 )
4048 .with_governor(
4049 memory_governor.clone(),
4050 crate::memory::MemoryClass::DecodedCache,
4051 )
4052 },
4053 ));
4054 memory_governor.register_reclaimable(&decoded_cache);
4055 let commit_lock = Arc::new(Mutex::new(()));
4056 let shared_wal = Arc::new(Mutex::new(if existing {
4057 crate::wal::SharedWal::open_durable_root_validated(
4058 Arc::clone(&durable_root),
4059 Epoch(cat.db_epoch),
4060 wal_dek.clone(),
4061 Some(&recovery_records),
4062 )?
4063 } else {
4064 crate::wal::SharedWal::create_with_durable_root(
4065 Arc::clone(&durable_root),
4066 Epoch(cat.db_epoch),
4067 wal_dek.clone(),
4068 )?
4069 }));
4070 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
4074 let lifecycle = Arc::new(crate::core::LifecycleController::new());
4079 let group = Arc::new(
4080 crate::txn::GroupCommit::new(shared_wal.lock().durable_seq())
4081 .with_lifecycle(Arc::clone(&lifecycle)),
4082 );
4083 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
4084 let txn_ids = Arc::new(Mutex::new(1u64));
4088 let _ = abandoned;
4089 let hlc = Arc::new(mongreldb_types::hlc::HlcClock::new(
4096 0,
4097 std::time::Duration::from_millis(500),
4098 ));
4099 let idempotency = crate::txn::IdempotencyLedger::open(Arc::clone(&durable_root), meta_dek)?;
4102 let standalone_commit_log = Arc::new(crate::commit_log::StandaloneCommitLog::new(
4103 Arc::clone(&shared_wal),
4104 Arc::clone(&group),
4105 Arc::clone(&epoch),
4106 Arc::clone(&commit_lock),
4107 Arc::clone(&txn_ids),
4108 root.clone(),
4109 wal_dek.clone(),
4110 Arc::clone(&hlc),
4111 ));
4112 let commit_log: Arc<dyn mongreldb_log::CommitLog> = standalone_commit_log.clone();
4113
4114 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
4119 let security_coordinator = security_coordinator(&root, cat.security_version);
4120 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> =
4121 if matches!(&mode_gate, OpenModeGate::SharedCore) {
4122 None
4126 } else {
4127 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
4128 auth_state.clone(),
4129 )))
4130 };
4131
4132 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
4138 for entry in &cat.tables {
4139 if !matches!(entry.state, TableState::Live) {
4140 continue;
4141 }
4142 let table_root = match table_roots.remove(&entry.table_id) {
4143 Some(root) => root,
4144 None => Arc::new(
4145 durable_root
4146 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
4147 ),
4148 };
4149 let tdir = table_root.io_path()?;
4150 let ctx = SharedCtx {
4151 root_guard: Some(table_root),
4152 epoch: Arc::clone(&epoch),
4153 page_cache: Arc::clone(&page_cache),
4154 decoded_cache: Arc::clone(&decoded_cache),
4155 snapshots: Arc::clone(&snapshots),
4156 kek: kek.clone(),
4157 commit_lock: Arc::clone(&commit_lock),
4158 shared: Some(crate::engine::SharedWalCtx {
4159 wal: Arc::clone(&shared_wal),
4160 group: Arc::clone(&group),
4161 poisoned: Arc::clone(&poisoned),
4162 txn_ids: Arc::clone(&txn_ids),
4163 change_wake: change_wake.clone(),
4164 lifecycle: Arc::clone(&lifecycle),
4165 hlc: Arc::clone(&hlc),
4166 }),
4167 table_name: Some(entry.name.clone()),
4168 auth: auth_checker.clone(),
4169 read_only,
4170 };
4171 let t = Table::open_in(&tdir, ctx)?;
4172 tables.insert(entry.table_id, TableHandle::new(t));
4173 }
4174
4175 if existing {
4181 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
4182 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
4183 if read_only {
4184 crate::replication::reconcile_replica_epoch_durable(
4185 &durable_root,
4186 epoch.visible().0,
4187 )?;
4188 }
4189 sweep_pending_txn_dirs(&root, &cat);
4192 }
4193
4194 catalog::write_generation(&durable_root, open_generation)?;
4196 match &mode_gate {
4201 OpenModeGate::Create(mode) => crate::storage_mode::write(&durable_root, mode)?,
4202 _ if existing && storage_mode.is_none() => {
4203 crate::storage_mode::write(
4204 &durable_root,
4205 &crate::storage_mode::StorageMode::Standalone,
4206 )?;
4207 }
4208 _ => {}
4209 }
4210 shared_wal.lock().seal_open_generation(open_generation)?;
4211 crate::replication::replication_identity_durable(&durable_root)?;
4212 let next_txn_id = (open_generation << 32) | 1;
4213 *txn_ids.lock() = next_txn_id;
4215 let mut lock = lock;
4216 lock.mark_open()?;
4217
4218 lifecycle.mark_open();
4222 let core = DatabaseCore {
4223 root,
4224 durable_root,
4225 lifecycle,
4226 registry: std::sync::OnceLock::new(),
4227 read_only,
4228 catalog: RwLock::new(cat),
4229 security_coordinator,
4230 security_catalog_disk_reads: AtomicU64::new(0),
4231 rls_cache: Mutex::new(RlsCache::default()),
4232 epoch,
4233 snapshots,
4234 memory_governor,
4235 page_cache,
4236 decoded_cache,
4237 spill_manager,
4238 resource_groups: crate::resource::ResourceGroupRegistry::with_defaults(),
4239 embedding_providers: crate::embedding::EmbeddingProviderRegistry::new(),
4240 service_principals: crate::service_principal::ServicePrincipalStore::new(),
4241 job_registry,
4242 commit_lock,
4243 kms_rotation_lock: Mutex::new(()),
4244 shared_wal,
4245 next_txn_id: txn_ids,
4246 tables: RwLock::new(tables),
4247 kek,
4248 ddl_lock: Mutex::new(()),
4249 meta_dek,
4250 conflicts: crate::txn::ConflictIndex::new(),
4251 active_txns: crate::txn::ActiveTxns::new(),
4252 lock_manager: Arc::new(crate::locks::LockManager::new()),
4253 poisoned,
4254 group,
4255 commit_log: RwLock::new(commit_log),
4256 standalone_commit_log,
4257 hlc,
4258 idempotency,
4259 commit_ts_ledger: Mutex::new(commit_ts_ledger_from_recovery(&recovery_records)),
4260 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
4261 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
4262 replication_barrier: parking_lot::RwLock::new(()),
4263 replication_wal_retention_segments: AtomicUsize::new(0),
4264 backup_pins: Arc::new(Mutex::new(HashMap::new())),
4265 spill_hook: Mutex::new(None),
4266 security_commit_hook: Mutex::new(None),
4267 catalog_commit_hook: Mutex::new(None),
4268 backup_hook: Mutex::new(None),
4269 fk_lock_hook: Mutex::new(None),
4270 replication_hook: Mutex::new(None),
4271 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
4272 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
4273 trigger_max_loop_iterations: AtomicU32::new(
4274 TriggerConfig::default().max_loop_iterations,
4275 ),
4276 notify: {
4277 let (tx, _rx) = tokio::sync::broadcast::channel(256);
4278 tx
4279 },
4280 change_wake,
4281 _lock: Mutex::new(Some(lock)),
4282 };
4283 Ok(Self {
4284 core: Arc::new(core),
4285 principal: RwLock::new(principal),
4286 auth_state,
4287 shared: false,
4288 })
4289 }
4290
4291 pub fn visible_epoch(&self) -> Epoch {
4293 self.epoch.visible()
4294 }
4295
4296 pub fn catalog_version(&self) -> u64 {
4298 self.catalog.read().catalog_version
4299 }
4300
4301 pub fn storage_mode(&self) -> Result<Option<crate::storage_mode::StorageMode>> {
4305 Ok(crate::storage_mode::read(&self.durable_root)?)
4306 }
4307
4308 pub fn memory_governor(&self) -> &crate::memory::MemoryGovernor {
4311 &self.memory_governor
4312 }
4313
4314 pub fn resource_groups(&self) -> &crate::resource::ResourceGroupRegistry {
4317 &self.resource_groups
4318 }
4319
4320 pub fn embedding_providers(&self) -> &crate::embedding::EmbeddingProviderRegistry {
4324 &self.embedding_providers
4325 }
4326
4327 pub fn unregister_embedding_provider(&self, provider_id: &str) -> Result<bool> {
4329 let references = self
4330 .catalog
4331 .read()
4332 .tables
4333 .iter()
4334 .flat_map(|table| &table.schema.columns)
4335 .filter(|column| {
4336 column
4337 .embedding_source
4338 .as_ref()
4339 .and_then(crate::embedding::EmbeddingSource::provider_id)
4340 == Some(provider_id)
4341 })
4342 .count();
4343 self.embedding_providers
4344 .unregister_unreferenced(provider_id, references)
4345 .map_err(embedding_error)
4346 }
4347
4348 fn materialize_generated_embeddings(
4349 &self,
4350 staging: &mut [(u64, crate::txn::Staged)],
4351 control: Option<&crate::ExecutionControl>,
4352 ) -> Result<()> {
4353 let schemas = {
4354 let catalog = self.catalog.read();
4355 catalog
4356 .tables
4357 .iter()
4358 .map(|table| (table.table_id, table.schema.clone()))
4359 .collect::<HashMap<_, _>>()
4360 };
4361 let fallback_control = crate::ExecutionControl::new(None);
4362 let control = control.unwrap_or(&fallback_control);
4363 for (table_id, staged) in &mut *staging {
4364 let (cells, mut update_changed_columns) = match staged {
4365 crate::txn::Staged::Put(cells) => (cells, None),
4366 crate::txn::Staged::Update {
4367 new_row,
4368 changed_columns,
4369 ..
4370 } => (new_row, Some(changed_columns)),
4371 crate::txn::Staged::Delete(_) | crate::txn::Staged::Truncate => continue,
4372 };
4373 let schema = schemas.get(table_id).ok_or_else(|| {
4374 MongrelError::Schema(format!("table id {table_id} disappeared during embedding"))
4375 })?;
4376 for target in &schema.columns {
4377 let Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) =
4378 target.embedding_source.as_ref()
4379 else {
4380 continue;
4381 };
4382 let text = render_embedding_input(schema, spec, cells)?;
4383 let reservation_bytes = text
4384 .len()
4385 .saturating_add((spec.dimension as usize).saturating_mul(4));
4386 let _reservation = self
4387 .memory_governor
4388 .try_reserve(
4389 reservation_bytes as u64,
4390 crate::memory::MemoryClass::AiCandidates,
4391 )
4392 .map_err(|error| MongrelError::ResourceLimitExceeded {
4393 resource: "embedding memory",
4394 requested: reservation_bytes,
4395 limit: match error {
4396 crate::memory::MemoryError::Exhausted { available, .. } => {
4397 available as usize
4398 }
4399 _ => 0,
4400 },
4401 })?;
4402 let source =
4403 crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec: spec.clone() };
4404 let (registry_generation, provider) = self
4405 .embedding_providers
4406 .resolve_with_generation(&source)
4407 .map_err(embedding_error)?;
4408 let semantic_identity = provider.semantic_identity();
4409 self.ensure_ann_semantic_identity_compatible(
4410 *table_id,
4411 target.id,
4412 &semantic_identity,
4413 )?;
4414 let vectors = self
4415 .embedding_providers
4416 .embed_controlled(
4417 &source,
4418 &[text.as_str()],
4419 spec.dimension,
4420 control,
4421 "generated-column-write",
4422 crate::embedding::EmbeddingLimits::default(),
4423 )
4424 .map_err(embedding_error)?;
4425 cells.retain(|(column, _)| *column != target.id);
4426 cells.push((
4427 target.id,
4428 crate::memtable::Value::GeneratedEmbedding(Box::new(
4429 crate::embedding::GeneratedEmbeddingValue {
4430 vector: vectors.into_iter().next().ok_or_else(|| {
4431 MongrelError::Other("embedding provider returned no vector".into())
4432 })?,
4433 metadata: crate::embedding::GeneratedEmbeddingMetadata {
4434 provider_id: provider.provider_id().to_owned(),
4435 model_id: provider.model_id().to_owned(),
4436 model_version: provider.model_version().to_owned(),
4437 preprocessing_version: provider.preprocessing_version().to_owned(),
4438 source_fingerprint: Sha256::digest(text.as_bytes()).into(),
4439 status: crate::embedding::EmbeddingGenerationStatus::Ready,
4440 last_error_category: None,
4441 attempt_count: 1,
4442 semantic_identity,
4443 provider_registry_generation: registry_generation,
4444 },
4445 },
4446 )),
4447 ));
4448 if let Some(changed_columns) = update_changed_columns.as_deref_mut() {
4449 changed_columns.push(target.id);
4450 }
4451 }
4452 if let Some(changed_columns) = update_changed_columns {
4453 changed_columns.sort_unstable();
4454 changed_columns.dedup();
4455 }
4456 }
4457 self.validate_staged_generated_embedding_identities(staging)?;
4458 Ok(())
4459 }
4460
4461 fn ensure_ann_semantic_identity_compatible(
4462 &self,
4463 table_id: u64,
4464 column_id: u16,
4465 identity: &crate::embedding::EmbeddingProviderRef,
4466 ) -> Result<()> {
4467 let tables = self.tables.read();
4468 let Some(handle) = tables.get(&table_id) else {
4469 return Ok(());
4470 };
4471 let table = handle.lock();
4472 let Some(ann) = table.ann_index(column_id) else {
4473 return Ok(());
4474 };
4475 if let Some(existing) = ann.semantic_identity() {
4476 if existing != identity {
4477 return Err(embedding_error(
4478 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
4479 column_id,
4480 expected: existing.fingerprint_sha256(),
4481 got: identity.fingerprint_sha256(),
4482 },
4483 ));
4484 }
4485 }
4486 Ok(())
4487 }
4488
4489 fn validate_staged_generated_embedding_identities(
4490 &self,
4491 staging: &[(u64, crate::txn::Staged)],
4492 ) -> Result<()> {
4493 for (table_id, staged) in staging {
4494 let cells = match staged {
4495 crate::txn::Staged::Put(cells) => cells.as_slice(),
4496 crate::txn::Staged::Update { new_row, .. } => new_row.as_slice(),
4497 crate::txn::Staged::Delete(_) | crate::txn::Staged::Truncate => continue,
4498 };
4499 for (column_id, value) in cells {
4500 let Some(meta) = value.generated_embedding_metadata() else {
4501 continue;
4502 };
4503 self.ensure_ann_semantic_identity_compatible(
4504 *table_id,
4505 *column_id,
4506 &meta.semantic_identity,
4507 )?;
4508 }
4509 }
4510 Ok(())
4511 }
4512
4513 pub fn lock_rows_for_update(
4517 &self,
4518 txn_id: u64,
4519 table_id: u64,
4520 row_ids: &[crate::rowid::RowId],
4521 control: Option<&crate::ExecutionControl>,
4522 ) -> Result<()> {
4523 for &row_id in row_ids {
4524 self.acquire_txn_lock(
4525 txn_id,
4526 crate::locks::LockKey::row(table_id, row_id),
4527 crate::locks::LockMode::Exclusive,
4528 control,
4529 )?;
4530 }
4531 Ok(())
4532 }
4533
4534 pub fn spill_manager(&self) -> &crate::spill::SpillManager {
4538 &self.spill_manager
4539 }
4540
4541 pub fn job_registry(&self) -> &Arc<crate::jobs::JobRegistry> {
4543 &self.job_registry
4544 }
4545
4546 pub fn version_pins_report(&self) -> Vec<TablePinsReport> {
4551 let names: HashMap<u64, String> = self
4552 .catalog
4553 .read()
4554 .tables
4555 .iter()
4556 .map(|entry| (entry.table_id, entry.name.clone()))
4557 .collect();
4558 let handles: Vec<_> = self
4559 .tables
4560 .read()
4561 .iter()
4562 .map(|(table_id, handle)| (*table_id, handle.clone()))
4563 .collect();
4564 handles
4565 .into_iter()
4566 .map(|(table_id, handle)| {
4567 let pins = handle.lock().version_pins_report();
4568 TablePinsReport {
4569 table_id,
4570 table: names.get(&table_id).cloned().unwrap_or_default(),
4571 pins,
4572 }
4573 })
4574 .collect()
4575 }
4576
4577 pub fn hlc_gc_floor(&self) -> crate::epoch::GcFloor {
4585 let mut combined = crate::epoch::GcFloor::ZERO;
4586 if let Some(epoch) = self.snapshots.min_pinned() {
4588 if let Some(ts) = self.commit_ts_for_epoch(epoch) {
4589 combined.transaction_snapshot = ts;
4590 }
4591 }
4592 if let Some(epoch) = self.snapshots.history_floor(self.epoch.visible()) {
4593 if let Some(ts) = self.commit_ts_for_epoch(epoch) {
4594 combined.history_retention = ts;
4595 }
4596 }
4597 let handles: Vec<_> = self.tables.read().values().cloned().collect();
4599 for handle in handles {
4600 let table_floor = handle
4601 .lock()
4602 .hlc_gc_floor(|epoch| self.commit_ts_for_epoch(epoch));
4603 for (label, ts) in table_floor.sources() {
4604 if ts == mongreldb_types::hlc::HlcTimestamp::ZERO {
4605 continue;
4606 }
4607 let slot = match label {
4608 "transaction_snapshot" => &mut combined.transaction_snapshot,
4609 "history_retention" => &mut combined.history_retention,
4610 "backup_pitr" => &mut combined.backup_pitr,
4611 "replication" => &mut combined.replication,
4612 "read_generation" => &mut combined.read_generation,
4613 "online_index_build" => &mut combined.online_index_build,
4614 _ => continue,
4615 };
4616 if *slot == mongreldb_types::hlc::HlcTimestamp::ZERO || ts < *slot {
4617 *slot = ts;
4618 }
4619 }
4620 }
4621 combined
4622 }
4623
4624 pub fn lock_manager(&self) -> &Arc<crate::locks::LockManager> {
4626 &self.lock_manager
4627 }
4628
4629 pub fn release_txn_locks(&self, txn_id: u64) {
4634 self.lock_manager.release_all(txn_id);
4635 }
4636
4637 fn txn_lock_request(
4640 txn_id: u64,
4641 mode: crate::locks::LockMode,
4642 control: Option<&crate::ExecutionControl>,
4643 ) -> crate::locks::LockRequest {
4644 let control = control
4645 .cloned()
4646 .unwrap_or_else(|| crate::ExecutionControl::new(None));
4647 crate::locks::LockRequest::new(txn_id, mode, control)
4648 }
4649
4650 pub(crate) fn acquire_txn_lock(
4654 &self,
4655 txn_id: u64,
4656 key: crate::locks::LockKey,
4657 mode: crate::locks::LockMode,
4658 control: Option<&crate::ExecutionControl>,
4659 ) -> Result<()> {
4660 self.lock_manager
4661 .acquire(key, Self::txn_lock_request(txn_id, mode, control))
4662 .map_err(MongrelError::from)
4663 }
4664
4665 fn acquire_schema_barrier_exclusive(&self) -> Result<TxnLockGuard<'_>> {
4670 let txn_id = self.alloc_txn_id()?;
4671 self.acquire_txn_lock(
4672 txn_id,
4673 crate::locks::LockKey::schema_barrier(),
4674 crate::locks::LockMode::Exclusive,
4675 None,
4676 )?;
4677 Ok(TxnLockGuard {
4678 locks: &self.lock_manager,
4679 txn_id,
4680 })
4681 }
4682
4683 pub fn commit_log(&self) -> Arc<dyn mongreldb_log::CommitLog> {
4688 Arc::clone(&self.commit_log.read())
4689 }
4690
4691 pub fn bind_cluster_commit_log(
4698 &self,
4699 commit_log: Arc<dyn mongreldb_log::CommitLog>,
4700 ) -> Result<()> {
4701 match self.storage_mode()? {
4702 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4703 *self.commit_log.write() = commit_log;
4704 Ok(())
4705 }
4706 mode => Err(MongrelError::InvalidArgument(format!(
4707 "commit-log substitution requires ClusterReplica storage, got {mode:?}"
4708 ))),
4709 }
4710 }
4711
4712 pub fn detach_cluster_commit_log(&self) -> Result<()> {
4718 match self.storage_mode()? {
4719 Some(crate::storage_mode::StorageMode::ClusterReplica { .. }) => {
4720 let standalone: Arc<dyn mongreldb_log::CommitLog> =
4721 self.standalone_commit_log.clone();
4722 *self.commit_log.write() = standalone;
4723 Ok(())
4724 }
4725 mode => Err(MongrelError::InvalidArgument(format!(
4726 "commit-log detachment requires ClusterReplica storage, got {mode:?}"
4727 ))),
4728 }
4729 }
4730
4731 pub fn hlc(&self) -> &mongreldb_types::hlc::HlcClock {
4737 self.hlc_clock()
4738 }
4739
4740 pub(crate) fn hlc_clock(&self) -> &mongreldb_types::hlc::HlcClock {
4741 &self.hlc
4742 }
4743
4744 pub fn catalog_snapshot(&self) -> Catalog {
4746 self.catalog.read().clone()
4747 }
4748
4749 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
4751 let catalog = self.catalog.read();
4752 match key {
4753 "user_version" => Ok(catalog.user_version),
4754 "application_id" => Ok(catalog.application_id),
4755 _ => Err(MongrelError::InvalidArgument(format!(
4756 "unsupported persistent SQL pragma {key:?}"
4757 ))),
4758 }
4759 }
4760
4761 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
4764 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
4765 }
4766
4767 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
4768 &self,
4769 key: &str,
4770 value: i64,
4771 mut before_commit: F,
4772 ) -> Result<Option<Epoch>>
4773 where
4774 F: FnMut() -> Result<()>,
4775 {
4776 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
4777 }
4778
4779 fn set_sql_pragma_i64_with_epoch_inner(
4780 &self,
4781 key: &str,
4782 value: i64,
4783 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
4784 ) -> Result<Option<Epoch>> {
4785 use crate::wal::DdlOp;
4786
4787 self.require(&crate::auth::Permission::Ddl)?;
4788 if self.read_only {
4789 return Err(MongrelError::ReadOnlyReplica);
4790 }
4791 if self.poisoned.load(Ordering::Relaxed) {
4792 return Err(MongrelError::Other(
4793 "database poisoned by fsync error".into(),
4794 ));
4795 }
4796 let _ddl = self.ddl_lock.lock();
4797 let _security_write = self.security_write()?;
4798 self.require(&crate::auth::Permission::Ddl)?;
4799 let mut next_catalog = self.catalog.read().clone();
4800 let target = match key {
4801 "user_version" => &mut next_catalog.user_version,
4802 "application_id" => &mut next_catalog.application_id,
4803 _ => {
4804 return Err(MongrelError::InvalidArgument(format!(
4805 "unsupported persistent SQL pragma {key:?}"
4806 )))
4807 }
4808 };
4809 if *target == Some(value) {
4810 return Ok(None);
4811 }
4812 *target = Some(value);
4813
4814 let _commit = self.commit_lock.lock();
4815 let epoch = self.epoch.bump_assigned();
4816 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4817 let txn_id = self.alloc_txn_id()?;
4818 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4819 let commit_seq = {
4820 let mut wal = self.shared_wal.lock();
4821 if let Some(before_commit) = before_commit {
4822 before_commit()?;
4823 }
4824 let append: Result<u64> = (|| {
4825 wal.append(
4826 txn_id,
4827 WAL_TABLE_ID,
4828 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
4829 key: key.to_string(),
4830 value,
4831 }),
4832 )?;
4833 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4834 wal.append_commit(txn_id, epoch, &[])
4835 })();
4836 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4837 };
4838 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
4839 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4840 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
4841 Ok(Some(epoch))
4842 }
4843
4844 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
4845 self.catalog
4846 .read()
4847 .materialized_views
4848 .iter()
4849 .find(|definition| definition.name == name)
4850 .cloned()
4851 }
4852
4853 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
4854 self.catalog.read().materialized_views.clone()
4855 }
4856
4857 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
4858 self.catalog.read().security.clone()
4859 }
4860
4861 pub fn security_active_for(&self, table: &str) -> bool {
4862 self.catalog.read().security.table_has_security(table)
4863 }
4864
4865 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
4866 if self.catalog.read().security_version == expected_version {
4867 return Ok(());
4868 }
4869 self.security_catalog_disk_reads
4870 .fetch_add(1, Ordering::Relaxed);
4871 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
4872 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
4873 let principal = self.principal.read().clone();
4874 let principal = if fresh.require_auth
4875 && principal
4876 .as_ref()
4877 .is_some_and(|principal| principal.user_id != 0)
4878 {
4879 principal
4880 .as_ref()
4881 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
4882 } else {
4883 principal
4884 };
4885 self.auth_state.set_require_auth(fresh.require_auth);
4886 *self.catalog.write() = fresh;
4887 *self.principal.write() = principal.clone();
4888 self.auth_state.set_principal(principal);
4889 Ok(())
4890 }
4891
4892 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
4893 let guard = self.security_coordinator.gate.write();
4894 let version = self.security_coordinator.version.load(Ordering::Acquire);
4895 self.refresh_security_catalog_if_stale(version)?;
4896 Ok(guard)
4897 }
4898
4899 pub(crate) fn admit_operation(&self) -> Result<crate::core::OperationGuard> {
4926 self.core.operation_guard()
4927 }
4928
4929 fn apply_catalog_command_to(
4935 &self,
4936 next_catalog: &mut Catalog,
4937 command: crate::catalog_cmds::CatalogCommand,
4938 ) -> Result<crate::catalog_cmds::CatalogDelta> {
4939 let record = crate::catalog_cmds::CatalogCommandRecord::next(next_catalog, command);
4940 next_catalog.apply_command(&record)
4941 }
4942
4943 pub fn apply_replicated_records(&self, records: &[crate::wal::Record]) -> Result<bool> {
4961 use crate::wal::Op;
4962 use std::sync::atomic::Ordering;
4963
4964 let _operation = self.admit_operation()?;
4965 if self.poisoned.load(Ordering::Relaxed) {
4966 return Err(MongrelError::Other(
4967 "database poisoned by fsync error".into(),
4968 ));
4969 }
4970 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
4973 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
4974 })?;
4975 if records.iter().any(|record| record.txn_id != txn_id) {
4976 return Err(MongrelError::InvalidArgument(
4977 "replicated transaction payload mixes transaction ids".into(),
4978 ));
4979 }
4980 let commits = records
4981 .iter()
4982 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
4983 .count();
4984 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
4985 return Err(MongrelError::InvalidArgument(
4986 "replicated transaction payload must end in exactly one commit marker".into(),
4987 ));
4988 }
4989 let commit_epoch = match records.last().map(|r| &r.op) {
4990 Some(Op::TxnCommit { epoch, .. }) => *epoch,
4991 _ => unreachable!("validated above"),
4992 };
4993 if let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) {
5002 if !added_runs.is_empty() {
5003 return Err(MongrelError::InvalidArgument(
5004 "replicated spilled-run commits are not appliable: the leader must translate \
5005 the commit through translate_records_for_replication before proposal"
5006 .into(),
5007 ));
5008 }
5009 }
5010 if commit_epoch <= self.epoch.visible().0 {
5014 return Ok(false);
5015 }
5016 {
5021 let mut wal = self.shared_wal.lock();
5022 for record in records {
5023 wal.append(record.txn_id, 0, record.op.clone())?;
5024 }
5025 if let Err(error) = wal.group_sync() {
5026 self.poisoned.store(true, Ordering::Relaxed);
5027 return Err(error);
5028 }
5029 }
5030 let replication_pins: Vec<crate::retention::PinGuard> = {
5033 let tables = self.tables.read();
5034 let floor = Epoch(self.epoch.visible().0);
5035 tables
5036 .values()
5037 .map(|handle| {
5038 let t = handle.lock();
5039 Arc::clone(t.pin_registry())
5040 .pin(crate::retention::PinSource::Replication, floor)
5041 })
5042 .collect()
5043 };
5044 let tables = self.tables.read().clone();
5045 let catalog = self.catalog.read().clone();
5046 recover_shared_wal(&self.durable_root, &tables, &catalog, &self.epoch, records)?;
5047 if let Some(Op::TxnCommit { epoch, .. }) = records.last().map(|r| &r.op) {
5052 let commit_ts = records
5053 .iter()
5054 .rev()
5055 .find_map(|record| match &record.op {
5056 Op::CommitTimestamp { unix_nanos } => {
5057 Some(mongreldb_types::hlc::HlcTimestamp {
5058 physical_micros: unix_nanos / 1_000,
5059 logical: 0,
5060 node_tiebreaker: 0,
5061 })
5062 }
5063 _ => None,
5064 })
5065 .unwrap_or_else(|| {
5066 self.hlc
5067 .now()
5068 .unwrap_or(mongreldb_types::hlc::HlcTimestamp {
5069 physical_micros: 0,
5070 logical: 0,
5071 node_tiebreaker: 0,
5072 })
5073 });
5074 self.record_commit_ts(Epoch(*epoch), commit_ts);
5075 }
5076 drop(replication_pins);
5077 Ok(true)
5078 }
5079
5080 pub fn validate_staged_txn_writes(&self, staged: &[Vec<u8>]) -> Result<()> {
5090 let tables = self.tables.read();
5091 for payload in staged {
5092 match StagedTxnWrite::decode(payload)? {
5093 StagedTxnWrite::Put { table_id, rows } => {
5094 let handle = tables.get(&table_id).ok_or_else(|| {
5095 MongrelError::InvalidArgument(format!(
5096 "staged write targets unmounted table {table_id}"
5097 ))
5098 })?;
5099 let rows: Vec<crate::memtable::Row> =
5100 bincode::deserialize(&rows).map_err(|error| {
5101 MongrelError::InvalidArgument(format!(
5102 "staged put payload for table {table_id} cannot decode: {error}"
5103 ))
5104 })?;
5105 let schema = handle.lock().schema().clone();
5106 for row in &rows {
5107 validate_recovered_row(&schema, row).map_err(|error| {
5108 MongrelError::InvalidArgument(format!(
5109 "staged row for table {table_id} is not appliable: {error}"
5110 ))
5111 })?;
5112 }
5113 }
5114 StagedTxnWrite::Delete { table_id, row_ids } => {
5115 if !tables.contains_key(&table_id) {
5116 return Err(MongrelError::InvalidArgument(format!(
5117 "staged delete targets unmounted table {table_id}"
5118 )));
5119 }
5120 if row_ids.contains(&u64::MAX) {
5121 return Err(MongrelError::InvalidArgument(format!(
5122 "staged delete for table {table_id} names an exhausted row id"
5123 )));
5124 }
5125 }
5126 }
5127 }
5128 Ok(())
5129 }
5130
5131 pub fn apply_staged_txn_writes(
5153 &self,
5154 txn_tag: u64,
5155 staged: &[Vec<u8>],
5156 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5157 ) -> Result<bool> {
5158 use crate::wal::Op;
5159
5160 let mut writes = Vec::with_capacity(staged.len());
5162 for payload in staged {
5163 writes.push(StagedTxnWrite::decode(payload)?);
5164 }
5165 let generation = *self.next_txn_id.lock() >> 32;
5171 let txn_id = (generation << 32) | (txn_tag & 0x7FFF_FFFF) | 0x8000_0000;
5172 let mut records = Vec::with_capacity(writes.len() + 2);
5173 for write in writes {
5174 let op = match write {
5175 StagedTxnWrite::Put { table_id, rows } => {
5176 let decoded: Vec<crate::memtable::Row> =
5183 bincode::deserialize(&rows).map_err(|e| {
5184 MongrelError::Other(format!(
5185 "staged txn put rows could not be decoded: {e}"
5186 ))
5187 })?;
5188 let stamped = bincode::serialize(&decoded).map_err(|e| {
5189 MongrelError::Other(format!("staged txn put rows serialize: {e}"))
5190 })?;
5191 Op::Put {
5192 table_id,
5193 rows: stamped,
5194 }
5195 }
5196 StagedTxnWrite::Delete { table_id, row_ids } => Op::Delete {
5197 table_id,
5198 row_ids: row_ids.into_iter().map(crate::RowId).collect(),
5199 },
5200 };
5201 records.push(crate::wal::Record::new(Epoch(0), txn_id, op));
5202 }
5203 let epoch = self.epoch.visible().0 + 1;
5204 let unix_nanos = commit_ts.physical_micros.saturating_mul(1_000);
5208 records.push(crate::wal::Record::new(
5209 Epoch(0),
5210 txn_id,
5211 Op::CommitTimestamp { unix_nanos },
5212 ));
5213 records.push(crate::wal::Record::new(
5214 Epoch(0),
5215 txn_id,
5216 Op::TxnCommit {
5217 epoch,
5218 added_runs: Vec::new(),
5219 },
5220 ));
5221 let applied = self.apply_replicated_records(&records)?;
5222 if applied && commit_ts != mongreldb_types::hlc::HlcTimestamp::ZERO {
5223 self.record_commit_ts(Epoch(epoch), commit_ts);
5226 }
5227 Ok(applied)
5228 }
5229
5230 pub fn apply_committed_transaction(
5237 &self,
5238 transaction_id: u64,
5239 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5240 operations: &[Vec<u8>],
5241 ) -> Result<bool> {
5242 self.apply_staged_txn_writes(transaction_id, operations, commit_ts)
5243 }
5244
5245 pub fn apply_replicated_catalog_command(
5254 &self,
5255 record: &crate::catalog_cmds::CatalogCommandRecord,
5256 ) -> Result<crate::catalog_cmds::CatalogDelta> {
5257 let _operation = self.admit_operation()?;
5258 let _g = self.ddl_lock.lock();
5259 let mut next_catalog = self.catalog.read().clone();
5260 let delta = next_catalog.apply_command(record)?;
5261 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
5262 return Ok(delta);
5263 }
5264 let referenced_epoch = match &delta {
5270 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => Some(entry.created_epoch),
5271 crate::catalog_cmds::CatalogDelta::TableDropped { at_epoch, .. }
5272 | crate::catalog_cmds::CatalogDelta::TableRenamed { at_epoch, .. } => Some(*at_epoch),
5273 _ => None,
5274 };
5275 if let Some(referenced) = referenced_epoch {
5276 self.epoch.advance_recovered(Epoch(referenced));
5277 next_catalog.db_epoch = next_catalog.db_epoch.max(referenced);
5278 }
5279 match &delta {
5280 crate::catalog_cmds::CatalogDelta::TableCreated { entry } => {
5281 if !self.tables.read().contains_key(&entry.table_id) {
5286 self.mount_catalog_entry(entry)?;
5287 }
5288 }
5289 crate::catalog_cmds::CatalogDelta::TableDropped { table_id, .. } => {
5290 self.tables.write().remove(table_id);
5291 }
5292 _ => {}
5293 }
5294 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref())?;
5298 *self.catalog.write() = next_catalog;
5299 Ok(delta)
5300 }
5301
5302 fn mount_catalog_entry(&self, entry: &crate::catalog::CatalogEntry) -> Result<()> {
5307 let table_relative = Path::new(TABLES_DIR).join(entry.table_id.to_string());
5308 let table_root = Arc::new(
5309 self.durable_root
5310 .create_directory_all_pinned(&table_relative)?,
5311 );
5312 let tdir = table_root.io_path()?;
5313 let ctx = SharedCtx {
5314 root_guard: Some(table_root),
5315 epoch: Arc::clone(&self.epoch),
5316 page_cache: Arc::clone(&self.page_cache),
5317 decoded_cache: Arc::clone(&self.decoded_cache),
5318 snapshots: Arc::clone(&self.snapshots),
5319 kek: self.kek.clone(),
5320 commit_lock: Arc::clone(&self.commit_lock),
5321 shared: Some(crate::engine::SharedWalCtx {
5322 wal: Arc::clone(&self.shared_wal),
5323 group: Arc::clone(&self.group),
5324 poisoned: Arc::clone(&self.poisoned),
5325 txn_ids: Arc::clone(&self.next_txn_id),
5326 change_wake: self.change_wake.clone(),
5327 lifecycle: Arc::clone(&self.lifecycle),
5328 hlc: Arc::clone(&self.hlc),
5329 }),
5330 table_name: Some(entry.name.clone()),
5331 auth: self.table_auth_checker(),
5332 read_only: self.read_only,
5333 };
5334 let table = Table::create_in(&tdir, entry.schema.clone(), entry.table_id, ctx)?;
5335 self.tables
5336 .write()
5337 .insert(entry.table_id, TableHandle::new(table));
5338 Ok(())
5339 }
5340
5341 fn publish_catalog_candidate(
5342 &self,
5343 catalog: Catalog,
5344 epoch: Epoch,
5345 epoch_guard: &mut EpochGuard<'_>,
5346 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5347 ) -> Result<()> {
5348 self.publish_catalog_candidate_with_prelude(
5349 catalog,
5350 epoch,
5351 epoch_guard,
5352 before_publish,
5353 Vec::new(),
5354 )
5355 }
5356
5357 fn publish_catalog_candidate_with_prelude(
5358 &self,
5359 catalog: Catalog,
5360 epoch: Epoch,
5361 epoch_guard: &mut EpochGuard<'_>,
5362 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5363 prelude: Vec<(u64, crate::wal::Op)>,
5364 ) -> Result<()> {
5365 use crate::wal::DdlOp;
5366
5367 if self.read_only {
5368 return Err(MongrelError::ReadOnlyReplica);
5369 }
5370 if self.poisoned.load(Ordering::Relaxed) {
5371 return Err(MongrelError::Other(
5372 "database poisoned by fsync error".into(),
5373 ));
5374 }
5375 let _operation = self.admit_operation()?;
5377 if let Some(before_publish) = before_publish.as_mut() {
5378 (**before_publish)()?;
5379 }
5380 if catalog.db_epoch != epoch.0 {
5381 return Err(MongrelError::InvalidArgument(format!(
5382 "catalog epoch {} does not match commit epoch {}",
5383 catalog.db_epoch, epoch.0
5384 )));
5385 }
5386 {
5387 let current = self.catalog.read();
5388 validate_catalog_transition(¤t, &catalog)?;
5389 }
5390 validate_recovered_catalog(&catalog)?;
5391 let catalog_json = DdlOp::encode_catalog(&catalog)?;
5392 let txn_id = self.alloc_txn_id()?;
5393 let commit_seq = {
5394 let mut wal = self.shared_wal.lock();
5395 let append: Result<u64> = (|| {
5396 for (table_id, op) in prelude {
5397 wal.append(txn_id, table_id, op)?;
5398 }
5399 wal.append(
5400 txn_id,
5401 WAL_TABLE_ID,
5402 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
5403 )?;
5404 wal.append_commit(txn_id, epoch, &[])
5405 })();
5406 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5407 };
5408 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5409 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
5410 self.finish_durable_publish(epoch, epoch_guard, &receipt, checkpoint)
5411 }
5412
5413 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
5417 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
5418 let version = catalog.security_version;
5419 let principal = self.principal.read().clone();
5420 let principal = if catalog.require_auth
5421 && principal
5422 .as_ref()
5423 .is_some_and(|principal| principal.user_id != 0)
5424 {
5425 principal.as_ref().and_then(|principal| {
5426 Self::resolve_bound_principal_from_catalog(&catalog, principal)
5427 })
5428 } else {
5429 principal
5430 };
5431 *self.catalog.write() = catalog;
5432 self.security_coordinator
5433 .version
5434 .store(version, Ordering::Release);
5435 self.auth_state
5436 .set_require_auth(self.catalog.read().require_auth);
5437 *self.principal.write() = principal.clone();
5438 self.auth_state.set_principal(principal);
5439 checkpoint
5440 }
5441
5442 fn finish_durable_publish(
5443 &self,
5444 epoch: Epoch,
5445 epoch_guard: &mut EpochGuard<'_>,
5446 receipt: &mongreldb_log::CommitReceipt,
5447 post_step: Result<()>,
5448 ) -> Result<()> {
5449 if let Err(error) = self.publish_committed(receipt, epoch) {
5450 self.poisoned.store(true, Ordering::Relaxed);
5455 self.lifecycle.poison();
5456 return Err(MongrelError::DurableCommit {
5457 epoch: epoch.0,
5458 message: error.to_string(),
5459 });
5460 }
5461 epoch_guard.disarm();
5462 match post_step {
5463 Ok(()) => Ok(()),
5464 Err(error) => {
5465 self.poisoned.store(true, Ordering::Relaxed);
5466 self.lifecycle.poison();
5467 Err(MongrelError::DurableCommit {
5468 epoch: epoch.0,
5469 message: error.to_string(),
5470 })
5471 }
5472 }
5473 }
5474
5475 fn publish_committed(
5481 &self,
5482 receipt: &mongreldb_log::CommitReceipt,
5483 epoch: Epoch,
5484 ) -> Result<()> {
5485 debug_assert_eq!(
5486 receipt.log_position.index, epoch.0,
5487 "commit receipt position must match the published epoch"
5488 );
5489 mongreldb_fault::inject("commit.publish.before").map_err(crate::commit_log::fault_as_io)?;
5490 self.epoch.publish_in_order(epoch);
5491 mongreldb_fault::inject("commit.publish.after").map_err(crate::commit_log::fault_as_io)?;
5492 Ok(())
5493 }
5494
5495 fn await_durable_commit(
5504 &self,
5505 txn_id: u64,
5506 commit_seq: u64,
5507 epoch: Epoch,
5508 ) -> Result<mongreldb_log::CommitReceipt> {
5509 match self
5510 .standalone_commit_log
5511 .seal_transaction(txn_id, epoch, commit_seq, None)
5512 {
5513 Ok(receipt) => {
5514 self.record_commit_ts(epoch, receipt.commit_ts);
5515 Ok(receipt)
5516 }
5517 Err(error) => {
5518 self.poisoned.store(true, Ordering::Relaxed);
5519 self.lifecycle.poison();
5520 Err(MongrelError::CommitOutcomeUnknown {
5521 epoch: epoch.0,
5522 message: error.to_string(),
5523 })
5524 }
5525 }
5526 }
5527
5528 fn await_durable_commit_with_ts(
5533 &self,
5534 txn_id: u64,
5535 commit_seq: u64,
5536 epoch: Epoch,
5537 commit_ts: mongreldb_types::hlc::HlcTimestamp,
5538 ) -> Result<mongreldb_log::CommitReceipt> {
5539 match self.standalone_commit_log.seal_transaction(
5540 txn_id,
5541 epoch,
5542 commit_seq,
5543 Some(commit_ts),
5544 ) {
5545 Ok(receipt) => {
5546 self.record_commit_ts(epoch, receipt.commit_ts);
5547 Ok(receipt)
5548 }
5549 Err(error) => {
5550 self.poisoned.store(true, Ordering::Relaxed);
5551 self.lifecycle.poison();
5552 Err(MongrelError::CommitOutcomeUnknown {
5553 epoch: epoch.0,
5554 message: error.to_string(),
5555 })
5556 }
5557 }
5558 }
5559
5560 fn record_commit_ts(&self, epoch: Epoch, commit_ts: mongreldb_types::hlc::HlcTimestamp) {
5565 let mut ledger = self.commit_ts_ledger.lock();
5566 ledger.insert(epoch.0, commit_ts);
5567 while ledger.len() > COMMIT_TS_LEDGER_CAP {
5568 ledger.pop_first();
5569 }
5570 }
5571
5572 pub fn commit_ts_for_epoch(&self, epoch: Epoch) -> Option<mongreldb_types::hlc::HlcTimestamp> {
5586 self.commit_ts_ledger.lock().get(&epoch.0).copied()
5587 }
5588
5589 pub fn epoch_at_or_before_commit_ts(
5595 &self,
5596 timestamp: mongreldb_types::hlc::HlcTimestamp,
5597 ) -> Option<Epoch> {
5598 self.commit_ts_ledger
5599 .lock()
5600 .iter()
5601 .rev()
5602 .find_map(|(epoch, commit_ts)| (*commit_ts <= timestamp).then_some(Epoch(*epoch)))
5603 }
5604
5605 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
5606 self.poisoned.store(true, Ordering::Relaxed);
5607 self.lifecycle.poison();
5608 MongrelError::CommitOutcomeUnknown {
5609 epoch: epoch.0,
5610 message: error.to_string(),
5611 }
5612 }
5613
5614 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
5616 self.set_security_catalog_as_with_epoch(security, None)
5617 .map(|_| ())
5618 }
5619
5620 pub fn set_security_catalog_as(
5622 &self,
5623 security: crate::security::SecurityCatalog,
5624 principal: Option<&crate::auth::Principal>,
5625 ) -> Result<()> {
5626 self.set_security_catalog_as_with_epoch(security, principal)
5627 .map(|_| ())
5628 }
5629
5630 pub fn set_security_catalog_as_with_epoch(
5632 &self,
5633 security: crate::security::SecurityCatalog,
5634 principal: Option<&crate::auth::Principal>,
5635 ) -> Result<Epoch> {
5636 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
5637 }
5638
5639 pub fn set_security_catalog_as_with_epoch_controlled<F>(
5642 &self,
5643 security: crate::security::SecurityCatalog,
5644 principal: Option<&crate::auth::Principal>,
5645 mut before_commit: F,
5646 ) -> Result<Epoch>
5647 where
5648 F: FnMut() -> Result<()>,
5649 {
5650 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
5651 }
5652
5653 fn set_security_catalog_as_with_epoch_inner(
5654 &self,
5655 security: crate::security::SecurityCatalog,
5656 principal: Option<&crate::auth::Principal>,
5657 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
5658 ) -> Result<Epoch> {
5659 use crate::wal::DdlOp;
5660 use std::sync::atomic::Ordering;
5661
5662 let command = crate::catalog_cmds::CatalogCommand::SetSecurityCatalog {
5666 security: security.clone(),
5667 };
5668 self.require_for(
5669 principal,
5670 &crate::catalog_cmds::required_permission(&command),
5671 )?;
5672 if self.poisoned.load(Ordering::Relaxed) {
5673 return Err(MongrelError::Other(
5674 "database poisoned by fsync error".into(),
5675 ));
5676 }
5677 let _operation = self.admit_operation()?;
5680 let _ddl = self.ddl_lock.lock();
5681 let _security_write = self.security_write()?;
5684 self.require_for(
5685 principal,
5686 &crate::catalog_cmds::required_permission(&command),
5687 )?;
5688 let mut next_catalog = self.catalog.read().clone();
5689 validate_security_catalog(&next_catalog, &security)?;
5690 let payload = DdlOp::encode_security(&security)?;
5691 let _commit = self.commit_lock.lock();
5692 let epoch = self.epoch.bump_assigned();
5693 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5694 let txn_id = self.alloc_txn_id()?;
5695 self.apply_catalog_command_to(&mut next_catalog, command)?;
5696 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
5697 let commit_seq = {
5698 let mut wal = self.shared_wal.lock();
5699 if let Some(before_commit) = before_commit {
5700 before_commit()?;
5701 }
5702 let append: Result<u64> = (|| {
5703 wal.append(
5704 txn_id,
5705 WAL_TABLE_ID,
5706 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
5707 security_json: payload,
5708 }),
5709 )?;
5710 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
5711 wal.append_commit(txn_id, epoch, &[])
5712 })();
5713 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
5714 };
5715 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
5716 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
5717 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
5718 Ok(epoch)
5719 }
5720
5721 pub fn require_for(
5722 &self,
5723 principal: Option<&crate::auth::Principal>,
5724 permission: &crate::auth::Permission,
5725 ) -> Result<()> {
5726 let Some(principal) = principal else {
5727 return self.require(permission);
5728 };
5729 let resolved;
5730 let principal = if principal.user_id != 0 {
5731 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5732 .ok_or(MongrelError::AuthRequired)?;
5733 &resolved
5734 } else {
5735 principal
5736 };
5737 #[cfg(test)]
5738 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5739 if principal.has_permission(permission) {
5740 Ok(())
5741 } else {
5742 Err(MongrelError::PermissionDenied {
5743 required: permission.clone(),
5744 principal: principal.username.clone(),
5745 })
5746 }
5747 }
5748
5749 fn require_exact_principal_current(
5753 &self,
5754 principal: Option<&crate::auth::Principal>,
5755 permission: &crate::auth::Permission,
5756 ) -> Result<()> {
5757 let catalog = self.catalog.read();
5758 if !catalog.require_auth {
5759 return Ok(());
5760 }
5761 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
5762 let current = if supplied.user_id == 0 {
5763 supplied.clone()
5764 } else {
5765 Self::resolve_bound_principal_from_catalog(&catalog, supplied)
5766 .ok_or(MongrelError::AuthRequired)?
5767 };
5768 if current.has_permission(permission) {
5769 Ok(())
5770 } else {
5771 Err(MongrelError::PermissionDenied {
5772 required: permission.clone(),
5773 principal: current.username,
5774 })
5775 }
5776 }
5777
5778 pub(crate) fn with_exact_principal_current<T, F>(
5779 &self,
5780 principal: Option<&crate::auth::Principal>,
5781 permission: &crate::auth::Permission,
5782 operation: F,
5783 ) -> Result<T>
5784 where
5785 F: FnOnce() -> Result<T>,
5786 {
5787 let _security = self.security_coordinator.gate.read();
5788 self.require_exact_principal_current(principal, permission)?;
5789 operation()
5790 }
5791
5792 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
5793 self.principal.read().clone()
5794 }
5795
5796 #[cfg(test)]
5797 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
5798 *self.principal.write() = principal.clone();
5799 self.auth_state.set_principal(principal);
5800 }
5801
5802 pub fn require_columns_for(
5803 &self,
5804 table: &str,
5805 operation: crate::auth::ColumnOperation,
5806 column_ids: &[u16],
5807 principal: Option<&crate::auth::Principal>,
5808 ) -> Result<()> {
5809 if principal.is_none() && !self.auth_state.require_auth() {
5810 return Ok(());
5811 }
5812 let cached = self.principal.read().clone();
5813 let principal = principal.or(cached.as_ref());
5814 let Some(principal) = principal else {
5815 let permission = match operation {
5816 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5817 table: table.to_string(),
5818 },
5819 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5820 table: table.to_string(),
5821 },
5822 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5823 table: table.to_string(),
5824 },
5825 };
5826 return self.require(&permission);
5827 };
5828 let catalog = self.catalog.read();
5829 let resolved;
5830 let principal = if principal.user_id != 0 {
5831 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
5832 .ok_or(MongrelError::AuthRequired)?;
5833 &resolved
5834 } else {
5835 principal
5836 };
5837 let schema = &catalog
5838 .live(table)
5839 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5840 .schema;
5841 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
5842 }
5843
5844 fn require_columns_for_principal(
5845 table: &str,
5846 schema: &Schema,
5847 operation: crate::auth::ColumnOperation,
5848 column_ids: &[u16],
5849 principal: &crate::auth::Principal,
5850 ) -> Result<()> {
5851 #[cfg(test)]
5852 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
5853 match principal.column_access(table, operation) {
5854 crate::auth::ColumnAccess::All => Ok(()),
5855 crate::auth::ColumnAccess::Columns(allowed) => {
5856 let denied = column_ids.iter().find_map(|column_id| {
5857 schema
5858 .columns
5859 .iter()
5860 .find(|column| column.id == *column_id)
5861 .filter(|column| !allowed.contains(&column.name))
5862 });
5863 if denied.is_none() {
5864 Ok(())
5865 } else {
5866 Err(MongrelError::PermissionDenied {
5867 required: match operation {
5868 crate::auth::ColumnOperation::Select => {
5869 crate::auth::Permission::SelectColumns {
5870 table: table.to_string(),
5871 columns: denied
5872 .into_iter()
5873 .map(|column| column.name.clone())
5874 .collect(),
5875 }
5876 }
5877 crate::auth::ColumnOperation::Insert => {
5878 crate::auth::Permission::InsertColumns {
5879 table: table.to_string(),
5880 columns: denied
5881 .into_iter()
5882 .map(|column| column.name.clone())
5883 .collect(),
5884 }
5885 }
5886 crate::auth::ColumnOperation::Update => {
5887 crate::auth::Permission::UpdateColumns {
5888 table: table.to_string(),
5889 columns: denied
5890 .into_iter()
5891 .map(|column| column.name.clone())
5892 .collect(),
5893 }
5894 }
5895 },
5896 principal: principal.username.clone(),
5897 })
5898 }
5899 }
5900 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5901 required: match operation {
5902 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
5903 table: table.to_string(),
5904 },
5905 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
5906 table: table.to_string(),
5907 },
5908 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
5909 table: table.to_string(),
5910 },
5911 },
5912 principal: principal.username.clone(),
5913 }),
5914 }
5915 }
5916
5917 pub fn select_column_ids_for(
5918 &self,
5919 table: &str,
5920 principal: Option<&crate::auth::Principal>,
5921 ) -> Result<Vec<u16>> {
5922 let catalog = self.catalog.read();
5923 let columns = catalog
5924 .live(table)
5925 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
5926 .schema
5927 .columns
5928 .iter()
5929 .map(|column| (column.id, column.name.clone()))
5930 .collect::<Vec<_>>();
5931 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
5932 drop(catalog);
5933 let Some(principal) = principal.as_ref() else {
5934 self.require(&crate::auth::Permission::Select {
5935 table: table.to_string(),
5936 })?;
5937 return Ok(columns.iter().map(|(id, _)| *id).collect());
5938 };
5939 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
5940 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
5941 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
5942 .iter()
5943 .filter(|(_, name)| allowed.contains(name))
5944 .map(|(id, _)| *id)
5945 .collect()),
5946 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
5947 required: crate::auth::Permission::Select {
5948 table: table.to_string(),
5949 },
5950 principal: principal.username.clone(),
5951 }),
5952 }
5953 }
5954
5955 pub fn secure_rows_for(
5956 &self,
5957 table: &str,
5958 rows: Vec<crate::memtable::Row>,
5959 principal: Option<&crate::auth::Principal>,
5960 ) -> Result<Vec<crate::memtable::Row>> {
5961 self.secure_rows_for_with_context(table, rows, principal, None)
5962 }
5963
5964 pub fn secure_rows_for_with_context(
5965 &self,
5966 table: &str,
5967 rows: Vec<crate::memtable::Row>,
5968 principal: Option<&crate::auth::Principal>,
5969 context: Option<&crate::query::AiExecutionContext>,
5970 ) -> Result<Vec<crate::memtable::Row>> {
5971 let (security, principal) = {
5972 let catalog = self.catalog.read();
5973 (
5974 catalog.security.clone(),
5975 self.principal_for_authorized_read(&catalog, principal, false)?,
5976 )
5977 };
5978 if !security.table_has_security(table) {
5979 return Ok(rows);
5980 }
5981 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
5982 let mut output = Vec::new();
5983 for mut row in rows {
5984 if let Some(context) = context {
5985 context.consume(1)?;
5986 }
5987 if security.row_allowed(
5988 table,
5989 crate::security::PolicyCommand::Select,
5990 &row,
5991 principal,
5992 false,
5993 ) {
5994 security.apply_masks(table, &mut row, principal);
5995 output.push(row);
5996 }
5997 }
5998 Ok(output)
5999 }
6000
6001 pub fn mask_search_hits_for(
6004 &self,
6005 table: &str,
6006 hits: &mut [crate::query::SearchHit],
6007 principal: Option<&crate::auth::Principal>,
6008 ) -> Result<()> {
6009 let (security, principal) = {
6010 let catalog = self.catalog.read();
6011 (
6012 catalog.security.clone(),
6013 self.principal_for_authorized_read(&catalog, principal, false)?,
6014 )
6015 };
6016 if !security.table_has_security(table) {
6017 return Ok(());
6018 }
6019 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
6020 for hit in hits {
6021 security.apply_masks_to_cells(table, &mut hit.cells, principal);
6022 }
6023 Ok(())
6024 }
6025
6026 pub fn mask_rows_for(
6028 &self,
6029 table: &str,
6030 rows: &mut [crate::memtable::Row],
6031 principal: Option<&crate::auth::Principal>,
6032 ) -> Result<()> {
6033 let (security, principal) = {
6034 let catalog = self.catalog.read();
6035 (
6036 catalog.security.clone(),
6037 self.principal_for_authorized_read(&catalog, principal, false)?,
6038 )
6039 };
6040 if !security.table_has_security(table) {
6041 return Ok(());
6042 }
6043 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
6044 for row in rows {
6045 security.apply_masks(table, row, principal);
6046 }
6047 Ok(())
6048 }
6049
6050 pub fn authorized_candidate_ids_for(
6052 &self,
6053 table: &str,
6054 principal: Option<&crate::auth::Principal>,
6055 ) -> Result<Option<std::collections::HashSet<RowId>>> {
6056 Ok(self
6057 .authorized_read_snapshot(table, principal)?
6058 .allowed_row_ids)
6059 }
6060
6061 fn allowed_row_ids_locked(
6062 &self,
6063 table_name: &str,
6064 table: &Table,
6065 table_snapshot: Snapshot,
6066 security_state: (&crate::security::SecurityCatalog, u64),
6067 principal: Option<&crate::auth::Principal>,
6068 context: Option<&crate::query::AiExecutionContext>,
6069 ) -> Result<Option<Arc<HashSet<RowId>>>> {
6070 let (security, security_version) = security_state;
6071 if !security.rls_enabled(table_name) {
6072 return Ok(None);
6073 }
6074 let authorization_started = std::time::Instant::now();
6075 let principal = principal.ok_or(MongrelError::AuthRequired)?;
6076 let mut roles = principal.roles.clone();
6077 roles.sort_unstable();
6078 let principal_key = format!(
6079 "{}:{}:{}:{}:{roles:?}",
6080 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
6081 );
6082 let cache_key = (
6083 table_name.to_string(),
6084 table.data_generation(),
6085 security_version,
6086 principal_key,
6087 );
6088 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
6089 crate::trace::QueryTrace::record(|trace| {
6090 trace.rls_cache_hit = true;
6091 trace.authorization_nanos = trace
6092 .authorization_nanos
6093 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
6094 });
6095 return Ok(Some(allowed));
6096 }
6097 if let Some(context) = context {
6098 context.checkpoint()?;
6099 }
6100 let started = std::time::Instant::now();
6102 let rows = table.visible_rows(table_snapshot)?;
6103 let rows_evaluated = rows.len() as u64;
6104 let mut allowed = HashSet::new();
6105 for chunk in rows.chunks(256) {
6106 if let Some(context) = context {
6107 context.consume(chunk.len())?;
6108 }
6109 allowed.extend(chunk.iter().filter_map(|row| {
6110 security
6111 .row_allowed(
6112 table_name,
6113 crate::security::PolicyCommand::Select,
6114 row,
6115 principal,
6116 false,
6117 )
6118 .then_some(row.row_id)
6119 }));
6120 }
6121 let allowed = Arc::new(allowed);
6122 let mut cache = self.rls_cache.lock();
6123 cache.build_nanos = cache
6124 .build_nanos
6125 .saturating_add(started.elapsed().as_nanos() as u64);
6126 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
6127 cache.insert(cache_key, Arc::clone(&allowed));
6128 crate::trace::QueryTrace::record(|trace| {
6129 trace.rls_rows_evaluated = trace
6130 .rls_rows_evaluated
6131 .saturating_add(rows_evaluated as usize);
6132 trace.authorization_nanos = trace
6133 .authorization_nanos
6134 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
6135 });
6136 Ok(Some(allowed))
6137 }
6138
6139 fn principal_for_authorized_read(
6140 &self,
6141 catalog: &Catalog,
6142 principal: Option<&crate::auth::Principal>,
6143 catalog_bound: bool,
6144 ) -> Result<Option<crate::auth::Principal>> {
6145 let principal = principal.cloned().or_else(|| self.principal.read().clone());
6146 let Some(principal) = principal else {
6147 return Ok(None);
6148 };
6149 if catalog_bound || principal.user_id != 0 {
6150 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
6151 .map(Some)
6152 .ok_or(MongrelError::AuthRequired);
6153 }
6154 Ok(Some(principal))
6155 }
6156
6157 pub fn with_authorized_read<T, F>(
6161 &self,
6162 table_name: &str,
6163 principal: Option<&crate::auth::Principal>,
6164 catalog_bound: bool,
6165 read: F,
6166 ) -> Result<T>
6167 where
6168 F: FnMut(
6169 &mut Table,
6170 Snapshot,
6171 Option<&HashSet<RowId>>,
6172 Option<&crate::auth::Principal>,
6173 ) -> Result<T>,
6174 {
6175 self.with_authorized_read_context(
6176 table_name,
6177 principal,
6178 catalog_bound,
6179 None,
6180 None,
6181 None,
6182 read,
6183 )
6184 }
6185
6186 #[allow(clippy::too_many_arguments)]
6187 pub fn with_authorized_read_context<T, F>(
6188 &self,
6189 table_name: &str,
6190 principal: Option<&crate::auth::Principal>,
6191 catalog_bound: bool,
6192 authorization: Option<&ReadAuthorization>,
6193 context: Option<&crate::query::AiExecutionContext>,
6194 snapshot_override: Option<Snapshot>,
6195 read: F,
6196 ) -> Result<T>
6197 where
6198 F: FnMut(
6199 &mut Table,
6200 Snapshot,
6201 Option<&HashSet<RowId>>,
6202 Option<&crate::auth::Principal>,
6203 ) -> Result<T>,
6204 {
6205 self.with_authorized_read_context_stamped(
6206 table_name,
6207 principal,
6208 catalog_bound,
6209 authorization,
6210 context,
6211 snapshot_override,
6212 read,
6213 )
6214 .map(|(result, _)| result)
6215 }
6216
6217 #[allow(clippy::too_many_arguments)]
6218 pub fn with_authorized_read_context_stamped<T, F>(
6219 &self,
6220 table_name: &str,
6221 principal: Option<&crate::auth::Principal>,
6222 catalog_bound: bool,
6223 authorization: Option<&ReadAuthorization>,
6224 context: Option<&crate::query::AiExecutionContext>,
6225 snapshot_override: Option<Snapshot>,
6226 mut read: F,
6227 ) -> Result<(T, AuthorizedReadStamp)>
6228 where
6229 F: FnMut(
6230 &mut Table,
6231 Snapshot,
6232 Option<&HashSet<RowId>>,
6233 Option<&crate::auth::Principal>,
6234 ) -> Result<T>,
6235 {
6236 if principal.is_none() && self.principal.read().is_some() {
6237 self.refresh_principal()?;
6238 }
6239 const RETRIES: usize = 3;
6240 let handle = self.table(table_name)?;
6241 for attempt in 0..RETRIES {
6242 crate::trace::QueryTrace::record(|trace| {
6243 trace.authorization_retries = attempt;
6244 });
6245 let (security, security_version, effective_principal) = {
6246 let catalog = self.catalog.read();
6247 (
6248 catalog.security.clone(),
6249 catalog.security_version,
6250 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6251 )
6252 };
6253 if let Some(authorization) = authorization {
6254 for permission in &authorization.permissions {
6255 self.require_for(effective_principal.as_ref(), permission)?;
6256 }
6257 self.require_columns_for(
6258 table_name,
6259 authorization.operation,
6260 &authorization.columns,
6261 effective_principal.as_ref(),
6262 )?;
6263 }
6264 let result = {
6265 let mut table = lock_table_with_context(&handle, context)?;
6266 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
6267 let allowed = self.allowed_row_ids_locked(
6268 table_name,
6269 &table,
6270 snapshot,
6271 (&security, security_version),
6272 effective_principal.as_ref(),
6273 context,
6274 )?;
6275 let stamp = AuthorizedReadStamp {
6276 table_id: table.table_id(),
6277 schema_id: table.schema().schema_id,
6278 data_generation: table.data_generation(),
6279 security_version,
6280 snapshot,
6281 };
6282 let result = read(
6283 &mut table,
6284 snapshot,
6285 allowed.as_deref(),
6286 effective_principal.as_ref(),
6287 )?;
6288 (result, stamp)
6289 };
6290 if let Some(context) = context {
6291 context.checkpoint()?;
6292 }
6293 if self.catalog.read().security_version == security_version {
6294 return Ok(result);
6295 }
6296 if attempt + 1 == RETRIES {
6297 return Err(MongrelError::Conflict(
6298 "security policy changed during scored read".into(),
6299 ));
6300 }
6301 }
6302 Err(MongrelError::Conflict(
6303 "authorization retry loop exhausted".into(),
6304 ))
6305 }
6306
6307 fn with_authorized_aggregate_table<T, F>(
6308 &self,
6309 table_name: &str,
6310 columns: &[u16],
6311 principal: Option<&crate::auth::Principal>,
6312 catalog_bound: bool,
6313 allow_table_security: bool,
6314 mut aggregate: F,
6315 ) -> Result<T>
6316 where
6317 F: FnMut(
6318 &mut Table,
6319 Option<&crate::security::CandidateAuthorization<'_>>,
6320 Option<&crate::auth::Principal>,
6321 u64,
6322 ) -> Result<T>,
6323 {
6324 if principal.is_none() && self.principal.read().is_some() {
6325 self.refresh_principal()?;
6326 }
6327 const RETRIES: usize = 3;
6328 let handle = self.table(table_name)?;
6329 for attempt in 0..RETRIES {
6330 let (security, security_version, effective_principal) = {
6331 let catalog = self.catalog.read();
6332 (
6333 catalog.security.clone(),
6334 catalog.security_version,
6335 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6336 )
6337 };
6338 self.require_columns_for(
6339 table_name,
6340 crate::auth::ColumnOperation::Select,
6341 columns,
6342 effective_principal.as_ref(),
6343 )?;
6344 if !allow_table_security && security.table_has_security(table_name) {
6345 return Err(MongrelError::InvalidArgument(
6346 "incremental aggregate is unsupported while RLS or column masks are active"
6347 .into(),
6348 ));
6349 }
6350 let result = {
6351 let mut table = handle.lock();
6352 let authorization = if security.rls_enabled(table_name) {
6353 Some(crate::security::CandidateAuthorization {
6354 table: table_name,
6355 security: &security,
6356 principal: effective_principal
6357 .as_ref()
6358 .ok_or(MongrelError::AuthRequired)?,
6359 })
6360 } else {
6361 None
6362 };
6363 aggregate(
6364 &mut table,
6365 authorization.as_ref(),
6366 effective_principal.as_ref(),
6367 security_version,
6368 )?
6369 };
6370 if self.catalog.read().security_version == security_version {
6371 return Ok(result);
6372 }
6373 if attempt + 1 == RETRIES {
6374 return Err(MongrelError::Conflict(
6375 "security policy changed during aggregate read".into(),
6376 ));
6377 }
6378 }
6379 Err(MongrelError::Conflict(
6380 "aggregate authorization retry loop exhausted".into(),
6381 ))
6382 }
6383
6384 pub fn with_authorized_scored_read_context<T, F>(
6388 &self,
6389 table_name: &str,
6390 principal: Option<&crate::auth::Principal>,
6391 catalog_bound: bool,
6392 authorization: Option<&ReadAuthorization>,
6393 context: Option<&crate::query::AiExecutionContext>,
6394 mut read: F,
6395 ) -> Result<T>
6396 where
6397 F: FnMut(
6398 &mut Table,
6399 Snapshot,
6400 Option<&crate::security::CandidateAuthorization<'_>>,
6401 Option<&crate::auth::Principal>,
6402 ) -> Result<T>,
6403 {
6404 self.with_authorized_scored_read_context_at(
6405 table_name,
6406 principal,
6407 catalog_bound,
6408 authorization,
6409 context,
6410 None,
6411 |table, snapshot, authorization, principal| {
6412 let mut table = table.clone();
6413 read(&mut table, snapshot, authorization, principal)
6414 },
6415 )
6416 }
6417
6418 #[allow(clippy::too_many_arguments)]
6419 pub fn with_authorized_scored_read_context_at<T, F>(
6420 &self,
6421 table_name: &str,
6422 principal: Option<&crate::auth::Principal>,
6423 catalog_bound: bool,
6424 authorization: Option<&ReadAuthorization>,
6425 context: Option<&crate::query::AiExecutionContext>,
6426 snapshot_override: Option<Snapshot>,
6427 read: F,
6428 ) -> Result<T>
6429 where
6430 F: FnMut(
6431 &Table,
6432 Snapshot,
6433 Option<&crate::security::CandidateAuthorization<'_>>,
6434 Option<&crate::auth::Principal>,
6435 ) -> Result<T>,
6436 {
6437 self.with_authorized_scored_read_context_at_stamped(
6438 table_name,
6439 principal,
6440 catalog_bound,
6441 authorization,
6442 context,
6443 snapshot_override,
6444 read,
6445 )
6446 .map(|(result, _)| result)
6447 }
6448
6449 #[allow(clippy::too_many_arguments)]
6450 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
6451 &self,
6452 table_name: &str,
6453 principal: Option<&crate::auth::Principal>,
6454 catalog_bound: bool,
6455 authorization: Option<&ReadAuthorization>,
6456 context: Option<&crate::query::AiExecutionContext>,
6457 snapshot_override: Option<Snapshot>,
6458 mut read: F,
6459 ) -> Result<(T, AuthorizedReadStamp)>
6460 where
6461 F: FnMut(
6462 &Table,
6463 Snapshot,
6464 Option<&crate::security::CandidateAuthorization<'_>>,
6465 Option<&crate::auth::Principal>,
6466 ) -> Result<T>,
6467 {
6468 if principal.is_none() && self.principal.read().is_some() {
6469 self.refresh_principal()?;
6470 }
6471 const RETRIES: usize = 3;
6472 let handle = self.table(table_name)?;
6473 for attempt in 0..RETRIES {
6474 if let Some(context) = context {
6475 context.checkpoint()?;
6476 }
6477 crate::trace::QueryTrace::record(|trace| {
6478 trace.authorization_retries = attempt;
6479 });
6480 let (security, security_version, effective_principal) = {
6481 let catalog = self.catalog.read();
6482 (
6483 catalog.security.clone(),
6484 catalog.security_version,
6485 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
6486 )
6487 };
6488 if let Some(authorization) = authorization {
6489 for permission in &authorization.permissions {
6490 self.require_for(effective_principal.as_ref(), permission)?;
6491 }
6492 self.require_columns_for(
6493 table_name,
6494 authorization.operation,
6495 &authorization.columns,
6496 effective_principal.as_ref(),
6497 )?;
6498 }
6499 let result = {
6500 let (table, snapshot, _snapshot_guard, _run_pins) =
6501 self.scored_read_generation(&handle, context, snapshot_override)?;
6502 let candidate_authorization = if security.rls_enabled(table_name) {
6503 Some(crate::security::CandidateAuthorization {
6504 table: table_name,
6505 security: &security,
6506 principal: effective_principal
6507 .as_ref()
6508 .ok_or(MongrelError::AuthRequired)?,
6509 })
6510 } else {
6511 None
6512 };
6513 let stamp = AuthorizedReadStamp {
6514 table_id: table.table_id(),
6515 schema_id: table.schema().schema_id,
6516 data_generation: table.data_generation(),
6517 security_version,
6518 snapshot,
6519 };
6520 let result = read(
6521 table.as_ref(),
6522 snapshot,
6523 candidate_authorization.as_ref(),
6524 effective_principal.as_ref(),
6525 )?;
6526 (result, stamp)
6527 };
6528 if let Some(context) = context {
6529 context.checkpoint()?;
6530 }
6531 if self.catalog.read().security_version == security_version {
6532 return Ok(result);
6533 }
6534 if attempt + 1 == RETRIES {
6535 return Err(MongrelError::Conflict(
6536 "security policy changed during scored read".into(),
6537 ));
6538 }
6539 }
6540 Err(MongrelError::Conflict(
6541 "scored-read authorization retry loop exhausted".into(),
6542 ))
6543 }
6544
6545 fn scored_read_generation(
6546 &self,
6547 handle: &TableHandle,
6548 context: Option<&crate::query::AiExecutionContext>,
6549 snapshot_override: Option<Snapshot>,
6550 ) -> Result<(
6551 Arc<TableReadGeneration>,
6552 Snapshot,
6553 crate::retention::OwnedSnapshotGuard,
6554 RunPins,
6555 )> {
6556 let mut table = if let Some(context) = context {
6557 loop {
6558 context.checkpoint()?;
6559 let wait = context
6560 .remaining_duration()
6561 .unwrap_or(std::time::Duration::from_millis(5))
6562 .min(std::time::Duration::from_millis(5));
6563 if let Some(table) = handle.try_lock_for(wait) {
6564 break table;
6565 }
6566 }
6567 } else {
6568 handle.lock()
6569 };
6570 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
6571 self.snapshot_at_owned(snapshot.epoch)?
6572 } else {
6573 let snapshot = table.snapshot();
6574 let guard = self.snapshots.register_owned(snapshot.epoch);
6575 (snapshot, guard)
6576 };
6577 let table_id = table.table_id();
6578 let run_keys: Vec<_> = table
6579 .active_run_ids()
6580 .map(|run_id| (table_id, run_id))
6581 .collect();
6582 let generation = handle
6583 .generation_metrics
6584 .activate(table.clone_read_generation()?);
6585 let run_pins = self.pin_runs(&run_keys);
6586 Ok((generation, snapshot, snapshot_guard, run_pins))
6587 }
6588
6589 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
6590 let mut pins = self.backup_pins.lock();
6591 for run in runs {
6592 *pins.entry(*run).or_insert(0) += 1;
6593 }
6594 drop(pins);
6595 RunPins {
6596 pins: Arc::clone(&self.backup_pins),
6597 runs: runs.to_vec(),
6598 }
6599 }
6600
6601 pub fn query_for_current_principal(
6605 &self,
6606 table_name: &str,
6607 query: &crate::query::Query,
6608 projection: Option<&[u16]>,
6609 ) -> Result<Vec<crate::memtable::Row>> {
6610 let condition_columns = crate::query::condition_columns(&query.conditions);
6611 let catalog_bound = self
6612 .principal
6613 .read()
6614 .as_ref()
6615 .is_some_and(|principal| principal.user_id != 0);
6616 self.with_authorized_read(
6617 table_name,
6618 None,
6619 catalog_bound,
6620 |table, snapshot, allowed, principal| {
6621 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6622 self.require_columns_for(
6623 table_name,
6624 crate::auth::ColumnOperation::Select,
6625 &condition_columns,
6626 principal,
6627 )?;
6628 if let Some(projection) = projection {
6629 self.require_columns_for(
6630 table_name,
6631 crate::auth::ColumnOperation::Select,
6632 projection,
6633 principal,
6634 )?;
6635 }
6636 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
6637 let projection =
6638 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6639 for row in &mut rows {
6640 row.columns.retain(|column, _| {
6641 allowed_columns.contains(column)
6642 && projection
6643 .as_ref()
6644 .is_none_or(|projection| projection.contains(column))
6645 });
6646 }
6647 self.secure_rows_for(table_name, rows, principal)
6648 },
6649 )
6650 }
6651
6652 pub fn query_for_current_principal_controlled(
6656 &self,
6657 table_name: &str,
6658 query: &crate::query::Query,
6659 projection: Option<&[u16]>,
6660 control: &crate::ExecutionControl,
6661 ) -> Result<Vec<crate::memtable::Row>> {
6662 let catalog_bound = self
6663 .principal
6664 .read()
6665 .as_ref()
6666 .is_some_and(|principal| principal.user_id != 0);
6667 self.query_for_principal_controlled(
6668 table_name,
6669 query,
6670 projection,
6671 None,
6672 catalog_bound,
6673 control,
6674 )
6675 }
6676
6677 pub fn query_as_principal_controlled(
6681 &self,
6682 table_name: &str,
6683 query: &crate::query::Query,
6684 projection: Option<&[u16]>,
6685 principal: Option<&crate::auth::Principal>,
6686 control: &crate::ExecutionControl,
6687 ) -> Result<Vec<crate::memtable::Row>> {
6688 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6689 self.query_for_principal_controlled(
6690 table_name,
6691 query,
6692 projection,
6693 principal,
6694 catalog_bound,
6695 control,
6696 )
6697 }
6698
6699 fn query_for_principal_controlled(
6700 &self,
6701 table_name: &str,
6702 query: &crate::query::Query,
6703 projection: Option<&[u16]>,
6704 principal: Option<&crate::auth::Principal>,
6705 catalog_bound: bool,
6706 control: &crate::ExecutionControl,
6707 ) -> Result<Vec<crate::memtable::Row>> {
6708 control.checkpoint()?;
6709 let context = crate::query::AiExecutionContext::with_control(
6710 control.clone(),
6711 usize::MAX,
6712 crate::query::MAX_FUSED_CANDIDATES,
6713 );
6714 let condition_columns = crate::query::condition_columns(&query.conditions);
6715 self.with_authorized_read_context(
6716 table_name,
6717 principal,
6718 catalog_bound,
6719 None,
6720 Some(&context),
6721 None,
6722 |table, snapshot, allowed, principal| {
6723 control.checkpoint()?;
6724 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6725 self.require_columns_for(
6726 table_name,
6727 crate::auth::ColumnOperation::Select,
6728 &condition_columns,
6729 principal,
6730 )?;
6731 if let Some(projection) = projection {
6732 self.require_columns_for(
6733 table_name,
6734 crate::auth::ColumnOperation::Select,
6735 projection,
6736 principal,
6737 )?;
6738 }
6739 let rows =
6740 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
6741 let projection =
6742 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
6743 let mut projected = Vec::with_capacity(rows.len());
6744 for (index, mut row) in rows.into_iter().enumerate() {
6745 if index & 255 == 0 {
6746 control.checkpoint()?;
6747 }
6748 row.columns.retain(|column, _| {
6749 allowed_columns.contains(column)
6750 && projection
6751 .as_ref()
6752 .is_none_or(|projection| projection.contains(column))
6753 });
6754 projected.push(row);
6755 }
6756 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
6757 },
6758 )
6759 }
6760
6761 pub fn approx_aggregate_for_current_principal(
6764 &self,
6765 table_name: &str,
6766 conditions: &[crate::query::Condition],
6767 column: Option<u16>,
6768 agg: crate::engine::ApproxAgg,
6769 z: f64,
6770 ) -> Result<Option<crate::engine::ApproxResult>> {
6771 if !z.is_finite() || z <= 0.0 {
6772 return Err(MongrelError::InvalidArgument(
6773 "z must be finite and > 0".into(),
6774 ));
6775 }
6776 let mut columns = crate::query::condition_columns(conditions);
6777 columns.extend(column);
6778 columns.sort_unstable();
6779 columns.dedup();
6780 self.with_authorized_aggregate_table(
6781 table_name,
6782 &columns,
6783 None,
6784 true,
6785 true,
6786 |table, authorization, _, _| {
6787 table.approx_aggregate_with_candidate_authorization(
6788 conditions,
6789 column,
6790 agg,
6791 z,
6792 authorization,
6793 )
6794 },
6795 )
6796 }
6797
6798 pub fn incremental_aggregate_for_current_principal(
6802 &self,
6803 table_name: &str,
6804 conditions: &[crate::query::Condition],
6805 column: Option<u16>,
6806 agg: crate::engine::NativeAgg,
6807 ) -> Result<crate::engine::IncrementalAggResult> {
6808 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
6809 }
6810
6811 pub fn incremental_aggregate_for_principal(
6815 &self,
6816 table_name: &str,
6817 conditions: &[crate::query::Condition],
6818 column: Option<u16>,
6819 agg: crate::engine::NativeAgg,
6820 principal: Option<&crate::auth::Principal>,
6821 catalog_bound: bool,
6822 ) -> Result<crate::engine::IncrementalAggResult> {
6823 let mut columns = crate::query::condition_columns(conditions);
6824 columns.extend(column);
6825 columns.sort_unstable();
6826 columns.dedup();
6827 self.with_authorized_aggregate_table(
6828 table_name,
6829 &columns,
6830 principal,
6831 catalog_bound,
6832 false,
6833 |table, _, principal, security_version| {
6834 let cache_key = incremental_aggregate_cache_key(
6835 table_name,
6836 conditions,
6837 column,
6838 agg,
6839 principal,
6840 security_version,
6841 );
6842 table.aggregate_incremental(cache_key, conditions, column, agg)
6843 },
6844 )
6845 }
6846
6847 pub fn get_for_current_principal(
6850 &self,
6851 table_name: &str,
6852 row_id: RowId,
6853 ) -> Result<Option<crate::memtable::Row>> {
6854 self.with_authorized_read(
6855 table_name,
6856 None,
6857 true,
6858 |table, snapshot, allowed, principal| {
6859 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6860 let Some(row) = table.get(row_id, snapshot) else {
6861 return Ok(None);
6862 };
6863 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
6864 return Ok(None);
6865 }
6866 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6867 if let Some(row) = rows.first_mut() {
6868 row.columns
6869 .retain(|column, _| allowed_columns.contains(column));
6870 }
6871 Ok(rows.pop())
6872 },
6873 )
6874 }
6875
6876 pub fn ann_rerank_for_current_principal(
6879 &self,
6880 table_name: &str,
6881 request: &crate::query::AnnRerankRequest,
6882 ) -> Result<Vec<crate::query::AnnRerankHit>> {
6883 self.with_authorized_scored_read_context_at(
6884 table_name,
6885 None,
6886 true,
6887 Some(&ReadAuthorization {
6888 operation: crate::auth::ColumnOperation::Select,
6889 columns: vec![request.column_id],
6890 permissions: Vec::new(),
6891 }),
6892 None,
6893 None,
6894 |table, snapshot, authorization, principal| {
6895 self.require_columns_for(
6896 table_name,
6897 crate::auth::ColumnOperation::Select,
6898 &[request.column_id],
6899 principal,
6900 )?;
6901 table.ann_rerank_at_with_candidate_authorization_on_generation(
6902 request,
6903 snapshot,
6904 authorization,
6905 None,
6906 )
6907 },
6908 )
6909 }
6910
6911 pub fn search_for_current_principal(
6915 &self,
6916 table_name: &str,
6917 request: &crate::query::SearchRequest,
6918 ) -> Result<Vec<crate::query::SearchHit>> {
6919 let principal = self.principal_snapshot();
6920 self.search_for_principal_with_context(table_name, request, principal.as_ref(), None)
6921 }
6922
6923 pub fn search_for_principal_with_context(
6929 &self,
6930 table_name: &str,
6931 request: &crate::query::SearchRequest,
6932 principal: Option<&crate::auth::Principal>,
6933 context: Option<&crate::query::AiExecutionContext>,
6934 ) -> Result<Vec<crate::query::SearchHit>> {
6935 let mut columns = crate::query::condition_columns(&request.must);
6936 for named in &request.retrievers {
6937 columns.push(named.retriever.column_id());
6938 }
6939 if let Some(crate::query::Rerank::ExactVector {
6940 embedding_column, ..
6941 }) = &request.rerank
6942 {
6943 columns.push(*embedding_column);
6944 }
6945 if let Some(proj) = &request.projection {
6946 columns.extend(proj);
6947 }
6948 columns.sort_unstable();
6949 columns.dedup();
6950 let catalog_bound = principal.is_some_and(|principal| principal.user_id != 0);
6951 self.with_authorized_scored_read_context_at(
6952 table_name,
6953 principal,
6954 catalog_bound,
6955 Some(&ReadAuthorization {
6956 operation: crate::auth::ColumnOperation::Select,
6957 columns: columns.clone(),
6958 permissions: Vec::new(),
6959 }),
6960 context,
6961 None,
6962 |table, snapshot, authorization, principal| {
6963 self.require_columns_for(
6964 table_name,
6965 crate::auth::ColumnOperation::Select,
6966 &columns,
6967 principal,
6968 )?;
6969 let hits = table.search_at_with_candidate_authorization_on_generation(
6970 request,
6971 snapshot,
6972 authorization,
6973 context,
6974 )?;
6975 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
6976 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
6977 for mut hit in hits {
6978 let row = crate::memtable::Row {
6979 row_id: hit.row_id,
6980 committed_epoch: crate::Epoch(0),
6981 columns: hit
6982 .cells
6983 .iter()
6984 .cloned()
6985 .collect::<std::collections::HashMap<u16, crate::Value>>(),
6986 deleted: false,
6987 commit_ts: None,
6988 };
6989 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
6990 if let Some(mut row) = rows.pop() {
6991 row.columns
6992 .retain(|column, _| allowed_columns.contains(column));
6993 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
6994 hit.cells.sort_by_key(|(column, _)| *column);
6995 secured.push(hit);
6996 }
6997 }
6998 Ok(secured)
6999 },
7000 )
7001 }
7002
7003 pub fn retrieve_text(
7006 &self,
7007 table: &str,
7008 embedding_column: u16,
7009 text: &str,
7010 search_options: crate::embedding::TextSearchOptions,
7011 ) -> Result<crate::embedding::TextRetrieveResult> {
7012 let principal = self.principal_snapshot();
7013 self.retrieve_text_for_principal(
7014 table,
7015 embedding_column,
7016 text,
7017 search_options,
7018 principal.as_ref(),
7019 )
7020 }
7021
7022 pub fn retrieve_text_for_principal(
7023 &self,
7024 table: &str,
7025 embedding_column: u16,
7026 text: &str,
7027 search_options: crate::embedding::TextSearchOptions,
7028 principal: Option<&crate::auth::Principal>,
7029 ) -> Result<crate::embedding::TextRetrieveResult> {
7030 if search_options.k == 0 {
7031 return Err(MongrelError::InvalidArgument(
7032 "retrieve_text k must be greater than zero".into(),
7033 ));
7034 }
7035 let catalog_bound = principal.is_some_and(|p| p.user_id != 0);
7036 let (semantic_identity, registry_generation, expected_dim) = {
7037 let handle = self.table(table)?;
7038 let mut g = handle.lock();
7039 g.ensure_indexes_complete()?;
7040 let schema = g.schema().clone();
7041 let column = schema
7042 .columns
7043 .iter()
7044 .find(|c| c.id == embedding_column)
7045 .ok_or_else(|| {
7046 MongrelError::ColumnNotFound(format!(
7047 "embedding column {embedding_column} not found on table {table}"
7048 ))
7049 })?;
7050 let dim = match column.ty {
7051 crate::schema::TypeId::Embedding { dim } => dim,
7052 _ => {
7053 return Err(MongrelError::InvalidArgument(format!(
7054 "column {embedding_column} is not an embedding column"
7055 )))
7056 }
7057 };
7058 if let Some(identity) = g
7059 .ann_index(embedding_column)
7060 .and_then(|a| a.semantic_identity().cloned())
7061 {
7062 let (generation, provider) = self
7063 .embedding_providers
7064 .resolve_by_semantic_identity(&identity)
7065 .map_err(embedding_error)?;
7066 if provider.dimension() != dim {
7067 return Err(embedding_error(
7068 crate::embedding::EmbeddingError::DimensionMismatch {
7069 expected: dim,
7070 got: provider.dimension(),
7071 },
7072 ));
7073 }
7074 (identity, generation, dim)
7075 } else {
7076 let source = column.embedding_source.clone().ok_or_else(|| {
7077 embedding_error(crate::embedding::EmbeddingError::NoActiveAnnIdentity(
7078 embedding_column,
7079 ))
7080 })?;
7081 let (generation, provider) = self
7082 .embedding_providers
7083 .resolve_with_generation(&source)
7084 .map_err(embedding_error)?;
7085 let identity = provider.semantic_identity();
7086 if identity.dimension != dim {
7087 return Err(embedding_error(
7088 crate::embedding::EmbeddingError::DimensionMismatch {
7089 expected: dim,
7090 got: identity.dimension,
7091 },
7092 ));
7093 }
7094 (identity, generation, dim)
7095 }
7096 };
7097 let (_g, provider) = self
7098 .embedding_providers
7099 .resolve_by_semantic_identity(&semantic_identity)
7100 .map_err(embedding_error)?;
7101 let control = crate::ExecutionControl::new(None);
7102 let response = provider
7103 .embed(crate::embedding::EmbeddingRequest {
7104 texts: &[text],
7105 control: &control,
7106 trace_id: "retrieve-text",
7107 })
7108 .map_err(embedding_error)?;
7109 crate::embedding::validate_response(
7110 provider.as_ref(),
7111 1,
7112 expected_dim,
7113 crate::embedding::EmbeddingLimits::default(),
7114 &response.vectors,
7115 )
7116 .map_err(embedding_error)?;
7117 let query = response.vectors.into_iter().next().ok_or_else(|| {
7118 MongrelError::Other("embedding provider returned no query vector".into())
7119 })?;
7120 if provider.semantic_identity() != semantic_identity {
7121 return Err(embedding_error(
7122 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7123 column_id: embedding_column,
7124 expected: semantic_identity.fingerprint_sha256(),
7125 got: provider.semantic_identity().fingerprint_sha256(),
7126 },
7127 ));
7128 }
7129 let retriever = crate::query::Retriever::Ann {
7130 column_id: embedding_column,
7131 query,
7132 k: search_options.k,
7133 };
7134 let hits = self.with_authorized_scored_read_context_at(
7135 table,
7136 principal,
7137 catalog_bound,
7138 Some(&ReadAuthorization {
7139 operation: crate::auth::ColumnOperation::Select,
7140 columns: vec![embedding_column],
7141 permissions: Vec::new(),
7142 }),
7143 None,
7144 None,
7145 |table_guard, snapshot, authorization, principal| {
7146 self.require_columns_for(
7147 table,
7148 crate::auth::ColumnOperation::Select,
7149 &[embedding_column],
7150 principal,
7151 )?;
7152 if let Some(ann) = table_guard.ann_index(embedding_column) {
7153 if let Some(active) = ann.semantic_identity() {
7154 if active != &semantic_identity {
7155 return Err(embedding_error(
7156 crate::embedding::EmbeddingError::AnnSemanticIdentityMismatch {
7157 column_id: embedding_column,
7158 expected: semantic_identity.fingerprint_sha256(),
7159 got: active.fingerprint_sha256(),
7160 },
7161 ));
7162 }
7163 }
7164 }
7165 table_guard.retrieve_at_with_candidate_authorization_on_generation(
7166 &retriever,
7167 snapshot,
7168 authorization,
7169 None,
7170 )
7171 },
7172 )?;
7173 Ok(crate::embedding::TextRetrieveResult {
7174 hits,
7175 provenance: crate::embedding::TextRetrieveProvenance {
7176 semantic_identity,
7177 provider_registry_generation: registry_generation,
7178 query_source_fingerprint: Sha256::digest(text.as_bytes()).into(),
7179 embedding_column,
7180 },
7181 })
7182 }
7183
7184 pub fn authorized_read_snapshot(
7187 &self,
7188 table: &str,
7189 principal: Option<&crate::auth::Principal>,
7190 ) -> Result<AuthorizedReadSnapshot> {
7191 let (security, security_version, effective_principal) = {
7192 let catalog = self.catalog.read();
7193 (
7194 catalog.security.clone(),
7195 catalog.security_version,
7196 self.principal_for_authorized_read(&catalog, principal, false)?,
7197 )
7198 };
7199 let handle = self.table(table)?;
7200 let (table_snapshot, data_generation, allowed_row_ids) = {
7201 let table_handle = handle.lock();
7202 let table_snapshot = table_handle.snapshot();
7203 let data_generation = table_handle.data_generation();
7204 let allowed = self.allowed_row_ids_locked(
7205 table,
7206 &table_handle,
7207 table_snapshot,
7208 (&security, security_version),
7209 effective_principal.as_ref(),
7210 None,
7211 )?;
7212 (
7213 table_snapshot,
7214 data_generation,
7215 allowed.map(|allowed| (*allowed).clone()),
7216 )
7217 };
7218 Ok(AuthorizedReadSnapshot {
7219 table: table.to_string(),
7220 table_snapshot,
7221 data_generation,
7222 security_version,
7223 allowed_row_ids,
7224 })
7225 }
7226
7227 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
7228 if self.catalog.read().security_version != snapshot.security_version {
7229 return false;
7230 }
7231 self.table(&snapshot.table)
7232 .ok()
7233 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
7234 }
7235
7236 pub fn rls_cache_stats(&self) -> RlsCacheStats {
7237 self.rls_cache.lock().stats()
7238 }
7239
7240 pub fn rows_for(
7242 &self,
7243 table: &str,
7244 principal: Option<&crate::auth::Principal>,
7245 ) -> Result<Vec<crate::memtable::Row>> {
7246 if principal.is_none() && self.principal.read().is_some() {
7247 self.refresh_principal()?;
7248 }
7249 let allowed = self.select_column_ids_for(table, principal)?;
7250 let handle = self.table(table)?;
7251 let rows = {
7252 let table = handle.lock();
7253 table.visible_rows(table.snapshot())?
7254 };
7255 let mut rows = self.secure_rows_for(table, rows, principal)?;
7256 for row in &mut rows {
7257 row.columns.retain(|column, _| allowed.contains(column));
7258 }
7259 Ok(rows)
7260 }
7261
7262 pub fn rows_at_epoch_for_current_principal(
7265 &self,
7266 table_name: &str,
7267 snapshot: Snapshot,
7268 ) -> Result<Vec<crate::memtable::Row>> {
7269 self.with_authorized_read_context(
7270 table_name,
7271 None,
7272 true,
7273 Some(&ReadAuthorization {
7274 operation: crate::auth::ColumnOperation::Select,
7275 columns: Vec::new(),
7276 permissions: Vec::new(),
7277 }),
7278 None,
7279 Some(snapshot),
7280 |table, snapshot, allowed, principal| {
7281 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
7282 let mut rows = table.visible_rows(snapshot)?;
7283 if let Some(allowed) = allowed {
7284 rows.retain(|row| allowed.contains(&row.row_id));
7285 }
7286 rows = self.secure_rows_for(table_name, rows, principal)?;
7287 for row in &mut rows {
7288 row.columns
7289 .retain(|column, _| allowed_columns.contains(column));
7290 }
7291 Ok(rows)
7292 },
7293 )
7294 }
7295
7296 pub fn count_for(
7298 &self,
7299 table: &str,
7300 principal: Option<&crate::auth::Principal>,
7301 ) -> Result<u64> {
7302 if principal.is_none() && self.principal.read().is_some() {
7303 self.refresh_principal()?;
7304 }
7305 self.select_column_ids_for(table, principal)?;
7306 if self.security_active_for(table) {
7307 Ok(self.rows_for(table, principal)?.len() as u64)
7308 } else {
7309 Ok(self.table(table)?.lock().count())
7310 }
7311 }
7312
7313 pub fn put_for(
7315 &self,
7316 table: &str,
7317 mut cells: Vec<(u16, crate::memtable::Value)>,
7318 principal: Option<&crate::auth::Principal>,
7319 ) -> Result<RowId> {
7320 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
7321 self.require_columns_for(
7322 table,
7323 crate::auth::ColumnOperation::Insert,
7324 &columns,
7325 principal,
7326 )?;
7327 let handle = self.table(table)?;
7328 let mut table_handle = handle.lock();
7329 table_handle.fill_auto_inc(&mut cells)?;
7330 table_handle.apply_defaults(&mut cells)?;
7331 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
7332 row.columns.extend(cells.iter().cloned());
7333 self.check_row_policy_for(
7334 table,
7335 crate::security::PolicyCommand::Insert,
7336 &row,
7337 true,
7338 principal,
7339 )?;
7340 table_handle.put(cells)
7341 }
7342
7343 pub fn check_row_policy_for(
7344 &self,
7345 table: &str,
7346 command: crate::security::PolicyCommand,
7347 row: &crate::memtable::Row,
7348 check_new: bool,
7349 principal: Option<&crate::auth::Principal>,
7350 ) -> Result<()> {
7351 let security = self.catalog.read().security.clone();
7352 if !security.rls_enabled(table) {
7353 return Ok(());
7354 }
7355 let cached = self.principal.read().clone();
7356 let principal = principal
7357 .or(cached.as_ref())
7358 .ok_or(MongrelError::AuthRequired)?;
7359 if security.row_allowed(table, command, row, principal, check_new) {
7360 return Ok(());
7361 }
7362 let required = match command {
7363 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
7364 table: table.to_string(),
7365 },
7366 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
7367 table: table.to_string(),
7368 },
7369 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
7370 table: table.to_string(),
7371 },
7372 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
7373 crate::auth::Permission::Delete {
7374 table: table.to_string(),
7375 }
7376 }
7377 };
7378 Err(MongrelError::PermissionDenied {
7379 required,
7380 principal: principal.username.clone(),
7381 })
7382 }
7383
7384 pub fn set_materialized_view(
7387 &self,
7388 definition: crate::catalog::MaterializedViewEntry,
7389 ) -> Result<()> {
7390 self.set_materialized_view_with_epoch(definition)
7391 .map(|_| ())
7392 }
7393
7394 pub fn set_materialized_view_with_epoch(
7396 &self,
7397 definition: crate::catalog::MaterializedViewEntry,
7398 ) -> Result<Epoch> {
7399 use crate::wal::DdlOp;
7400 use std::sync::atomic::Ordering;
7401
7402 let command = crate::catalog_cmds::CatalogCommand::CreateMaterializedView {
7403 definition: definition.clone(),
7404 };
7405 self.require(&crate::catalog_cmds::required_permission(&command))?;
7406 if self.poisoned.load(Ordering::Relaxed) {
7407 return Err(MongrelError::Other(
7408 "database poisoned by fsync error".into(),
7409 ));
7410 }
7411 if definition.name.is_empty() || definition.query.trim().is_empty() {
7412 return Err(MongrelError::InvalidArgument(
7413 "materialized view name and query must not be empty".into(),
7414 ));
7415 }
7416 let _operation = self.admit_operation()?;
7419 let _ddl = self.ddl_lock.lock();
7420 let _security_write = self.security_write()?;
7421 self.require(&crate::catalog_cmds::required_permission(&command))?;
7422 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
7426 let table_id = self
7427 .catalog
7428 .read()
7429 .live(&definition.name)
7430 .ok_or_else(|| {
7431 MongrelError::NotFound(format!(
7432 "materialized view table {:?} not found",
7433 definition.name
7434 ))
7435 })?
7436 .table_id;
7437 let definition_json = DdlOp::encode_materialized_view(&definition)?;
7438 let _commit = self.commit_lock.lock();
7439 let epoch = self.epoch.bump_assigned();
7440 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7441 let txn_id = self.alloc_txn_id()?;
7442 let mut next_catalog = self.catalog.read().clone();
7443 self.apply_catalog_command_to(&mut next_catalog, command)?;
7444 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
7445 let commit_seq = {
7446 let mut wal = self.shared_wal.lock();
7447 let append: Result<u64> = (|| {
7448 wal.append(
7449 txn_id,
7450 table_id,
7451 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
7452 name: definition.name.clone(),
7453 definition_json,
7454 }),
7455 )?;
7456 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
7457 wal.append_commit(txn_id, epoch, &[])
7458 })();
7459 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
7460 };
7461 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
7462
7463 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
7464 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
7465 Ok(epoch)
7466 }
7467
7468 pub fn root(&self) -> &Path {
7470 self.durable_root.canonical_path()
7471 }
7472
7473 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
7476 Arc::clone(&self.durable_root)
7477 }
7478
7479 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
7483 self.kek
7484 .as_deref()
7485 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
7486 }
7487
7488 pub fn is_read_only_replica(&self) -> bool {
7489 self.read_only
7490 }
7491
7492 pub fn ensure_consistent_read(&self) -> Result<()> {
7496 self.ensure_owner_process()?;
7497 if self.poisoned.load(Ordering::Relaxed) {
7498 return Err(MongrelError::Other(
7499 "database poisoned by post-commit failure; reopen required".into(),
7500 ));
7501 }
7502 Ok(())
7503 }
7504
7505 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
7506 self.replication_wal_retention_segments
7507 .store(segments, std::sync::atomic::Ordering::Relaxed);
7508 }
7509
7510 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
7515 let admin = crate::auth::Permission::Admin;
7516 self.require(&admin)?;
7517 let _operation = self.admit_operation()?;
7519 let operation_principal = self.principal_snapshot();
7520 let _barrier = self.replication_barrier.write();
7521 let _ddl = self.ddl_lock.lock();
7522 let _security = self.security_coordinator.gate.read();
7523 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7524 let mut handles: Vec<_> = self
7525 .tables
7526 .read()
7527 .iter()
7528 .map(|(id, handle)| (*id, handle.clone()))
7529 .collect();
7530 handles.sort_by_key(|(id, _)| *id);
7531 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7532 let _commit = self.commit_lock.lock();
7533 let mut wal = self.shared_wal.lock();
7534 wal.group_sync()?;
7535 let io_root = self.durable_root.io_path()?;
7536 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7537 let records = crate::wal::SharedWal::replay_with_dek(&io_root, wal_dek.as_ref())?;
7538 let epoch = records
7539 .iter()
7540 .filter_map(|record| match &record.op {
7541 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
7542 _ => None,
7543 })
7544 .max()
7545 .unwrap_or(0)
7546 .max(self.epoch.committed().0);
7547 let files = crate::replication::capture_files(&io_root)?;
7548 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7549 drop(wal);
7550 Ok(crate::replication::ReplicationSnapshot::new(
7551 source_id, epoch, files,
7552 ))
7553 }
7554
7555 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
7563 let control = crate::ExecutionControl::new(None);
7564 self.hot_backup_controlled(destination, &control, || true)
7565 }
7566
7567 pub(crate) fn hot_backup_to_durable_child(
7568 &self,
7569 parent: &crate::durable_file::DurableRoot,
7570 child: &Path,
7571 control: &crate::ExecutionControl,
7572 ) -> Result<crate::backup::BackupReport> {
7573 let mut components = child.components();
7574 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
7575 || components.next().is_some()
7576 {
7577 return Err(MongrelError::InvalidArgument(
7578 "durable backup child must be one relative path component".into(),
7579 ));
7580 }
7581 let destination_name = child.file_name().ok_or_else(|| {
7582 MongrelError::InvalidArgument("durable backup child has no filename".into())
7583 })?;
7584 let source_root = self.durable_root.io_path()?;
7585 let prepared = prepare_backup_destination_in(&source_root, parent, destination_name)?;
7586 self.hot_backup_prepared(prepared, control, || true)
7587 }
7588
7589 #[doc(hidden)]
7592 pub fn hot_backup_controlled<F>(
7593 &self,
7594 destination: impl AsRef<Path>,
7595 control: &crate::ExecutionControl,
7596 before_publish: F,
7597 ) -> Result<crate::backup::BackupReport>
7598 where
7599 F: FnOnce() -> bool,
7600 {
7601 let source_root = self.durable_root.io_path()?;
7602 let prepared = prepare_backup_destination(&source_root, destination.as_ref())?;
7603 self.hot_backup_prepared(prepared, control, before_publish)
7604 }
7605
7606 fn hot_backup_prepared<F>(
7607 &self,
7608 mut prepared: PreparedBackupDestination,
7609 control: &crate::ExecutionControl,
7610 before_publish: F,
7611 ) -> Result<crate::backup::BackupReport>
7612 where
7613 F: FnOnce() -> bool,
7614 {
7615 let admin = crate::auth::Permission::Admin;
7616 self.require(&admin)?;
7617 let _operation = self.admit_operation()?;
7620 let operation_principal = self.principal_snapshot();
7621 control.checkpoint()?;
7622 let destination = prepared.destination_path.clone();
7623 let mut before_publish = Some(before_publish);
7624
7625 let outcome = (|| {
7626 control.checkpoint()?;
7627 let barrier = self.replication_barrier.write();
7628 let ddl = self.ddl_lock.lock();
7629 let security = self.security_coordinator.gate.read();
7630 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7631 let mut handles: Vec<_> = self
7632 .tables
7633 .read()
7634 .iter()
7635 .map(|(id, handle)| (*id, handle.clone()))
7636 .collect();
7637 handles.sort_by_key(|(id, _)| *id);
7638 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
7639 let commit = self.commit_lock.lock();
7640 let mut wal = self.shared_wal.lock();
7641 wal.group_sync()?;
7642 let epoch = self.epoch.committed().0;
7643 let boundary_unix_nanos = current_unix_nanos();
7644 let source_root = self.durable_root.io_path()?;
7645
7646 let pin_nonce = std::time::SystemTime::now()
7647 .duration_since(std::time::UNIX_EPOCH)
7648 .unwrap_or_default()
7649 .as_nanos();
7650 let file_pin_root = source_root
7651 .join(META_DIR)
7652 .join("backup-pins")
7653 .join(format!("{}-{pin_nonce}", std::process::id()));
7654 std::fs::create_dir_all(&file_pin_root)?;
7655 let _file_pins = BackupFilePins {
7656 root: file_pin_root.clone(),
7657 };
7658 let mut run_files = Vec::new();
7659 for (index, (table_id, _)) in handles.iter().enumerate() {
7660 if index % 256 == 0 {
7661 control.checkpoint()?;
7662 }
7663 let table = &table_guards[index];
7664 for (run_index, run) in table.run_refs().iter().enumerate() {
7665 if run_index % 256 == 0 {
7666 control.checkpoint()?;
7667 }
7668 let source = table.run_path(run.run_id as u64);
7669 let relative = Path::new(TABLES_DIR)
7670 .join(table_id.to_string())
7671 .join(crate::engine::RUNS_DIR)
7672 .join(format!("r-{}.sr", run.run_id));
7673 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
7674 if std::fs::hard_link(&source, &pinned).is_err() {
7675 crate::backup::copy_file_synced(&source, &pinned)?;
7676 }
7677 run_files.push(((*table_id, run.run_id), pinned, relative));
7678 }
7679 }
7680 crate::durable_file::sync_directory(&file_pin_root)?;
7681 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
7682 {
7683 let mut pins = self.backup_pins.lock();
7684 for key in &run_keys {
7685 *pins.entry(*key).or_insert(0) += 1;
7686 }
7687 }
7688 let _run_pins = RunPins {
7689 pins: Arc::clone(&self.backup_pins),
7690 runs: run_keys,
7691 };
7692 let _registry_pins: Vec<crate::retention::PinGuard> = table_guards
7699 .iter()
7700 .map(|table| {
7701 table
7702 .pin_registry()
7703 .pin(crate::retention::PinSource::BackupPitr, Epoch(epoch))
7704 })
7705 .collect();
7706 let deferred: HashSet<_> = run_files
7707 .iter()
7708 .map(|(_, _, relative)| relative.clone())
7709 .collect();
7710 let mut copied = Vec::new();
7711 copy_backup_boundary(
7712 &source_root,
7713 prepared.stage.as_deref().ok_or_else(|| {
7714 MongrelError::Other("backup staging root was already released".into())
7715 })?,
7716 &deferred,
7717 &mut copied,
7718 Some(control),
7719 )?;
7720
7721 drop(wal);
7722 drop(commit);
7723 drop(table_guards);
7724 drop(security);
7725 drop(ddl);
7726 drop(barrier);
7727
7728 if let Some(hook) = self.backup_hook.lock().as_ref() {
7729 hook();
7730 }
7731 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
7732 if index % 256 == 0 {
7733 control.checkpoint()?;
7734 }
7735 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
7736 prepared
7737 .stage
7738 .as_deref()
7739 .ok_or_else(|| {
7740 MongrelError::Other("backup staging root was already released".into())
7741 })?
7742 .copy_new_from(&relative, &mut source)?;
7743 copied.push(relative);
7744 }
7745
7746 let manifest = crate::backup::BackupManifest::create_controlled_durable(
7747 prepared.stage.as_deref().ok_or_else(|| {
7748 MongrelError::Other("backup staging root was already released".into())
7749 })?,
7750 epoch,
7751 &copied,
7752 control,
7753 )?;
7754 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
7755 MongrelError::Other("backup staging root was already released".into())
7756 })?)?;
7757 control.checkpoint()?;
7758 let publish = before_publish.take().ok_or_else(|| {
7759 MongrelError::Other("backup publication callback already consumed".into())
7760 })?;
7761 if !publish() {
7762 return Err(MongrelError::Cancelled);
7763 }
7764 let final_security = self.security_coordinator.gate.read();
7765 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7766 drop(prepared.stage.take().ok_or_else(|| {
7770 MongrelError::Other("backup staging root was already released".into())
7771 })?);
7772 let published = std::cell::Cell::new(false);
7773 if let Err(error) = prepared.parent.rename_directory_new_with_after(
7774 Path::new(&prepared.stage_name),
7775 &prepared.parent,
7776 Path::new(&prepared.destination_name),
7777 || published.set(true),
7778 ) {
7779 if published.get() {
7780 return Err(MongrelError::CommitOutcomeUnknown {
7781 epoch,
7782 message: format!("backup publication was not durable: {error}"),
7783 });
7784 }
7785 return Err(error.into());
7786 }
7787 drop(final_security);
7788 Ok(crate::backup::BackupReport {
7789 destination,
7790 epoch,
7791 boundary_unix_nanos,
7792 files: manifest.files.len(),
7793 bytes: manifest.total_bytes(),
7794 })
7795 })();
7796
7797 if outcome.is_err() {
7798 drop(prepared.stage.take());
7799 let _ = prepared
7800 .parent
7801 .remove_directory_all(Path::new(&prepared.stage_name));
7802 }
7803 outcome
7804 }
7805
7806 pub fn replication_batch_since(
7809 &self,
7810 since_epoch: u64,
7811 ) -> Result<crate::replication::ReplicationBatch> {
7812 use crate::wal::Op;
7813
7814 let admin = crate::auth::Permission::Admin;
7815 self.require(&admin)?;
7816 let _operation = self.admit_operation()?;
7818 let operation_principal = self.principal_snapshot();
7819
7820 let mut wal = self.shared_wal.lock();
7821 wal.group_sync()?;
7822 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7823 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
7824 drop(wal);
7825
7826 let commits: HashMap<u64, u64> = records
7827 .iter()
7828 .filter_map(|record| match &record.op {
7829 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
7830 _ => None,
7831 })
7832 .collect();
7833 let earliest_epoch = commits.values().copied().min();
7834 let current_epoch = commits
7835 .values()
7836 .copied()
7837 .max()
7838 .unwrap_or(0)
7839 .max(self.epoch.committed().0);
7840 let selected: HashSet<u64> = commits
7841 .iter()
7842 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
7843 .collect();
7844 let retention_gap = since_epoch < current_epoch
7845 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
7846 let spilled = records.iter().any(|record| {
7847 selected.contains(&record.txn_id)
7848 && matches!(
7849 &record.op,
7850 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
7851 )
7852 });
7853 let records = records
7854 .into_iter()
7855 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
7856 .filter(|record| selected.contains(&record.txn_id))
7857 .collect();
7858 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
7859 let batch = crate::replication::ReplicationBatch::complete_for_source(
7860 source_id,
7861 since_epoch,
7862 current_epoch,
7863 earliest_epoch,
7864 retention_gap,
7865 spilled,
7866 records,
7867 )?;
7868 if let Some(hook) = self.replication_hook.lock().as_ref() {
7869 hook();
7870 }
7871 let _security = self.security_coordinator.gate.read();
7872 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
7873 Ok(batch)
7874 }
7875
7876 pub fn append_replication_batch(
7880 &self,
7881 batch: &crate::replication::ReplicationBatch,
7882 ) -> Result<u64> {
7883 use crate::wal::Op;
7884
7885 if !self.read_only {
7886 return Err(MongrelError::InvalidArgument(
7887 "replication batches may only target a marked replica".into(),
7888 ));
7889 }
7890 let _operation = self.admit_operation()?;
7892 let current = crate::replication::replica_epoch(&self.root)?;
7893 if batch.is_source_bound() {
7894 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
7895 if batch.source_id != source_id {
7896 return Err(MongrelError::Conflict(
7897 "replication batch source does not match follower binding".into(),
7898 ));
7899 }
7900 }
7901 if batch.requires_snapshot {
7902 return Err(MongrelError::Conflict(
7903 "replication snapshot required for this batch".into(),
7904 ));
7905 }
7906 batch.validate_proof()?;
7907 if batch.from_epoch != current {
7908 if batch.from_epoch < current && batch.current_epoch == current {
7909 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
7910 let _wal = self.shared_wal.lock();
7911 let existing: HashSet<(u64, u64)> =
7912 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
7913 .into_iter()
7914 .filter_map(|record| match record.op {
7915 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
7916 _ => None,
7917 })
7918 .collect();
7919 let already_applied = batch.records.iter().all(|record| match &record.op {
7920 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
7921 _ => true,
7922 });
7923 if already_applied {
7924 return Ok(current);
7925 }
7926 }
7927 return Err(MongrelError::Conflict(format!(
7928 "replication batch starts at epoch {}, follower is at epoch {current}",
7929 batch.from_epoch
7930 )));
7931 }
7932 if batch.current_epoch < current {
7933 return Err(MongrelError::InvalidArgument(format!(
7934 "replication batch current epoch {} precedes follower epoch {current}",
7935 batch.current_epoch
7936 )));
7937 }
7938 let records = &batch.records;
7939 let mut commits = HashMap::new();
7940 let mut commit_epochs = HashSet::new();
7941 let mut commit_timestamps = HashMap::new();
7942 for record in records {
7943 match &record.op {
7944 Op::TxnCommit { epoch, added_runs } => {
7945 if !added_runs.is_empty() {
7946 return Err(MongrelError::Conflict(
7947 "replication snapshot required for spilled-run transaction".into(),
7948 ));
7949 }
7950 if commits.insert(record.txn_id, *epoch).is_some() {
7951 return Err(MongrelError::InvalidArgument(format!(
7952 "duplicate commit for replication transaction {}",
7953 record.txn_id
7954 )));
7955 }
7956 if *epoch <= current || *epoch > batch.current_epoch {
7957 return Err(MongrelError::InvalidArgument(format!(
7958 "replication commit epoch {epoch} is outside ({current}, {}]",
7959 batch.current_epoch
7960 )));
7961 }
7962 if !commit_epochs.insert(*epoch) {
7963 return Err(MongrelError::InvalidArgument(format!(
7964 "duplicate replication commit epoch {epoch}"
7965 )));
7966 }
7967 }
7968 Op::CommitTimestamp { unix_nanos } => {
7969 commit_timestamps.insert(record.txn_id, *unix_nanos);
7970 }
7971 _ => {}
7972 }
7973 }
7974 for record in records {
7975 if record.txn_id == crate::wal::SYSTEM_TXN_ID
7976 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
7977 {
7978 return Err(MongrelError::InvalidArgument(
7979 "replication batch contains a non-committed record".into(),
7980 ));
7981 }
7982 if !commits.contains_key(&record.txn_id) {
7983 return Err(MongrelError::InvalidArgument(format!(
7984 "incomplete replication transaction {}",
7985 record.txn_id
7986 )));
7987 }
7988 }
7989 let target_epoch = commits
7990 .values()
7991 .copied()
7992 .filter(|epoch| *epoch > current)
7993 .max()
7994 .unwrap_or(current);
7995 if target_epoch != batch.current_epoch {
7996 return Err(MongrelError::InvalidArgument(format!(
7997 "replication batch ends at epoch {target_epoch}, expected {}",
7998 batch.current_epoch
7999 )));
8000 }
8001 let mut selected: HashSet<u64> = commits
8002 .iter()
8003 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
8004 .collect();
8005 if selected.is_empty() {
8006 return Ok(current);
8007 }
8008 let mut wal = self.shared_wal.lock();
8009 wal.group_sync()?;
8010 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
8011 let existing: HashSet<(u64, u64)> =
8012 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
8013 .into_iter()
8014 .filter_map(|record| match record.op {
8015 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
8016 _ => None,
8017 })
8018 .collect();
8019 selected.retain(|txn_id| {
8020 commits
8021 .get(txn_id)
8022 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
8023 });
8024 for record in records {
8025 if !selected.contains(&record.txn_id) {
8026 continue;
8027 }
8028 match &record.op {
8029 Op::TxnCommit { epoch, added_runs } => {
8030 let timestamp = commit_timestamps
8031 .get(&record.txn_id)
8032 .copied()
8033 .unwrap_or_else(current_unix_nanos);
8034 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
8035 }
8036 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
8037 op => {
8038 wal.append(record.txn_id, 0, op.clone())?;
8039 }
8040 }
8041 }
8042 if !selected.is_empty() {
8043 wal.group_sync()?;
8044 }
8045 drop(wal);
8046
8047 let mut recovered_catalog = self.catalog.read().clone();
8051 if let Err(error) = recover_ddl_from_wal(
8052 &self.root,
8053 Some(&self.durable_root),
8054 &mut recovered_catalog,
8055 self.meta_dek.as_ref(),
8056 wal_dek.as_ref(),
8057 true,
8058 None,
8059 ) {
8060 return Err(MongrelError::DurableCommit {
8061 epoch: target_epoch,
8062 message: format!(
8063 "replication WAL is durable but catalog checkpoint failed: {error}"
8064 ),
8065 });
8066 }
8067 let _security = self.security_coordinator.gate.write();
8068 let old_security_version = self.catalog.read().security_version;
8069 let security_changed = old_security_version != recovered_catalog.security_version
8070 || self.catalog.read().require_auth != recovered_catalog.require_auth;
8071 let require_auth = recovered_catalog.require_auth;
8072 let principal = if security_changed {
8073 None
8074 } else {
8075 self.principal.read().as_ref().and_then(|principal| {
8076 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
8077 })
8078 };
8079 if require_auth {
8080 self.auth_state.set_require_auth(true);
8081 }
8082 self.auth_state.set_principal(principal.clone());
8083 *self.principal.write() = principal;
8084 let security_version = recovered_catalog.security_version;
8085 *self.catalog.write() = recovered_catalog;
8086 self.security_coordinator
8087 .version
8088 .store(security_version, Ordering::Release);
8089 if !require_auth {
8090 self.auth_state.set_require_auth(false);
8091 }
8092 if let Err(error) =
8093 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
8094 {
8095 return Err(MongrelError::DurableCommit {
8096 epoch: target_epoch,
8097 message: format!(
8098 "replication WAL and catalog are durable but follower watermark failed: {error}"
8099 ),
8100 });
8101 }
8102 Ok(target_epoch)
8103 }
8104
8105 pub fn table_id(&self, name: &str) -> Result<u64> {
8108 let cat = self.catalog.read();
8109 cat.live(name)
8110 .map(|e| e.table_id)
8111 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
8112 }
8113
8114 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
8119 let catalog = self.catalog.read();
8120 catalog
8121 .live(name)
8122 .map(|entry| (entry.table_id, entry.schema.schema_id))
8123 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
8124 }
8125
8126 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
8127 self.catalog
8128 .read()
8129 .building(name)
8130 .map(|entry| entry.table_id)
8131 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
8132 }
8133
8134 pub fn procedures(&self) -> Vec<StoredProcedure> {
8135 self.catalog
8136 .read()
8137 .procedures
8138 .iter()
8139 .map(|p| p.procedure.clone())
8140 .collect()
8141 }
8142
8143 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
8144 self.catalog
8145 .read()
8146 .procedures
8147 .iter()
8148 .find(|p| p.procedure.name == name)
8149 .map(|p| p.procedure.clone())
8150 }
8151
8152 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
8153 self.create_procedure_inner(procedure, None)
8154 }
8155
8156 pub fn create_procedure_controlled<F>(
8157 &self,
8158 procedure: StoredProcedure,
8159 mut before_publish: F,
8160 ) -> Result<StoredProcedure>
8161 where
8162 F: FnMut() -> Result<()>,
8163 {
8164 self.create_procedure_inner(procedure, Some(&mut before_publish))
8165 }
8166
8167 fn create_procedure_inner(
8168 &self,
8169 mut procedure: StoredProcedure,
8170 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8171 ) -> Result<StoredProcedure> {
8172 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8173 procedure: procedure.clone(),
8174 };
8175 self.require(&crate::catalog_cmds::required_permission(&command))?;
8176 let _g = self.ddl_lock.lock();
8177 let _security_write = self.security_write()?;
8178 self.require(&crate::catalog_cmds::required_permission(&command))?;
8179 procedure.validate()?;
8180 self.validate_procedure_references(&procedure)?;
8181 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8185 let commit_lock = Arc::clone(&self.commit_lock);
8186 let _c = commit_lock.lock();
8187 let epoch = self.epoch.bump_assigned();
8188 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8189 procedure.created_epoch = epoch.0;
8190 procedure.updated_epoch = epoch.0;
8191 let command = crate::catalog_cmds::CatalogCommand::CreateProcedure {
8192 procedure: procedure.clone(),
8193 };
8194 let mut next_catalog = self.catalog.read().clone();
8195 self.apply_catalog_command_to(&mut next_catalog, command)?;
8196 next_catalog.db_epoch = epoch.0;
8197 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8198 Ok(procedure)
8199 }
8200
8201 pub fn create_or_replace_procedure(
8202 &self,
8203 procedure: StoredProcedure,
8204 ) -> Result<StoredProcedure> {
8205 self.create_or_replace_procedure_inner(procedure, None)
8206 }
8207
8208 pub fn create_or_replace_procedure_controlled<F>(
8209 &self,
8210 procedure: StoredProcedure,
8211 mut before_publish: F,
8212 ) -> Result<StoredProcedure>
8213 where
8214 F: FnMut() -> Result<()>,
8215 {
8216 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
8217 }
8218
8219 fn create_or_replace_procedure_inner(
8220 &self,
8221 procedure: StoredProcedure,
8222 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8223 ) -> Result<StoredProcedure> {
8224 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8225 procedure: procedure.clone(),
8226 };
8227 self.require(&crate::catalog_cmds::required_permission(&command))?;
8228 let _g = self.ddl_lock.lock();
8229 let _security_write = self.security_write()?;
8230 self.require(&crate::catalog_cmds::required_permission(&command))?;
8231 procedure.validate()?;
8232 self.validate_procedure_references(&procedure)?;
8233 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8237 let commit_lock = Arc::clone(&self.commit_lock);
8238 let _c = commit_lock.lock();
8239 let epoch = self.epoch.bump_assigned();
8240 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8241 let mut next_catalog = self.catalog.read().clone();
8242 let replaced = match next_catalog
8247 .procedures
8248 .iter()
8249 .position(|p| p.procedure.name == procedure.name)
8250 {
8251 Some(idx) => next_catalog.procedures[idx]
8252 .procedure
8253 .replaced(procedure.clone(), epoch.0)?,
8254 None => {
8255 let mut next = procedure;
8256 next.created_epoch = epoch.0;
8257 next.updated_epoch = epoch.0;
8258 next
8259 }
8260 };
8261 let command = crate::catalog_cmds::CatalogCommand::ReplaceProcedure {
8262 procedure: replaced.clone(),
8263 };
8264 self.apply_catalog_command_to(&mut next_catalog, command)?;
8265 next_catalog.db_epoch = epoch.0;
8266 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8267 Ok(replaced)
8268 }
8269
8270 pub fn drop_procedure(&self, name: &str) -> Result<()> {
8271 self.drop_procedure_with_epoch(name).map(|_| ())
8272 }
8273
8274 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
8275 self.drop_procedure_with_epoch_inner(name, None)
8276 }
8277
8278 pub fn drop_procedure_with_epoch_controlled<F>(
8279 &self,
8280 name: &str,
8281 mut before_publish: F,
8282 ) -> Result<Epoch>
8283 where
8284 F: FnMut() -> Result<()>,
8285 {
8286 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
8287 }
8288
8289 fn drop_procedure_with_epoch_inner(
8290 &self,
8291 name: &str,
8292 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8293 ) -> Result<Epoch> {
8294 let command = crate::catalog_cmds::CatalogCommand::DropProcedure {
8295 name: name.to_string(),
8296 };
8297 self.require(&crate::catalog_cmds::required_permission(&command))?;
8298 let _g = self.ddl_lock.lock();
8299 let _security_write = self.security_write()?;
8300 self.require(&crate::catalog_cmds::required_permission(&command))?;
8301 let commit_lock = Arc::clone(&self.commit_lock);
8302 let _c = commit_lock.lock();
8303 let epoch = self.epoch.bump_assigned();
8304 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8305 let mut next_catalog = self.catalog.read().clone();
8306 self.apply_catalog_command_to(&mut next_catalog, command)?;
8307 next_catalog.db_epoch = epoch.0;
8308 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8309 Ok(epoch)
8310 }
8311
8312 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
8317 self.catalog.read().users.clone()
8318 }
8319
8320 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
8323 self.catalog
8324 .read()
8325 .users
8326 .iter()
8327 .find(|user| user.username == username)
8328 .map(|user| (user.id, user.created_epoch))
8329 }
8330
8331 pub fn user_scram_verifier(
8333 &self,
8334 username: &str,
8335 ) -> Option<crate::security_hardening::ScramVerifier> {
8336 self.catalog
8337 .read()
8338 .users
8339 .iter()
8340 .find(|user| user.username == username)
8341 .and_then(|user| user.scram_sha_256.clone())
8342 }
8343
8344 pub fn security_version(&self) -> u64 {
8347 self.catalog.read().security_version
8348 }
8349
8350 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
8352 self.catalog.read().roles.clone()
8353 }
8354
8355 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
8357 self.require(&crate::auth::Permission::Admin)?;
8358 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
8359 let scram = crate::auth::scram_verifier(password).map_err(MongrelError::Other)?;
8360 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(password);
8361 self.create_user_with_password_hash_inner(username, hash, Some((scram, mysql)), None)
8362 }
8363
8364 pub fn create_user_with_password_hash(
8366 &self,
8367 username: &str,
8368 hash: String,
8369 ) -> Result<crate::auth::UserEntry> {
8370 self.create_user_with_password_hash_inner(username, hash, None, None)
8371 }
8372
8373 pub fn create_user_with_password_hash_controlled<F>(
8374 &self,
8375 username: &str,
8376 hash: String,
8377 mut before_publish: F,
8378 ) -> Result<crate::auth::UserEntry>
8379 where
8380 F: FnMut() -> Result<()>,
8381 {
8382 self.create_user_with_password_hash_inner(username, hash, None, Some(&mut before_publish))
8383 }
8384
8385 fn create_user_with_password_hash_inner(
8386 &self,
8387 username: &str,
8388 hash: String,
8389 verifiers: Option<(
8390 crate::security_hardening::ScramVerifier,
8391 crate::auth::MysqlCachingSha2Verifier,
8392 )>,
8393 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8394 ) -> Result<crate::auth::UserEntry> {
8395 let command = match &verifiers {
8399 Some((scram_sha_256, mysql_caching_sha2)) => {
8400 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8401 username: username.to_string(),
8402 password_hash: hash.clone(),
8403 scram_sha_256: scram_sha_256.clone(),
8404 mysql_caching_sha2: mysql_caching_sha2.clone(),
8405 is_admin: false,
8406 created_epoch: 0,
8407 }
8408 }
8409 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8410 username: username.to_string(),
8411 password_hash: hash.clone(),
8412 is_admin: false,
8413 created_epoch: 0,
8414 },
8415 };
8416 self.require(&crate::catalog_cmds::required_permission(&command))?;
8417 let _ddl = self.ddl_lock.lock();
8418 let _security_write = self.security_write()?;
8419 self.require(&crate::catalog_cmds::required_permission(&command))?;
8420 let _commit = self.commit_lock.lock();
8421 let epoch = self.epoch.bump_assigned();
8422 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8423 let command = match verifiers {
8424 Some((scram_sha_256, mysql_caching_sha2)) => {
8425 crate::catalog_cmds::CatalogCommand::CreateUserWithAuthVerifiers {
8426 username: username.to_string(),
8427 password_hash: hash,
8428 scram_sha_256,
8429 mysql_caching_sha2,
8430 is_admin: false,
8431 created_epoch: epoch.0,
8432 }
8433 }
8434 None => crate::catalog_cmds::CatalogCommand::CreateUser {
8435 username: username.to_string(),
8436 password_hash: hash,
8437 is_admin: false,
8438 created_epoch: epoch.0,
8439 },
8440 };
8441 let mut next_catalog = self.catalog.read().clone();
8442 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8443 let crate::catalog_cmds::CatalogDelta::UserUpserted(entry) = delta else {
8444 return Err(MongrelError::Other(
8445 "CreateUser resolved to an unexpected catalog delta".into(),
8446 ));
8447 };
8448 next_catalog.db_epoch = epoch.0;
8449 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8450 Ok(entry)
8451 }
8452
8453 pub fn drop_user(&self, username: &str) -> Result<()> {
8455 self.drop_user_with_epoch(username).map(|_| ())
8456 }
8457
8458 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
8459 self.drop_user_with_epoch_inner(username, None)
8460 }
8461
8462 pub fn drop_user_with_epoch_controlled<F>(
8463 &self,
8464 username: &str,
8465 mut before_publish: F,
8466 ) -> Result<Epoch>
8467 where
8468 F: FnMut() -> Result<()>,
8469 {
8470 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
8471 }
8472
8473 fn drop_user_with_epoch_inner(
8474 &self,
8475 username: &str,
8476 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8477 ) -> Result<Epoch> {
8478 let command = crate::catalog_cmds::CatalogCommand::DropUser {
8479 username: username.to_string(),
8480 };
8481 self.require(&crate::catalog_cmds::required_permission(&command))?;
8482 let _ddl = self.ddl_lock.lock();
8483 let _security_write = self.security_write()?;
8484 self.require(&crate::catalog_cmds::required_permission(&command))?;
8485 let _commit = self.commit_lock.lock();
8486 let epoch = self.epoch.bump_assigned();
8487 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8488 let mut next_catalog = self.catalog.read().clone();
8489 self.apply_catalog_command_to(&mut next_catalog, command)?;
8490 next_catalog.db_epoch = epoch.0;
8491 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8492 Ok(epoch)
8493 }
8494
8495 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
8497 self.alter_user_password_with_epoch(username, new_password)
8498 .map(|_| ())
8499 }
8500
8501 pub fn alter_user_password_with_epoch(
8502 &self,
8503 username: &str,
8504 new_password: &str,
8505 ) -> Result<Epoch> {
8506 self.require(&crate::auth::Permission::Admin)?;
8507 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
8508 let scram = crate::auth::scram_verifier(new_password).map_err(MongrelError::Other)?;
8509 let mysql = crate::auth::MysqlCachingSha2Verifier::from_password(new_password);
8510 self.alter_user_password_hash_with_epoch_inner(username, hash, Some((scram, mysql)), None)
8511 }
8512
8513 pub fn alter_user_password_hash_with_epoch(
8514 &self,
8515 username: &str,
8516 hash: String,
8517 ) -> Result<Epoch> {
8518 self.alter_user_password_hash_with_epoch_inner(username, hash, None, None)
8519 }
8520
8521 pub fn alter_user_password_hash_with_epoch_controlled<F>(
8522 &self,
8523 username: &str,
8524 hash: String,
8525 mut before_publish: F,
8526 ) -> Result<Epoch>
8527 where
8528 F: FnMut() -> Result<()>,
8529 {
8530 self.alter_user_password_hash_with_epoch_inner(
8531 username,
8532 hash,
8533 None,
8534 Some(&mut before_publish),
8535 )
8536 }
8537
8538 fn alter_user_password_hash_with_epoch_inner(
8539 &self,
8540 username: &str,
8541 hash: String,
8542 verifiers: Option<(
8543 crate::security_hardening::ScramVerifier,
8544 crate::auth::MysqlCachingSha2Verifier,
8545 )>,
8546 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8547 ) -> Result<Epoch> {
8548 let command = match verifiers {
8549 Some((scram_sha_256, mysql_caching_sha2)) => {
8550 crate::catalog_cmds::CatalogCommand::AlterUserPasswordWithAuthVerifiers {
8551 username: username.to_string(),
8552 password_hash: hash,
8553 scram_sha_256,
8554 mysql_caching_sha2,
8555 }
8556 }
8557 None => crate::catalog_cmds::CatalogCommand::AlterUserPassword {
8558 username: username.to_string(),
8559 password_hash: hash,
8560 },
8561 };
8562 self.require(&crate::catalog_cmds::required_permission(&command))?;
8563 let _ddl = self.ddl_lock.lock();
8564 let _security_write = self.security_write()?;
8565 self.require(&crate::catalog_cmds::required_permission(&command))?;
8566 let _commit = self.commit_lock.lock();
8567 let epoch = self.epoch.bump_assigned();
8568 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8569 let mut next_catalog = self.catalog.read().clone();
8570 self.apply_catalog_command_to(&mut next_catalog, command)?;
8571 next_catalog.db_epoch = epoch.0;
8572 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8573 Ok(epoch)
8574 }
8575
8576 pub fn verify_user(
8579 &self,
8580 username: &str,
8581 password: &str,
8582 ) -> Result<Option<crate::auth::UserEntry>> {
8583 let cat = self.catalog.read();
8584 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
8585 return Ok(None);
8586 };
8587 if user.password_hash.is_empty() {
8588 return Ok(None);
8589 }
8590 let ok = crate::auth::verify_password(password, &user.password_hash)
8591 .map_err(MongrelError::Other)?;
8592 if ok {
8593 Ok(Some(user.clone()))
8594 } else {
8595 Ok(None)
8596 }
8597 }
8598
8599 pub fn authenticate_principal(
8603 &self,
8604 username: &str,
8605 password: &str,
8606 ) -> Result<Option<crate::auth::Principal>> {
8607 self.authenticate_principal_inner(username, password, || {})
8608 }
8609
8610 fn authenticate_principal_inner<F>(
8611 &self,
8612 username: &str,
8613 password: &str,
8614 after_verify: F,
8615 ) -> Result<Option<crate::auth::Principal>>
8616 where
8617 F: FnOnce(),
8618 {
8619 let catalog = self.catalog.read();
8620 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
8621 return Ok(None);
8622 };
8623 if user.password_hash.is_empty()
8624 || !crate::auth::verify_password(password, &user.password_hash)
8625 .map_err(MongrelError::Other)?
8626 {
8627 return Ok(None);
8628 }
8629 after_verify();
8630 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
8631 }
8632
8633 pub fn authenticate_mysql_caching_sha2_principal(
8636 &self,
8637 username: &str,
8638 nonce: &[u8],
8639 proof: &[u8],
8640 ) -> Option<crate::auth::Principal> {
8641 let catalog = self.catalog.read();
8642 let user = catalog
8643 .users
8644 .iter()
8645 .find(|user| user.username == username)?;
8646 if !user.mysql_caching_sha2.as_ref()?.verify(nonce, proof) {
8647 return None;
8648 }
8649 Self::resolve_user_principal_from_catalog(&catalog, user)
8650 }
8651
8652 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
8654 self.set_user_admin_with_epoch(username, is_admin)
8655 .map(|_| ())
8656 }
8657
8658 pub fn set_user_admin_with_epoch(
8659 &self,
8660 username: &str,
8661 is_admin: bool,
8662 ) -> Result<Option<Epoch>> {
8663 self.set_user_admin_with_epoch_inner(username, is_admin, None)
8664 }
8665
8666 pub fn set_user_admin_with_epoch_controlled<F>(
8667 &self,
8668 username: &str,
8669 is_admin: bool,
8670 mut before_publish: F,
8671 ) -> Result<Option<Epoch>>
8672 where
8673 F: FnMut() -> Result<()>,
8674 {
8675 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
8676 }
8677
8678 fn set_user_admin_with_epoch_inner(
8679 &self,
8680 username: &str,
8681 is_admin: bool,
8682 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8683 ) -> Result<Option<Epoch>> {
8684 let command = crate::catalog_cmds::CatalogCommand::SetUserAdmin {
8685 username: username.to_string(),
8686 is_admin,
8687 };
8688 self.require(&crate::catalog_cmds::required_permission(&command))?;
8689 let _ddl = self.ddl_lock.lock();
8690 let _security_write = self.security_write()?;
8691 self.require(&crate::catalog_cmds::required_permission(&command))?;
8692 let _commit = self.commit_lock.lock();
8693 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8697 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8698 return Ok(None);
8699 }
8700 let epoch = self.epoch.bump_assigned();
8701 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8702 let mut next_catalog = self.catalog.read().clone();
8703 self.apply_catalog_command_to(&mut next_catalog, command)?;
8704 next_catalog.db_epoch = epoch.0;
8705 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8706 Ok(Some(epoch))
8707 }
8708
8709 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
8711 self.create_role_inner(name, None)
8712 }
8713
8714 pub fn create_role_controlled<F>(
8715 &self,
8716 name: &str,
8717 mut before_publish: F,
8718 ) -> Result<crate::auth::RoleEntry>
8719 where
8720 F: FnMut() -> Result<()>,
8721 {
8722 self.create_role_inner(name, Some(&mut before_publish))
8723 }
8724
8725 fn create_role_inner(
8726 &self,
8727 name: &str,
8728 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8729 ) -> Result<crate::auth::RoleEntry> {
8730 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8731 name: name.to_string(),
8732 created_epoch: 0, };
8734 self.require(&crate::catalog_cmds::required_permission(&command))?;
8735 let _ddl = self.ddl_lock.lock();
8736 let _security_write = self.security_write()?;
8737 self.require(&crate::catalog_cmds::required_permission(&command))?;
8738 let _commit = self.commit_lock.lock();
8739 let epoch = self.epoch.bump_assigned();
8740 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8741 let command = crate::catalog_cmds::CatalogCommand::CreateRole {
8742 name: name.to_string(),
8743 created_epoch: epoch.0,
8744 };
8745 let mut next_catalog = self.catalog.read().clone();
8746 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
8747 let crate::catalog_cmds::CatalogDelta::RoleUpserted(entry) = delta else {
8748 return Err(MongrelError::Other(
8749 "CreateRole resolved to an unexpected catalog delta".into(),
8750 ));
8751 };
8752 next_catalog.db_epoch = epoch.0;
8753 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8754 Ok(entry)
8755 }
8756
8757 pub fn drop_role(&self, name: &str) -> Result<()> {
8759 self.drop_role_with_epoch(name).map(|_| ())
8760 }
8761
8762 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
8763 self.drop_role_with_epoch_inner(name, None)
8764 }
8765
8766 pub fn drop_role_with_epoch_controlled<F>(
8767 &self,
8768 name: &str,
8769 mut before_publish: F,
8770 ) -> Result<Epoch>
8771 where
8772 F: FnMut() -> Result<()>,
8773 {
8774 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
8775 }
8776
8777 fn drop_role_with_epoch_inner(
8778 &self,
8779 name: &str,
8780 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8781 ) -> Result<Epoch> {
8782 let command = crate::catalog_cmds::CatalogCommand::DropRole {
8783 name: name.to_string(),
8784 };
8785 self.require(&crate::catalog_cmds::required_permission(&command))?;
8786 let _ddl = self.ddl_lock.lock();
8787 let _security_write = self.security_write()?;
8788 self.require(&crate::catalog_cmds::required_permission(&command))?;
8789 let _commit = self.commit_lock.lock();
8790 let epoch = self.epoch.bump_assigned();
8791 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8792 let mut next_catalog = self.catalog.read().clone();
8793 self.apply_catalog_command_to(&mut next_catalog, command)?;
8794 next_catalog.db_epoch = epoch.0;
8795 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8796 Ok(epoch)
8797 }
8798
8799 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
8801 self.grant_role_with_epoch(username, role_name).map(|_| ())
8802 }
8803
8804 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8805 self.grant_role_with_epoch_inner(username, role_name, None)
8806 }
8807
8808 pub fn grant_role_with_epoch_controlled<F>(
8809 &self,
8810 username: &str,
8811 role_name: &str,
8812 mut before_publish: F,
8813 ) -> Result<Option<Epoch>>
8814 where
8815 F: FnMut() -> Result<()>,
8816 {
8817 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8818 }
8819
8820 fn grant_role_with_epoch_inner(
8821 &self,
8822 username: &str,
8823 role_name: &str,
8824 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8825 ) -> Result<Option<Epoch>> {
8826 let command = crate::catalog_cmds::CatalogCommand::GrantRole {
8827 username: username.to_string(),
8828 role: role_name.to_string(),
8829 };
8830 self.require(&crate::catalog_cmds::required_permission(&command))?;
8831 let _ddl = self.ddl_lock.lock();
8832 let _security_write = self.security_write()?;
8833 self.require(&crate::catalog_cmds::required_permission(&command))?;
8834 let _commit = self.commit_lock.lock();
8835 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8839 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8840 return Ok(None);
8841 }
8842 let epoch = self.epoch.bump_assigned();
8843 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8844 let mut next_catalog = self.catalog.read().clone();
8845 self.apply_catalog_command_to(&mut next_catalog, command)?;
8846 next_catalog.db_epoch = epoch.0;
8847 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8848 Ok(Some(epoch))
8849 }
8850
8851 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
8853 self.revoke_role_with_epoch(username, role_name).map(|_| ())
8854 }
8855
8856 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
8857 self.revoke_role_with_epoch_inner(username, role_name, None)
8858 }
8859
8860 pub fn revoke_role_with_epoch_controlled<F>(
8861 &self,
8862 username: &str,
8863 role_name: &str,
8864 mut before_publish: F,
8865 ) -> Result<Option<Epoch>>
8866 where
8867 F: FnMut() -> Result<()>,
8868 {
8869 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
8870 }
8871
8872 fn revoke_role_with_epoch_inner(
8873 &self,
8874 username: &str,
8875 role_name: &str,
8876 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8877 ) -> Result<Option<Epoch>> {
8878 let command = crate::catalog_cmds::CatalogCommand::RevokeRole {
8879 username: username.to_string(),
8880 role: role_name.to_string(),
8881 };
8882 self.require(&crate::catalog_cmds::required_permission(&command))?;
8883 let _ddl = self.ddl_lock.lock();
8884 let _security_write = self.security_write()?;
8885 self.require(&crate::catalog_cmds::required_permission(&command))?;
8886 let _commit = self.commit_lock.lock();
8887 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8891 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8892 return Ok(None);
8893 }
8894 let epoch = self.epoch.bump_assigned();
8895 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8896 let mut next_catalog = self.catalog.read().clone();
8897 self.apply_catalog_command_to(&mut next_catalog, command)?;
8898 next_catalog.db_epoch = epoch.0;
8899 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8900 Ok(Some(epoch))
8901 }
8902
8903 pub fn grant_permission(
8905 &self,
8906 role_name: &str,
8907 permission: crate::auth::Permission,
8908 ) -> Result<()> {
8909 self.grant_permission_with_epoch(role_name, permission)
8910 .map(|_| ())
8911 }
8912
8913 pub fn grant_permission_with_epoch(
8914 &self,
8915 role_name: &str,
8916 permission: crate::auth::Permission,
8917 ) -> Result<Option<Epoch>> {
8918 self.grant_permission_with_epoch_inner(role_name, permission, None)
8919 }
8920
8921 pub fn grant_permission_with_epoch_controlled<F>(
8922 &self,
8923 role_name: &str,
8924 permission: crate::auth::Permission,
8925 mut before_publish: F,
8926 ) -> Result<Option<Epoch>>
8927 where
8928 F: FnMut() -> Result<()>,
8929 {
8930 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8931 }
8932
8933 fn grant_permission_with_epoch_inner(
8934 &self,
8935 role_name: &str,
8936 permission: crate::auth::Permission,
8937 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8938 ) -> Result<Option<Epoch>> {
8939 let command = crate::catalog_cmds::CatalogCommand::GrantPermission {
8940 role: role_name.to_string(),
8941 permission,
8942 };
8943 self.require(&crate::catalog_cmds::required_permission(&command))?;
8944 let _ddl = self.ddl_lock.lock();
8945 let _security_write = self.security_write()?;
8946 self.require(&crate::catalog_cmds::required_permission(&command))?;
8947 let _commit = self.commit_lock.lock();
8948 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
8952 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
8953 return Ok(None);
8954 }
8955 let epoch = self.epoch.bump_assigned();
8956 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
8957 let mut next_catalog = self.catalog.read().clone();
8958 self.apply_catalog_command_to(&mut next_catalog, command)?;
8959 next_catalog.db_epoch = epoch.0;
8960 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
8961 Ok(Some(epoch))
8962 }
8963
8964 pub fn revoke_permission(
8966 &self,
8967 role_name: &str,
8968 permission: crate::auth::Permission,
8969 ) -> Result<()> {
8970 self.revoke_permission_with_epoch(role_name, permission)
8971 .map(|_| ())
8972 }
8973
8974 pub fn revoke_permission_with_epoch(
8975 &self,
8976 role_name: &str,
8977 permission: crate::auth::Permission,
8978 ) -> Result<Option<Epoch>> {
8979 self.revoke_permission_with_epoch_inner(role_name, permission, None)
8980 }
8981
8982 pub fn revoke_permission_with_epoch_controlled<F>(
8983 &self,
8984 role_name: &str,
8985 permission: crate::auth::Permission,
8986 mut before_publish: F,
8987 ) -> Result<Option<Epoch>>
8988 where
8989 F: FnMut() -> Result<()>,
8990 {
8991 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
8992 }
8993
8994 fn revoke_permission_with_epoch_inner(
8995 &self,
8996 role_name: &str,
8997 permission: crate::auth::Permission,
8998 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
8999 ) -> Result<Option<Epoch>> {
9000 let command = crate::catalog_cmds::CatalogCommand::RevokePermission {
9001 role: role_name.to_string(),
9002 permission,
9003 };
9004 self.require(&crate::catalog_cmds::required_permission(&command))?;
9005 let _ddl = self.ddl_lock.lock();
9006 let _security_write = self.security_write()?;
9007 self.require(&crate::catalog_cmds::required_permission(&command))?;
9008 let _commit = self.commit_lock.lock();
9009 let delta = crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9013 if matches!(delta, crate::catalog_cmds::CatalogDelta::NoOp) {
9014 return Ok(None);
9015 }
9016 let epoch = self.epoch.bump_assigned();
9017 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9018 let mut next_catalog = self.catalog.read().clone();
9019 self.apply_catalog_command_to(&mut next_catalog, command)?;
9020 next_catalog.db_epoch = epoch.0;
9021 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9022 Ok(Some(epoch))
9023 }
9024
9025 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
9028 let cat = self.catalog.read();
9029 Self::resolve_principal_from_catalog(&cat, username)
9030 }
9031
9032 pub fn resolve_current_principal(
9035 &self,
9036 principal: &crate::auth::Principal,
9037 ) -> Option<crate::auth::Principal> {
9038 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
9039 }
9040
9041 fn resolve_principal_from_catalog(
9046 cat: &Catalog,
9047 username: &str,
9048 ) -> Option<crate::auth::Principal> {
9049 let user = cat.users.iter().find(|u| u.username == username)?;
9050 Self::resolve_user_principal_from_catalog(cat, user)
9051 }
9052
9053 fn resolve_bound_principal_from_catalog(
9054 cat: &Catalog,
9055 principal: &crate::auth::Principal,
9056 ) -> Option<crate::auth::Principal> {
9057 let user = cat.users.iter().find(|user| {
9058 user.id == principal.user_id
9059 && user.created_epoch == principal.created_epoch
9060 && user.username == principal.username
9061 })?;
9062 Self::resolve_user_principal_from_catalog(cat, user)
9063 }
9064
9065 fn resolve_user_principal_from_catalog(
9066 cat: &Catalog,
9067 user: &crate::auth::UserEntry,
9068 ) -> Option<crate::auth::Principal> {
9069 let mut permissions = Vec::new();
9070 for role_name in &user.roles {
9071 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
9072 permissions.extend(role.permissions.iter().cloned());
9073 }
9074 }
9075 Some(crate::auth::Principal {
9076 user_id: user.id,
9077 created_epoch: user.created_epoch,
9078 username: user.username.clone(),
9079 is_admin: user.is_admin,
9080 roles: user.roles.clone(),
9081 permissions,
9082 })
9083 }
9084
9085 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
9087 match self.resolve_principal(username) {
9088 Some(p) => p.has_permission(permission),
9089 None => false,
9090 }
9091 }
9092
9093 pub fn require_auth_enabled(&self) -> bool {
9097 self.catalog.read().require_auth
9098 }
9099
9100 pub fn principal(&self) -> Option<crate::auth::Principal> {
9104 self.principal.read().clone()
9105 }
9106
9107 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
9113 if self.shared {
9114 return None;
9115 }
9116 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
9117 self.auth_state.clone(),
9118 )))
9119 }
9120
9121 pub fn refresh_principal(&self) -> Result<()> {
9135 let previous = match self.principal.read().clone() {
9136 Some(principal) => principal,
9137 None => return Ok(()),
9138 };
9139 if previous.user_id == 0 {
9143 return Ok(());
9144 }
9145 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
9146 self.refresh_security_catalog_if_stale(observed_version)?;
9147 let cat = self.catalog.read();
9148 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
9149 Some(p) => {
9150 *self.principal.write() = Some(p.clone());
9151 self.auth_state.set_principal(Some(p));
9155 Ok(())
9156 }
9157 None => Err(MongrelError::InvalidCredentials {
9158 username: previous.username,
9159 }),
9160 }
9161 }
9162
9163 pub fn security_catalog_disk_read_count(&self) -> u64 {
9166 self.security_catalog_disk_reads.load(Ordering::Relaxed)
9167 }
9168
9169 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
9181 if self.shared {
9182 return Err(MongrelError::Conflict(
9188 "enable_auth requires an exclusive Database owner; shared cores reject \
9189 auth-mode transitions"
9190 .into(),
9191 ));
9192 }
9193 let password_hash =
9194 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
9195 let scram_sha_256 =
9196 crate::auth::scram_verifier(admin_password).map_err(MongrelError::Other)?;
9197 let mysql_caching_sha2 =
9198 crate::auth::MysqlCachingSha2Verifier::from_password(admin_password);
9199 let _ddl = self.ddl_lock.lock();
9200 let _security_write = self.security_write()?;
9201 let _commit = self.commit_lock.lock();
9202 let epoch = self.epoch.bump_assigned();
9203 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9204 let mut next_catalog = self.catalog.read().clone();
9205 if next_catalog.require_auth {
9206 return Err(MongrelError::InvalidArgument(
9207 "database already has require_auth enabled".into(),
9208 ));
9209 }
9210 if next_catalog
9211 .users
9212 .iter()
9213 .any(|u| u.username == admin_username)
9214 {
9215 return Err(MongrelError::InvalidArgument(format!(
9216 "user {admin_username:?} already exists"
9217 )));
9218 }
9219 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
9220 let id = next_catalog.next_user_id;
9221 next_catalog.next_user_id = id
9222 .checked_add(1)
9223 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
9224 next_catalog.users.push(crate::auth::UserEntry {
9225 id,
9226 username: admin_username.to_string(),
9227 password_hash,
9228 scram_sha_256: Some(scram_sha_256),
9229 mysql_caching_sha2: Some(mysql_caching_sha2),
9230 roles: Vec::new(),
9231 is_admin: true,
9232 created_epoch: epoch.0,
9233 });
9234 next_catalog.require_auth = true;
9235 advance_security_version(&mut next_catalog)?;
9236 next_catalog.db_epoch = epoch.0;
9237 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9238 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9242 let principal = crate::auth::Principal {
9243 user_id: id,
9244 created_epoch: epoch.0,
9245 username: admin_username.to_string(),
9246 is_admin: true,
9247 roles: Vec::new(),
9248 permissions: Vec::new(),
9249 };
9250 *self.principal.write() = Some(principal.clone());
9251 self.auth_state.set_principal(Some(principal));
9252 }
9253 publish
9254 }
9255
9256 pub fn disable_auth(&self) -> Result<()> {
9273 let _ddl = self.ddl_lock.lock();
9274 let _security_write = self.security_write()?;
9275 let _commit = self.commit_lock.lock();
9276 let epoch = self.epoch.bump_assigned();
9277 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9278 let mut next_catalog = self.catalog.read().clone();
9279 if !next_catalog.require_auth {
9280 return Err(MongrelError::InvalidArgument(
9281 "database does not have require_auth enabled".into(),
9282 ));
9283 }
9284 next_catalog.require_auth = false;
9285 advance_security_version(&mut next_catalog)?;
9286 next_catalog.db_epoch = epoch.0;
9287 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
9288 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
9290 *self.principal.write() = None;
9291 }
9292 publish
9293 }
9294
9295 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
9302 self.ensure_owner_process()?;
9303 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
9304 return Err(MongrelError::ReadOnlyReplica);
9305 }
9306 if self.principal.read().is_some() {
9307 self.refresh_principal().map_err(|error| match error {
9308 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
9309 error => error,
9310 })?;
9311 }
9312 if !self.catalog.read().require_auth {
9313 return Ok(());
9314 }
9315 let guard = self.principal.read();
9316 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
9317 if p.has_permission(perm) {
9318 Ok(())
9319 } else {
9320 Err(MongrelError::PermissionDenied {
9321 required: perm.clone(),
9322 principal: p.username.clone(),
9323 })
9324 }
9325 }
9326
9327 pub fn require_table(
9332 &self,
9333 table: &str,
9334 perm: crate::auth_state::RequiredPermission,
9335 ) -> Result<()> {
9336 self.require(&perm.into_permission(table))
9337 }
9338
9339 pub fn triggers(&self) -> Vec<StoredTrigger> {
9340 self.catalog
9341 .read()
9342 .triggers
9343 .iter()
9344 .map(|t| t.trigger.clone())
9345 .collect()
9346 }
9347
9348 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
9349 self.catalog
9350 .read()
9351 .triggers
9352 .iter()
9353 .find(|t| t.trigger.name == name)
9354 .map(|t| t.trigger.clone())
9355 }
9356
9357 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9358 self.create_trigger_inner(trigger, None, None)
9359 }
9360
9361 pub fn create_trigger_controlled<F>(
9362 &self,
9363 trigger: StoredTrigger,
9364 mut before_publish: F,
9365 ) -> Result<StoredTrigger>
9366 where
9367 F: FnMut() -> Result<()>,
9368 {
9369 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
9370 }
9371
9372 pub fn create_trigger_as_controlled<F>(
9373 &self,
9374 trigger: StoredTrigger,
9375 principal: Option<&crate::auth::Principal>,
9376 mut before_publish: F,
9377 ) -> Result<StoredTrigger>
9378 where
9379 F: FnMut() -> Result<()>,
9380 {
9381 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
9382 }
9383
9384 fn create_trigger_inner(
9385 &self,
9386 mut trigger: StoredTrigger,
9387 principal: Option<&crate::auth::Principal>,
9388 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9389 ) -> Result<StoredTrigger> {
9390 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9391 trigger: trigger.clone(),
9392 };
9393 self.require_for(
9394 principal,
9395 &crate::catalog_cmds::required_permission(&command),
9396 )?;
9397 let _g = self.ddl_lock.lock();
9398 let _security_write = self.security_write()?;
9399 self.require_for(
9400 principal,
9401 &crate::catalog_cmds::required_permission(&command),
9402 )?;
9403 trigger.validate()?;
9404 self.validate_trigger_references(&trigger)
9405 .map_err(trigger_validation_error)?;
9406 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
9410 let commit_lock = Arc::clone(&self.commit_lock);
9411 let _c = commit_lock.lock();
9412 let epoch = self.epoch.bump_assigned();
9413 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9414 trigger.created_epoch = epoch.0;
9415 trigger.updated_epoch = epoch.0;
9416 let command = crate::catalog_cmds::CatalogCommand::CreateTrigger {
9417 trigger: trigger.clone(),
9418 };
9419 let mut next_catalog = self.catalog.read().clone();
9420 self.apply_catalog_command_to(&mut next_catalog, command)?;
9421 next_catalog.db_epoch = epoch.0;
9422 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9423 Ok(trigger)
9424 }
9425
9426 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
9427 self.create_or_replace_trigger_inner(trigger, None, None)
9428 }
9429
9430 pub fn create_or_replace_trigger_controlled<F>(
9431 &self,
9432 trigger: StoredTrigger,
9433 mut before_publish: F,
9434 ) -> Result<StoredTrigger>
9435 where
9436 F: FnMut() -> Result<()>,
9437 {
9438 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
9439 }
9440
9441 pub fn create_or_replace_trigger_as_controlled<F>(
9442 &self,
9443 trigger: StoredTrigger,
9444 principal: Option<&crate::auth::Principal>,
9445 mut before_publish: F,
9446 ) -> Result<StoredTrigger>
9447 where
9448 F: FnMut() -> Result<()>,
9449 {
9450 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
9451 }
9452
9453 fn create_or_replace_trigger_inner(
9454 &self,
9455 trigger: StoredTrigger,
9456 principal: Option<&crate::auth::Principal>,
9457 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9458 ) -> Result<StoredTrigger> {
9459 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9460 trigger: trigger.clone(),
9461 };
9462 self.require_for(
9463 principal,
9464 &crate::catalog_cmds::required_permission(&command),
9465 )?;
9466 let _g = self.ddl_lock.lock();
9467 let _security_write = self.security_write()?;
9468 self.require_for(
9469 principal,
9470 &crate::catalog_cmds::required_permission(&command),
9471 )?;
9472 trigger.validate()?;
9473 self.validate_trigger_references(&trigger)
9474 .map_err(trigger_validation_error)?;
9475 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
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 let replaced = match next_catalog
9489 .triggers
9490 .iter()
9491 .position(|t| t.trigger.name == trigger.name)
9492 {
9493 Some(idx) => next_catalog.triggers[idx]
9494 .trigger
9495 .replaced(trigger.clone(), epoch.0)?,
9496 None => {
9497 let mut next = trigger;
9498 next.created_epoch = epoch.0;
9499 next.updated_epoch = epoch.0;
9500 next
9501 }
9502 };
9503 let command = crate::catalog_cmds::CatalogCommand::ReplaceTrigger {
9504 trigger: replaced.clone(),
9505 };
9506 self.apply_catalog_command_to(&mut next_catalog, command)?;
9507 next_catalog.db_epoch = epoch.0;
9508 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9509 Ok(replaced)
9510 }
9511
9512 pub fn drop_trigger(&self, name: &str) -> Result<()> {
9513 self.drop_trigger_with_epoch(name).map(|_| ())
9514 }
9515
9516 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
9518 self.drop_triggers_with_epoch(&[name.to_string()])
9519 }
9520
9521 pub fn drop_trigger_with_epoch_controlled<F>(
9522 &self,
9523 name: &str,
9524 before_publish: F,
9525 ) -> Result<Epoch>
9526 where
9527 F: FnMut() -> Result<()>,
9528 {
9529 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
9530 }
9531
9532 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
9534 self.drop_triggers_with_epoch_inner(names, None, None)
9535 }
9536
9537 pub fn drop_triggers_with_epoch_controlled<F>(
9538 &self,
9539 names: &[String],
9540 mut before_publish: F,
9541 ) -> Result<Epoch>
9542 where
9543 F: FnMut() -> Result<()>,
9544 {
9545 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
9546 }
9547
9548 pub fn drop_triggers_with_epoch_as_controlled<F>(
9549 &self,
9550 names: &[String],
9551 principal: Option<&crate::auth::Principal>,
9552 mut before_publish: F,
9553 ) -> Result<Epoch>
9554 where
9555 F: FnMut() -> Result<()>,
9556 {
9557 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
9558 }
9559
9560 fn drop_triggers_with_epoch_inner(
9561 &self,
9562 names: &[String],
9563 principal: Option<&crate::auth::Principal>,
9564 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9565 ) -> Result<Epoch> {
9566 if names.is_empty() {
9567 return Err(MongrelError::InvalidArgument(
9568 "at least one trigger name is required".into(),
9569 ));
9570 }
9571 let commands: Vec<crate::catalog_cmds::CatalogCommand> = names
9572 .iter()
9573 .map(|name| crate::catalog_cmds::CatalogCommand::DropTrigger { name: name.clone() })
9574 .collect();
9575 for command in &commands {
9576 self.require_for(
9577 principal,
9578 &crate::catalog_cmds::required_permission(command),
9579 )?;
9580 }
9581 let _g = self.ddl_lock.lock();
9582 let _security_write = self.security_write()?;
9583 for command in &commands {
9584 self.require_for(
9585 principal,
9586 &crate::catalog_cmds::required_permission(command),
9587 )?;
9588 }
9589 {
9594 let cat = self.catalog.read();
9595 for command in &commands {
9596 crate::catalog_cmds::apply(&cat, command)?;
9597 }
9598 }
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 for command in commands {
9605 self.apply_catalog_command_to(&mut next_catalog, command)?;
9606 }
9607 next_catalog.db_epoch = epoch.0;
9608 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
9609 Ok(epoch)
9610 }
9611
9612 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
9613 self.catalog.read().external_tables.clone()
9614 }
9615
9616 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
9617 self.catalog
9618 .read()
9619 .external_tables
9620 .iter()
9621 .find(|t| t.name == name)
9622 .cloned()
9623 }
9624
9625 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
9626 self.create_external_table_inner(entry, None)
9627 }
9628
9629 pub fn create_external_table_controlled<F>(
9630 &self,
9631 entry: ExternalTableEntry,
9632 mut before_publish: F,
9633 ) -> Result<ExternalTableEntry>
9634 where
9635 F: FnMut() -> Result<()>,
9636 {
9637 self.create_external_table_inner(entry, Some(&mut before_publish))
9638 }
9639
9640 fn create_external_table_inner(
9641 &self,
9642 mut entry: ExternalTableEntry,
9643 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9644 ) -> Result<ExternalTableEntry> {
9645 self.require(&crate::auth::Permission::Ddl)?;
9646 let _g = self.ddl_lock.lock();
9647 let _security_write = self.security_write()?;
9648 self.require(&crate::auth::Permission::Ddl)?;
9649 entry.validate()?;
9650 {
9651 let cat = self.catalog.read();
9652 if cat.live(&entry.name).is_some()
9653 || cat.external_tables.iter().any(|t| t.name == entry.name)
9654 {
9655 return Err(MongrelError::InvalidArgument(format!(
9656 "table {:?} already exists",
9657 entry.name
9658 )));
9659 }
9660 }
9661 let commit_lock = Arc::clone(&self.commit_lock);
9662 let _c = commit_lock.lock();
9663 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
9667 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
9668 let epoch = self.epoch.bump_assigned();
9669 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9670 entry.created_epoch = epoch.0;
9671 let mut next_catalog = self.catalog.read().clone();
9672 next_catalog.external_tables.push(entry.clone());
9673 next_catalog.db_epoch = epoch.0;
9674 self.publish_catalog_candidate_with_prelude(
9675 next_catalog,
9676 epoch,
9677 &mut _epoch_guard,
9678 before_publish,
9679 vec![(
9680 EXTERNAL_TABLE_ID,
9681 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9682 name: entry.name.clone(),
9683 generation_epoch: epoch.0,
9684 }),
9685 )],
9686 )?;
9687 Ok(entry)
9688 }
9689
9690 pub fn drop_external_table(&self, name: &str) -> Result<()> {
9691 self.drop_external_table_with_epoch(name).map(|_| ())
9692 }
9693
9694 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
9696 self.drop_external_table_with_epoch_inner(name, None)
9697 }
9698
9699 pub fn drop_external_table_with_epoch_controlled<F>(
9700 &self,
9701 name: &str,
9702 mut before_publish: F,
9703 ) -> Result<Epoch>
9704 where
9705 F: FnMut() -> Result<()>,
9706 {
9707 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
9708 }
9709
9710 fn drop_external_table_with_epoch_inner(
9711 &self,
9712 name: &str,
9713 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
9714 ) -> Result<Epoch> {
9715 self.require(&crate::auth::Permission::Ddl)?;
9716 let _g = self.ddl_lock.lock();
9717 let _security_write = self.security_write()?;
9718 self.require(&crate::auth::Permission::Ddl)?;
9719 let commit_lock = Arc::clone(&self.commit_lock);
9720 let _c = commit_lock.lock();
9721 let epoch = self.epoch.bump_assigned();
9722 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
9723 let mut next_catalog = self.catalog.read().clone();
9724 let before = next_catalog.external_tables.len();
9725 next_catalog.external_tables.retain(|t| t.name != name);
9726 if next_catalog.external_tables.len() == before {
9727 return Err(MongrelError::NotFound(format!(
9728 "external table {name:?} not found"
9729 )));
9730 }
9731 next_catalog.db_epoch = epoch.0;
9732 self.publish_catalog_candidate_with_prelude(
9733 next_catalog,
9734 epoch,
9735 &mut _epoch_guard,
9736 before_publish,
9737 vec![(
9738 EXTERNAL_TABLE_ID,
9739 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
9740 name: name.to_string(),
9741 generation_epoch: epoch.0,
9742 }),
9743 )],
9744 )?;
9745 let state_dir = self.root.join(VTAB_DIR).join(name);
9746 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
9747 return Err(MongrelError::DurableCommit {
9748 epoch: epoch.0,
9749 message: format!(
9750 "external table was dropped but connector-state cleanup failed: {error}"
9751 ),
9752 });
9753 }
9754 Ok(epoch)
9755 }
9756
9757 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
9758 let txn_id = self.alloc_txn_id()?;
9759 let (principal, catalog_bound) = self.transaction_principal_snapshot();
9760 self.commit_transaction_with_external_states(
9761 txn_id,
9762 self.epoch.visible(),
9763 Vec::new(),
9764 vec![(name.to_string(), state.to_vec())],
9765 Vec::new(),
9766 principal,
9767 catalog_bound,
9768 None,
9769 crate::txn::TxnCommitContext::internal(),
9770 )
9771 .map(|(epoch, _)| epoch)
9772 }
9773
9774 pub fn trigger_config(&self) -> TriggerConfig {
9775 use std::sync::atomic::Ordering;
9776 TriggerConfig {
9777 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
9778 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
9779 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
9780 }
9781 }
9782
9783 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
9784 use std::sync::atomic::Ordering;
9785 if config.max_depth == 0 {
9786 return Err(MongrelError::InvalidArgument(
9787 "trigger max_depth must be greater than 0".into(),
9788 ));
9789 }
9790 self.trigger_recursive
9791 .store(config.recursive_triggers, Ordering::Relaxed);
9792 self.trigger_max_depth
9793 .store(config.max_depth, Ordering::Relaxed);
9794 self.trigger_max_loop_iterations
9795 .store(config.max_loop_iterations, Ordering::Relaxed);
9796 Ok(())
9797 }
9798
9799 pub fn set_recursive_triggers(&self, recursive: bool) {
9800 use std::sync::atomic::Ordering;
9801 self.trigger_recursive.store(recursive, Ordering::Relaxed);
9802 }
9803
9804 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
9808 self.notify.subscribe()
9809 }
9810
9811 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
9812 self.change_wake.subscribe()
9813 }
9814
9815 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
9820 let control = crate::ExecutionControl::new(None);
9821 self.change_events_since_controlled(last_event_id, &control)
9822 }
9823
9824 pub fn change_events_since_controlled(
9826 &self,
9827 last_event_id: Option<&str>,
9828 control: &crate::ExecutionControl,
9829 ) -> Result<CdcBatch> {
9830 use crate::wal::Op;
9831
9832 control.checkpoint()?;
9833 let resume = match last_event_id {
9834 Some(id) => {
9835 let (epoch, index) = id.split_once(':').ok_or_else(|| {
9836 MongrelError::InvalidArgument(format!(
9837 "invalid CDC event id {id:?}; expected <epoch>:<index>"
9838 ))
9839 })?;
9840 Some((
9841 epoch.parse::<u64>().map_err(|error| {
9842 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
9843 })?,
9844 index.parse::<u32>().map_err(|error| {
9845 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
9846 })?,
9847 ))
9848 }
9849 None => None,
9850 };
9851
9852 let mut wal = self.shared_wal.lock();
9853 wal.group_sync()?;
9854 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
9855 let records = crate::wal::SharedWal::replay_with_dek_controlled(
9856 &self.root,
9857 wal_dek.as_ref(),
9858 control,
9859 CDC_MAX_WAL_RECORDS,
9860 CDC_MAX_WAL_REPLAY_BYTES,
9861 )?;
9862 drop(wal);
9863 control.checkpoint()?;
9864
9865 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
9866 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
9867 for (index, record) in records.iter().enumerate() {
9868 if index % 256 == 0 {
9869 control.checkpoint()?;
9870 }
9871 if let Op::TxnCommit { epoch, added_runs } = &record.op {
9872 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
9873 }
9874 if let Op::SpilledRows { table_id, rows } = &record.op {
9875 spilled_payloads
9876 .entry((record.txn_id, *table_id))
9877 .or_default()
9878 .push(rows);
9879 }
9880 }
9881 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
9882 let current_epoch = self.epoch.committed().0;
9883 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
9884 let gap = resume.is_some_and(|(epoch, _)| {
9885 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
9886 });
9887 if gap {
9888 return Ok(CdcBatch {
9889 events: Vec::new(),
9890 current_epoch,
9891 earliest_epoch,
9892 gap: true,
9893 });
9894 }
9895
9896 let table_names: HashMap<u64, String> = self
9897 .catalog
9898 .read()
9899 .tables
9900 .iter()
9901 .map(|entry| (entry.table_id, entry.name.clone()))
9902 .collect();
9903 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
9904 let mut retained_bytes = 0_usize;
9905 for (index, record) in records.iter().enumerate() {
9906 if index % 256 == 0 {
9907 control.checkpoint()?;
9908 }
9909 if !commits.contains_key(&record.txn_id) {
9910 continue;
9911 }
9912 let Op::BeforeImage {
9913 table_id,
9914 row_id,
9915 row,
9916 } = &record.op
9917 else {
9918 continue;
9919 };
9920 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9921 return Err(MongrelError::ResourceLimitExceeded {
9922 resource: "CDC before-image bytes",
9923 requested: row.len(),
9924 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9925 });
9926 }
9927 let before: crate::memtable::Row = bincode::deserialize(row)?;
9928 if before_images.len() >= CDC_MAX_ROWS {
9929 return Err(MongrelError::ResourceLimitExceeded {
9930 resource: "CDC before-image rows",
9931 requested: before_images.len().saturating_add(1),
9932 limit: CDC_MAX_ROWS,
9933 });
9934 }
9935 charge_cdc_bytes(
9936 &mut retained_bytes,
9937 cdc_row_storage_bytes(&before),
9938 "CDC retained bytes",
9939 )?;
9940 before_images.insert((record.txn_id, *table_id, row_id.0), before);
9941 }
9942 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
9943 let mut events = Vec::new();
9944 let mut decoded_rows = before_images.len();
9945 for (record_index, record) in records.iter().enumerate() {
9946 if record_index % 256 == 0 {
9947 control.checkpoint()?;
9948 }
9949 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
9950 continue;
9951 };
9952 let event = match &record.op {
9953 Op::Put { table_id, rows } => {
9954 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
9955 return Err(MongrelError::ResourceLimitExceeded {
9956 resource: "CDC inline row bytes",
9957 requested: rows.len(),
9958 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
9959 });
9960 }
9961 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
9962 decoded_rows = decoded_rows.saturating_add(rows.len());
9963 if decoded_rows > CDC_MAX_ROWS {
9964 return Err(MongrelError::ResourceLimitExceeded {
9965 resource: "CDC decoded rows",
9966 requested: decoded_rows,
9967 limit: CDC_MAX_ROWS,
9968 });
9969 }
9970 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
9971 let mut peak_bytes = retained_bytes;
9972 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9973 let data = serde_json::to_value(rows)
9974 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
9975 Some((*table_id, "put", data, event_bytes))
9976 }
9977 Op::Delete { table_id, row_ids } => {
9978 let before = row_ids
9979 .iter()
9980 .filter_map(|row_id| {
9981 before_images
9982 .get(&(record.txn_id, *table_id, row_id.0))
9983 .cloned()
9984 })
9985 .collect::<Vec<_>>();
9986 let event_bytes = cdc_rows_json_bytes(&before)
9987 .saturating_add(
9988 row_ids
9989 .len()
9990 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
9991 )
9992 .saturating_add(512);
9993 let mut peak_bytes = retained_bytes;
9994 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
9995 Some((
9996 *table_id,
9997 "delete",
9998 serde_json::json!({
9999 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
10000 "before": before,
10001 }),
10002 event_bytes,
10003 ))
10004 }
10005 Op::TruncateTable { table_id } => {
10006 Some((*table_id, "truncate", serde_json::Value::Null, 512))
10007 }
10008 _ => None,
10009 };
10010 if let Some((table_id, op, data, event_bytes)) = event {
10011 let index = operation_indices.entry(record.txn_id).or_insert(0);
10012 let event_position = (*commit_epoch, *index);
10013 *index = index.saturating_add(1);
10014 if resume.is_some_and(|position| event_position <= position) {
10015 continue;
10016 }
10017 if events.len() >= CDC_MAX_EVENTS {
10018 return Err(MongrelError::ResourceLimitExceeded {
10019 resource: "CDC events",
10020 requested: events.len().saturating_add(1),
10021 limit: CDC_MAX_EVENTS,
10022 });
10023 }
10024 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
10025 events.push(ChangeEvent {
10026 id: Some(format!("{}:{}", event_position.0, event_position.1)),
10027 channel: "changes".into(),
10028 table_id: Some(table_id),
10029 table: table_names.get(&table_id).cloned().unwrap_or_default(),
10030 op: op.into(),
10031 epoch: *commit_epoch,
10032 txn_id: Some(record.txn_id),
10033 message: None,
10034 data: Some(data),
10035 });
10036 }
10037 if let Op::TxnCommit { added_runs, .. } = &record.op {
10038 for run in added_runs {
10039 control.checkpoint()?;
10040 let index = operation_indices.entry(record.txn_id).or_insert(0);
10041 let event_position = (*commit_epoch, *index);
10042 *index = index.saturating_add(1);
10043 if resume.is_some_and(|position| event_position <= position) {
10044 continue;
10045 }
10046 let mut rows = if let Some(payloads) =
10047 spilled_payloads.get(&(record.txn_id, run.table_id))
10048 {
10049 let mut rows = Vec::new();
10050 for payload in payloads {
10051 control.checkpoint()?;
10052 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
10053 return Err(MongrelError::ResourceLimitExceeded {
10054 resource: "CDC spilled row bytes",
10055 requested: payload.len(),
10056 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
10057 });
10058 }
10059 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
10060 if decoded_rows
10061 .saturating_add(rows.len())
10062 .saturating_add(chunk.len())
10063 > CDC_MAX_ROWS
10064 {
10065 return Err(MongrelError::ResourceLimitExceeded {
10066 resource: "CDC decoded rows",
10067 requested: decoded_rows
10068 .saturating_add(rows.len())
10069 .saturating_add(chunk.len()),
10070 limit: CDC_MAX_ROWS,
10071 });
10072 }
10073 rows.extend(chunk);
10074 }
10075 rows
10076 } else {
10077 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
10078 return Ok(CdcBatch {
10079 events: Vec::new(),
10080 current_epoch,
10081 earliest_epoch,
10082 gap: true,
10083 });
10084 };
10085 let table = handle.lock();
10086 let mut reader = match table.open_reader(run.run_id) {
10087 Ok(reader) => reader,
10088 Err(_) => {
10089 return Ok(CdcBatch {
10090 events: Vec::new(),
10091 current_epoch,
10092 earliest_epoch,
10093 gap: true,
10094 })
10095 }
10096 };
10097 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
10098 let rows = reader.all_rows_controlled(control, remaining)?;
10099 drop(reader);
10100 drop(table);
10101 rows
10102 };
10103 for row in &mut rows {
10104 row.committed_epoch = Epoch(*commit_epoch);
10105 }
10106 decoded_rows = decoded_rows.saturating_add(rows.len());
10107 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
10108 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
10109 if events.len() >= CDC_MAX_EVENTS {
10110 return Err(MongrelError::ResourceLimitExceeded {
10111 resource: "CDC events",
10112 requested: events.len().saturating_add(1),
10113 limit: CDC_MAX_EVENTS,
10114 });
10115 }
10116 events.push(ChangeEvent {
10117 id: Some(format!("{}:{}", event_position.0, event_position.1)),
10118 channel: "changes".into(),
10119 table_id: Some(run.table_id),
10120 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
10121 op: "put_run".into(),
10122 epoch: *commit_epoch,
10123 txn_id: Some(record.txn_id),
10124 message: None,
10125 data: Some(serde_json::json!({
10126 "run_id": run.run_id.to_string(),
10127 "row_count": run.row_count,
10128 "min_row_id": run.min_row_id,
10129 "max_row_id": run.max_row_id,
10130 "rows": rows,
10131 })),
10132 });
10133 }
10134 }
10135 }
10136 control.checkpoint()?;
10137 Ok(CdcBatch {
10138 events,
10139 current_epoch,
10140 earliest_epoch,
10141 gap: false,
10142 })
10143 }
10144
10145 pub fn notify(&self, channel: &str, message: Option<String>) {
10148 let _ = self.notify.send(ChangeEvent {
10149 id: None,
10150 channel: channel.to_string(),
10151 table_id: None,
10152 table: String::new(),
10153 op: "notify".into(),
10154 epoch: self.epoch.visible().0,
10155 txn_id: None,
10156 message,
10157 data: None,
10158 });
10159 }
10160
10161 pub fn call_procedure(
10162 &self,
10163 name: &str,
10164 args: HashMap<String, crate::Value>,
10165 ) -> Result<ProcedureCallResult> {
10166 self.call_procedure_as(name, args, None)
10167 }
10168
10169 pub fn call_procedure_as(
10170 &self,
10171 name: &str,
10172 args: HashMap<String, crate::Value>,
10173 principal: Option<&crate::auth::Principal>,
10174 ) -> Result<ProcedureCallResult> {
10175 let control = crate::ExecutionControl::new(None);
10176 self.call_procedure_as_controlled(name, args, principal, &control, || true)
10177 }
10178
10179 #[doc(hidden)]
10182 pub fn call_procedure_as_bound(
10183 &self,
10184 expected: &StoredProcedure,
10185 args: HashMap<String, crate::Value>,
10186 principal: Option<&crate::auth::Principal>,
10187 ) -> Result<ProcedureCallResult> {
10188 self.require_for(principal, &crate::auth::Permission::All)?;
10189 let procedure = self.procedure(&expected.name).ok_or_else(|| {
10190 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
10191 })?;
10192 if &procedure != expected {
10193 return Err(MongrelError::Conflict(format!(
10194 "procedure {:?} changed after request authorization",
10195 expected.name
10196 )));
10197 }
10198 let control = crate::ExecutionControl::new(None);
10199 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
10200 }
10201
10202 #[doc(hidden)]
10207 pub fn call_procedure_as_controlled<F>(
10208 &self,
10209 name: &str,
10210 args: HashMap<String, crate::Value>,
10211 principal: Option<&crate::auth::Principal>,
10212 control: &crate::ExecutionControl,
10213 before_commit: F,
10214 ) -> Result<ProcedureCallResult>
10215 where
10216 F: FnOnce() -> bool,
10217 {
10218 self.require_for(principal, &crate::auth::Permission::All)?;
10222 let procedure = self
10223 .procedure(name)
10224 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
10225 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
10226 }
10227
10228 fn execute_procedure_as_controlled<F>(
10229 &self,
10230 procedure: StoredProcedure,
10231 args: HashMap<String, crate::Value>,
10232 principal: Option<&crate::auth::Principal>,
10233 control: &crate::ExecutionControl,
10234 before_commit: F,
10235 ) -> Result<ProcedureCallResult>
10236 where
10237 F: FnOnce() -> bool,
10238 {
10239 let args = bind_procedure_args(&procedure, args)?;
10240 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
10241 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
10242 if has_writes {
10243 let mut tx = self.begin_as(principal.cloned());
10244 let run = (|| {
10245 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10246 if step_index % 256 == 0 {
10247 control.checkpoint()?;
10248 }
10249 let output = self.execute_procedure_step(
10250 step,
10251 &args,
10252 &outputs,
10253 Some(&mut tx),
10254 principal,
10255 Some(control),
10256 )?;
10257 outputs.insert(step.id().to_string(), output);
10258 }
10259 control.checkpoint()?;
10260 eval_return_output(&procedure.body.return_value, &args, &outputs)
10261 })();
10262 match run {
10263 Ok(output) => {
10264 control.checkpoint()?;
10265 if !before_commit() {
10266 tx.rollback();
10267 return Err(MongrelError::Cancelled);
10268 }
10269 let epoch = tx.commit()?.0;
10270 Ok(ProcedureCallResult {
10271 epoch: Some(epoch),
10272 output,
10273 })
10274 }
10275 Err(e) => {
10276 tx.rollback();
10277 Err(e)
10278 }
10279 }
10280 } else {
10281 for (step_index, step) in procedure.body.steps.iter().enumerate() {
10282 if step_index % 256 == 0 {
10283 control.checkpoint()?;
10284 }
10285 let output = self.execute_procedure_step(
10286 step,
10287 &args,
10288 &outputs,
10289 None,
10290 principal,
10291 Some(control),
10292 )?;
10293 outputs.insert(step.id().to_string(), output);
10294 }
10295 control.checkpoint()?;
10296 Ok(ProcedureCallResult {
10297 epoch: None,
10298 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
10299 })
10300 }
10301 }
10302
10303 fn execute_procedure_step(
10304 &self,
10305 step: &ProcedureStep,
10306 args: &HashMap<String, crate::Value>,
10307 outputs: &HashMap<String, ProcedureCallOutput>,
10308 tx: Option<&mut crate::txn::Transaction<'_>>,
10309 principal: Option<&crate::auth::Principal>,
10310 control: Option<&crate::ExecutionControl>,
10311 ) -> Result<ProcedureCallOutput> {
10312 if let Some(control) = control {
10313 control.checkpoint()?;
10314 }
10315 match step {
10316 ProcedureStep::NativeQuery {
10317 table,
10318 conditions,
10319 projection,
10320 limit,
10321 ..
10322 } => {
10323 let mut q = crate::Query::new();
10324 for condition in conditions {
10325 q = q.and(eval_condition(condition, args, outputs)?);
10326 }
10327 let fallback_control = crate::ExecutionControl::new(None);
10328 let query_control = control.unwrap_or(&fallback_control);
10329 let mut rows = self.query_for_principal_controlled(
10330 table,
10331 &q,
10332 projection.as_deref(),
10333 principal,
10334 false,
10335 query_control,
10336 )?;
10337 if let Some(limit) = limit {
10338 rows.truncate(*limit);
10339 }
10340 let mut output = Vec::with_capacity(rows.len());
10341 for (row_index, row) in rows.into_iter().enumerate() {
10342 if row_index % 256 == 0 {
10343 if let Some(control) = control {
10344 control.checkpoint()?;
10345 }
10346 }
10347 output.push(ProcedureCallRow {
10348 row_id: Some(row.row_id),
10349 columns: row.columns,
10350 });
10351 }
10352 Ok(ProcedureCallOutput::Rows(output))
10353 }
10354 ProcedureStep::Put {
10355 table,
10356 cells,
10357 returning,
10358 ..
10359 } => {
10360 let tx = tx.ok_or_else(|| {
10361 MongrelError::InvalidArgument(
10362 "write procedure step requires a transaction".into(),
10363 )
10364 })?;
10365 let cells = eval_cells(cells, args, outputs)?;
10366 if *returning {
10367 let out = tx.put_returning(table, cells)?;
10368 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10369 row_id: None,
10370 columns: out.row.columns.into_iter().collect(),
10371 }))
10372 } else {
10373 tx.put(table, cells)?;
10374 Ok(ProcedureCallOutput::Null)
10375 }
10376 }
10377 ProcedureStep::Upsert {
10378 table,
10379 cells,
10380 update_cells,
10381 returning,
10382 ..
10383 } => {
10384 let tx = tx.ok_or_else(|| {
10385 MongrelError::InvalidArgument(
10386 "write procedure step requires a transaction".into(),
10387 )
10388 })?;
10389 let cells = eval_cells(cells, args, outputs)?;
10390 let action = match update_cells {
10391 Some(update_cells) => {
10392 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
10393 }
10394 None => crate::UpsertAction::DoNothing,
10395 };
10396 let out = tx.upsert(table, cells, action)?;
10397 if *returning {
10398 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
10399 row_id: None,
10400 columns: out.row.columns.into_iter().collect(),
10401 }))
10402 } else {
10403 Ok(ProcedureCallOutput::Null)
10404 }
10405 }
10406 ProcedureStep::DeleteByPk { table, pk, .. } => {
10407 let tx = tx.ok_or_else(|| {
10408 MongrelError::InvalidArgument(
10409 "write procedure step requires a transaction".into(),
10410 )
10411 })?;
10412 let pk = eval_value(pk, args, outputs)?;
10413 let handle = self.table(table)?;
10414 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
10415 MongrelError::NotFound("procedure delete_by_pk target not found".into())
10416 })?;
10417 tx.delete(table, row_id)?;
10418 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
10419 }
10420 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
10421 "DeleteRows procedure step is not supported by the core executor yet".into(),
10422 )),
10423 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
10424 "SqlQuery procedure step must be executed by mongreldb-query".into(),
10425 )),
10426 }
10427 }
10428
10429 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
10430 let cat = self.catalog.read();
10431 for step in &procedure.body.steps {
10432 let Some(table_name) = step.table() else {
10433 continue;
10434 };
10435 let schema = &cat
10436 .live(table_name)
10437 .ok_or_else(|| {
10438 MongrelError::InvalidArgument(format!(
10439 "procedure {:?} references unknown table {table_name:?}",
10440 procedure.name
10441 ))
10442 })?
10443 .schema;
10444 match step {
10445 ProcedureStep::NativeQuery {
10446 conditions,
10447 projection,
10448 ..
10449 } => {
10450 for condition in conditions {
10451 validate_condition_columns(condition, schema)?;
10452 }
10453 if let Some(projection) = projection {
10454 for id in projection {
10455 validate_column_id(*id, schema)?;
10456 }
10457 }
10458 }
10459 ProcedureStep::Put { cells, .. } => {
10460 for cell in cells {
10461 validate_column_id(cell.column_id, schema)?;
10462 }
10463 }
10464 ProcedureStep::Upsert {
10465 cells,
10466 update_cells,
10467 ..
10468 } => {
10469 for cell in cells {
10470 validate_column_id(cell.column_id, schema)?;
10471 }
10472 if let Some(update_cells) = update_cells {
10473 for cell in update_cells {
10474 validate_column_id(cell.column_id, schema)?;
10475 }
10476 }
10477 }
10478 ProcedureStep::DeleteByPk { .. } => {
10479 if schema.primary_key().is_none() {
10480 return Err(MongrelError::InvalidArgument(format!(
10481 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
10482 procedure.name
10483 )));
10484 }
10485 }
10486 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
10487 }
10488 }
10489 Ok(())
10490 }
10491
10492 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
10493 let cat = self.catalog.read();
10494 let target_schema = match &trigger.target {
10495 TriggerTarget::Table(target_name) => cat
10496 .live(target_name)
10497 .ok_or_else(|| {
10498 MongrelError::InvalidArgument(format!(
10499 "trigger {:?} references unknown target table {target_name:?}",
10500 trigger.name
10501 ))
10502 })?
10503 .schema
10504 .clone(),
10505 TriggerTarget::View(_) => Schema {
10506 columns: trigger.target_columns.clone(),
10507 ..Schema::default()
10508 },
10509 };
10510 for col in &trigger.update_of {
10511 if target_schema.column(col).is_none() {
10512 return Err(MongrelError::InvalidArgument(format!(
10513 "trigger {:?} UPDATE OF references unknown column {col:?}",
10514 trigger.name
10515 )));
10516 }
10517 }
10518 if let Some(expr) = &trigger.when {
10519 validate_trigger_expr(expr, &target_schema, trigger.event)?;
10520 }
10521 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
10522 for step in &trigger.program.steps {
10523 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
10524 {
10525 return Err(MongrelError::InvalidArgument(
10526 "SetNew trigger steps are only valid in BEFORE triggers".into(),
10527 ));
10528 }
10529 validate_trigger_step(
10530 step,
10531 &cat,
10532 &target_schema,
10533 trigger.event,
10534 &mut select_schemas,
10535 )?;
10536 }
10537 Ok(())
10538 }
10539
10540 pub fn begin(&self) -> crate::txn::Transaction<'_> {
10542 self.begin_with_isolation(crate::txn::IsolationLevel::default())
10543 }
10544
10545 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
10546 let principal = self.principal.read().clone();
10547 let catalog_bound = principal
10548 .as_ref()
10549 .is_some_and(|principal| principal.user_id != 0);
10550 (principal, catalog_bound)
10551 }
10552
10553 pub fn begin_as(
10554 &self,
10555 principal: Option<crate::auth::Principal>,
10556 ) -> crate::txn::Transaction<'_> {
10557 let catalog_bound = principal
10558 .as_ref()
10559 .is_some_and(|principal| principal.user_id != 0);
10560 let txn_id = self.alloc_txn_id();
10561 let read = self.visible_snapshot();
10562 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10563 .with_principal(principal, catalog_bound)
10564 }
10565
10566 pub fn begin_with_isolation(
10568 &self,
10569 level: crate::txn::IsolationLevel,
10570 ) -> crate::txn::Transaction<'_> {
10571 let txn_id = self.alloc_txn_id();
10572 let read = self.visible_snapshot();
10575 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10576 crate::txn::Transaction::new(self, txn_id, read, level)
10577 .with_principal(principal, catalog_bound)
10578 }
10579
10580 pub fn begin_with_external_trigger_bridge<'a>(
10583 &'a self,
10584 bridge: &'a dyn ExternalTriggerBridge,
10585 ) -> crate::txn::Transaction<'a> {
10586 let txn_id = self.alloc_txn_id();
10587 let read = self.visible_snapshot();
10588 let (principal, catalog_bound) = self.transaction_principal_snapshot();
10589 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10590 .with_external_trigger_bridge(bridge)
10591 .with_principal(principal, catalog_bound)
10592 }
10593
10594 pub fn begin_with_external_trigger_bridge_as<'a>(
10595 &'a self,
10596 bridge: &'a dyn ExternalTriggerBridge,
10597 principal: Option<crate::auth::Principal>,
10598 ) -> crate::txn::Transaction<'a> {
10599 let catalog_bound = principal
10600 .as_ref()
10601 .is_some_and(|principal| principal.user_id != 0);
10602 let txn_id = self.alloc_txn_id();
10603 let read = self.visible_snapshot();
10604 crate::txn::Transaction::new(self, txn_id, read, crate::txn::IsolationLevel::default())
10605 .with_external_trigger_bridge(bridge)
10606 .with_principal(principal, catalog_bound)
10607 }
10608
10609 pub fn transaction<T>(
10611 &self,
10612 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10613 ) -> Result<T> {
10614 let mut tx = self.begin();
10615 match f(&mut tx) {
10616 Ok(out) => {
10617 tx.commit()?;
10618 Ok(out)
10619 }
10620 Err(e) => {
10621 tx.rollback();
10622 Err(e)
10623 }
10624 }
10625 }
10626
10627 pub fn transaction_with_row_ids<T>(
10628 &self,
10629 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10630 ) -> Result<(T, Vec<RowId>)> {
10631 let mut tx = self.begin();
10632 match f(&mut tx) {
10633 Ok(output) => {
10634 let (_, row_ids) = tx.commit_with_row_ids()?;
10635 Ok((output, row_ids))
10636 }
10637 Err(error) => {
10638 tx.rollback();
10639 Err(error)
10640 }
10641 }
10642 }
10643
10644 pub fn transaction_for_current_principal<T>(
10645 &self,
10646 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10647 ) -> Result<T> {
10648 if self.principal.read().is_some() {
10649 self.refresh_principal()?;
10650 }
10651 let mut transaction = self.begin_as(self.principal.read().clone());
10652 match f(&mut transaction) {
10653 Ok(output) => {
10654 transaction.commit()?;
10655 Ok(output)
10656 }
10657 Err(error) => {
10658 transaction.rollback();
10659 Err(error)
10660 }
10661 }
10662 }
10663
10664 pub fn transaction_for_current_principal_with_epoch<T>(
10665 &self,
10666 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10667 ) -> Result<(Epoch, T)> {
10668 if self.principal.read().is_some() {
10669 self.refresh_principal()?;
10670 }
10671 let mut transaction = self.begin_as(self.principal.read().clone());
10672 match f(&mut transaction) {
10673 Ok(output) => {
10674 let epoch = transaction.commit()?;
10675 Ok((epoch, output))
10676 }
10677 Err(error) => {
10678 transaction.rollback();
10679 Err(error)
10680 }
10681 }
10682 }
10683
10684 pub fn transaction_with_row_ids_for_current_principal<T>(
10685 &self,
10686 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10687 ) -> Result<(T, Vec<RowId>)> {
10688 if self.principal.read().is_some() {
10689 self.refresh_principal()?;
10690 }
10691 let mut transaction = self.begin_as(self.principal.read().clone());
10692 match f(&mut transaction) {
10693 Ok(output) => {
10694 let (_, row_ids) = transaction.commit_with_row_ids()?;
10695 Ok((output, row_ids))
10696 }
10697 Err(error) => {
10698 transaction.rollback();
10699 Err(error)
10700 }
10701 }
10702 }
10703
10704 pub fn transaction_with_external_trigger_bridge<'a, T>(
10707 &'a self,
10708 bridge: &'a dyn ExternalTriggerBridge,
10709 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10710 ) -> Result<T> {
10711 let mut tx = self.begin_with_external_trigger_bridge(bridge);
10712 match f(&mut tx) {
10713 Ok(out) => {
10714 tx.commit()?;
10715 Ok(out)
10716 }
10717 Err(e) => {
10718 tx.rollback();
10719 Err(e)
10720 }
10721 }
10722 }
10723
10724 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
10725 &'a self,
10726 bridge: &'a dyn ExternalTriggerBridge,
10727 principal: Option<crate::auth::Principal>,
10728 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
10729 ) -> Result<T> {
10730 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
10731 match f(&mut tx) {
10732 Ok(output) => {
10733 tx.commit()?;
10734 Ok(output)
10735 }
10736 Err(error) => {
10737 tx.rollback();
10738 Err(error)
10739 }
10740 }
10741 }
10742
10743 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
10746 self.active_txns.register(epoch)
10747 }
10748
10749 fn fill_auto_increment_for_staging(
10750 &self,
10751 txn_id: u64,
10752 staging: &mut [(u64, crate::txn::Staged)],
10753 control: Option<&crate::ExecutionControl>,
10754 ) -> Result<()> {
10755 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
10756 for (index, (table_id, staged)) in staging.iter().enumerate() {
10757 commit_prepare_checkpoint(control, index)?;
10758 if matches!(staged, crate::txn::Staged::Put(_)) {
10759 puts_by_table.entry(*table_id).or_default().push(index);
10760 }
10761 }
10762
10763 {
10770 let mut barrier_tables: Vec<u64> = puts_by_table
10771 .iter()
10772 .filter(|(table_id, indexes)| {
10773 indexes.iter().any(|index| {
10774 matches!(
10775 &staging[*index].1,
10776 crate::txn::Staged::Put(cells)
10777 if self.table_auto_inc_would_allocate(**table_id, cells)
10778 )
10779 })
10780 })
10781 .map(|(table_id, _)| *table_id)
10782 .collect();
10783 barrier_tables.sort_unstable();
10784 for table_id in barrier_tables {
10785 self.acquire_txn_lock(
10786 txn_id,
10787 crate::locks::LockKey::sequence_barrier(&format!("auto_inc:{table_id}")),
10788 crate::locks::LockMode::Exclusive,
10789 control,
10790 )?;
10791 }
10792 }
10793
10794 let tables = self.tables.read();
10795 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
10796 commit_prepare_checkpoint(control, table_index)?;
10797 if let Some(handle) = tables.get(&table_id) {
10798 #[cfg(test)]
10799 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10800 let mut t = handle.lock();
10801 for (fill_index, index) in indexes.into_iter().enumerate() {
10802 commit_prepare_checkpoint(control, fill_index)?;
10803 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
10804 t.fill_auto_inc(cells)?;
10805 }
10806 }
10807 }
10808 }
10809 Ok(())
10810 }
10811
10812 fn expand_table_triggers(
10813 &self,
10814 txn_id: u64,
10815 staging: &mut Vec<(u64, crate::txn::Staged)>,
10816 read_epoch: Epoch,
10817 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
10818 external_states: &mut Vec<(String, Vec<u8>)>,
10819 control: Option<&crate::ExecutionControl>,
10820 ) -> Result<()> {
10821 commit_prepare_checkpoint(control, 0)?;
10822 let mut external_writes = Vec::new();
10823 let config = self.trigger_config();
10824 if config.recursive_triggers {
10825 let chunk = std::mem::take(staging);
10826 let stacks = vec![Vec::new(); chunk.len()];
10827 *staging = self.expand_trigger_chunk(
10828 txn_id,
10829 chunk,
10830 stacks,
10831 read_epoch,
10832 0,
10833 config.max_depth,
10834 &mut external_writes,
10835 &config,
10836 control,
10837 )?;
10838 self.apply_external_trigger_writes(
10839 external_writes,
10840 external_trigger_bridge,
10841 external_states,
10842 staging,
10843 control,
10844 )?;
10845 return Ok(());
10846 }
10847
10848 let mut expansion =
10849 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
10850 if !expansion.before.is_empty() {
10851 let mut final_staging = expansion.before;
10852 final_staging.extend(filter_ignored_staging(
10853 std::mem::take(staging),
10854 &expansion.ignored_indices,
10855 ));
10856 *staging = final_staging;
10857 } else if !expansion.ignored_indices.is_empty() {
10858 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
10859 }
10860 staging.append(&mut expansion.after);
10861 external_writes.append(&mut expansion.before_external);
10862 external_writes.append(&mut expansion.after_external);
10863 self.apply_external_trigger_writes(
10864 external_writes,
10865 external_trigger_bridge,
10866 external_states,
10867 staging,
10868 control,
10869 )?;
10870 Ok(())
10871 }
10872
10873 #[allow(clippy::too_many_arguments)]
10874 fn expand_trigger_chunk(
10875 &self,
10876 txn_id: u64,
10877 mut chunk: Vec<(u64, crate::txn::Staged)>,
10878 stacks: Vec<Vec<String>>,
10879 read_epoch: Epoch,
10880 depth: u32,
10881 max_depth: u32,
10882 external_writes: &mut Vec<ExternalTriggerWrite>,
10883 config: &TriggerConfig,
10884 control: Option<&crate::ExecutionControl>,
10885 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
10886 if chunk.is_empty() {
10887 return Ok(Vec::new());
10888 }
10889 commit_prepare_checkpoint(control, 0)?;
10890 self.fill_auto_increment_for_staging(txn_id, &mut chunk, control)?;
10891 let expansion = self.expand_table_triggers_once(
10892 &mut chunk,
10893 read_epoch,
10894 Some(&stacks),
10895 config,
10896 control,
10897 )?;
10898 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
10899 let stack = expansion
10900 .before_stacks
10901 .first()
10902 .or_else(|| expansion.after_stacks.first())
10903 .cloned()
10904 .unwrap_or_default();
10905 return Err(MongrelError::TriggerValidation(format!(
10906 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
10907 Self::format_trigger_stack(&stack)
10908 )));
10909 }
10910
10911 let mut out = Vec::new();
10912 external_writes.extend(expansion.before_external);
10913 out.extend(self.expand_trigger_chunk(
10914 txn_id,
10915 expansion.before,
10916 expansion.before_stacks,
10917 read_epoch,
10918 depth + 1,
10919 max_depth,
10920 external_writes,
10921 config,
10922 control,
10923 )?);
10924 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
10925 external_writes.extend(expansion.after_external);
10926 out.extend(self.expand_trigger_chunk(
10927 txn_id,
10928 expansion.after,
10929 expansion.after_stacks,
10930 read_epoch,
10931 depth + 1,
10932 max_depth,
10933 external_writes,
10934 config,
10935 control,
10936 )?);
10937 Ok(out)
10938 }
10939
10940 fn apply_external_trigger_writes(
10941 &self,
10942 writes: Vec<ExternalTriggerWrite>,
10943 bridge: Option<&dyn ExternalTriggerBridge>,
10944 external_states: &mut Vec<(String, Vec<u8>)>,
10945 staging: &mut Vec<(u64, crate::txn::Staged)>,
10946 control: Option<&crate::ExecutionControl>,
10947 ) -> Result<()> {
10948 if writes.is_empty() {
10949 return Ok(());
10950 }
10951 let bridge = bridge.ok_or_else(|| {
10952 MongrelError::TriggerValidation(
10953 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
10954 )
10955 })?;
10956 for (write_index, write) in writes.into_iter().enumerate() {
10957 commit_prepare_checkpoint(control, write_index)?;
10958 let table = write.table().to_string();
10959 let entry = self.external_table(&table).ok_or_else(|| {
10960 MongrelError::NotFound(format!("external table {table:?} not found"))
10961 })?;
10962 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
10963 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
10964 external_states.push((table, result.state));
10965 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
10966 commit_prepare_checkpoint(control, base_index)?;
10967 match base_write {
10968 ExternalTriggerBaseWrite::Put { table, cells } => {
10969 let table_id = self.table_id(&table)?;
10970 staging.push((table_id, crate::txn::Staged::Put(cells)));
10971 }
10972 ExternalTriggerBaseWrite::Delete { table, row_id } => {
10973 let table_id = self.table_id(&table)?;
10974 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
10975 }
10976 }
10977 }
10978 }
10979 dedup_external_states_in_place(external_states);
10980 Ok(())
10981 }
10982
10983 fn expand_table_triggers_once(
10984 &self,
10985 staging: &mut Vec<(u64, crate::txn::Staged)>,
10986 read_epoch: Epoch,
10987 trigger_stacks: Option<&[Vec<String>]>,
10988 config: &TriggerConfig,
10989 control: Option<&crate::ExecutionControl>,
10990 ) -> Result<TriggerExpansion> {
10991 commit_prepare_checkpoint(control, 0)?;
10992 let triggers: Vec<StoredTrigger> = self
10993 .catalog
10994 .read()
10995 .triggers
10996 .iter()
10997 .filter(|entry| {
10998 entry.trigger.enabled
10999 && matches!(
11000 entry.trigger.timing,
11001 TriggerTiming::Before | TriggerTiming::After
11002 )
11003 && matches!(entry.trigger.target, TriggerTarget::Table(_))
11004 })
11005 .map(|entry| entry.trigger.clone())
11006 .collect();
11007 if triggers.is_empty() || staging.is_empty() {
11008 return Ok(TriggerExpansion::default());
11009 }
11010
11011 let before_triggers = triggers
11012 .iter()
11013 .filter(|trigger| trigger.timing == TriggerTiming::Before)
11014 .cloned()
11015 .collect::<Vec<_>>();
11016 let after_triggers = triggers
11017 .iter()
11018 .filter(|trigger| trigger.timing == TriggerTiming::After)
11019 .cloned()
11020 .collect::<Vec<_>>();
11021
11022 let mut before_added = Vec::new();
11023 let mut before_stacks = Vec::new();
11024 let mut before_external = Vec::new();
11025 let mut ignored_indices = std::collections::BTreeSet::new();
11026 if !before_triggers.is_empty() {
11027 let before_events =
11028 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
11029 let mut out = TriggerProgramOutput {
11030 added: &mut before_added,
11031 added_stacks: &mut before_stacks,
11032 added_external: &mut before_external,
11033 ignored_indices: &mut ignored_indices,
11034 };
11035 self.execute_triggers_for_events(
11036 &before_triggers,
11037 &before_events,
11038 Some(staging),
11039 &mut out,
11040 config,
11041 read_epoch,
11042 control,
11043 )?;
11044 }
11045
11046 let after_events = if after_triggers.is_empty() {
11047 Vec::new()
11048 } else {
11049 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
11050 .into_iter()
11051 .filter(|event| {
11052 !event
11053 .op_indices
11054 .iter()
11055 .any(|idx| ignored_indices.contains(idx))
11056 })
11057 .collect()
11058 };
11059
11060 let mut after_added = Vec::new();
11061 let mut after_stacks = Vec::new();
11062 let mut after_external = Vec::new();
11063 let mut out = TriggerProgramOutput {
11064 added: &mut after_added,
11065 added_stacks: &mut after_stacks,
11066 added_external: &mut after_external,
11067 ignored_indices: &mut ignored_indices,
11068 };
11069 self.execute_triggers_for_events(
11070 &after_triggers,
11071 &after_events,
11072 None,
11073 &mut out,
11074 config,
11075 read_epoch,
11076 control,
11077 )?;
11078 Ok(TriggerExpansion {
11079 before: before_added,
11080 before_stacks,
11081 before_external,
11082 after: after_added,
11083 after_stacks,
11084 after_external,
11085 ignored_indices,
11086 })
11087 }
11088
11089 #[allow(clippy::too_many_arguments)]
11090 fn execute_triggers_for_events(
11091 &self,
11092 triggers: &[StoredTrigger],
11093 events: &[WriteEvent],
11094 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11095 out: &mut TriggerProgramOutput<'_>,
11096 config: &TriggerConfig,
11097 read_epoch: Epoch,
11098 control: Option<&crate::ExecutionControl>,
11099 ) -> Result<()> {
11100 let mut checkpoint_index = 0_usize;
11101 for event in events {
11102 for trigger in triggers {
11103 commit_prepare_checkpoint(control, checkpoint_index)?;
11104 checkpoint_index += 1;
11105 if event
11106 .op_indices
11107 .iter()
11108 .any(|idx| out.ignored_indices.contains(idx))
11109 {
11110 break;
11111 }
11112 let matches = {
11113 let cat = self.catalog.read();
11114 trigger_matches_event(trigger, event, &cat)?
11115 };
11116 if !matches {
11117 continue;
11118 }
11119 if let Some(when) = &trigger.when {
11120 if !eval_trigger_expr(when, event)? {
11121 continue;
11122 }
11123 }
11124 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
11125 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
11126 return Err(MongrelError::TriggerValidation(format!(
11127 "trigger recursion cycle detected; trigger stack: {}",
11128 Self::format_trigger_stack(&trigger_stack)
11129 )));
11130 }
11131 let outcome = match staging.as_mut() {
11132 Some(staging) => self.execute_trigger_program(
11133 trigger,
11134 event,
11135 Some(&mut **staging),
11136 out,
11137 &trigger_stack,
11138 config,
11139 read_epoch,
11140 control,
11141 )?,
11142 None => self.execute_trigger_program(
11143 trigger,
11144 event,
11145 None,
11146 out,
11147 &trigger_stack,
11148 config,
11149 read_epoch,
11150 control,
11151 )?,
11152 };
11153 if outcome == TriggerProgramOutcome::Ignore {
11154 out.ignored_indices.extend(event.op_indices.iter().copied());
11155 break;
11156 }
11157 }
11158 }
11159 Ok(())
11160 }
11161
11162 fn trigger_events_for_staging(
11163 &self,
11164 staging: &[(u64, crate::txn::Staged)],
11165 read_epoch: Epoch,
11166 trigger_stacks: Option<&[Vec<String>]>,
11167 control: Option<&crate::ExecutionControl>,
11168 ) -> Result<Vec<WriteEvent>> {
11169 use crate::txn::Staged;
11170 use std::collections::{HashMap, VecDeque};
11171
11172 let snapshot = self.snapshot_for_epoch(read_epoch);
11173 let cat = self.catalog.read();
11174 let mut table_names = HashMap::new();
11175 let mut table_schemas = HashMap::new();
11176 for entry in cat
11177 .tables
11178 .iter()
11179 .filter(|entry| matches!(entry.state, TableState::Live))
11180 {
11181 table_names.insert(entry.table_id, entry.name.clone());
11182 table_schemas.insert(entry.table_id, entry.schema.clone());
11183 }
11184 drop(cat);
11185
11186 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
11187 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11188 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
11189
11190 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11191 commit_prepare_checkpoint(control, idx)?;
11192 let Some(schema) = table_schemas.get(table_id) else {
11193 continue;
11194 };
11195 let Some(pk) = schema.primary_key() else {
11196 continue;
11197 };
11198 match staged {
11199 Staged::Delete(row_id) => {
11200 let handle = self.table_by_id(*table_id)?;
11201 let Some(row) = handle.lock().get(*row_id, snapshot) else {
11202 continue;
11203 };
11204 let Some(pk_value) = row.columns.get(&pk.id) else {
11205 continue;
11206 };
11207 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
11208 delete_by_key
11209 .entry((*table_id, pk_value.encode_key()))
11210 .or_default()
11211 .push_back(idx);
11212 }
11213 Staged::Put(cells) => {
11214 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
11215 put_by_key
11216 .entry((*table_id, value.encode_key()))
11217 .or_default()
11218 .push_back(idx);
11219 }
11220 }
11221 Staged::Update { row_id, .. } => {
11222 let handle = self.table_by_id(*table_id)?;
11223 let row = handle.lock().get(*row_id, snapshot);
11224 if let Some(row) = row {
11225 old_rows.insert(idx, TriggerRowImage::from_row(row));
11226 }
11227 }
11228 Staged::Truncate => {}
11229 }
11230 }
11231
11232 let mut paired_delete = std::collections::HashSet::new();
11233 let mut paired_put = std::collections::HashSet::new();
11234 let mut events = Vec::new();
11235
11236 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
11237 commit_prepare_checkpoint(control, pair_index)?;
11238 let Some(puts) = put_by_key.get_mut(key) else {
11239 continue;
11240 };
11241 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
11242 paired_delete.insert(delete_idx);
11243 paired_put.insert(put_idx);
11244 let (table_id, _) = &staging[put_idx];
11245 let Some(table_name) = table_names.get(table_id).cloned() else {
11246 continue;
11247 };
11248 let old = old_rows.get(&delete_idx).cloned();
11249 let new = match &staging[put_idx].1 {
11250 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
11251 _ => None,
11252 };
11253 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11254 events.push(WriteEvent {
11255 table: table_name,
11256 kind: TriggerEvent::Update,
11257 old,
11258 new,
11259 changed_columns,
11260 op_indices: vec![delete_idx, put_idx],
11261 put_idx: Some(put_idx),
11262 trigger_stack: Self::trigger_stack_for_indices(
11263 trigger_stacks,
11264 &[delete_idx, put_idx],
11265 ),
11266 });
11267 }
11268 }
11269
11270 for (idx, (table_id, staged)) in staging.iter().enumerate() {
11271 commit_prepare_checkpoint(control, idx)?;
11272 let Some(table_name) = table_names.get(table_id).cloned() else {
11273 continue;
11274 };
11275 match staged {
11276 Staged::Put(cells) if !paired_put.contains(&idx) => {
11277 let new = Some(TriggerRowImage::from_cells(cells));
11278 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
11279 events.push(WriteEvent {
11280 table: table_name,
11281 kind: TriggerEvent::Insert,
11282 old: None,
11283 new,
11284 changed_columns,
11285 op_indices: vec![idx],
11286 put_idx: Some(idx),
11287 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11288 });
11289 }
11290 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
11291 let old = match old_rows.get(&idx).cloned() {
11292 Some(old) => Some(old),
11293 None => {
11294 let handle = self.table_by_id(*table_id)?;
11295 let row = handle.lock().get(*row_id, snapshot);
11296 row.map(TriggerRowImage::from_row)
11297 }
11298 };
11299 let Some(old) = old else {
11300 continue;
11301 };
11302 let changed_columns = old.columns.keys().copied().collect();
11303 events.push(WriteEvent {
11304 table: table_name,
11305 kind: TriggerEvent::Delete,
11306 old: Some(old),
11307 new: None,
11308 changed_columns,
11309 op_indices: vec![idx],
11310 put_idx: None,
11311 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11312 });
11313 }
11314 Staged::Update { new_row: cells, .. } => {
11315 let old = old_rows.get(&idx).cloned();
11316 let new = Some(TriggerRowImage::from_cells(cells));
11317 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
11318 events.push(WriteEvent {
11319 table: table_name,
11320 kind: TriggerEvent::Update,
11321 old,
11322 new,
11323 changed_columns,
11324 op_indices: vec![idx],
11325 put_idx: Some(idx),
11326 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
11327 });
11328 }
11329 Staged::Truncate => {}
11330 _ => {}
11331 }
11332 }
11333
11334 Ok(events)
11335 }
11336
11337 #[allow(clippy::too_many_arguments)]
11338 fn execute_trigger_program(
11339 &self,
11340 trigger: &StoredTrigger,
11341 event: &WriteEvent,
11342 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11343 out: &mut TriggerProgramOutput<'_>,
11344 trigger_stack: &[String],
11345 config: &TriggerConfig,
11346 read_epoch: Epoch,
11347 control: Option<&crate::ExecutionControl>,
11348 ) -> Result<TriggerProgramOutcome> {
11349 let mut event = event.clone();
11350 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
11351 self.execute_trigger_steps(
11352 trigger,
11353 &trigger.program.steps,
11354 &mut event,
11355 staging,
11356 out,
11357 trigger_stack,
11358 config,
11359 &mut select_results,
11360 0,
11361 None,
11362 read_epoch,
11363 control,
11364 )
11365 }
11366
11367 #[allow(clippy::too_many_arguments)]
11368 fn execute_trigger_steps(
11369 &self,
11370 trigger: &StoredTrigger,
11371 steps: &[TriggerStep],
11372 event: &mut WriteEvent,
11373 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
11374 out: &mut TriggerProgramOutput<'_>,
11375 trigger_stack: &[String],
11376 config: &TriggerConfig,
11377 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
11378 depth: u32,
11379 selected: Option<&TriggerRowImage>,
11380 read_epoch: Epoch,
11381 control: Option<&crate::ExecutionControl>,
11382 ) -> Result<TriggerProgramOutcome> {
11383 let _ = depth;
11384 for (step_index, step) in steps.iter().enumerate() {
11385 commit_prepare_checkpoint(control, step_index)?;
11386 match step {
11387 TriggerStep::SetNew { cells } => {
11388 if trigger.timing != TriggerTiming::Before {
11389 return Err(MongrelError::InvalidArgument(
11390 "SetNew trigger step is only valid in BEFORE triggers".into(),
11391 ));
11392 }
11393 let put_idx = event.put_idx.ok_or_else(|| {
11394 MongrelError::InvalidArgument(
11395 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
11396 )
11397 })?;
11398 let staging = staging.as_deref_mut().ok_or_else(|| {
11399 MongrelError::InvalidArgument(
11400 "SetNew trigger step requires mutable trigger staging".into(),
11401 )
11402 })?;
11403 let mut update_changed_columns = None;
11404 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
11405 Some(crate::txn::Staged::Put(cells)) => cells,
11406 Some(crate::txn::Staged::Update {
11407 new_row,
11408 changed_columns,
11409 ..
11410 }) => {
11411 update_changed_columns = Some(changed_columns);
11412 new_row
11413 }
11414 _ => {
11415 return Err(MongrelError::InvalidArgument(
11416 "SetNew trigger step target row is not mutable".into(),
11417 ))
11418 }
11419 };
11420 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
11421 row_cells.retain(|(id, _)| *id != column_id);
11422 row_cells.push((column_id, value.clone()));
11423 if let Some(changed_columns) = &mut update_changed_columns {
11424 changed_columns.push(column_id);
11425 }
11426 if let Some(new) = &mut event.new {
11427 new.columns.insert(column_id, value);
11428 }
11429 }
11430 row_cells.sort_by_key(|(id, _)| *id);
11431 if let Some(changed_columns) = update_changed_columns {
11432 changed_columns.sort_unstable();
11433 changed_columns.dedup();
11434 }
11435 }
11436 TriggerStep::Insert { table, cells } => {
11437 let cells = eval_trigger_cells(cells, event, selected)?;
11438 if let Ok(table_id) = self.table_id(table) {
11439 out.added.push((table_id, crate::txn::Staged::Put(cells)));
11440 out.added_stacks.push(trigger_stack.to_vec());
11441 } else if self.external_table(table).is_some() {
11442 out.added_external.push(ExternalTriggerWrite::Insert {
11443 table: table.clone(),
11444 cells,
11445 });
11446 } else {
11447 return Err(MongrelError::NotFound(format!(
11448 "trigger {:?} insert target {table:?} not found",
11449 trigger.name
11450 )));
11451 }
11452 }
11453 TriggerStep::UpdateByPk { table, pk, cells } => {
11454 let pk = eval_trigger_value(pk, event, selected)?;
11455 let cells = eval_trigger_cells(cells, event, selected)?;
11456 if self.external_table(table).is_some() {
11457 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
11458 table: table.clone(),
11459 pk,
11460 cells,
11461 });
11462 } else {
11463 let row_id = self
11464 .table(table)?
11465 .lock()
11466 .lookup_pk(&pk.encode_key())
11467 .ok_or_else(|| {
11468 MongrelError::NotFound(format!(
11469 "trigger {:?} update target not found",
11470 trigger.name
11471 ))
11472 })?;
11473 let handle = self.table(table)?;
11474 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
11475 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
11476 MongrelError::NotFound(format!(
11477 "trigger {:?} update target not visible",
11478 trigger.name
11479 ))
11480 })?;
11481 let mut changed_columns = cells
11482 .iter()
11483 .map(|(column_id, _)| *column_id)
11484 .collect::<Vec<_>>();
11485 changed_columns.sort_unstable();
11486 changed_columns.dedup();
11487 let mut merged = old.columns;
11488 for (column_id, value) in cells {
11489 merged.insert(column_id, value);
11490 }
11491 out.added.push((
11492 self.table_id(table)?,
11493 crate::txn::Staged::Update {
11494 row_id,
11495 new_row: merged.into_iter().collect(),
11496 changed_columns,
11497 },
11498 ));
11499 out.added_stacks.push(trigger_stack.to_vec());
11500 }
11501 }
11502 TriggerStep::DeleteByPk { table, pk } => {
11503 let pk = eval_trigger_value(pk, event, selected)?;
11504 if self.external_table(table).is_some() {
11505 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
11506 table: table.clone(),
11507 pk,
11508 });
11509 } else {
11510 let row_id = self
11511 .table(table)?
11512 .lock()
11513 .lookup_pk(&pk.encode_key())
11514 .ok_or_else(|| {
11515 MongrelError::NotFound(format!(
11516 "trigger {:?} delete target not found",
11517 trigger.name
11518 ))
11519 })?;
11520 out.added
11521 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
11522 out.added_stacks.push(trigger_stack.to_vec());
11523 }
11524 }
11525 TriggerStep::Select {
11526 id,
11527 table,
11528 conditions,
11529 } => {
11530 let schema = self.table(table)?.lock().schema().clone();
11531 let snapshot = self.snapshot_for_epoch(read_epoch);
11532 let handle = self.table(table)?;
11533 let rows = match control {
11534 Some(control) => {
11535 handle.lock().visible_rows_controlled(snapshot, control)?
11536 }
11537 None => handle.lock().visible_rows(snapshot)?,
11538 };
11539 let mut matched = Vec::new();
11540 for (row_index, row) in rows.into_iter().enumerate() {
11541 commit_prepare_checkpoint(control, row_index)?;
11542 let image = TriggerRowImage::from_row(row);
11543 let passes = conditions
11544 .iter()
11545 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11546 .collect::<Result<Vec<_>>>()?
11547 .into_iter()
11548 .all(|b| b);
11549 if passes {
11550 matched.push(image);
11551 }
11552 }
11553 if let Some(pk) = schema.primary_key() {
11554 matched.sort_by(|a, b| {
11555 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
11556 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
11557 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
11558 });
11559 }
11560 select_results.insert(id.clone(), matched);
11561 }
11562 TriggerStep::Foreach { id, steps } => {
11563 let rows = select_results.get(id).ok_or_else(|| {
11564 MongrelError::InvalidArgument(format!(
11565 "trigger {:?} foreach references unknown select id {id:?}",
11566 trigger.name
11567 ))
11568 })?;
11569 if rows.len() > config.max_loop_iterations as usize {
11570 return Err(MongrelError::InvalidArgument(format!(
11571 "trigger {:?} foreach exceeded max_loop_iterations ({})",
11572 trigger.name, config.max_loop_iterations
11573 )));
11574 }
11575 for (row_index, row) in rows.clone().into_iter().enumerate() {
11576 commit_prepare_checkpoint(control, row_index)?;
11577 let result = self.execute_trigger_steps(
11578 trigger,
11579 steps,
11580 event,
11581 staging.as_deref_mut(),
11582 out,
11583 trigger_stack,
11584 config,
11585 select_results,
11586 depth + 1,
11587 Some(&row),
11588 read_epoch,
11589 control,
11590 )?;
11591 if result == TriggerProgramOutcome::Ignore {
11592 return Ok(TriggerProgramOutcome::Ignore);
11593 }
11594 }
11595 }
11596 TriggerStep::DeleteWhere { table, conditions } => {
11597 let schema = self.table(table)?.lock().schema().clone();
11598 let snapshot = self.snapshot_for_epoch(read_epoch);
11599 let handle = self.table(table)?;
11600 let rows = match control {
11601 Some(control) => {
11602 handle.lock().visible_rows_controlled(snapshot, control)?
11603 }
11604 None => handle.lock().visible_rows(snapshot)?,
11605 };
11606 let table_id = self.table_id(table)?;
11607 let mut to_delete = Vec::new();
11608 for (row_index, row) in rows.into_iter().enumerate() {
11609 commit_prepare_checkpoint(control, row_index)?;
11610 let image = TriggerRowImage::from_row(row.clone());
11611 let passes = conditions
11612 .iter()
11613 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11614 .collect::<Result<Vec<_>>>()?
11615 .into_iter()
11616 .all(|b| b);
11617 if passes {
11618 to_delete.push((table_id, row.row_id));
11619 }
11620 }
11621 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
11622 commit_prepare_checkpoint(control, row_index)?;
11623 out.added
11624 .push((table_id, crate::txn::Staged::Delete(row_id)));
11625 out.added_stacks.push(trigger_stack.to_vec());
11626 }
11627 }
11628 TriggerStep::UpdateWhere {
11629 table,
11630 conditions,
11631 cells,
11632 } => {
11633 let schema = self.table(table)?.lock().schema().clone();
11634 let snapshot = self.snapshot_for_epoch(read_epoch);
11635 let handle = self.table(table)?;
11636 let rows = match control {
11637 Some(control) => {
11638 handle.lock().visible_rows_controlled(snapshot, control)?
11639 }
11640 None => handle.lock().visible_rows(snapshot)?,
11641 };
11642 let table_id = self.table_id(table)?;
11643 let mut changed_columns =
11644 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
11645 changed_columns.sort_unstable();
11646 changed_columns.dedup();
11647 let mut to_update = Vec::new();
11648 for (row_index, row) in rows.into_iter().enumerate() {
11649 commit_prepare_checkpoint(control, row_index)?;
11650 let image = TriggerRowImage::from_row(row.clone());
11651 let passes = conditions
11652 .iter()
11653 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
11654 .collect::<Result<Vec<_>>>()?
11655 .into_iter()
11656 .all(|b| b);
11657 if passes {
11658 let new_cells = cells
11659 .iter()
11660 .map(|cell| {
11661 Ok((
11662 cell.column_id,
11663 eval_trigger_value(&cell.value, event, Some(&image))?,
11664 ))
11665 })
11666 .collect::<Result<Vec<_>>>()?;
11667 let mut merged = row.columns.clone();
11668 for (column_id, value) in new_cells {
11669 merged.insert(column_id, value);
11670 }
11671 to_update.push((table_id, row.row_id, merged));
11672 }
11673 }
11674 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
11675 {
11676 commit_prepare_checkpoint(control, row_index)?;
11677 out.added.push((
11678 table_id,
11679 crate::txn::Staged::Update {
11680 row_id,
11681 new_row: merged.into_iter().collect(),
11682 changed_columns: changed_columns.clone(),
11683 },
11684 ));
11685 out.added_stacks.push(trigger_stack.to_vec());
11686 }
11687 }
11688 TriggerStep::Raise { action, message } => match action {
11689 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
11690 TriggerRaiseAction::Abort
11691 | TriggerRaiseAction::Fail
11692 | TriggerRaiseAction::Rollback => {
11693 let message = eval_trigger_value(message, event, selected)?;
11694 return Err(MongrelError::TriggerValidation(format!(
11695 "trigger {:?} raised: {}; trigger stack: {}",
11696 trigger.name,
11697 trigger_message(message),
11698 Self::format_trigger_stack(trigger_stack)
11699 )));
11700 }
11701 },
11702 }
11703 }
11704 Ok(TriggerProgramOutcome::Continue)
11705 }
11706
11707 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
11708 let Some(stacks) = stacks else {
11709 return Vec::new();
11710 };
11711 let mut out = Vec::new();
11712 for idx in indices {
11713 let Some(stack) = stacks.get(*idx) else {
11714 continue;
11715 };
11716 for name in stack {
11717 if !out.iter().any(|existing| existing == name) {
11718 out.push(name.clone());
11719 }
11720 }
11721 }
11722 out
11723 }
11724
11725 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
11726 let mut out = stack.to_vec();
11727 out.push(trigger_name.to_string());
11728 out
11729 }
11730
11731 fn format_trigger_stack(stack: &[String]) -> String {
11732 if stack.is_empty() {
11733 "<root>".into()
11734 } else {
11735 stack.join(" -> ")
11736 }
11737 }
11738
11739 fn acquire_unique_key_claims(
11760 &self,
11761 txn_id: u64,
11762 staging: &[(u64, crate::txn::Staged)],
11763 control: Option<&crate::ExecutionControl>,
11764 ) -> Result<()> {
11765 let catalog = self.catalog.read();
11766 let has_uniques = staging.iter().any(|(table_id, staged)| {
11767 matches!(
11768 staged,
11769 crate::txn::Staged::Put(_) | crate::txn::Staged::Update { .. }
11770 ) && catalog.tables.iter().any(|entry| {
11771 entry.table_id == *table_id
11772 && (entry.schema.primary_key().is_some()
11773 || !entry.schema.constraints.uniques.is_empty())
11774 })
11775 });
11776 if !has_uniques {
11777 return Ok(());
11778 }
11779 let mut claims: Vec<(u64, Vec<u8>)> = Vec::new();
11780 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
11781 commit_prepare_checkpoint(control, staged_index)?;
11782 let cells = match staged {
11783 crate::txn::Staged::Put(cells) => cells,
11784 crate::txn::Staged::Update { new_row, .. } => new_row,
11785 _ => continue,
11786 };
11787 let Some(entry) = catalog
11788 .tables
11789 .iter()
11790 .find(|entry| entry.table_id == *table_id)
11791 else {
11792 continue;
11793 };
11794 for column in &entry.schema.columns {
11795 if !column
11796 .flags
11797 .contains(crate::schema::ColumnFlags::PRIMARY_KEY)
11798 {
11799 continue;
11800 }
11801 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == column.id) {
11802 let mut key = b"pk:".to_vec();
11803 key.extend_from_slice(&value.encode_key());
11804 claims.push((*table_id, key));
11805 }
11806 }
11807 let cells_map: HashMap<u16, Value> = cells.iter().cloned().collect();
11812 for uc in &entry.schema.constraints.uniques {
11813 if let Some(composite) =
11814 crate::constraint::encode_composite_key(&uc.columns, &cells_map)
11815 {
11816 let mut key = format!("uq{}:", uc.id).into_bytes();
11817 key.extend_from_slice(&composite);
11818 claims.push((*table_id, key));
11819 }
11820 }
11821 }
11822 claims.sort();
11823 claims.dedup();
11824 for (table_id, key) in claims {
11825 self.acquire_txn_lock(
11826 txn_id,
11827 crate::locks::LockKey::key(table_id, key),
11828 crate::locks::LockMode::Exclusive,
11829 control,
11830 )?;
11831 }
11832 Ok(())
11833 }
11834
11835 fn acquire_fk_lock(
11839 &self,
11840 txn_id: u64,
11841 table_id: u64,
11842 key: &[u8],
11843 mode: crate::locks::LockMode,
11844 control: Option<&crate::ExecutionControl>,
11845 ) -> Result<()> {
11846 let mut namespaced = b"fk:".to_vec();
11847 namespaced.extend_from_slice(key);
11848 self.acquire_txn_lock(
11849 txn_id,
11850 crate::locks::LockKey::key(table_id, namespaced),
11851 mode,
11852 control,
11853 )?;
11854 let hook = self.fk_lock_hook.lock().clone();
11857 if let Some(hook) = hook {
11858 hook();
11859 }
11860 Ok(())
11861 }
11862
11863 fn validate_constraints(
11864 &self,
11865 txn_id: u64,
11866 staging: &mut Vec<(u64, crate::txn::Staged)>,
11867 read_epoch: Epoch,
11868 principal: Option<&crate::auth::Principal>,
11869 control: Option<&crate::ExecutionControl>,
11870 ) -> Result<()> {
11871 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
11872 use crate::memtable::Row;
11873 use crate::txn::Staged;
11874 use std::collections::HashSet;
11875
11876 commit_prepare_checkpoint(control, 0)?;
11877 let snapshot = self.snapshot_for_epoch(read_epoch);
11880 let (live, table_ids_by_name) = {
11884 let catalog = self.catalog.read();
11885 let any_constraints = catalog.tables.iter().any(|entry| {
11886 matches!(entry.state, TableState::Live | TableState::Building { .. })
11887 && !entry.schema.constraints.is_empty()
11888 });
11889 if !any_constraints {
11890 drop(catalog);
11891 self.materialize_generated_embeddings(staging, control)?;
11892 return Ok(());
11893 }
11894 let live: Vec<(u64, String, crate::schema::Schema)> = catalog
11895 .tables
11896 .iter()
11897 .filter(|entry| {
11898 matches!(entry.state, TableState::Live | TableState::Building { .. })
11899 })
11900 .map(|entry| (entry.table_id, entry.name.clone(), entry.schema.clone()))
11901 .collect();
11902 let table_ids_by_name: HashMap<String, u64> = catalog
11903 .tables
11904 .iter()
11905 .map(|entry| (entry.name.clone(), entry.table_id))
11906 .collect();
11907 (live, table_ids_by_name)
11908 };
11909
11910 let mut rows_cache: HashMap<u64, Arc<Vec<Row>>> = HashMap::new();
11913 let mut load_rows = |table_id: u64| -> Result<Arc<Vec<Row>>> {
11914 if let Some(rows) = rows_cache.get(&table_id) {
11915 return Ok(Arc::clone(rows));
11916 }
11917 let handle = self.table_by_id(table_id)?;
11918 let rows = Arc::new(match control {
11919 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
11920 None => handle.lock().visible_rows(snapshot)?,
11921 });
11922 rows_cache.insert(table_id, Arc::clone(&rows));
11923 Ok(rows)
11924 };
11925
11926 let mut processed_updates = HashSet::new();
11931 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
11932 let mut update_pass = 0_usize;
11933 loop {
11934 commit_prepare_checkpoint(control, update_pass)?;
11935 update_pass += 1;
11936 let updates: Vec<PendingUpdate> = staging
11937 .iter()
11938 .enumerate()
11939 .filter_map(|(index, (table_id, op))| match op {
11940 Staged::Update {
11941 row_id,
11942 new_row: cells,
11943 ..
11944 } if !processed_updates.contains(&index) => {
11945 Some((index, *table_id, *row_id, cells.clone()))
11946 }
11947 _ => None,
11948 })
11949 .collect();
11950 if updates.is_empty() {
11951 break;
11952 }
11953 let mut new_ops = Vec::new();
11954 for (update_index, (index, table_id, row_id, new_cells)) in
11955 updates.into_iter().enumerate()
11956 {
11957 commit_prepare_checkpoint(control, update_index)?;
11958 processed_updates.insert(index);
11959 let Some(tname) = live
11960 .iter()
11961 .find(|(id, _, _)| *id == table_id)
11962 .map(|(_, name, _)| name.as_str())
11963 else {
11964 continue;
11965 };
11966 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
11967 continue;
11968 };
11969 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
11970 for (child_id, _child_name, child_schema) in &live {
11971 for fk in &child_schema.constraints.foreign_keys {
11972 if fk.ref_table != tname {
11973 continue;
11974 }
11975 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
11976 else {
11977 continue;
11978 };
11979 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
11980 == Some(old_key.as_slice())
11981 {
11982 continue;
11983 }
11984 if fk.on_update == FkAction::Restrict {
11985 continue;
11986 }
11987 self.acquire_fk_lock(
11992 txn_id,
11993 table_id,
11994 &old_key,
11995 crate::locks::LockMode::Exclusive,
11996 control,
11997 )?;
11998 let child_rows = load_rows(*child_id)?;
11999 for (child_index, child) in child_rows.iter().enumerate() {
12000 commit_prepare_checkpoint(control, child_index)?;
12001 if encode_composite_key(&fk.columns, &child.columns).as_deref()
12002 != Some(old_key.as_slice())
12003 {
12004 continue;
12005 }
12006 if staging.iter().any(|(id, op)| {
12007 *id == *child_id
12008 && matches!(op, Staged::Delete(id) if *id == child.row_id)
12009 }) {
12010 continue;
12011 }
12012 let mut cells: Vec<(u16, Value)> = child
12013 .columns
12014 .iter()
12015 .map(|(column_id, value)| (*column_id, value.clone()))
12016 .collect();
12017 for (child_column, parent_column) in
12018 fk.columns.iter().zip(&fk.ref_columns)
12019 {
12020 cells.retain(|(column_id, _)| column_id != child_column);
12021 let value = match fk.on_update {
12022 FkAction::Cascade => {
12023 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
12024 }
12025 FkAction::SetNull => Value::Null,
12026 FkAction::Restrict => {
12027 return Err(MongrelError::Other(
12028 "restricted foreign-key update reached cascade preparation"
12029 .into(),
12030 ));
12031 }
12032 };
12033 cells.push((*child_column, value));
12034 }
12035 cells.sort_by_key(|(column_id, _)| *column_id);
12036 if let Some(existing_index) = staging.iter().position(|(id, op)| {
12037 *id == *child_id
12038 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
12039 }) {
12040 if let Staged::Update {
12041 new_row: existing,
12042 changed_columns,
12043 ..
12044 } = &mut staging[existing_index].1 {
12045 changed_columns.extend(fk.columns.iter().copied());
12046 changed_columns.sort_unstable();
12047 changed_columns.dedup();
12048 if *existing != cells {
12049 *existing = cells;
12050 processed_updates.remove(&existing_index);
12051 }
12052 }
12053 } else {
12054 new_ops.push((
12055 *child_id,
12056 Staged::Update {
12057 row_id: child.row_id,
12058 new_row: cells,
12059 changed_columns: fk.columns.clone(),
12060 },
12061 ));
12062 }
12063 }
12064 }
12065 }
12066 }
12067 staging.extend(new_ops);
12068 }
12069
12070 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
12075 let mut cascade_pass = 0_usize;
12076 loop {
12077 commit_prepare_checkpoint(control, cascade_pass)?;
12078 cascade_pass += 1;
12079 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
12080 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
12081 .iter()
12082 .filter_map(|(t, op)| match op {
12083 Staged::Delete(rid) => Some((*t, *rid)),
12084 _ => None,
12085 })
12086 .collect();
12087 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
12088 commit_prepare_checkpoint(control, delete_index)?;
12089 if !cascaded.insert((table_id, rid.0)) {
12090 continue;
12091 }
12092 let Some(tname) = live
12093 .iter()
12094 .find(|(t, _, _)| *t == table_id)
12095 .map(|(_, n, _)| n.as_str())
12096 else {
12097 continue;
12098 };
12099 let parent_handle = self.table_by_id(table_id)?;
12100 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
12101 continue;
12102 };
12103 for (child_id, _child_name, child_schema) in &live {
12104 for fk in &child_schema.constraints.foreign_keys {
12105 if fk.ref_table != tname {
12106 continue;
12107 }
12108 let Some(parent_key) =
12109 encode_composite_key(&fk.ref_columns, &parent_row.columns)
12110 else {
12111 continue;
12112 };
12113 let key_preserved = staging.iter().any(|(t, op)| {
12122 if *t != table_id {
12123 return false;
12124 }
12125 let Staged::Put(cells) = op else {
12126 return false;
12127 };
12128 let map: HashMap<u16, crate::memtable::Value> =
12129 cells.iter().cloned().collect();
12130 encode_composite_key(&fk.ref_columns, &map).as_deref()
12131 == Some(parent_key.as_slice())
12132 });
12133 if key_preserved {
12134 continue;
12135 }
12136 self.acquire_fk_lock(
12143 txn_id,
12144 table_id,
12145 &parent_key,
12146 crate::locks::LockMode::Exclusive,
12147 control,
12148 )?;
12149 match fk.on_delete {
12150 FkAction::Restrict => continue,
12151 FkAction::Cascade => {
12152 let child_rows = load_rows(*child_id)?;
12153 for (child_index, cr) in child_rows.iter().enumerate() {
12154 commit_prepare_checkpoint(control, child_index)?;
12155 if !cascaded.contains(&(*child_id, cr.row_id.0))
12156 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12157 == Some(parent_key.as_slice())
12158 {
12159 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
12160 }
12161 }
12162 }
12163 FkAction::SetNull => {
12164 let child_rows = load_rows(*child_id)?;
12165 for (child_index, cr) in child_rows.iter().enumerate() {
12166 commit_prepare_checkpoint(control, child_index)?;
12167 if !cascaded.contains(&(*child_id, cr.row_id.0))
12168 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
12169 == Some(parent_key.as_slice())
12170 {
12171 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
12174 .columns
12175 .iter()
12176 .map(|(k, v)| (*k, v.clone()))
12177 .collect();
12178 for cid in &fk.columns {
12179 cells.retain(|(k, _)| k != cid);
12180 cells.push((*cid, crate::memtable::Value::Null));
12181 }
12182 new_ops.push((
12183 *child_id,
12184 Staged::Update {
12185 row_id: cr.row_id,
12186 new_row: cells,
12187 changed_columns: fk.columns.clone(),
12188 },
12189 ));
12190 }
12191 }
12192 }
12193 }
12194 }
12195 }
12196 }
12197 if new_ops.is_empty() {
12198 break;
12199 }
12200 staging.extend(new_ops);
12201 }
12202
12203 self.validate_write_permissions(staging, principal, control)?;
12207 self.validate_security_writes(staging, read_epoch, principal, control)?;
12208 self.materialize_generated_embeddings(staging, control)?;
12209
12210 let staged_deletes: HashSet<(u64, u64)> = staging
12214 .iter()
12215 .filter_map(|(t, op)| match op {
12216 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
12217 _ => None,
12218 })
12219 .collect();
12220
12221 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
12223
12224 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
12226 commit_prepare_checkpoint(control, operation_index)?;
12227 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id) else {
12228 continue;
12229 };
12230 let cells_map: HashMap<u16, crate::memtable::Value>;
12231 match op {
12232 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
12233 cells_map = cells.iter().cloned().collect();
12234
12235 if !schema.constraints.checks.is_empty() {
12237 validate_checks(&schema.constraints.checks, &cells_map)?;
12238 }
12239
12240 for uc in &schema.constraints.uniques {
12242 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
12243 continue; };
12245 let marker = (*table_id, uc.id, key.clone());
12246 if !seen_unique.insert(marker) {
12247 return Err(MongrelError::Conflict(format!(
12248 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
12249 uc.name
12250 )));
12251 }
12252 let rows = load_rows(*table_id)?;
12253 for (row_index, r) in rows.iter().enumerate() {
12254 commit_prepare_checkpoint(control, row_index)?;
12255 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
12258 continue;
12259 }
12260 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
12261 if theirs == key {
12262 return Err(MongrelError::Conflict(format!(
12263 "UNIQUE constraint '{}' on table '{tname}' violated",
12264 uc.name
12265 )));
12266 }
12267 }
12268 }
12269 }
12270
12271 for fk in &schema.constraints.foreign_keys {
12273 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
12274 continue; };
12276 let Some(parent_id) = table_ids_by_name.get(&fk.ref_table).copied() else {
12277 return Err(MongrelError::InvalidArgument(format!(
12278 "FOREIGN KEY '{}' references unknown table '{}'",
12279 fk.name, fk.ref_table
12280 )));
12281 };
12282 self.acquire_fk_lock(
12287 txn_id,
12288 parent_id,
12289 &child_key,
12290 crate::locks::LockMode::Shared,
12291 control,
12292 )?;
12293 let parent_rows = load_rows(parent_id)?;
12294 let mut found = false;
12295 for (row_index, r) in parent_rows.iter().enumerate() {
12296 commit_prepare_checkpoint(control, row_index)?;
12297 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
12298 continue;
12299 }
12300 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
12301 if pkey == child_key {
12302 found = true;
12303 break;
12304 }
12305 }
12306 }
12307 if !found {
12314 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
12315 commit_prepare_checkpoint(control, staged_index)?;
12316 if *st_table != parent_id {
12317 continue;
12318 }
12319 if let Staged::Put(pcells)
12320 | Staged::Update {
12321 new_row: pcells, ..
12322 } = st_op
12323 {
12324 let pmap: HashMap<u16, crate::memtable::Value> =
12325 pcells.iter().cloned().collect();
12326 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
12327 {
12328 if pkey == child_key {
12329 found = true;
12330 break;
12331 }
12332 }
12333 }
12334 }
12335 }
12336 if !found {
12337 return Err(MongrelError::Conflict(format!(
12338 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
12339 fk.name, fk.ref_table
12340 )));
12341 }
12342 }
12343
12344 if let Staged::Update { row_id, .. } = op {
12349 let parent_handle = self.table_by_id(*table_id)?;
12350 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
12351 continue;
12352 };
12353 for (child_id, child_name, child_schema) in &live {
12354 for fk in &child_schema.constraints.foreign_keys {
12355 if fk.ref_table != *tname || fk.on_update != FkAction::Restrict {
12356 continue;
12357 }
12358 let Some(old_key) =
12359 encode_composite_key(&fk.ref_columns, &old_parent.columns)
12360 else {
12361 continue;
12362 };
12363 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
12364 == Some(old_key.as_slice())
12365 {
12366 continue;
12367 }
12368 self.acquire_fk_lock(
12373 txn_id,
12374 *table_id,
12375 &old_key,
12376 crate::locks::LockMode::Exclusive,
12377 control,
12378 )?;
12379 for (child_index, child) in load_rows(*child_id)?.iter().enumerate()
12380 {
12381 commit_prepare_checkpoint(control, child_index)?;
12382 if encode_composite_key(&fk.columns, &child.columns).as_deref()
12383 != Some(old_key.as_slice())
12384 {
12385 continue;
12386 }
12387 let replacement = staging.iter().find_map(|(id, op)| {
12388 if *id != *child_id {
12389 return None;
12390 }
12391 match op {
12392 Staged::Delete(id) if *id == child.row_id => Some(None),
12393 Staged::Update {
12394 row_id,
12395 new_row: cells,
12396 ..
12397 } if *row_id == child.row_id => {
12398 let map: HashMap<u16, Value> =
12399 cells.iter().cloned().collect();
12400 Some(encode_composite_key(&fk.columns, &map))
12401 }
12402 _ => None,
12403 }
12404 });
12405 if replacement.is_some_and(|key| {
12406 key.as_deref() != Some(old_key.as_slice())
12407 }) {
12408 continue;
12409 }
12410 return Err(MongrelError::Conflict(format!(
12411 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
12412 fk.name
12413 )));
12414 }
12415 }
12416 }
12417 }
12418 }
12419 Staged::Delete(rid) => {
12420 let parent_handle = self.table_by_id(*table_id)?;
12424 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
12425 continue;
12426 };
12427 for (child_id, child_name, child_schema) in &live {
12428 for fk in &child_schema.constraints.foreign_keys {
12429 if fk.ref_table != *tname || fk.on_delete != FkAction::Restrict {
12430 continue;
12431 }
12432 let Some(parent_key) =
12433 encode_composite_key(&fk.ref_columns, &parent_row.columns)
12434 else {
12435 continue;
12436 };
12437 let child_rows = load_rows(*child_id)?;
12438 for (row_index, r) in child_rows.iter().enumerate() {
12439 commit_prepare_checkpoint(control, row_index)?;
12440 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
12443 continue;
12444 }
12445 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
12446 if ck == parent_key {
12447 return Err(MongrelError::Conflict(format!(
12448 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
12449 fk.name
12450 )));
12451 }
12452 }
12453 }
12454 }
12455 }
12456 }
12457 Staged::Truncate => {
12458 for (child_id, child_name, child_schema) in &live {
12462 for fk in &child_schema.constraints.foreign_keys {
12463 if fk.ref_table != *tname {
12464 continue;
12465 }
12466 let child_rows = load_rows(*child_id)?;
12467 if child_rows
12468 .iter()
12469 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
12470 {
12471 return Err(MongrelError::Conflict(format!(
12472 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
12473 fk.name
12474 )));
12475 }
12476 }
12477 }
12478 }
12479 }
12480 }
12481 Ok(())
12482 }
12483
12484 fn validate_write_permissions(
12485 &self,
12486 staging: &[(u64, crate::txn::Staged)],
12487 principal: Option<&crate::auth::Principal>,
12488 control: Option<&crate::ExecutionControl>,
12489 ) -> Result<()> {
12490 commit_prepare_checkpoint(control, 0)?;
12491 if principal.is_none() && !self.auth_state.require_auth() {
12492 return Ok(());
12493 }
12494 let principal = principal.ok_or(MongrelError::AuthRequired)?;
12495 let needs = summarize_write_permissions(staging);
12496 let catalog = self.catalog.read();
12497
12498 if needs.values().any(|need| need.truncate) {
12499 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
12500 }
12501 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
12502 commit_prepare_checkpoint(control, need_index)?;
12503 let entry = catalog
12504 .tables
12505 .iter()
12506 .find(|entry| {
12507 entry.table_id == table_id
12508 && matches!(entry.state, TableState::Live | TableState::Building { .. })
12509 })
12510 .ok_or_else(|| {
12511 MongrelError::NotFound(format!(
12512 "live table {table_id} not found during write validation"
12513 ))
12514 })?;
12515 if matches!(entry.state, TableState::Building { .. }) {
12516 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
12517 continue;
12518 }
12519 if need.insert {
12520 Self::require_columns_for_principal(
12521 &entry.name,
12522 &entry.schema,
12523 crate::auth::ColumnOperation::Insert,
12524 &need.insert_columns,
12525 principal,
12526 )?;
12527 }
12528 if need.update {
12529 Self::require_columns_for_principal(
12530 &entry.name,
12531 &entry.schema,
12532 crate::auth::ColumnOperation::Update,
12533 &need.update_columns,
12534 principal,
12535 )?;
12536 }
12537 if need.delete {
12538 self.require_for(
12539 Some(principal),
12540 &crate::auth::Permission::Delete {
12541 table: entry.name.clone(),
12542 },
12543 )?;
12544 }
12545 }
12546 Ok(())
12547 }
12548
12549 fn validate_security_writes(
12550 &self,
12551 staging: &[(u64, crate::txn::Staged)],
12552 read_epoch: Epoch,
12553 explicit_principal: Option<&crate::auth::Principal>,
12554 control: Option<&crate::ExecutionControl>,
12555 ) -> Result<()> {
12556 commit_prepare_checkpoint(control, 0)?;
12557 use crate::security::PolicyCommand;
12558 use crate::txn::Staged;
12559
12560 let catalog = self.catalog.read();
12561 if catalog.security.rls_tables.is_empty() {
12562 return Ok(());
12563 }
12564 let security = catalog.security.clone();
12565 let table_names = catalog
12566 .tables
12567 .iter()
12568 .filter(|entry| matches!(entry.state, TableState::Live))
12569 .map(|entry| (entry.table_id, entry.name.clone()))
12570 .collect::<HashMap<_, _>>();
12571 drop(catalog);
12572 if !staging.iter().any(|(table_id, _)| {
12573 table_names
12574 .get(table_id)
12575 .is_some_and(|table| security.rls_enabled(table))
12576 }) {
12577 return Ok(());
12578 }
12579 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
12580
12581 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
12582 commit_prepare_checkpoint(control, operation_index)?;
12583 let Some(table) = table_names.get(table_id) else {
12584 continue;
12585 };
12586 if !security.rls_enabled(table) || principal.is_admin {
12587 continue;
12588 }
12589 let denied = |command| MongrelError::PermissionDenied {
12590 required: match command {
12591 PolicyCommand::Insert => crate::auth::Permission::Insert {
12592 table: table.clone(),
12593 },
12594 PolicyCommand::Update => crate::auth::Permission::Update {
12595 table: table.clone(),
12596 },
12597 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
12598 crate::auth::Permission::Delete {
12599 table: table.clone(),
12600 }
12601 }
12602 },
12603 principal: principal.username.clone(),
12604 };
12605 match operation {
12606 Staged::Put(cells) => {
12607 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
12608 row.columns.extend(cells.iter().cloned());
12609 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
12610 return Err(denied(PolicyCommand::Insert));
12611 }
12612 }
12613 Staged::Update {
12614 row_id,
12615 new_row: cells,
12616 ..
12617 } => {
12618 let old = self
12619 .table_by_id(*table_id)?
12620 .lock()
12621 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12622 .ok_or_else(|| {
12623 MongrelError::NotFound(format!("row {} not found", row_id.0))
12624 })?;
12625 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
12626 return Err(denied(PolicyCommand::Update));
12627 }
12628 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
12629 new.columns.extend(cells.iter().cloned());
12630 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
12631 return Err(denied(PolicyCommand::Update));
12632 }
12633 }
12634 Staged::Delete(row_id) => {
12635 let old = self
12636 .table_by_id(*table_id)?
12637 .lock()
12638 .get(*row_id, self.snapshot_for_epoch(read_epoch))
12639 .ok_or_else(|| {
12640 MongrelError::NotFound(format!("row {} not found", row_id.0))
12641 })?;
12642 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
12643 return Err(denied(PolicyCommand::Delete));
12644 }
12645 }
12646 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
12647 }
12648 }
12649 Ok(())
12650 }
12651
12652 #[allow(clippy::too_many_arguments)]
12659 pub(crate) fn commit_transaction_with_external_states(
12660 &self,
12661 txn_id: u64,
12662 read_epoch: Epoch,
12663 staging: Vec<(u64, crate::txn::Staged)>,
12664 external_states: Vec<(String, Vec<u8>)>,
12665 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12666 security_principal: Option<crate::auth::Principal>,
12667 principal_catalog_bound: bool,
12668 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12669 context: crate::txn::TxnCommitContext,
12670 ) -> Result<(Epoch, Vec<RowId>)> {
12671 self.commit_transaction_with_external_states_inner(
12672 txn_id,
12673 read_epoch,
12674 staging,
12675 external_states,
12676 materialized_view_updates,
12677 security_principal,
12678 principal_catalog_bound,
12679 external_trigger_bridge,
12680 context,
12681 None,
12682 None,
12683 )
12684 }
12685
12686 #[allow(clippy::too_many_arguments)]
12687 pub(crate) fn commit_transaction_with_external_states_controlled(
12688 &self,
12689 txn_id: u64,
12690 read_epoch: Epoch,
12691 staging: Vec<(u64, crate::txn::Staged)>,
12692 external_states: Vec<(String, Vec<u8>)>,
12693 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12694 security_principal: Option<crate::auth::Principal>,
12695 principal_catalog_bound: bool,
12696 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12697 context: crate::txn::TxnCommitContext,
12698 control: &crate::ExecutionControl,
12699 before_commit: &mut dyn FnMut() -> Result<()>,
12700 ) -> Result<(Epoch, Vec<RowId>)> {
12701 self.commit_transaction_with_external_states_inner(
12702 txn_id,
12703 read_epoch,
12704 staging,
12705 external_states,
12706 materialized_view_updates,
12707 security_principal,
12708 principal_catalog_bound,
12709 external_trigger_bridge,
12710 context,
12711 Some(control),
12712 Some(before_commit),
12713 )
12714 }
12715
12716 #[allow(clippy::too_many_arguments)]
12717 fn commit_transaction_with_external_states_inner(
12718 &self,
12719 txn_id: u64,
12720 read_epoch: Epoch,
12721 mut staging: Vec<(u64, crate::txn::Staged)>,
12722 external_states: Vec<(String, Vec<u8>)>,
12723 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
12724 mut security_principal: Option<crate::auth::Principal>,
12725 principal_catalog_bound: bool,
12726 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
12727 context: crate::txn::TxnCommitContext,
12728 control: Option<&crate::ExecutionControl>,
12729 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
12730 ) -> Result<(Epoch, Vec<RowId>)> {
12731 use crate::memtable::Row;
12732 use crate::txn::{Staged, StagedOp, WriteKey};
12733 use crate::wal::Op;
12734 use std::collections::hash_map::DefaultHasher;
12735 use std::hash::{Hash, Hasher};
12736 use std::sync::atomic::Ordering;
12737
12738 if txn_id == crate::wal::SYSTEM_TXN_ID {
12739 return Err(MongrelError::Full(
12740 "per-open transaction id namespace exhausted; reopen the database".into(),
12741 ));
12742 }
12743 if self.read_only {
12744 return Err(MongrelError::ReadOnlyReplica);
12745 }
12746 commit_prepare_checkpoint(control, 0)?;
12747 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
12748 self.refresh_security_catalog_if_stale(observed_security_version)?;
12749 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
12750 if self.auth_state.require_auth() && security_principal.is_none() {
12751 return Err(MongrelError::AuthRequired);
12752 }
12753 {
12754 let catalog = self.catalog.read();
12755 if principal_catalog_bound
12756 || security_principal
12757 .as_ref()
12758 .is_some_and(|principal| principal.user_id != 0)
12759 {
12760 let principal = security_principal
12761 .as_ref()
12762 .ok_or(MongrelError::AuthRequired)?;
12763 security_principal =
12764 Self::resolve_bound_principal_from_catalog(&catalog, principal);
12765 if security_principal.is_none() {
12766 return Err(MongrelError::AuthRequired);
12767 }
12768 }
12769 }
12770 let _replication_guard = self.replication_barrier.read();
12771 if self.poisoned.load(Ordering::Relaxed) {
12772 return Err(MongrelError::Other(
12773 "database poisoned by fsync error".into(),
12774 ));
12775 }
12776 let _operation = self.admit_operation()?;
12780
12781 if context.state.is_some() {
12790 self.acquire_txn_lock(
12791 txn_id,
12792 crate::locks::LockKey::schema_barrier(),
12793 crate::locks::LockMode::Shared,
12794 control,
12795 )?;
12796 }
12797 let _txn_lock_guard = TxnLockGuard {
12798 locks: &self.lock_manager,
12799 txn_id,
12800 };
12801
12802 let idempotency_request = match &context.idempotency {
12808 Some(request) => match self.idempotency.check_and_reserve(request)? {
12809 crate::txn::IdempotencyCheck::Replay(receipt) => {
12810 let epoch = Epoch(receipt.log_position.index);
12811 if let Some(state) = &context.state {
12812 state.committed(receipt);
12813 }
12814 return Ok((epoch, Vec::new()));
12815 }
12816 crate::txn::IdempotencyCheck::Reserved => Some(request.clone()),
12817 },
12818 None => None,
12819 };
12820 let mut idempotency_guard = idempotency_request.as_ref().map(|request| {
12822 crate::txn::IdempotencyReservationGuard::new(&self.idempotency, request.clone())
12823 });
12824 let mut external_states = dedup_external_states(external_states);
12825 if !external_states.is_empty() {
12826 let cat = self.catalog.read();
12827 for (name, _) in &external_states {
12828 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
12829 return Err(MongrelError::NotFound(format!(
12830 "external table {name:?} not found"
12831 )));
12832 }
12833 }
12834 }
12835 let prepared_materialized_views = {
12836 let mut deduplicated = HashMap::new();
12837 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
12838 {
12839 commit_prepare_checkpoint(control, definition_index)?;
12840 if definition.name.is_empty() || definition.query.trim().is_empty() {
12841 return Err(MongrelError::InvalidArgument(
12842 "materialized view name and query must not be empty".into(),
12843 ));
12844 }
12845 deduplicated.insert(definition.name.clone(), definition);
12846 }
12847 let catalog = self.catalog.read();
12848 let mut prepared = Vec::with_capacity(deduplicated.len());
12849 for (definition_index, definition) in deduplicated.into_values().enumerate() {
12850 commit_prepare_checkpoint(control, definition_index)?;
12851 let table_id = catalog
12852 .live(&definition.name)
12853 .ok_or_else(|| {
12854 MongrelError::NotFound(format!(
12855 "materialized view table {:?} not found",
12856 definition.name
12857 ))
12858 })?
12859 .table_id;
12860 prepared.push((table_id, definition));
12861 }
12862 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
12863 prepared
12864 };
12865
12866 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12869 self.expand_table_triggers(
12870 txn_id,
12871 &mut staging,
12872 read_epoch,
12873 external_trigger_bridge,
12874 &mut external_states,
12875 control,
12876 )?;
12877 self.fill_auto_increment_for_staging(txn_id, &mut staging, control)?;
12878 external_states = dedup_external_states(external_states);
12879 let expected_external_generations = {
12880 let catalog = self.catalog.read();
12881 let mut generations = HashMap::with_capacity(external_states.len());
12882 for (name, _) in &external_states {
12883 let entry = catalog
12884 .external_tables
12885 .iter()
12886 .find(|entry| entry.name == *name)
12887 .ok_or_else(|| {
12888 MongrelError::NotFound(format!("external table {name:?} not found"))
12889 })?;
12890 generations.insert(name.clone(), entry.created_epoch);
12891 }
12892 generations
12893 };
12894
12895 self.acquire_unique_key_claims(txn_id, &staging, control)?;
12901 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
12905 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
12906 self.validate_constraints(
12912 txn_id,
12913 &mut staging,
12914 read_epoch,
12915 security_principal.as_ref(),
12916 control,
12917 )?;
12918 let mut normalized = Vec::with_capacity(staging.len() * 2);
12919 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
12920 commit_prepare_checkpoint(control, staged_index)?;
12921 match op {
12922 crate::txn::Staged::Update {
12923 row_id,
12924 new_row: cells,
12925 ..
12926 } => {
12927 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
12928 normalized.push((table_id, crate::txn::Staged::Put(cells)));
12929 }
12930 op => normalized.push((table_id, op)),
12931 }
12932 }
12933 staging = normalized;
12934 let has_changes = !staging.is_empty()
12935 || !external_states.is_empty()
12936 || !prepared_materialized_views.is_empty();
12937 let truncated_tables: HashSet<u64> = staging
12938 .iter()
12939 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
12940 .collect();
12941
12942 let write_keys = {
12943 let cat = self.catalog.read();
12944 let mut keys: Vec<WriteKey> = Vec::new();
12945 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
12946 commit_prepare_checkpoint(control, staged_index)?;
12947 match staged {
12948 Staged::Put(cells) => {
12949 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
12950 for col in &entry.schema.columns {
12951 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
12952 if let Some((_, val)) =
12953 cells.iter().find(|(id, _)| *id == col.id)
12954 {
12955 let mut h = DefaultHasher::new();
12956 val.encode_key().hash(&mut h);
12957 keys.push(WriteKey::Unique {
12958 table_id: *table_id,
12959 index_id: 0,
12960 key_hash: h.finish(),
12961 });
12962 }
12963 }
12964 }
12965 if !entry.schema.constraints.uniques.is_empty() {
12972 let cells_map: HashMap<u16, Value> =
12973 cells.iter().cloned().collect();
12974 for uc in &entry.schema.constraints.uniques {
12975 if let Some(key_bytes) = crate::constraint::encode_composite_key(
12976 &uc.columns,
12977 &cells_map,
12978 ) {
12979 let mut h = DefaultHasher::new();
12980 key_bytes.hash(&mut h);
12981 keys.push(WriteKey::Unique {
12982 table_id: *table_id,
12983 index_id: uc.id | 0x8000,
12984 key_hash: h.finish(),
12985 });
12986 }
12987 }
12988 }
12989 }
12990 }
12991 Staged::Delete(rid) => keys.push(WriteKey::Row {
12992 table_id: *table_id,
12993 row_id: rid.0,
12994 }),
12995 Staged::Truncate => keys.push(WriteKey::Table {
12996 table_id: *table_id,
12997 }),
12998 Staged::Update { .. } => {
12999 return Err(MongrelError::Other(
13000 "transaction contains an unnormalized update during preparation".into(),
13001 ));
13002 }
13003 }
13004 }
13005 for (external_index, (name, _)) in external_states.iter().enumerate() {
13006 commit_prepare_checkpoint(control, external_index)?;
13007 let mut h = DefaultHasher::new();
13008 name.hash(&mut h);
13009 keys.push(WriteKey::Unique {
13010 table_id: EXTERNAL_TABLE_ID,
13011 index_id: 0,
13012 key_hash: h.finish(),
13013 });
13014 }
13015 keys
13016 };
13017
13018 let min_active = self.active_txns.min_read_epoch();
13020 if min_active < u64::MAX {
13021 self.conflicts.prune_below(Epoch(min_active));
13022 }
13023
13024 let ssi_keys = match context.isolation.canonical() {
13031 crate::txn::IsolationLevel::Serializable => {
13032 crate::txn::ssi_validation_keys(&context.read_set, &context.predicate_set)
13033 }
13034 _ => Vec::new(),
13035 };
13036
13037 if self.conflicts.conflicts(&write_keys, read_epoch) {
13041 return Err(MongrelError::Conflict(
13042 "write-write conflict (pre-validate, first-committer-wins)".into(),
13043 ));
13044 }
13045 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
13046 return Err(MongrelError::SerializationFailure {
13047 message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
13048 .into(),
13049 });
13050 }
13051 let pre_validate_version = self.conflicts.version();
13052
13053 let mut spilled: Vec<SpilledRun> = Vec::new();
13057 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
13058 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
13062 {
13063 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
13064 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
13065 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
13066 commit_prepare_checkpoint(control, staged_index)?;
13067 if let Staged::Put(cells) = staged {
13068 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
13069 bytes.saturating_add(value.estimated_bytes())
13070 });
13071 let table_bytes = table_bytes.entry(*table_id).or_default();
13072 *table_bytes = table_bytes.saturating_add(bytes);
13073 put_indexes.entry(*table_id).or_default().push(staged_index);
13074 }
13075 }
13076 let tables = self.tables.read();
13077 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
13078 commit_prepare_checkpoint(control, table_index)?;
13079 if bytes
13080 <= self
13081 .spill_threshold
13082 .load(std::sync::atomic::Ordering::Relaxed)
13083 {
13084 continue;
13085 }
13086 let Some(handle) = tables.get(&table_id) else {
13087 continue;
13088 };
13089 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
13090 let mut t = handle.lock();
13091 let tdir = t.table_dir().to_path_buf();
13092 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
13093 std::fs::create_dir_all(&txn_dir)?;
13094 let run_id = t.alloc_run_id()? as u128;
13095 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
13096 let final_path = t.run_path(run_id as u64);
13097
13098 let mut rows: Vec<Row> = Vec::new();
13099 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
13100 commit_prepare_checkpoint(control, put_index)?;
13101 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
13102 return Err(MongrelError::Other(
13103 "transaction put index no longer references a put".into(),
13104 ));
13105 };
13106 t.validate_cells_not_null(cells)?;
13107 let row_id = t.alloc_row_id()?;
13108 let mut row = Row::new(row_id, Epoch(0));
13109 row.columns.extend(std::mem::take(cells));
13110 rows.push(row);
13111 }
13112 let schema = t.schema_ref().clone();
13113 let kek = t.kek_ref().cloned();
13114 let specs = t.indexable_column_specs();
13115 drop(t);
13116
13117 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
13118 .uniform_epoch(true);
13119 if let Some(ref kek) = kek {
13120 writer = writer.with_encryption(kek.as_ref(), specs);
13121 }
13122 commit_prepare_checkpoint(control, 0)?;
13123 let header = writer.write(&pending_path, &rows)?;
13124 commit_prepare_checkpoint(control, 0)?;
13125 let row_count = header.row_count;
13126 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
13127 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
13128
13129 spilled.push(SpilledRun {
13130 table_id,
13131 run_id,
13132 pending_path,
13133 final_path,
13134 rows,
13135 row_count,
13136 min_rid,
13137 max_rid,
13138 content_hash: header.content_hash,
13139 });
13140 spilled_tables.insert(table_id);
13141 }
13142 }
13143
13144 if spill_guard.is_some() {
13146 if let Some(hook) = self.spill_hook.lock().as_ref() {
13147 hook();
13148 }
13149 }
13150
13151 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13162 .take(staging.len())
13163 .collect();
13164 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
13165 .take(staging.len())
13166 .collect();
13167 {
13168 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
13169 for (index, (table_id, staged)) in staging.iter().enumerate() {
13170 commit_prepare_checkpoint(control, index)?;
13171 if matches!(staged, Staged::Delete(_))
13172 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
13173 {
13174 indexes_by_table.entry(*table_id).or_default().push(index);
13175 }
13176 }
13177 let tables = self.tables.read();
13178 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
13179 commit_prepare_checkpoint(control, table_index)?;
13180 let handle = tables.get(&table_id).ok_or_else(|| {
13181 MongrelError::NotFound(format!("table {table_id} not mounted"))
13182 })?;
13183 #[cfg(test)]
13184 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13185 let mut t = handle.lock();
13186 for (prepare_index, index) in indexes.into_iter().enumerate() {
13187 commit_prepare_checkpoint(control, prepare_index)?;
13188 match &staging[index].1 {
13189 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
13190 t.validate_cells_not_null(cells)?;
13191 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
13192 for (column, value) in cells {
13193 row.columns.insert(*column, value.clone());
13194 }
13195 prebuilt[index] = Some(row);
13196 }
13197 Staged::Delete(row_id) => {
13198 delete_images[index] =
13199 t.get(*row_id, self.snapshot_for_epoch(read_epoch));
13200 }
13201 Staged::Put(_) | Staged::Truncate => {}
13202 Staged::Update { .. } => {
13203 return Err(MongrelError::Other(
13204 "transaction contains an unnormalized update during row preparation"
13205 .into(),
13206 ));
13207 }
13208 }
13209 }
13210 }
13211 }
13212
13213 let prepared_table_handles = {
13217 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
13218 let put_table_ids: HashSet<u64> = staging
13219 .iter()
13220 .filter_map(|(table_id, staged)| {
13221 matches!(staged, Staged::Put(_)).then_some(*table_id)
13222 })
13223 .collect();
13224 let tables = self.tables.read();
13225 let mut handles = HashMap::with_capacity(table_ids.len());
13226 for (table_index, table_id) in table_ids.into_iter().enumerate() {
13227 commit_prepare_checkpoint(control, table_index)?;
13228 let handle = tables.get(&table_id).ok_or_else(|| {
13229 MongrelError::NotFound(format!("table {table_id} not mounted"))
13230 })?;
13231 if put_table_ids.contains(&table_id) {
13232 match control {
13233 Some(control) => {
13234 handle.lock().prepare_durable_publish_controlled(control)?
13235 }
13236 None => handle.lock().prepare_durable_publish()?,
13237 }
13238 }
13239 handles.insert(table_id, handle.clone());
13240 }
13241 handles
13242 };
13243
13244 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
13248
13249 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
13250 .iter()
13251 .map(|run| {
13252 (
13253 run.table_id,
13254 run.rows.iter().map(|row| row.row_id).collect(),
13255 )
13256 })
13257 .collect();
13258 let committed_row_ids = staging
13259 .iter()
13260 .enumerate()
13261 .filter_map(|(index, (table_id, staged))| {
13262 if !matches!(staged, Staged::Put(_)) {
13263 return None;
13264 }
13265 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
13266 spilled_row_ids
13267 .get_mut(table_id)
13268 .and_then(VecDeque::pop_front)
13269 })
13270 })
13271 .collect();
13272
13273 let mut prepared_external = Vec::with_capacity(external_states.len());
13274 for (external_index, (name, state)) in external_states.iter().enumerate() {
13275 commit_prepare_checkpoint(control, external_index)?;
13276 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
13277 prepared_external.push((name.clone(), state.clone(), pending));
13278 }
13279
13280 if context.state.is_some() {
13290 self.lock_manager
13291 .release(txn_id, &crate::locks::LockKey::schema_barrier());
13292 }
13293 let added_runs: Vec<crate::wal::AddedRun> = spilled
13294 .iter()
13295 .map(|s| crate::wal::AddedRun {
13296 table_id: s.table_id,
13297 run_id: s.run_id,
13298 row_count: s.row_count,
13299 level: 0,
13300 min_row_id: s.min_rid,
13301 max_row_id: s.max_rid,
13302 content_hash: s.content_hash,
13303 })
13304 .collect();
13305 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
13306 hook();
13307 }
13308 let security_guard = self.security_coordinator.gate.read();
13312 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
13313 return Err(MongrelError::Conflict(
13314 "security policy changed during write".into(),
13315 ));
13316 }
13317 if spill_guard.is_some() {
13318 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
13319 hook();
13320 }
13321 }
13322 let commit_guard = self.commit_lock.lock();
13323 let catalog_generation_result = (|| {
13324 {
13325 let catalog = self.catalog.read();
13326 for table_id in prepared_table_handles.keys() {
13327 let is_current = catalog.tables.iter().any(|entry| {
13328 entry.table_id == *table_id
13329 && matches!(entry.state, TableState::Live | TableState::Building { .. })
13330 });
13331 if !is_current {
13332 return Err(MongrelError::Conflict(format!(
13333 "table {table_id} changed during transaction preparation"
13334 )));
13335 }
13336 }
13337 for (name, created_epoch) in &expected_external_generations {
13338 let current = catalog
13339 .external_tables
13340 .iter()
13341 .find(|entry| entry.name == *name)
13342 .map(|entry| entry.created_epoch);
13343 if current != Some(*created_epoch) {
13344 return Err(MongrelError::Conflict(format!(
13345 "external table {name:?} changed during transaction preparation"
13346 )));
13347 }
13348 }
13349 for (table_id, definition) in &prepared_materialized_views {
13350 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
13351 if current != Some(*table_id) {
13352 return Err(MongrelError::Conflict(format!(
13353 "materialized view {:?} changed during transaction preparation",
13354 definition.name
13355 )));
13356 }
13357 }
13358 if trigger_catalog_binding(&catalog) != trigger_binding {
13359 return Err(MongrelError::Conflict(
13360 "trigger or referenced table generation changed during transaction preparation"
13361 .into(),
13362 ));
13363 }
13364 }
13365 let tables = self.tables.read();
13366 for (table_id, prepared) in &prepared_table_handles {
13367 if !tables
13368 .get(table_id)
13369 .is_some_and(|current| current.ptr_eq(prepared))
13370 {
13371 return Err(MongrelError::Conflict(format!(
13372 "table {table_id} mount changed during transaction preparation"
13373 )));
13374 }
13375 }
13376 Ok(())
13377 })();
13378 if let Err(error) = catalog_generation_result {
13379 drop(commit_guard);
13380 for (_, _, pending) in &prepared_external {
13381 let _ = std::fs::remove_file(pending);
13382 }
13383 return Err(error);
13384 }
13385 let new_epoch = self.epoch.assigned().next();
13389 let commit_ts = match context.read_ts {
13395 Some(read_ts) => self.hlc.commit_timestamp([read_ts]),
13396 None => self.hlc.now().map_err(|skew| {
13397 MongrelError::Other(format!(
13398 "clock skew rejected commit timestamp allocation: {skew}"
13399 ))
13400 })?,
13401 };
13402 let mut spilled_wal_bytes = 0;
13403 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
13404 let spill_prepare = (|| {
13405 for run in &mut spilled {
13406 for row in &mut run.rows {
13407 row.committed_epoch = new_epoch;
13408 row.commit_ts = Some(commit_ts);
13409 }
13410 for rows in encode_spilled_row_chunks(
13411 &run.rows,
13412 &mut spilled_wal_bytes,
13413 SPILLED_WAL_TOTAL_MAX_BYTES,
13414 control,
13415 )? {
13416 spilled_wal_records.push((
13417 run.table_id,
13418 Op::SpilledRows {
13419 table_id: run.table_id,
13420 rows,
13421 },
13422 ));
13423 }
13424 }
13425 Result::<()>::Ok(())
13426 })();
13427 if let Err(error) = spill_prepare {
13428 for (_, _, pending) in &prepared_external {
13429 let _ = std::fs::remove_file(pending);
13430 }
13431 return Err(error);
13432 }
13433 let (
13434 new_epoch,
13435 mut _epoch_guard,
13436 applies,
13437 committed_materialized_views,
13438 commit_seq,
13439 commit_ts,
13440 ) = {
13441 let mut wal = self.shared_wal.lock();
13442
13443 if self.conflicts.version() != pre_validate_version {
13448 if self.conflicts.conflicts(&write_keys, read_epoch) {
13449 drop(wal);
13452 for (_, _, pending) in &prepared_external {
13453 let _ = std::fs::remove_file(pending);
13454 }
13455 return Err(MongrelError::Conflict(
13456 "write-write conflict (sequencer delta re-check)".into(),
13457 ));
13458 }
13459 if !ssi_keys.is_empty() && self.conflicts.conflicts(&ssi_keys, read_epoch) {
13460 drop(wal);
13464 for (_, _, pending) in &prepared_external {
13465 let _ = std::fs::remove_file(pending);
13466 }
13467 return Err(MongrelError::SerializationFailure {
13468 message:
13469 "a concurrent commit invalidated this transaction's reads (sequencer re-check)"
13470 .into(),
13471 });
13472 }
13473 }
13474
13475 if let Some(control) = control {
13476 if let Err(error) = control.checkpoint() {
13477 drop(wal);
13478 for (_, _, pending) in &prepared_external {
13479 let _ = std::fs::remove_file(pending);
13480 }
13481 return Err(error);
13482 }
13483 }
13484 let mut applies = Vec::<TableApplyBatch>::new();
13485 let mut apply_indexes = HashMap::<u64, usize>::new();
13486 let mut committed_materialized_views = Vec::new();
13487 let mut wal_records = spilled_wal_records;
13488
13489 let mut index = 0;
13490 while index < staging.len() {
13491 let table_id = staging[index].0;
13492 let handle = prepared_table_handles
13493 .get(&table_id)
13494 .cloned()
13495 .ok_or_else(|| {
13496 MongrelError::NotFound(format!("table {table_id} not prepared"))
13497 })?;
13498 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
13499 let index = applies.len();
13500 applies.push(TableApplyBatch {
13501 table_id,
13502 handle,
13503 ops: Vec::new(),
13504 });
13505 index
13506 });
13507
13508 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
13511 {
13512 index += 1;
13513 continue;
13514 }
13515
13516 match &staging[index].1 {
13517 Staged::Put(_) => {
13518 let mut rows = Vec::new();
13519 while index < staging.len()
13520 && staging[index].0 == table_id
13521 && matches!(&staging[index].1, Staged::Put(_))
13522 {
13523 let mut row = prebuilt[index].take().ok_or_else(|| {
13524 MongrelError::Other(
13525 "transaction prepare lost a prebuilt put row".into(),
13526 )
13527 })?;
13528 row.committed_epoch = new_epoch;
13529 row.commit_ts = Some(commit_ts);
13530 rows.push(row);
13531 index += 1;
13532 }
13533 let payload = bincode::serialize(&rows)
13534 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
13535 wal_records.push((
13536 table_id,
13537 Op::Put {
13538 table_id,
13539 rows: payload,
13540 },
13541 ));
13542 applies[batch_index].ops.push(StagedOp::Put(rows));
13543 }
13544 Staged::Delete(_) => {
13545 let mut row_ids = Vec::new();
13546 while index < staging.len()
13547 && staging[index].0 == table_id
13548 && matches!(&staging[index].1, Staged::Delete(_))
13549 {
13550 let Staged::Delete(row_id) = &staging[index].1 else {
13551 return Err(MongrelError::Other(
13552 "transaction delete batch changed during WAL preparation"
13553 .into(),
13554 ));
13555 };
13556 if let Some(before) = &delete_images[index] {
13557 wal_records.push((
13558 table_id,
13559 Op::BeforeImage {
13560 table_id,
13561 row_id: *row_id,
13562 row: bincode::serialize(before).map_err(|error| {
13563 MongrelError::Other(format!(
13564 "before-image serialize: {error}"
13565 ))
13566 })?,
13567 },
13568 ));
13569 }
13570 row_ids.push(*row_id);
13571 index += 1;
13572 }
13573 wal_records.push((
13574 table_id,
13575 Op::Delete {
13576 table_id,
13577 row_ids: row_ids.clone(),
13578 },
13579 ));
13580 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
13581 }
13582 Staged::Truncate => {
13583 wal_records.push((table_id, Op::TruncateTable { table_id }));
13584 applies[batch_index].ops.push(StagedOp::Truncate);
13585 index += 1;
13586 }
13587 Staged::Update { .. } => {
13588 return Err(MongrelError::Other(
13589 "transaction contains an unnormalized update at the sequencer".into(),
13590 ));
13591 }
13592 }
13593 }
13594
13595 for (name, state, _) in &prepared_external {
13596 wal_records.push((
13597 EXTERNAL_TABLE_ID,
13598 Op::ExternalTableState {
13599 name: name.clone(),
13600 state: state.clone(),
13601 },
13602 ));
13603 }
13604
13605 for (table_id, definition) in &prepared_materialized_views {
13606 let mut definition = definition.clone();
13607 definition.last_refresh_epoch = new_epoch.0;
13608 wal_records.push((
13609 *table_id,
13610 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
13611 name: definition.name.clone(),
13612 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
13613 }),
13614 ));
13615 committed_materialized_views.push(definition);
13616 }
13617 if !committed_materialized_views.is_empty() {
13618 let mut next_catalog = self.catalog.read().clone();
13619 for definition in &committed_materialized_views {
13620 if let Some(existing) = next_catalog
13621 .materialized_views
13622 .iter_mut()
13623 .find(|existing| existing.name == definition.name)
13624 {
13625 *existing = definition.clone();
13626 } else {
13627 next_catalog.materialized_views.push(definition.clone());
13628 }
13629 }
13630 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13631 wal_records.push((
13632 WAL_TABLE_ID,
13633 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
13634 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
13635 }),
13636 ));
13637 }
13638
13639 if let Some(control) = control {
13640 if let Err(error) = control.checkpoint() {
13641 drop(wal);
13642 for (_, _, pending) in &prepared_external {
13643 let _ = std::fs::remove_file(pending);
13644 }
13645 return Err(error);
13646 }
13647 }
13648 if let Some(before_commit) = before_commit.as_mut() {
13649 if let Err(error) = before_commit() {
13650 drop(wal);
13651 for (_, _, pending) in &prepared_external {
13652 let _ = std::fs::remove_file(pending);
13653 }
13654 return Err(error);
13655 }
13656 }
13657
13658 let assigned_epoch = self.epoch.bump_assigned();
13659 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
13660 if assigned_epoch != new_epoch {
13661 for (_, _, pending) in &prepared_external {
13662 let _ = std::fs::remove_file(pending);
13663 }
13664 return Err(MongrelError::Conflict(
13665 "commit epoch changed while sequencer lock was held".into(),
13666 ));
13667 }
13668
13669 if let Err(fault) = mongreldb_fault::inject("txn.decision.before") {
13676 for (_, _, pending) in &prepared_external {
13677 let _ = std::fs::remove_file(pending);
13678 }
13679 return Err(crate::commit_log::fault_as_io(fault));
13680 }
13681 if let Some(state) = &context.state {
13682 state.enter_commit_critical();
13683 }
13684
13685 prepared_run_links.disarm();
13689
13690 let commit_seq = self
13694 .standalone_commit_log
13695 .append_transaction(
13696 &mut wal,
13697 txn_id,
13698 new_epoch,
13699 commit_ts,
13700 wal_records,
13701 &added_runs,
13702 )
13703 .map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
13704
13705 self.conflicts.record(&write_keys, new_epoch);
13710 (
13711 new_epoch,
13712 _epoch_guard,
13713 applies,
13714 committed_materialized_views,
13715 commit_seq,
13716 commit_ts,
13717 )
13718 };
13719 drop(commit_guard);
13720
13721 let receipt =
13723 self.await_durable_commit_with_ts(txn_id, commit_seq, new_epoch, commit_ts)?;
13724 drop(security_guard);
13725
13726 if let Some(request) = &idempotency_request {
13730 if let Err(error) =
13731 self.idempotency
13732 .complete(request, txn_id, new_epoch, receipt.commit_ts)
13733 {
13734 return Err(MongrelError::DurableCommit {
13735 epoch: new_epoch.0,
13736 message: format!("idempotency record was not durable: {error}"),
13737 });
13738 }
13739 if let Some(guard) = idempotency_guard.as_mut() {
13740 guard.disarm();
13741 }
13742 }
13743
13744 let publish_result: Result<()> = {
13746 let mut first_error = None;
13747 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
13748 for run in &spilled {
13749 spilled_by_table.entry(run.table_id).or_default().push(run);
13750 }
13751 let mut modified_tables = Vec::with_capacity(applies.len());
13752 for batch in applies {
13755 #[cfg(test)]
13756 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
13757 let mut t = batch.handle.lock();
13758 for op in batch.ops {
13759 match op {
13760 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
13761 StagedOp::Delete(row_ids) => {
13762 for row_id in row_ids {
13763 t.apply_delete_at(row_id, new_epoch, Some(commit_ts));
13764 }
13765 }
13766 StagedOp::Truncate => t.apply_truncate(new_epoch),
13767 }
13768 }
13769 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
13770 for run in runs {
13771 t.link_run(crate::manifest::RunRef {
13772 run_id: run.run_id,
13773 level: 0,
13774 epoch_created: new_epoch.0,
13775 row_count: run.row_count,
13776 });
13777 t.apply_run_metadata_prepared(&run.rows)?;
13778 if truncated_tables.contains(&batch.table_id) {
13779 t.set_flushed_epoch(new_epoch);
13784 }
13785 }
13786 }
13787 t.invalidate_pending_cache();
13788 drop(t);
13789 modified_tables.push(batch.handle);
13790 }
13791
13792 for handle in modified_tables {
13796 #[cfg(test)]
13797 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
13798 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
13799 first_error.get_or_insert(error);
13800 }
13801 }
13802 for (name, _, pending) in &prepared_external {
13803 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
13804 first_error.get_or_insert(error);
13805 }
13806 }
13807 if !committed_materialized_views.is_empty() {
13808 let mut next_catalog = self.catalog.read().clone();
13809 for definition in committed_materialized_views {
13810 if let Some(existing) = next_catalog
13811 .materialized_views
13812 .iter_mut()
13813 .find(|existing| existing.name == definition.name)
13814 {
13815 *existing = definition;
13816 } else {
13817 next_catalog.materialized_views.push(definition);
13818 }
13819 }
13820 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
13821 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
13822 first_error.get_or_insert(error);
13823 }
13824 }
13825 match first_error {
13826 Some(error) => Err(error),
13827 None => Ok(()),
13828 }
13829 };
13830
13831 if has_changes {
13832 let _ = self.change_wake.send(());
13833 }
13834 self.finish_durable_publish(new_epoch, &mut _epoch_guard, &receipt, publish_result)?;
13835 if let Some(state) = &context.state {
13839 state.committed(receipt.clone());
13840 }
13841 mongreldb_fault::inject("txn.decision.after").map_err(|fault| {
13845 MongrelError::DurableCommit {
13846 epoch: new_epoch.0,
13847 message: fault.to_string(),
13848 }
13849 })?;
13850 Ok((new_epoch, committed_row_ids))
13851 }
13852
13853 pub fn visible_snapshot(&self) -> Snapshot {
13857 self.snapshot_for_epoch(self.epoch.visible())
13858 }
13859
13860 pub fn snapshot_for_epoch(&self, epoch: Epoch) -> Snapshot {
13865 let commit_ts = self
13866 .commit_ts_for_epoch(epoch)
13867 .filter(|ts| *ts != mongreldb_types::hlc::HlcTimestamp::ZERO)
13868 .or_else(|| self.hlc.now().ok())
13869 .unwrap_or(mongreldb_types::hlc::HlcTimestamp::MAX);
13870 Snapshot::at_hlc(epoch, commit_ts)
13871 }
13872
13873 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
13876 let snap = self.visible_snapshot();
13877 let g = self.snapshots.register(snap.epoch);
13878 (snap, g)
13879 }
13880
13881 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
13884 let snap = self.visible_snapshot();
13885 let g = self.snapshots.register_owned(snap.epoch);
13886 (snap, g)
13887 }
13888
13889 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
13894 let _guard = self.ddl_lock.lock();
13895 let current = self.epoch.visible();
13896 let (old_epochs, old_start) = self.snapshots.history_config();
13897 let earliest_already_guaranteed = if old_epochs == 0 {
13898 current
13899 } else {
13900 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
13901 };
13902 let start = if epochs == 0 {
13903 current
13904 } else {
13905 earliest_already_guaranteed
13906 };
13907 let published = std::cell::Cell::new(false);
13908 let result = write_history_retention(&self.root, epochs, start, || {
13909 self.snapshots.configure_history(epochs, start);
13910 published.set(true);
13911 });
13912 match result {
13913 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
13914 epoch: current.0,
13915 message: format!("history-retention publication was not durable: {error}"),
13916 }),
13917 result => result,
13918 }
13919 }
13920
13921 pub fn history_retention_epochs(&self) -> u64 {
13922 self.snapshots.history_config().0
13923 }
13924
13925 pub fn earliest_retained_epoch(&self) -> Epoch {
13926 let current = self.epoch.visible();
13927 self.snapshots.history_floor(current).unwrap_or(current)
13928 }
13929
13930 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
13935 let current = self.epoch.visible();
13936 if epoch > current {
13937 return Err(MongrelError::InvalidArgument(format!(
13938 "epoch {} is in the future; current epoch is {}",
13939 epoch.0, current.0
13940 )));
13941 }
13942 let earliest = self.earliest_retained_epoch();
13943 if epoch < earliest {
13944 return Err(MongrelError::InvalidArgument(format!(
13945 "epoch {} is no longer retained; earliest available epoch is {}",
13946 epoch.0, earliest.0
13947 )));
13948 }
13949 let guard = self.snapshots.register_owned(epoch);
13950 let snap = match self.commit_ts_for_epoch(epoch) {
13951 Some(commit_ts) => Snapshot::at_hlc(epoch, commit_ts),
13952 None => Snapshot::at(epoch),
13953 };
13954 Ok((snap, guard))
13955 }
13956
13957 pub fn table_names(&self) -> Vec<String> {
13959 self.catalog
13960 .read()
13961 .tables
13962 .iter()
13963 .filter(|t| matches!(t.state, TableState::Live))
13964 .map(|t| t.name.clone())
13965 .collect()
13966 }
13967
13968 pub fn close(&self) -> Result<()> {
13974 for name in self.table_names() {
13975 if let Ok(handle) = self.table(&name) {
13976 if let Err(e) = handle.lock().close() {
13977 eprintln!("[close] flush failed for {name}: {e}");
13978 }
13979 }
13980 }
13981 Ok(())
13982 }
13983
13984 pub fn compact(&self) -> Result<(usize, usize)> {
13991 self.require(&crate::auth::Permission::Ddl)?;
13992 let _operation = self.admit_operation()?;
13994 let mut compacted = 0;
13995 let mut skipped = 0;
13996 for name in self.table_names() {
13997 let Ok(handle) = self.table(&name) else {
13998 continue;
13999 };
14000 {
14001 let mut t = handle.lock();
14002 let before = t.run_count();
14003 if before < 2 && !t.should_compact() {
14004 skipped += 1;
14005 continue;
14006 }
14007 match t.compact() {
14008 Ok(()) => {
14009 let after = t.run_count();
14010 compacted += 1;
14011 eprintln!("[compact] {name}: {before} -> {after} runs");
14012 }
14013 Err(e) => {
14014 eprintln!("[compact] {name}: compaction failed: {e}");
14015 skipped += 1;
14016 }
14017 }
14018 }
14019 }
14020 Ok((compacted, skipped))
14021 }
14022
14023 pub fn compact_table(&self, name: &str) -> Result<bool> {
14026 self.require(&crate::auth::Permission::Ddl)?;
14027 let handle = self.table(name)?;
14028 let mut t = handle.lock();
14029 let before = t.run_count();
14030 if before < 2 {
14031 return Ok(false);
14032 }
14033 t.compact()?;
14034 Ok(t.run_count() < before)
14035 }
14036
14037 pub fn table(&self, name: &str) -> Result<TableHandle> {
14039 self.ensure_owner_process()?;
14040 let cat = self.catalog.read();
14041 let entry = cat
14042 .live(name)
14043 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14044 let id = entry.table_id;
14045 drop(cat);
14046 self.tables
14047 .read()
14048 .get(&id)
14049 .cloned()
14050 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
14051 }
14052
14053 pub fn has_ttl_tables(&self) -> bool {
14056 self.tables
14057 .read()
14058 .values()
14059 .any(|table| table.lock().ttl().is_some())
14060 }
14061
14062 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
14065 self.tables
14066 .read()
14067 .get(&id)
14068 .cloned()
14069 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
14070 }
14071
14072 pub(crate) fn table_auto_inc_would_allocate(
14078 &self,
14079 table_id: u64,
14080 cells: &[(u16, Value)],
14081 ) -> bool {
14082 let catalog = self.catalog.read();
14083 let Some(entry) = catalog
14084 .tables
14085 .iter()
14086 .find(|entry| entry.table_id == table_id)
14087 else {
14088 return false;
14089 };
14090 let Some(column) = entry.schema.auto_increment_column() else {
14091 return false;
14092 };
14093 matches!(
14094 cells.iter().find(|(id, _)| *id == column.id),
14095 None | Some((_, Value::Null))
14096 )
14097 }
14098
14099 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
14105 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
14106 return Err(MongrelError::InvalidArgument(format!(
14107 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
14108 )));
14109 }
14110 self.create_table_with_state(name, schema, TableState::Live)
14111 }
14112
14113 #[doc(hidden)]
14115 pub fn create_building_table(
14116 &self,
14117 build_name: &str,
14118 intended_name: &str,
14119 query_id: &str,
14120 schema: Schema,
14121 ) -> Result<u64> {
14122 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14123 || intended_name.is_empty()
14124 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14125 || query_id.is_empty()
14126 {
14127 return Err(MongrelError::InvalidArgument(
14128 "invalid CTAS building-table identity".into(),
14129 ));
14130 }
14131 self.create_table_with_state(
14132 build_name,
14133 schema,
14134 TableState::Building {
14135 intended_name: intended_name.to_string(),
14136 query_id: query_id.to_string(),
14137 created_at_unix_nanos: current_unix_nanos(),
14138 replaces_table_id: None,
14139 },
14140 )
14141 }
14142
14143 #[doc(hidden)]
14146 pub fn create_rebuilding_table(
14147 &self,
14148 build_name: &str,
14149 intended_name: &str,
14150 query_id: &str,
14151 schema: Schema,
14152 ) -> Result<u64> {
14153 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14154 || intended_name.is_empty()
14155 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14156 || query_id.is_empty()
14157 {
14158 return Err(MongrelError::InvalidArgument(
14159 "invalid rebuilding-table identity".into(),
14160 ));
14161 }
14162 let replaces_table_id = self
14163 .catalog
14164 .read()
14165 .live(intended_name)
14166 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
14167 .table_id;
14168 self.create_table_with_state(
14169 build_name,
14170 schema,
14171 TableState::Building {
14172 intended_name: intended_name.to_string(),
14173 query_id: query_id.to_string(),
14174 created_at_unix_nanos: current_unix_nanos(),
14175 replaces_table_id: Some(replaces_table_id),
14176 },
14177 )
14178 }
14179
14180 fn create_table_with_state(
14181 &self,
14182 name: &str,
14183 schema: Schema,
14184 state: TableState,
14185 ) -> Result<u64> {
14186 use crate::wal::DdlOp;
14187 use std::sync::atomic::Ordering;
14188
14189 self.require(&crate::auth::Permission::Ddl)?;
14190 if self.poisoned.load(Ordering::Relaxed) {
14191 return Err(MongrelError::Other(
14192 "database poisoned by fsync error".into(),
14193 ));
14194 }
14195 let _operation = self.admit_operation()?;
14197 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14198 let _g = self.ddl_lock.lock();
14199 let _security_write = self.security_write()?;
14200 self.require(&crate::auth::Permission::Ddl)?;
14201 {
14202 let cat = self.catalog.read();
14203 match &state {
14204 TableState::Live => {
14205 if cat.live(name).is_some() || cat.building_for(name).is_some() {
14206 return Err(MongrelError::InvalidArgument(format!(
14207 "table {name:?} already exists or is being built"
14208 )));
14209 }
14210 }
14211 TableState::Building {
14212 intended_name,
14213 replaces_table_id,
14214 ..
14215 } => {
14216 let target_matches = match replaces_table_id {
14217 Some(table_id) => cat
14218 .live(intended_name)
14219 .is_some_and(|entry| entry.table_id == *table_id),
14220 None => cat.live(intended_name).is_none(),
14221 };
14222 if !target_matches || cat.building_for(intended_name).is_some() {
14223 return Err(MongrelError::InvalidArgument(format!(
14224 "table {intended_name:?} changed or is already being built"
14225 )));
14226 }
14227 if cat.building(name).is_some() {
14228 return Err(MongrelError::InvalidArgument(format!(
14229 "building table {name:?} already exists"
14230 )));
14231 }
14232 }
14233 TableState::Dropped { .. } => {
14234 return Err(MongrelError::InvalidArgument(
14235 "cannot create a dropped table".into(),
14236 ));
14237 }
14238 }
14239 }
14240
14241 let commit_lock = Arc::clone(&self.commit_lock);
14246 let _c = commit_lock.lock();
14247 let live_create = matches!(state, TableState::Live);
14248 let legacy_table_id = if live_create {
14249 None
14250 } else {
14251 let mut cat = self.catalog.write();
14252 let id = cat.next_table_id;
14253 cat.next_table_id = id
14254 .checked_add(1)
14255 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
14256 Some(id)
14257 };
14258 let epoch = self.epoch.bump_assigned();
14259 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14260 let txn_id = self.alloc_txn_id()?;
14261
14262 let mut schema = schema;
14263 if let Some(table_id) = legacy_table_id {
14264 schema.schema_id = table_id;
14268 schema.validate_auto_increment()?;
14276 schema.validate_defaults()?;
14277 schema.validate_ai()?;
14278 for index in &schema.indexes {
14279 index.validate_options()?;
14280 }
14281 for constraint in &schema.constraints.checks {
14282 constraint.expr.validate()?;
14283 }
14284 }
14285
14286 let mut next_catalog = self.catalog.read().clone();
14287 let (table_id, schema) = match legacy_table_id {
14288 Some(table_id) => {
14289 next_catalog.tables.push(CatalogEntry {
14290 table_id,
14291 name: name.to_string(),
14292 schema: schema.clone(),
14293 state: state.clone(),
14294 created_epoch: epoch.0,
14295 });
14296 (table_id, schema)
14297 }
14298 None => {
14299 let command = crate::catalog_cmds::CatalogCommand::CreateTable {
14303 name: name.to_string(),
14304 schema,
14305 created_epoch: epoch.0,
14306 };
14307 let delta = self.apply_catalog_command_to(&mut next_catalog, command)?;
14308 let crate::catalog_cmds::CatalogDelta::TableCreated { entry } = delta else {
14309 return Err(MongrelError::Other(
14310 "CreateTable resolved to an unexpected catalog delta".into(),
14311 ));
14312 };
14313 (entry.table_id, entry.schema)
14314 }
14315 };
14316 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14317
14318 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
14321 let canonical_tdir = self.root.join(&table_relative);
14322 let table_root = Arc::new(
14323 self.durable_root
14324 .create_directory_all_pinned(&table_relative)?,
14325 );
14326 let tdir = table_root.io_path()?;
14327 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
14328 let ctx = SharedCtx {
14329 root_guard: Some(table_root),
14330 epoch: Arc::clone(&self.epoch),
14331 page_cache: Arc::clone(&self.page_cache),
14332 decoded_cache: Arc::clone(&self.decoded_cache),
14333 snapshots: Arc::clone(&self.snapshots),
14334 kek: self.kek.clone(),
14335 commit_lock: Arc::clone(&self.commit_lock),
14336 shared: Some(crate::engine::SharedWalCtx {
14337 wal: Arc::clone(&self.shared_wal),
14338 group: Arc::clone(&self.group),
14339 poisoned: Arc::clone(&self.poisoned),
14340 txn_ids: Arc::clone(&self.next_txn_id),
14341 change_wake: self.change_wake.clone(),
14342 lifecycle: Arc::clone(&self.lifecycle),
14343 hlc: Arc::clone(&self.hlc),
14344 }),
14345 table_name: Some(name.to_string()),
14346 auth: self.table_auth_checker(),
14347 read_only: self.read_only,
14348 };
14349 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
14350
14351 let schema_json = DdlOp::encode_schema(&schema)?;
14354 let ddl = match &state {
14355 TableState::Live => DdlOp::CreateTable {
14356 table_id,
14357 name: name.to_string(),
14358 schema_json,
14359 },
14360 TableState::Building {
14361 intended_name,
14362 query_id,
14363 created_at_unix_nanos,
14364 replaces_table_id,
14365 } => match replaces_table_id {
14366 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
14367 table_id,
14368 build_name: name.to_string(),
14369 intended_name: intended_name.clone(),
14370 query_id: query_id.clone(),
14371 created_at_unix_nanos: *created_at_unix_nanos,
14372 replaces_table_id: *replaces_table_id,
14373 schema_json,
14374 },
14375 None => DdlOp::CreateBuildingTable {
14376 table_id,
14377 build_name: name.to_string(),
14378 intended_name: intended_name.clone(),
14379 query_id: query_id.clone(),
14380 created_at_unix_nanos: *created_at_unix_nanos,
14381 schema_json,
14382 },
14383 },
14384 TableState::Dropped { .. } => {
14385 return Err(MongrelError::InvalidArgument(
14386 "cannot create a table in dropped state".into(),
14387 ));
14388 }
14389 };
14390 let commit_seq = {
14391 let mut wal = self.shared_wal.lock();
14392 let append: Result<u64> = (|| {
14393 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
14394 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14395 wal.append_commit(txn_id, epoch, &[])
14396 })();
14397 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14398 };
14399 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14400 pending_table_dir.disarm();
14401
14402 self.tables
14405 .write()
14406 .insert(table_id, TableHandle::new(table));
14407 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14408 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14409 Ok(table_id)
14410 }
14411
14412 pub fn drop_table(&self, name: &str) -> Result<()> {
14414 self.drop_table_with_epoch(name).map(|_| ())
14415 }
14416
14417 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
14419 self.drop_table_with_state(name, false, None)
14420 }
14421
14422 pub fn drop_table_with_epoch_controlled<F>(
14423 &self,
14424 name: &str,
14425 mut before_commit: F,
14426 ) -> Result<Epoch>
14427 where
14428 F: FnMut() -> Result<()>,
14429 {
14430 self.drop_table_with_state(name, false, Some(&mut before_commit))
14431 }
14432
14433 #[doc(hidden)]
14435 pub fn discard_building_table(&self, name: &str) -> Result<()> {
14436 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
14437 return Err(MongrelError::InvalidArgument(
14438 "not a CTAS building table".into(),
14439 ));
14440 }
14441 self.drop_table_with_state(name, true, None).map(|_| ())
14442 }
14443
14444 fn drop_table_with_state(
14445 &self,
14446 name: &str,
14447 building: bool,
14448 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14449 ) -> Result<Epoch> {
14450 use crate::wal::DdlOp;
14451 use std::sync::atomic::Ordering;
14452
14453 self.require(&crate::auth::Permission::Ddl)?;
14454 if self.poisoned.load(Ordering::Relaxed) {
14455 return Err(MongrelError::Other(
14456 "database poisoned by fsync error".into(),
14457 ));
14458 }
14459 let _operation = self.admit_operation()?;
14461 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14462 let _g = self.ddl_lock.lock();
14463 let _security_write = self.security_write()?;
14464 self.require(&crate::auth::Permission::Ddl)?;
14465 let table_id = {
14466 let cat = self.catalog.read();
14467 if building {
14468 cat.building(name)
14469 } else {
14470 cat.live(name)
14471 }
14472 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
14473 .table_id
14474 };
14475
14476 let commit_lock = Arc::clone(&self.commit_lock);
14477 let _c = commit_lock.lock();
14478 let epoch = self.epoch.bump_assigned();
14479 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14480 let txn_id = self.alloc_txn_id()?;
14481 let mut next_catalog = self.catalog.read().clone();
14482 if building {
14483 let entry = next_catalog
14486 .tables
14487 .iter_mut()
14488 .find(|t| t.table_id == table_id)
14489 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14490 entry.state = TableState::Dropped { at_epoch: epoch.0 };
14491 next_catalog.triggers.retain(|trigger| {
14492 !matches!(
14493 &trigger.trigger.target,
14494 TriggerTarget::Table(target) if target == name
14495 )
14496 });
14497 next_catalog
14498 .materialized_views
14499 .retain(|definition| definition.name != name);
14500 next_catalog
14501 .security
14502 .rls_tables
14503 .retain(|table| table != name);
14504 next_catalog
14505 .security
14506 .policies
14507 .retain(|policy| policy.table != name);
14508 next_catalog
14509 .security
14510 .masks
14511 .retain(|mask| mask.table != name);
14512 for role in &mut next_catalog.roles {
14513 role.permissions
14514 .retain(|permission| permission_table(permission) != Some(name));
14515 }
14516 advance_security_version(&mut next_catalog)?;
14517 } else {
14518 let command = crate::catalog_cmds::CatalogCommand::DropTable {
14522 name: name.to_string(),
14523 at_epoch: epoch.0,
14524 };
14525 self.apply_catalog_command_to(&mut next_catalog, command)?;
14526 }
14527 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14528 let commit_seq = {
14529 let mut wal = self.shared_wal.lock();
14530 if let Some(before_commit) = before_commit {
14531 before_commit()?;
14532 }
14533 let append: Result<u64> = (|| {
14534 wal.append(
14535 txn_id,
14536 table_id,
14537 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
14538 )?;
14539 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14540 wal.append_commit(txn_id, epoch, &[])
14541 })();
14542 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14543 };
14544 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14545
14546 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14547 self.tables.write().remove(&table_id);
14548 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
14549 Ok(epoch)
14550 }
14551
14552 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
14561 self.rename_table_with_epoch(name, new_name).map(|_| ())
14562 }
14563
14564 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
14566 self.rename_table_with_epoch_inner(name, new_name, None)
14567 }
14568
14569 pub fn rename_table_with_epoch_controlled<F>(
14570 &self,
14571 name: &str,
14572 new_name: &str,
14573 mut before_commit: F,
14574 ) -> Result<Epoch>
14575 where
14576 F: FnMut() -> Result<()>,
14577 {
14578 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
14579 }
14580
14581 fn rename_table_with_epoch_inner(
14582 &self,
14583 name: &str,
14584 new_name: &str,
14585 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14586 ) -> Result<Epoch> {
14587 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14588 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14589 {
14590 return Err(MongrelError::InvalidArgument(
14591 "the CTAS building-table namespace is reserved".into(),
14592 ));
14593 }
14594 self.rename_table_with_state(name, new_name, false, None, before_commit)
14595 }
14596
14597 #[doc(hidden)]
14599 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14600 self.publish_building_table_inner(build_name, new_name, None)
14601 }
14602
14603 #[doc(hidden)]
14604 pub fn publish_building_table_controlled<F>(
14605 &self,
14606 build_name: &str,
14607 new_name: &str,
14608 mut before_commit: F,
14609 ) -> Result<Epoch>
14610 where
14611 F: FnMut() -> Result<()>,
14612 {
14613 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
14614 }
14615
14616 fn publish_building_table_inner(
14617 &self,
14618 build_name: &str,
14619 new_name: &str,
14620 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14621 ) -> Result<Epoch> {
14622 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14623 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14624 {
14625 return Err(MongrelError::InvalidArgument(
14626 "invalid CTAS publish identity".into(),
14627 ));
14628 }
14629 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
14630 }
14631
14632 #[doc(hidden)]
14634 pub fn publish_materialized_building_table(
14635 &self,
14636 build_name: &str,
14637 new_name: &str,
14638 definition: crate::catalog::MaterializedViewEntry,
14639 ) -> Result<Epoch> {
14640 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
14641 }
14642
14643 #[doc(hidden)]
14644 pub fn publish_materialized_building_table_controlled<F>(
14645 &self,
14646 build_name: &str,
14647 new_name: &str,
14648 definition: crate::catalog::MaterializedViewEntry,
14649 mut before_commit: F,
14650 ) -> Result<Epoch>
14651 where
14652 F: FnMut() -> Result<()>,
14653 {
14654 self.publish_materialized_building_table_inner(
14655 build_name,
14656 new_name,
14657 definition,
14658 Some(&mut before_commit),
14659 )
14660 }
14661
14662 fn publish_materialized_building_table_inner(
14663 &self,
14664 build_name: &str,
14665 new_name: &str,
14666 definition: crate::catalog::MaterializedViewEntry,
14667 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14668 ) -> Result<Epoch> {
14669 if definition.name != new_name || definition.query.trim().is_empty() {
14670 return Err(MongrelError::InvalidArgument(
14671 "invalid materialized-view publication".into(),
14672 ));
14673 }
14674 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
14675 }
14676
14677 #[doc(hidden)]
14679 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
14680 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
14681 }
14682
14683 #[doc(hidden)]
14684 pub fn publish_rebuilding_table_controlled<F>(
14685 &self,
14686 build_name: &str,
14687 new_name: &str,
14688 mut before_commit: F,
14689 ) -> Result<Epoch>
14690 where
14691 F: FnMut() -> Result<()>,
14692 {
14693 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
14694 }
14695
14696 #[doc(hidden)]
14698 pub fn publish_materialized_rebuilding_table_controlled<F>(
14699 &self,
14700 build_name: &str,
14701 new_name: &str,
14702 definition: crate::catalog::MaterializedViewEntry,
14703 mut before_commit: F,
14704 ) -> Result<Epoch>
14705 where
14706 F: FnMut() -> Result<()>,
14707 {
14708 self.publish_rebuilding_table_inner(
14709 build_name,
14710 new_name,
14711 Some(definition),
14712 Some(&mut before_commit),
14713 )
14714 }
14715
14716 fn publish_rebuilding_table_inner(
14717 &self,
14718 build_name: &str,
14719 new_name: &str,
14720 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14721 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14722 ) -> Result<Epoch> {
14723 use crate::wal::DdlOp;
14724
14725 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14726 || new_name.is_empty()
14727 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
14728 {
14729 return Err(MongrelError::InvalidArgument(
14730 "invalid rebuilding-table publish identity".into(),
14731 ));
14732 }
14733 if materialized_view.as_ref().is_some_and(|definition| {
14734 definition.name != new_name || definition.query.trim().is_empty()
14735 }) {
14736 return Err(MongrelError::InvalidArgument(
14737 "invalid materialized-view replacement".into(),
14738 ));
14739 }
14740 self.require(&crate::auth::Permission::Ddl)?;
14741 if self.poisoned.load(Ordering::Relaxed) {
14742 return Err(MongrelError::Other(
14743 "database poisoned by fsync error".into(),
14744 ));
14745 }
14746 let _operation = self.admit_operation()?;
14748 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14749 let _ddl = self.ddl_lock.lock();
14750 let _security_write = self.security_write()?;
14751 let (table_id, replaced_table_id) = {
14752 let catalog = self.catalog.read();
14753 let build = catalog.building(build_name).ok_or_else(|| {
14754 MongrelError::NotFound(format!("building table {build_name:?} not found"))
14755 })?;
14756 let replaced_table_id = match &build.state {
14757 TableState::Building {
14758 intended_name,
14759 replaces_table_id: Some(replaced_table_id),
14760 ..
14761 } if intended_name == new_name => *replaced_table_id,
14762 _ => {
14763 return Err(MongrelError::InvalidArgument(format!(
14764 "building table {build_name:?} is not a replacement for {new_name:?}"
14765 )))
14766 }
14767 };
14768 if catalog
14769 .live(new_name)
14770 .is_none_or(|entry| entry.table_id != replaced_table_id)
14771 {
14772 return Err(MongrelError::Conflict(format!(
14773 "table {new_name:?} changed while its replacement was built"
14774 )));
14775 }
14776 (build.table_id, replaced_table_id)
14777 };
14778
14779 let _commit = self.commit_lock.lock();
14780 let epoch = self.epoch.assigned().next();
14781 let txn_id = self.alloc_txn_id()?;
14782 let mut next_catalog = self.catalog.read().clone();
14783 apply_rebuilding_publish(
14784 &mut next_catalog,
14785 table_id,
14786 replaced_table_id,
14787 new_name,
14788 epoch.0,
14789 )?;
14790 if let Some(definition) = materialized_view.as_mut() {
14791 definition.last_refresh_epoch = epoch.0;
14792 }
14793 let materialized_view_json = materialized_view
14794 .as_ref()
14795 .map(DdlOp::encode_materialized_view)
14796 .transpose()?;
14797 if let Some(definition) = materialized_view {
14798 if let Some(existing) = next_catalog
14799 .materialized_views
14800 .iter_mut()
14801 .find(|existing| existing.name == definition.name)
14802 {
14803 *existing = definition;
14804 } else {
14805 next_catalog.materialized_views.push(definition);
14806 }
14807 }
14808 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14809 if let Some(before_commit) = before_commit {
14810 before_commit()?;
14811 }
14812 let assigned_epoch = self.epoch.bump_assigned();
14813 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
14814 if assigned_epoch != epoch {
14815 return Err(MongrelError::Conflict(
14816 "commit epoch changed while sequencer lock was held".into(),
14817 ));
14818 }
14819 let commit_seq = {
14820 let mut wal = self.shared_wal.lock();
14821 let append: Result<u64> = (|| {
14822 wal.append(
14823 txn_id,
14824 table_id,
14825 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
14826 table_id,
14827 replaced_table_id,
14828 new_name: new_name.to_string(),
14829 }),
14830 )?;
14831 if let Some(definition_json) = materialized_view_json {
14832 wal.append(
14833 txn_id,
14834 table_id,
14835 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
14836 name: new_name.to_string(),
14837 definition_json,
14838 }),
14839 )?;
14840 }
14841 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
14842 wal.append_commit(txn_id, epoch, &[])
14843 })();
14844 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
14845 };
14846 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
14847
14848 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
14849 self.tables.write().remove(&replaced_table_id);
14850 if let Some(table) = self.tables.read().get(&table_id) {
14851 table.lock().set_catalog_name(new_name.to_string());
14852 }
14853 self.finish_durable_publish(epoch, &mut epoch_guard, &receipt, checkpoint)?;
14854 Ok(epoch)
14855 }
14856
14857 fn rename_table_with_state(
14858 &self,
14859 name: &str,
14860 new_name: &str,
14861 building: bool,
14862 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
14863 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
14864 ) -> Result<Epoch> {
14865 use crate::wal::DdlOp;
14866 use std::sync::atomic::Ordering;
14867
14868 self.require(&crate::auth::Permission::Ddl)?;
14869 if self.poisoned.load(Ordering::Relaxed) {
14870 return Err(MongrelError::Other(
14871 "database poisoned by fsync error".into(),
14872 ));
14873 }
14874
14875 if name == new_name {
14878 return Ok(self.visible_epoch());
14879 }
14880 if new_name.is_empty() {
14881 return Err(MongrelError::InvalidArgument(
14882 "rename_table: new name must not be empty".into(),
14883 ));
14884 }
14885 let _operation = self.admit_operation()?;
14887 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
14888 let _g = self.ddl_lock.lock();
14889 let _security_write = self.security_write()?;
14890 self.require(&crate::auth::Permission::Ddl)?;
14891 let table_id = {
14892 let cat = self.catalog.read();
14893 let src = if building {
14894 cat.building(name)
14895 } else {
14896 cat.live(name)
14897 }
14898 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14899 if building
14900 && !matches!(
14901 &src.state,
14902 TableState::Building { intended_name, .. } if intended_name == new_name
14903 )
14904 {
14905 return Err(MongrelError::InvalidArgument(format!(
14906 "building table {name:?} is not reserved for {new_name:?}"
14907 )));
14908 }
14909 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
14913 return Err(MongrelError::InvalidArgument(format!(
14914 "rename_table: a table named {new_name:?} already exists"
14915 )));
14916 }
14917 src.table_id
14918 };
14919
14920 let commit_lock = Arc::clone(&self.commit_lock);
14921 let _c = commit_lock.lock();
14922 let epoch = self.epoch.bump_assigned();
14923 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
14924 let txn_id = self.alloc_txn_id()?;
14925 if let Some(definition) = materialized_view.as_mut() {
14926 definition.last_refresh_epoch = epoch.0;
14927 }
14928 let materialized_view_json = materialized_view
14929 .as_ref()
14930 .map(DdlOp::encode_materialized_view)
14931 .transpose()?;
14932 let mut next_catalog = self.catalog.read().clone();
14933 if building {
14934 let entry = next_catalog
14937 .tables
14938 .iter_mut()
14939 .find(|t| t.table_id == table_id)
14940 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
14941 entry.name = new_name.to_string();
14942 entry.state = TableState::Live;
14943 for trigger in &mut next_catalog.triggers {
14944 if matches!(
14945 &trigger.trigger.target,
14946 TriggerTarget::Table(target) if target == name
14947 ) {
14948 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
14949 }
14950 }
14951 if let Some(definition) = next_catalog
14952 .materialized_views
14953 .iter_mut()
14954 .find(|definition| definition.name == name)
14955 {
14956 definition.name = new_name.to_string();
14957 }
14958 if let Some(definition) = materialized_view.take() {
14959 next_catalog.materialized_views.push(definition);
14960 }
14961 for table in &mut next_catalog.security.rls_tables {
14962 if table == name {
14963 *table = new_name.to_string();
14964 }
14965 }
14966 for policy in &mut next_catalog.security.policies {
14967 if policy.table == name {
14968 policy.table = new_name.to_string();
14969 }
14970 }
14971 for mask in &mut next_catalog.security.masks {
14972 if mask.table == name {
14973 mask.table = new_name.to_string();
14974 }
14975 }
14976 for role in &mut next_catalog.roles {
14977 for permission in &mut role.permissions {
14978 rename_permission_table(permission, name, new_name);
14979 }
14980 }
14981 advance_security_version(&mut next_catalog)?;
14982 } else {
14983 let command = crate::catalog_cmds::CatalogCommand::RenameTable {
14987 name: name.to_string(),
14988 new_name: new_name.to_string(),
14989 at_epoch: epoch.0,
14990 };
14991 self.apply_catalog_command_to(&mut next_catalog, command)?;
14992 }
14993 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
14994 let ddl = if building {
14995 DdlOp::PublishBuildingTable {
14996 table_id,
14997 new_name: new_name.to_string(),
14998 }
14999 } else {
15000 DdlOp::RenameTable {
15001 table_id,
15002 new_name: new_name.to_string(),
15003 }
15004 };
15005 let commit_seq = {
15006 let mut wal = self.shared_wal.lock();
15007 if let Some(before_commit) = before_commit {
15008 before_commit()?;
15009 }
15010 let append: Result<u64> = (|| {
15011 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
15012 if let Some(definition_json) = materialized_view_json {
15013 wal.append(
15014 txn_id,
15015 table_id,
15016 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
15017 name: new_name.to_string(),
15018 definition_json,
15019 }),
15020 )?;
15021 }
15022 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15023 wal.append_commit(txn_id, epoch, &[])
15024 })();
15025 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15026 };
15027 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15028
15029 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
15030 if let Some(table) = self.tables.read().get(&table_id) {
15033 table.lock().set_catalog_name(new_name.to_string());
15034 }
15035 self.finish_durable_publish(epoch, &mut _epoch_guard, &receipt, checkpoint)?;
15036 Ok(epoch)
15037 }
15038
15039 pub fn add_column(
15045 &self,
15046 table_name: &str,
15047 name: &str,
15048 ty: TypeId,
15049 flags: ColumnFlags,
15050 default_value: Option<crate::schema::DefaultExpr>,
15051 ) -> Result<ColumnDef> {
15052 self.add_column_with_id(table_name, name, ty, flags, default_value, None)
15053 }
15054
15055 pub fn add_column_with_id(
15057 &self,
15058 table_name: &str,
15059 name: &str,
15060 ty: TypeId,
15061 flags: ColumnFlags,
15062 default_value: Option<crate::schema::DefaultExpr>,
15063 requested_id: Option<u16>,
15064 ) -> Result<ColumnDef> {
15065 use crate::wal::DdlOp;
15066 use std::sync::atomic::Ordering;
15067
15068 self.require(&crate::auth::Permission::Ddl)?;
15069 if self.poisoned.load(Ordering::Relaxed) {
15070 return Err(MongrelError::Other(
15071 "database poisoned by fsync error".into(),
15072 ));
15073 }
15074 let _operation = self.admit_operation()?;
15075 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
15076 let _ddl = self.ddl_lock.lock();
15077 let _security_write = self.security_write()?;
15078 self.require(&crate::auth::Permission::Ddl)?;
15079 let table_id = {
15080 let catalog = self.catalog.read();
15081 catalog
15082 .live(table_name)
15083 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15084 .table_id
15085 };
15086 let handle =
15087 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15088 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15089 })?;
15090 let durable_epoch = std::cell::Cell::new(None);
15091 let result: Result<ColumnDef> = (|| {
15092 let mut table = handle.lock();
15093 let (column, prepared_schema) =
15094 table.prepare_add_column(name, ty, flags, default_value, requested_id)?;
15095 let command = crate::catalog_cmds::CatalogCommand::AddColumn {
15096 table: table_name.to_string(),
15097 column: column.clone(),
15098 };
15099 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
15100
15101 let commit_lock = Arc::clone(&self.commit_lock);
15102 let _commit = commit_lock.lock();
15103 let epoch = self.epoch.bump_assigned();
15104 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15105 let txn_id = self.alloc_txn_id()?;
15106 let column_json = DdlOp::encode_column(&column)?;
15107 let mut next_catalog = self.catalog.read().clone();
15108 let catalog_entry_index = next_catalog
15109 .tables
15110 .iter()
15111 .position(|entry| entry.table_id == table_id)
15112 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
15113 self.apply_catalog_command_to(&mut next_catalog, command)?;
15114 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
15115 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15116 let commit_seq = {
15117 let mut wal = self.shared_wal.lock();
15118 let append: Result<u64> = (|| {
15119 wal.append(
15120 txn_id,
15121 table_id,
15122 crate::wal::Op::Ddl(DdlOp::AlterTable {
15123 table_id,
15124 column_json,
15125 }),
15126 )?;
15127 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15128 wal.append_commit(txn_id, epoch, &[])
15129 })();
15130 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15131 };
15132 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15133 durable_epoch.set(Some(epoch));
15134
15135 table.apply_altered_schema_prepared(prepared_schema);
15136 let schema = table.schema().clone();
15137 let table_checkpoint = table.checkpoint_altered_schema();
15138 drop(table);
15139 next_catalog.tables[catalog_entry_index].schema = schema;
15140 let catalog_result =
15141 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15142 *self.catalog.write() = next_catalog;
15143 self.publish_committed(&receipt, epoch)?;
15144 epoch_guard.disarm();
15145 if let Err(error) = table_checkpoint.and(catalog_result) {
15146 self.poisoned.store(true, Ordering::Relaxed);
15147 self.lifecycle.poison();
15148 return Err(MongrelError::DurableCommit {
15149 epoch: epoch.0,
15150 message: error.to_string(),
15151 });
15152 }
15153 Ok(column)
15154 })();
15155 result.map_err(|error| match (durable_epoch.get(), error) {
15156 (_, error @ MongrelError::DurableCommit { .. }) => error,
15157 (Some(epoch), error) => MongrelError::DurableCommit {
15158 epoch: epoch.0,
15159 message: error.to_string(),
15160 },
15161 (None, error) => error,
15162 })
15163 }
15164
15165 pub fn alter_column(
15166 &self,
15167 table_name: &str,
15168 column_name: &str,
15169 change: AlterColumn,
15170 ) -> Result<ColumnDef> {
15171 self.alter_column_with_epoch(table_name, column_name, change)
15172 .map(|(column, _)| column)
15173 }
15174
15175 pub fn alter_column_with_epoch(
15176 &self,
15177 table_name: &str,
15178 column_name: &str,
15179 change: AlterColumn,
15180 ) -> Result<(ColumnDef, Option<Epoch>)> {
15181 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
15182 }
15183
15184 pub fn alter_column_with_epoch_controlled<B, A>(
15189 &self,
15190 table_name: &str,
15191 column_name: &str,
15192 change: AlterColumn,
15193 control: &crate::ExecutionControl,
15194 mut before_commit: B,
15195 mut after_commit: A,
15196 ) -> Result<(ColumnDef, Option<Epoch>)>
15197 where
15198 B: FnMut() -> Result<()>,
15199 A: FnMut(Option<Epoch>) -> Result<()>,
15200 {
15201 self.alter_column_with_epoch_inner(
15202 table_name,
15203 column_name,
15204 change,
15205 Some(control),
15206 Some(&mut before_commit),
15207 Some(&mut after_commit),
15208 )
15209 }
15210
15211 #[allow(clippy::too_many_arguments)]
15212 fn alter_column_with_epoch_inner(
15213 &self,
15214 table_name: &str,
15215 column_name: &str,
15216 change: AlterColumn,
15217 control: Option<&crate::ExecutionControl>,
15218 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
15219 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
15220 ) -> Result<(ColumnDef, Option<Epoch>)> {
15221 use crate::wal::DdlOp;
15222 use std::sync::atomic::Ordering;
15223
15224 self.require(&crate::auth::Permission::Ddl)?;
15225 commit_prepare_checkpoint(control, 0)?;
15226 if self.poisoned.load(Ordering::Relaxed) {
15227 return Err(MongrelError::Other(
15228 "database poisoned by fsync error".into(),
15229 ));
15230 }
15231 let _operation = self.admit_operation()?;
15233 let _schema_barrier = self.acquire_schema_barrier_exclusive()?;
15234 let _g = self.ddl_lock.lock();
15235 let table_id = {
15236 let cat = self.catalog.read();
15237 cat.live(table_name)
15238 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15239 .table_id
15240 };
15241 let handle =
15242 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15243 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15244 })?;
15245
15246 let backfill = {
15252 let table = handle.lock();
15253 let old = table
15254 .schema()
15255 .column(column_name)
15256 .cloned()
15257 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
15258 let next_flags = change.flags.unwrap_or(old.flags);
15259 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
15260 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
15261 && old.default_value.is_some()
15262 {
15263 let snapshot = self.snapshot_for_epoch(self.epoch.visible());
15264 let mut updates = Vec::new();
15265 let rows = match control {
15266 Some(control) => table.visible_rows_controlled(snapshot, control)?,
15267 None => table.visible_rows(snapshot)?,
15268 };
15269 for (row_index, row) in rows.into_iter().enumerate() {
15270 commit_prepare_checkpoint(control, row_index)?;
15271 if row
15272 .columns
15273 .get(&old.id)
15274 .is_some_and(|value| !matches!(value, Value::Null))
15275 {
15276 continue;
15277 }
15278 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
15279 table.apply_defaults(&mut cells)?;
15280 updates.push((
15281 table_id,
15282 crate::txn::Staged::Update {
15283 row_id: row.row_id,
15284 new_row: cells,
15285 changed_columns: vec![old.id],
15286 },
15287 ));
15288 }
15289 updates
15290 } else {
15291 Vec::new()
15292 }
15293 };
15294 let durable_epoch = std::cell::Cell::new(None);
15295 let backfill_epoch = if backfill.is_empty() {
15296 None
15297 } else {
15298 let (principal, catalog_bound) = self.transaction_principal_snapshot();
15299 let txn_id = self.alloc_txn_id()?;
15300 let mut entered_fence = false;
15301 let commit_result = match (control, before_commit.as_deref_mut()) {
15302 (Some(control), Some(before_commit)) => self
15303 .commit_transaction_with_external_states_controlled(
15304 txn_id,
15305 self.epoch.visible(),
15306 backfill,
15307 Vec::new(),
15308 Vec::new(),
15309 principal.clone(),
15310 catalog_bound,
15311 None,
15312 crate::txn::TxnCommitContext::internal(),
15313 control,
15314 &mut || {
15315 before_commit()?;
15316 entered_fence = true;
15317 Ok(())
15318 },
15319 )
15320 .map(|(epoch, _)| epoch),
15321 _ => self
15322 .commit_transaction_with_external_states(
15323 txn_id,
15324 self.epoch.visible(),
15325 backfill,
15326 Vec::new(),
15327 Vec::new(),
15328 principal,
15329 catalog_bound,
15330 None,
15331 crate::txn::TxnCommitContext::internal(),
15332 )
15333 .map(|(epoch, _)| epoch),
15334 };
15335 let commit_result = if entered_fence {
15336 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15337 } else {
15338 commit_result
15339 };
15340 match &commit_result {
15341 Ok(epoch) => durable_epoch.set(Some(*epoch)),
15342 Err(MongrelError::DurableCommit { epoch, .. }) => {
15343 durable_epoch.set(Some(Epoch(*epoch)));
15344 }
15345 Err(_) => {}
15346 }
15347 Some(commit_result?)
15348 };
15349 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
15350 let _security_write = self.security_write()?;
15351 self.require(&crate::auth::Permission::Ddl)?;
15352 if self
15353 .catalog
15354 .read()
15355 .live(table_name)
15356 .is_none_or(|entry| entry.table_id != table_id)
15357 {
15358 return Err(MongrelError::Conflict(format!(
15359 "table {table_name:?} changed during ALTER"
15360 )));
15361 }
15362 let mut table = handle.lock();
15363 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
15364 let renamed_column = (column.name != column_name).then(|| column.name.clone());
15365 let Some(prepared_schema) = prepared_schema else {
15366 return Ok((column, backfill_epoch));
15367 };
15368 let command = crate::catalog_cmds::CatalogCommand::AlterColumn {
15373 table: table_name.to_string(),
15374 column: column.clone(),
15375 };
15376 crate::catalog_cmds::apply(&self.catalog.read(), &command)?;
15377
15378 let commit_lock = Arc::clone(&self.commit_lock);
15379 let _c = commit_lock.lock();
15380 let epoch = self.epoch.bump_assigned();
15381 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15382 let txn_id = self.alloc_txn_id()?;
15383 let column_json = DdlOp::encode_column(&column)?;
15384 let mut next_catalog = self.catalog.read().clone();
15385 let catalog_entry_index = next_catalog
15386 .tables
15387 .iter()
15388 .position(|entry| entry.table_id == table_id)
15389 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
15390 if let Some(new_column_name) = &renamed_column {
15391 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
15392 commit_prepare_checkpoint(control, trigger_index)?;
15393 if matches!(
15394 &trigger.trigger.target,
15395 TriggerTarget::Table(target) if target == table_name
15396 ) {
15397 trigger.trigger = trigger.trigger.renamed_update_column(
15398 column_name,
15399 new_column_name.clone(),
15400 epoch.0,
15401 )?;
15402 }
15403 }
15404 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
15405 commit_prepare_checkpoint(control, role_index)?;
15406 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
15407 commit_prepare_checkpoint(control, permission_index)?;
15408 rename_permission_column(
15409 permission,
15410 table_name,
15411 column_name,
15412 new_column_name,
15413 );
15414 }
15415 }
15416 advance_security_version(&mut next_catalog)?;
15417 }
15418 self.apply_catalog_command_to(&mut next_catalog, command)?;
15424 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
15425 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15426 commit_prepare_checkpoint(control, 0)?;
15427 let mut entered_fence = false;
15428 if let Some(before_commit) = before_commit.as_deref_mut() {
15429 before_commit()?;
15430 entered_fence = true;
15431 }
15432 let commit_result: Result<Epoch> = (|| {
15433 let commit_seq = {
15434 let mut wal = self.shared_wal.lock();
15435 let append: Result<u64> = (|| {
15436 wal.append(
15437 txn_id,
15438 table_id,
15439 crate::wal::Op::Ddl(DdlOp::AlterTable {
15440 table_id,
15441 column_json,
15442 }),
15443 )?;
15444 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15445 wal.append_commit(txn_id, epoch, &[])
15446 })();
15447 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15448 };
15449 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15450 durable_epoch.set(Some(epoch));
15451
15452 table.apply_altered_schema_prepared(prepared_schema);
15453 let schema = table.schema().clone();
15454 let table_checkpoint = table.checkpoint_altered_schema();
15455 drop(table);
15456 next_catalog.tables[catalog_entry_index].schema = schema;
15457 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15458 let catalog_result =
15459 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
15460 let security_version = next_catalog.security_version;
15461 *self.catalog.write() = next_catalog;
15462 if renamed_column.is_some() {
15463 self.security_coordinator
15464 .version
15465 .store(security_version, Ordering::Release);
15466 }
15467 self.publish_committed(&receipt, epoch)?;
15468 _epoch_guard.disarm();
15469 if let Err(error) = table_checkpoint.and(catalog_result) {
15470 self.poisoned.store(true, Ordering::Relaxed);
15471 self.lifecycle.poison();
15472 return Err(MongrelError::DurableCommit {
15473 epoch: epoch.0,
15474 message: error.to_string(),
15475 });
15476 }
15477 Ok(epoch)
15478 })();
15479 let commit_result = if entered_fence {
15480 finish_controlled_commit_attempt(commit_result, &mut after_commit)
15481 } else {
15482 commit_result
15483 };
15484 let epoch = commit_result?;
15485 Ok((column, Some(epoch)))
15486 })();
15487 result.map_err(|error| match (durable_epoch.get(), error) {
15488 (_, error @ MongrelError::DurableCommit { .. }) => error,
15489 (Some(epoch), error) => MongrelError::DurableCommit {
15490 epoch: epoch.0,
15491 message: error.to_string(),
15492 },
15493 (None, error) => error,
15494 })
15495 }
15496
15497 pub fn set_table_ttl(
15500 &self,
15501 table_name: &str,
15502 column_name: &str,
15503 duration_nanos: u64,
15504 ) -> Result<crate::manifest::TtlPolicy> {
15505 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
15506 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
15507 }
15508
15509 #[doc(hidden)]
15511 pub fn set_building_table_ttl(
15512 &self,
15513 table_name: &str,
15514 column_name: &str,
15515 duration_nanos: u64,
15516 ) -> Result<crate::manifest::TtlPolicy> {
15517 let policy = self.replace_table_ttl_with_state(
15518 table_name,
15519 Some((column_name, duration_nanos)),
15520 true,
15521 )?;
15522 policy
15523 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
15524 }
15525
15526 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
15527 self.replace_table_ttl(table_name, None)?;
15528 Ok(())
15529 }
15530
15531 fn replace_table_ttl(
15532 &self,
15533 table_name: &str,
15534 requested: Option<(&str, u64)>,
15535 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15536 self.replace_table_ttl_with_state(table_name, requested, false)
15537 }
15538
15539 fn replace_table_ttl_with_state(
15540 &self,
15541 table_name: &str,
15542 requested: Option<(&str, u64)>,
15543 building: bool,
15544 ) -> Result<Option<crate::manifest::TtlPolicy>> {
15545 use crate::wal::DdlOp;
15546 use std::sync::atomic::Ordering;
15547
15548 self.require(&crate::auth::Permission::Ddl)?;
15549 if self.poisoned.load(Ordering::Relaxed) {
15550 return Err(MongrelError::Other(
15551 "database poisoned by fsync error".into(),
15552 ));
15553 }
15554
15555 let _g = self.ddl_lock.lock();
15556 let _security_write = self.security_write()?;
15557 self.require(&crate::auth::Permission::Ddl)?;
15558 let table_id = {
15559 let cat = self.catalog.read();
15560 if building {
15561 cat.building(table_name)
15562 } else {
15563 cat.live(table_name)
15564 }
15565 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
15566 .table_id
15567 };
15568 let handle =
15569 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
15570 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
15571 })?;
15572 let mut table = handle.lock();
15573 let policy = match requested {
15574 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
15575 None => None,
15576 };
15577 if table.ttl() == policy {
15578 return Ok(policy);
15579 }
15580
15581 let commit_lock = Arc::clone(&self.commit_lock);
15582 let _c = commit_lock.lock();
15583 let epoch = self.epoch.bump_assigned();
15584 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
15585 let txn_id = self.alloc_txn_id()?;
15586 let policy_json = DdlOp::encode_ttl(policy)?;
15587 let mut next_catalog = self.catalog.read().clone();
15588 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
15589 let commit_seq = {
15590 let mut wal = self.shared_wal.lock();
15591 let append: Result<u64> = (|| {
15592 wal.append(
15593 txn_id,
15594 table_id,
15595 crate::wal::Op::Ddl(DdlOp::SetTtl {
15596 table_id,
15597 policy_json,
15598 }),
15599 )?;
15600 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
15601 wal.append_commit(txn_id, epoch, &[])
15602 })();
15603 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
15604 };
15605 let receipt = self.await_durable_commit(txn_id, commit_seq, epoch)?;
15606
15607 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
15608 drop(table);
15609 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
15610 publish_error.get_or_insert(error);
15611 }
15612 self.finish_durable_publish(
15613 epoch,
15614 &mut _epoch_guard,
15615 &receipt,
15616 publish_error.map_or(Ok(()), Err),
15617 )?;
15618 Ok(policy)
15619 }
15620
15621 pub fn gc(&self) -> Result<usize> {
15627 let control = crate::ExecutionControl::new(None);
15628 self.gc_controlled(&control, || true)
15629 }
15630
15631 #[doc(hidden)]
15634 pub fn gc_controlled<F>(
15635 &self,
15636 control: &crate::ExecutionControl,
15637 before_publish: F,
15638 ) -> Result<usize>
15639 where
15640 F: FnOnce() -> bool,
15641 {
15642 self.gc_controlled_with_receipt(control, before_publish)
15643 .map(|(reclaimed, _)| reclaimed)
15644 }
15645
15646 #[doc(hidden)]
15649 pub fn gc_controlled_with_receipt<F>(
15650 &self,
15651 control: &crate::ExecutionControl,
15652 before_publish: F,
15653 ) -> Result<(usize, Option<MaintenanceReceipt>)>
15654 where
15655 F: FnOnce() -> bool,
15656 {
15657 enum Candidate {
15658 Directory(PathBuf),
15659 File(PathBuf),
15660 }
15661
15662 self.require(&crate::auth::Permission::Ddl)?;
15663 let _operation = self.admit_operation()?;
15665 let _ddl = self.ddl_lock.lock();
15666 self.require(&crate::auth::Permission::Ddl)?;
15667 control.checkpoint()?;
15668 let maintenance_epoch = self.epoch.visible();
15669 let min_active = self.snapshots.min_active(maintenance_epoch).0;
15670 let mut candidates = Vec::new();
15671
15672 let cat = self.catalog.read();
15674 for (entry_index, entry) in cat.tables.iter().enumerate() {
15675 if entry_index % 256 == 0 {
15676 control.checkpoint()?;
15677 }
15678 if let TableState::Dropped { at_epoch } = entry.state {
15679 if at_epoch <= min_active {
15680 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
15681 if tdir.exists() {
15682 candidates.push(Candidate::Directory(tdir));
15683 }
15684 }
15685 }
15686 }
15687 drop(cat);
15688
15689 let cat = self.catalog.read();
15694 for (entry_index, entry) in cat.tables.iter().enumerate() {
15695 if entry_index % 256 == 0 {
15696 control.checkpoint()?;
15697 }
15698 if !matches!(entry.state, TableState::Live) {
15699 continue;
15700 }
15701 let txn_dir = self
15702 .root
15703 .join(TABLES_DIR)
15704 .join(entry.table_id.to_string())
15705 .join("_txn");
15706 if !txn_dir.exists() {
15707 continue;
15708 }
15709 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
15710 if sub_index % 256 == 0 {
15711 control.checkpoint()?;
15712 }
15713 let sub = sub?;
15714 let name = sub.file_name();
15715 let Some(name) = name.to_str() else { continue };
15716 let is_active = name
15718 .parse::<u64>()
15719 .map(|id| self.active_spills.is_active(id))
15720 .unwrap_or(false);
15721 if is_active {
15722 continue;
15723 }
15724 candidates.push(Candidate::Directory(sub.path()));
15725 }
15726 }
15727 drop(cat);
15728
15729 let external_names = {
15730 let cat = self.catalog.read();
15731 cat.external_tables
15732 .iter()
15733 .map(|entry| entry.name.clone())
15734 .collect::<std::collections::HashSet<_>>()
15735 };
15736 let vtab_dir = self.root.join(VTAB_DIR);
15737 if vtab_dir.exists() {
15738 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
15739 if entry_index % 256 == 0 {
15740 control.checkpoint()?;
15741 }
15742 let entry = entry?;
15743 let name = entry.file_name();
15744 let Some(name) = name.to_str() else { continue };
15745 if external_names.contains(name) {
15746 continue;
15747 }
15748 let path = entry.path();
15749 if path.is_dir() {
15750 candidates.push(Candidate::Directory(path));
15751 } else {
15752 candidates.push(Candidate::File(path));
15753 }
15754 }
15755 }
15756
15757 let tables = self
15761 .tables
15762 .read()
15763 .iter()
15764 .map(|(table_id, handle)| (*table_id, handle.clone()))
15765 .collect::<Vec<_>>();
15766 let mut retiring = Vec::new();
15767 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
15768 if table_index % 256 == 0 {
15769 control.checkpoint()?;
15770 }
15771 let backup_pinned: HashSet<u128> = self
15772 .backup_pins
15773 .lock()
15774 .keys()
15775 .filter_map(|(pinned_table, run_id)| {
15776 (*pinned_table == *table_id).then_some(*run_id)
15777 })
15778 .collect();
15779 if handle
15780 .lock()
15781 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
15782 {
15783 retiring.push((handle.clone(), backup_pinned));
15784 }
15785 }
15786
15787 let all_durable = self.active_spills.is_idle()
15795 && tables.iter().all(|(_, handle)| {
15796 let g = handle.lock();
15797 g.memtable_len() == 0 && g.mutable_run_len() == 0
15798 });
15799 let retain = self
15800 .replication_wal_retention_segments
15801 .load(std::sync::atomic::Ordering::Relaxed);
15802 let reap_wal = all_durable
15803 && self
15804 .shared_wal
15805 .lock()
15806 .has_gc_segments_retain_recent(retain)?;
15807
15808 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
15809 return Ok((0, None));
15810 }
15811 control.checkpoint()?;
15812 if !before_publish() {
15813 return Err(MongrelError::Cancelled);
15814 }
15815
15816 let mut reclaimed = 0;
15817 for candidate in candidates {
15818 match candidate {
15819 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
15820 Candidate::File(path) => std::fs::remove_file(path)?,
15821 }
15822 reclaimed += 1;
15823 }
15824 for (handle, backup_pinned) in retiring {
15825 reclaimed += handle
15826 .lock()
15827 .reap_retiring(Epoch(min_active), &backup_pinned)?;
15828 }
15829 if reap_wal {
15830 reclaimed += self
15831 .shared_wal
15832 .lock()
15833 .gc_segments_retain_recent(u64::MAX, retain)?;
15834 }
15835
15836 Ok((
15837 reclaimed,
15838 Some(MaintenanceReceipt {
15839 epoch: maintenance_epoch,
15840 }),
15841 ))
15842 }
15843
15844 pub fn checkpoint(&self) -> Result<()> {
15862 self.checkpoint_controlled(|| Ok(()))
15863 }
15864
15865 #[doc(hidden)]
15868 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
15869 where
15870 F: FnOnce() -> Result<()>,
15871 {
15872 self.require(&crate::auth::Permission::Ddl)?;
15873 let _operation = self.admit_operation()?;
15875 let _replication = self.replication_barrier.write();
15879 let _ddl = self.ddl_lock.lock();
15880 let _security = self.security_coordinator.gate.read();
15881 self.require(&crate::auth::Permission::Ddl)?;
15882
15883 let mut handles = self
15884 .tables
15885 .read()
15886 .iter()
15887 .map(|(table_id, handle)| (*table_id, handle.clone()))
15888 .collect::<Vec<_>>();
15889 handles.sort_by_key(|(table_id, _)| *table_id);
15890 let mut tables = handles
15891 .iter()
15892 .map(|(table_id, handle)| (*table_id, handle.lock()))
15893 .collect::<Vec<_>>();
15894
15895 for (_, table) in &mut tables {
15897 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
15898 {
15899 table.force_flush()?;
15900 }
15901 }
15902
15903 for (_, table) in &mut tables {
15906 if table.run_count() >= 2 || table.should_compact() {
15907 table.compact()?;
15908 }
15909 }
15910
15911 before_wal_reset()?;
15912
15913 let maintenance_epoch = self.epoch.visible();
15915 let min_active = self.snapshots.min_active(maintenance_epoch);
15916 for (table_id, table) in &mut tables {
15917 let backup_pinned: HashSet<u128> = self
15918 .backup_pins
15919 .lock()
15920 .keys()
15921 .filter_map(|(pinned_table, run_id)| {
15922 (*pinned_table == *table_id).then_some(*run_id)
15923 })
15924 .collect();
15925 table.reap_retiring(min_active, &backup_pinned)?;
15926 }
15927
15928 self.shared_wal.lock().reset_after_checkpoint()?;
15931
15932 let catalog_snapshot = self.catalog.read().clone();
15934 for entry in &catalog_snapshot.tables {
15935 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
15936 crate::durable_file::remove_directory_all(
15937 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
15938 )?;
15939 }
15940 if !matches!(entry.state, TableState::Live) {
15941 continue;
15942 }
15943 let transaction_dir = self
15944 .root
15945 .join(TABLES_DIR)
15946 .join(entry.table_id.to_string())
15947 .join("_txn");
15948 if transaction_dir.is_dir() {
15949 for child in std::fs::read_dir(&transaction_dir)? {
15950 let child = child?;
15951 let active = child
15952 .file_name()
15953 .to_str()
15954 .and_then(|name| name.parse::<u64>().ok())
15955 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
15956 if !active {
15957 crate::durable_file::remove_directory_all(&child.path())?;
15958 }
15959 }
15960 }
15961 }
15962 let external_names = catalog_snapshot
15963 .external_tables
15964 .iter()
15965 .map(|entry| entry.name.as_str())
15966 .collect::<HashSet<_>>();
15967 let external_root = self.root.join(VTAB_DIR);
15968 if external_root.is_dir() {
15969 for entry in std::fs::read_dir(&external_root)? {
15970 let entry = entry?;
15971 let name = entry.file_name();
15972 if name
15973 .to_str()
15974 .is_some_and(|name| external_names.contains(name))
15975 {
15976 continue;
15977 }
15978 if entry.file_type()?.is_dir() {
15979 crate::durable_file::remove_directory_all(&entry.path())?;
15980 } else {
15981 std::fs::remove_file(entry.path())?;
15982 crate::durable_file::sync_directory(&external_root)?;
15983 }
15984 }
15985 }
15986
15987 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
15990 let visible = self.epoch.visible();
15991 for (_, table) in &tables {
15992 table.persist_manifest(visible)?;
15993 }
15994
15995 Ok(())
15996 }
15997 fn alloc_txn_id(&self) -> Result<u64> {
15998 self.ensure_owner_process()?;
15999 crate::txn::allocate_txn_id(&self.next_txn_id)
16000 }
16001
16002 pub fn allocate_lock_txn_id(&self) -> Result<u64> {
16006 self.alloc_txn_id()
16007 }
16008
16009 pub fn set_spill_threshold(&self, bytes: u64) {
16013 self.spill_threshold
16014 .store(bytes, std::sync::atomic::Ordering::Relaxed);
16015 }
16016
16017 #[doc(hidden)]
16021 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16022 *self.spill_hook.lock() = Some(Box::new(f));
16023 }
16024
16025 #[doc(hidden)]
16028 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16029 *self.security_commit_hook.lock() = Some(Box::new(f));
16030 }
16031
16032 #[doc(hidden)]
16035 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16036 *self.catalog_commit_hook.lock() = Some(Box::new(f));
16037 }
16038
16039 #[doc(hidden)]
16042 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16043 *self.backup_hook.lock() = Some(Box::new(f));
16044 }
16045
16046 #[doc(hidden)]
16050 pub fn __set_fk_lock_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16051 *self.fk_lock_hook.lock() = Some(Arc::new(f));
16052 }
16053
16054 #[doc(hidden)]
16056 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
16057 *self.replication_hook.lock() = Some(Box::new(f));
16058 }
16059
16060 #[doc(hidden)]
16064 pub fn __wal_group_sync_count(&self) -> u64 {
16065 self.shared_wal.lock().group_sync_count()
16066 }
16067
16068 #[doc(hidden)]
16071 pub fn __poison(&self) {
16072 self.poisoned
16073 .store(true, std::sync::atomic::Ordering::Relaxed);
16074 }
16075
16076 pub fn check(&self) -> Vec<CheckIssue> {
16089 match self.check_inner(None) {
16090 Ok(issues) => issues,
16091 Err(error) => vec![CheckIssue {
16092 table_id: WAL_TABLE_ID,
16093 table_name: "shared WAL".into(),
16094 severity: "error".into(),
16095 description: error.to_string(),
16096 }],
16097 }
16098 }
16099
16100 #[doc(hidden)]
16102 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
16103 self.check_inner(Some(control))
16104 }
16105
16106 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
16107 let mut issues = Vec::new();
16108 let io_root = self.durable_root.io_path()?;
16109 let cat = self.catalog.read();
16110 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
16111 for (table_index, entry) in cat.tables.iter().enumerate() {
16112 if table_index % 256 == 0 {
16113 if let Some(control) = control {
16114 control.checkpoint()?;
16115 }
16116 }
16117 if !matches!(entry.state, TableState::Live) {
16118 continue;
16119 }
16120 let tdir = io_root.join(TABLES_DIR).join(entry.table_id.to_string());
16121 let mut err = |sev: &str, desc: String| {
16122 issues.push(CheckIssue {
16123 table_id: entry.table_id,
16124 table_name: entry.name.clone(),
16125 severity: sev.into(),
16126 description: desc,
16127 });
16128 };
16129 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
16130 Ok(m) => m,
16131 Err(e) => {
16132 err("error", format!("manifest read failed: {e}"));
16133 continue;
16134 }
16135 };
16136 if m.flushed_epoch > m.current_epoch {
16137 err(
16138 "error",
16139 format!(
16140 "flushed_epoch {} exceeds current_epoch {} (impossible)",
16141 m.flushed_epoch, m.current_epoch
16142 ),
16143 );
16144 }
16145
16146 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
16147 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
16148 for (run_index, rr) in m.runs.iter().enumerate() {
16149 if run_index % 256 == 0 {
16150 if let Some(control) = control {
16151 control.checkpoint()?;
16152 }
16153 }
16154 referenced.insert(rr.run_id);
16155 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
16156 if !run_path.exists() {
16157 err("error", format!("missing run file: r-{}.sr", rr.run_id));
16158 continue;
16159 }
16160 match crate::sorted_run::RunReader::open(
16161 &run_path,
16162 entry.schema.clone(),
16163 self.kek.clone(),
16164 ) {
16165 Ok(reader) => {
16166 if reader.row_count() as u64 != rr.row_count {
16167 err(
16168 "error",
16169 format!(
16170 "run r-{} row count mismatch: manifest {} vs run {}",
16171 rr.run_id,
16172 rr.row_count,
16173 reader.row_count()
16174 ),
16175 );
16176 }
16177 }
16178 Err(e) => {
16179 err(
16180 "error",
16181 format!("run r-{} integrity check failed: {e}", rr.run_id),
16182 );
16183 }
16184 }
16185 }
16186
16187 for r in &m.retiring {
16191 referenced.insert(r.run_id);
16192 }
16193
16194 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
16196 for (entry_index, ent) in rd.flatten().enumerate() {
16197 if entry_index % 256 == 0 {
16198 if let Some(control) = control {
16199 control.checkpoint()?;
16200 }
16201 }
16202 let p = ent.path();
16203 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
16204 continue;
16205 }
16206 let run_id = p
16207 .file_stem()
16208 .and_then(|s| s.to_str())
16209 .and_then(|s| s.strip_prefix("r-"))
16210 .and_then(|s| s.parse::<u128>().ok());
16211 if let Some(id) = run_id {
16212 if !referenced.contains(&id) {
16213 err(
16214 "warning",
16215 format!("orphan run file r-{id}.sr not referenced by the manifest"),
16216 );
16217 }
16218 }
16219 }
16220 }
16221 }
16222
16223 let external_names = cat
16224 .external_tables
16225 .iter()
16226 .map(|entry| entry.name.clone())
16227 .collect::<std::collections::HashSet<_>>();
16228 let vtab_dir = io_root.join(VTAB_DIR);
16229 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
16230 for (entry_index, entry) in entries.flatten().enumerate() {
16231 if entry_index % 256 == 0 {
16232 if let Some(control) = control {
16233 control.checkpoint()?;
16234 }
16235 }
16236 let name = entry.file_name();
16237 let Some(name) = name.to_str() else { continue };
16238 if !external_names.contains(name) {
16239 issues.push(CheckIssue {
16240 table_id: EXTERNAL_TABLE_ID,
16241 table_name: name.to_string(),
16242 severity: "warning".into(),
16243 description: format!(
16244 "orphan external table state entry {:?} not referenced by the catalog",
16245 entry.path()
16246 ),
16247 });
16248 }
16249 }
16250 }
16251
16252 if let Some(control) = control {
16259 control.checkpoint()?;
16260 }
16261 for (seg, msg) in self.shared_wal.lock().verify_segments() {
16262 issues.push(CheckIssue {
16263 table_id: WAL_TABLE_ID,
16264 table_name: "<wal>".into(),
16265 severity: "error".into(),
16266 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
16267 });
16268 }
16269 Ok(issues)
16270 }
16271
16272 pub fn doctor(&self) -> Result<Vec<u64>> {
16276 let control = crate::ExecutionControl::new(None);
16277 self.doctor_controlled(&control, || true)
16278 }
16279
16280 #[doc(hidden)]
16284 pub fn doctor_controlled<F>(
16285 &self,
16286 control: &crate::ExecutionControl,
16287 before_publish: F,
16288 ) -> Result<Vec<u64>>
16289 where
16290 F: FnOnce() -> bool,
16291 {
16292 self.doctor_controlled_with_receipt(control, before_publish)
16293 .map(|(quarantined, _)| quarantined)
16294 }
16295
16296 #[doc(hidden)]
16299 pub fn doctor_controlled_with_receipt<F>(
16300 &self,
16301 control: &crate::ExecutionControl,
16302 before_publish: F,
16303 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
16304 where
16305 F: FnOnce() -> bool,
16306 {
16307 let _ddl = self.ddl_lock.lock();
16310 let _security_write = self.security_write()?;
16311 let issues = self.check_inner(Some(control))?;
16312 let bad_tables: std::collections::HashSet<u64> = issues
16317 .iter()
16318 .filter(|i| {
16319 i.severity == "error"
16320 && i.table_id != WAL_TABLE_ID
16321 && i.table_id != EXTERNAL_TABLE_ID
16322 })
16323 .map(|i| i.table_id)
16324 .collect();
16325 if bad_tables.is_empty() {
16326 return Ok((Vec::new(), None));
16327 }
16328 let _commit = self.commit_lock.lock();
16329 control.checkpoint()?;
16330 if !before_publish() {
16331 return Err(MongrelError::Cancelled);
16332 }
16333 let maintenance_epoch = self.epoch.bump_assigned();
16334 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
16335
16336 let qdir = self.root.join("_quarantine");
16337 crate::durable_file::create_directory(&qdir)?;
16338 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
16339 bad_tables.sort_unstable();
16340
16341 let mut handles = self
16345 .tables
16346 .read()
16347 .iter()
16348 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
16349 .map(|(table_id, handle)| (*table_id, handle.clone()))
16350 .collect::<Vec<_>>();
16351 handles.sort_by_key(|(table_id, _)| *table_id);
16352 let mut table_guards = handles
16353 .iter()
16354 .map(|(table_id, handle)| (*table_id, handle.lock()))
16355 .collect::<Vec<_>>();
16356
16357 let mut next_catalog = self.catalog.read().clone();
16358 for table_id in &bad_tables {
16359 if let Some(entry) = next_catalog
16360 .tables
16361 .iter_mut()
16362 .find(|entry| entry.table_id == *table_id)
16363 {
16364 entry.state = TableState::Dropped {
16365 at_epoch: maintenance_epoch.0,
16366 };
16367 }
16368 }
16369 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
16370
16371 let txn_id = self.alloc_txn_id()?;
16372 let commit_seq = {
16373 let mut wal = self.shared_wal.lock();
16374 let append: Result<u64> = (|| {
16375 for table_id in &bad_tables {
16376 wal.append(
16377 txn_id,
16378 *table_id,
16379 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
16380 table_id: *table_id,
16381 }),
16382 )?;
16383 }
16384 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
16385 wal.append_commit(txn_id, maintenance_epoch, &[])
16386 })();
16387 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
16388 };
16389 let receipt = self.await_durable_commit(txn_id, commit_seq, maintenance_epoch)?;
16390 for (_, table) in &mut table_guards {
16391 table.mark_unavailable_after_quarantine();
16392 }
16393 {
16394 let mut live_tables = self.tables.write();
16395 for table_id in &bad_tables {
16396 live_tables.remove(table_id);
16397 }
16398 }
16399 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
16400 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, &receipt, checkpoint)?;
16401
16402 drop(table_guards);
16406 drop(handles);
16407
16408 for table_id in &bad_tables {
16412 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
16413 if source.exists() {
16414 let destination = qdir.join(table_id.to_string());
16415 if let Err(error) = crate::durable_file::rename(&source, &destination) {
16416 return Err(MongrelError::DurableCommit {
16417 epoch: maintenance_epoch.0,
16418 message: format!(
16419 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
16420 ),
16421 });
16422 }
16423 }
16424 }
16425 Ok((
16426 bad_tables,
16427 Some(MaintenanceReceipt {
16428 epoch: maintenance_epoch,
16429 }),
16430 ))
16431 }
16432
16433 #[allow(dead_code)]
16435 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
16436 self.kek.as_ref()
16437 }
16438
16439 #[allow(dead_code)]
16441 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
16442 &self.epoch
16443 }
16444
16445 #[allow(dead_code)]
16447 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
16448 &self.snapshots
16449 }
16450}
16451
16452fn external_state_dir(root: &Path, name: &str) -> PathBuf {
16453 root.join(VTAB_DIR).join(name)
16454}
16455
16456fn append_catalog_snapshot(
16457 wal: &mut crate::wal::SharedWal,
16458 txn_id: u64,
16459 catalog: &Catalog,
16460) -> Result<()> {
16461 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
16462 wal.append(
16463 txn_id,
16464 WAL_TABLE_ID,
16465 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
16466 )?;
16467 Ok(())
16468}
16469
16470fn filter_ignored_staging(
16471 staging: Vec<(u64, crate::txn::Staged)>,
16472 ignored_indices: &std::collections::BTreeSet<usize>,
16473) -> Vec<(u64, crate::txn::Staged)> {
16474 if ignored_indices.is_empty() {
16475 return staging;
16476 }
16477 staging
16478 .into_iter()
16479 .enumerate()
16480 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
16481 .collect()
16482}
16483
16484fn external_state_file(root: &Path, name: &str) -> PathBuf {
16485 external_state_dir(root, name).join("state.json")
16486}
16487
16488fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
16489 let path = external_state_file(root, name);
16490 match std::fs::read(path) {
16491 Ok(bytes) => Ok(bytes),
16492 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
16493 Err(e) => Err(e.into()),
16494 }
16495}
16496
16497fn current_external_state_bytes(
16498 root: &Path,
16499 external_states: &[(String, Vec<u8>)],
16500 name: &str,
16501) -> Result<Vec<u8>> {
16502 for (table, state) in external_states.iter().rev() {
16503 if table == name {
16504 return Ok(state.clone());
16505 }
16506 }
16507 read_external_state_file(root, name)
16508}
16509
16510fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
16511 let mut out = external_states;
16512 dedup_external_states_in_place(&mut out);
16513 out
16514}
16515
16516fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
16517 let mut seen = std::collections::HashSet::new();
16518 let mut out = Vec::with_capacity(external_states.len());
16519 for (name, state) in std::mem::take(external_states).into_iter().rev() {
16520 if seen.insert(name.clone()) {
16521 out.push((name, state));
16522 }
16523 }
16524 out.reverse();
16525 *external_states = out;
16526}
16527
16528fn prepare_external_state_file(
16529 root: &Path,
16530 name: &str,
16531 state: &[u8],
16532 txn_id: u64,
16533) -> Result<PathBuf> {
16534 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
16535 let dir = external_state_dir(root, name);
16536 crate::durable_file::create_directory(&dir)?;
16537 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
16538 {
16539 let mut file = std::fs::OpenOptions::new()
16540 .create_new(true)
16541 .write(true)
16542 .open(&pending)?;
16543 file.write_all(state)?;
16544 file.sync_all()?;
16545 }
16546 Ok(pending)
16547}
16548
16549fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
16550 let path = external_state_file(root, name);
16551 crate::durable_file::replace(pending, &path)?;
16552 Ok(())
16553}
16554
16555fn write_external_state_file(
16556 durable: &crate::durable_file::DurableRoot,
16557 name: &str,
16558 state: &[u8],
16559) -> Result<()> {
16560 let directory = Path::new(VTAB_DIR).join(name);
16561 durable.create_directory_all(&directory)?;
16562 durable.write_atomic(directory.join("state.json"), state)?;
16563 Ok(())
16564}
16565
16566fn validate_recovered_data_table(
16567 catalog: &Catalog,
16568 tables: &HashMap<u64, TableHandle>,
16569 table_id: u64,
16570 commit_epoch: u64,
16571 offset: u64,
16572) -> Result<bool> {
16573 let entry = catalog
16574 .tables
16575 .iter()
16576 .find(|entry| entry.table_id == table_id)
16577 .ok_or_else(|| MongrelError::CorruptWal {
16578 offset,
16579 reason: format!("committed record references unknown table {table_id}"),
16580 })?;
16581 if commit_epoch < entry.created_epoch {
16582 return Err(MongrelError::CorruptWal {
16583 offset,
16584 reason: format!(
16585 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16586 entry.created_epoch
16587 ),
16588 });
16589 }
16590 match entry.state {
16591 TableState::Dropped { at_epoch } => {
16592 let abandoned_build_boundary =
16597 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16598 if commit_epoch >= at_epoch && !abandoned_build_boundary {
16599 Err(MongrelError::CorruptWal {
16600 offset,
16601 reason: format!(
16602 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16603 ),
16604 })
16605 } else {
16606 Ok(false)
16607 }
16608 }
16609 TableState::Live | TableState::Building { .. } => {
16610 if tables.contains_key(&table_id) {
16611 Ok(true)
16612 } else {
16613 Err(MongrelError::CorruptWal {
16614 offset,
16615 reason: format!("live table {table_id} has no mounted recovery handle"),
16616 })
16617 }
16618 }
16619 }
16620}
16621
16622type RecoveryTableStage = (
16623 Vec<crate::memtable::Row>,
16624 Vec<(crate::rowid::RowId, Epoch)>,
16625 Option<Epoch>,
16626 Epoch,
16627);
16628
16629#[derive(Clone)]
16630struct RecoveryValidationTable {
16631 schema: Schema,
16632 flushed_epoch: u64,
16633}
16634
16635fn validate_shared_wal_recovery_plan(
16636 durable_root: &crate::durable_file::DurableRoot,
16637 catalog: &Catalog,
16638 recovered_table_ids: &HashSet<u64>,
16639 reconciled_table_ids: &HashSet<u64>,
16640 meta_dek: Option<&[u8; META_DEK_LEN]>,
16641 kek: Option<Arc<crate::encryption::Kek>>,
16642 records: &[crate::wal::Record],
16643) -> Result<()> {
16644 use crate::wal::{DdlOp, Op};
16645
16646 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
16647 for entry in &catalog.tables {
16648 if !matches!(entry.state, TableState::Live) {
16649 continue;
16650 }
16651 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
16652 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
16653 Ok(manifest) => Some(manifest),
16654 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
16655 Err(error) => return Err(error),
16656 };
16657 let flushed_epoch = if let Some(manifest) = manifest {
16658 if manifest.table_id != entry.table_id {
16659 return Err(MongrelError::Conflict(format!(
16660 "catalog table {} storage identity mismatch",
16661 entry.table_id
16662 )));
16663 }
16664 if (manifest.schema_id != entry.schema.schema_id
16665 && !reconciled_table_ids.contains(&entry.table_id))
16666 || manifest.flushed_epoch > manifest.current_epoch
16667 || manifest.global_idx_epoch > manifest.current_epoch
16668 || manifest.next_row_id == u64::MAX
16669 || manifest.auto_inc_next < 0
16670 || manifest.auto_inc_next == i64::MAX
16671 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
16672 {
16673 return Err(MongrelError::InvalidArgument(format!(
16674 "table {} manifest counters or schema identity are invalid",
16675 entry.table_id
16676 )));
16677 }
16678 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
16679 crate::global_idx::read_durable_for(
16680 durable_root,
16681 &relative_dir,
16682 entry.table_id,
16683 &entry.schema,
16684 idx_dek.as_deref(),
16685 )?;
16686 let mut run_ids = HashSet::new();
16687 let mut maximum_row_id = None::<u64>;
16688 for run in &manifest.runs {
16689 if run.run_id >= u64::MAX as u128
16690 || run.epoch_created > manifest.current_epoch
16691 || !run_ids.insert(run.run_id)
16692 {
16693 return Err(MongrelError::InvalidArgument(format!(
16694 "table {} manifest contains an invalid or duplicate run id",
16695 entry.table_id
16696 )));
16697 }
16698 let relative = relative_dir
16699 .join(crate::engine::RUNS_DIR)
16700 .join(format!("r-{}.sr", run.run_id as u64));
16701 let file = durable_root.open_regular(&relative)?;
16702 let mut reader = crate::sorted_run::RunReader::open_file(
16703 file,
16704 entry.schema.clone(),
16705 kek.clone(),
16706 )?;
16707 let header = reader.header();
16708 if header.run_id != run.run_id
16709 || header.level != run.level
16710 || header.row_count != run.row_count
16711 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
16712 || header.is_uniform_epoch() && header.epoch_created != 0
16713 || header.schema_id > entry.schema.schema_id
16714 {
16715 return Err(MongrelError::InvalidArgument(format!(
16716 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
16717 entry.table_id,
16718 run.run_id,
16719 header.run_id,
16720 header.level,
16721 header.row_count,
16722 header.epoch_created,
16723 header.schema_id,
16724 run.run_id,
16725 run.level,
16726 run.row_count,
16727 run.epoch_created,
16728 entry.schema.schema_id,
16729 )));
16730 }
16731 if header.row_count != 0 {
16732 maximum_row_id = Some(
16733 maximum_row_id
16734 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
16735 );
16736 }
16737 reader.validate_all_pages()?;
16738 }
16739 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
16740 return Err(MongrelError::InvalidArgument(format!(
16741 "table {} next_row_id does not advance beyond persisted rows",
16742 entry.table_id
16743 )));
16744 }
16745 for run in &manifest.retiring {
16746 if run.run_id >= u64::MAX as u128
16747 || run.retire_epoch > manifest.current_epoch
16748 || !run_ids.insert(run.run_id)
16749 {
16750 return Err(MongrelError::InvalidArgument(format!(
16751 "table {} manifest contains an invalid or aliased retired run",
16752 entry.table_id
16753 )));
16754 }
16755 }
16756 manifest.flushed_epoch
16757 } else {
16758 if !recovered_table_ids.contains(&entry.table_id) {
16759 return Err(MongrelError::NotFound(format!(
16760 "live table {} manifest is missing",
16761 entry.table_id
16762 )));
16763 }
16764 0
16765 };
16766 tables.insert(
16767 entry.table_id,
16768 RecoveryValidationTable {
16769 schema: entry.schema.clone(),
16770 flushed_epoch,
16771 },
16772 );
16773 }
16774
16775 let committed = records
16776 .iter()
16777 .filter_map(|record| match record.op {
16778 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
16779 _ => None,
16780 })
16781 .collect::<HashMap<_, _>>();
16782 let mut run_ids = HashSet::new();
16783 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
16784 for record in records {
16785 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
16786 continue;
16787 };
16788 match &record.op {
16789 Op::Put { table_id, rows } => {
16790 let table = validate_recovery_data_table_plan(
16791 catalog,
16792 &tables,
16793 *table_id,
16794 commit_epoch,
16795 record.seq.0,
16796 )?;
16797 let decoded: Vec<crate::memtable::Row> =
16798 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
16799 offset: record.seq.0,
16800 reason: format!(
16801 "committed Put payload for transaction {} could not be decoded: {error}",
16802 record.txn_id
16803 ),
16804 })?;
16805 if let Some(table) = table {
16806 for row in &decoded {
16807 if !recovered_row_ids
16808 .entry(*table_id)
16809 .or_default()
16810 .insert(row.row_id.0)
16811 {
16812 return Err(MongrelError::CorruptWal {
16813 offset: record.seq.0,
16814 reason: format!(
16815 "committed WAL repeats recovered row id {} for table {table_id}",
16816 row.row_id.0
16817 ),
16818 });
16819 }
16820 validate_recovered_row(&table.schema, row)?;
16821 }
16822 }
16823 }
16824 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
16825 validate_recovery_data_table_plan(
16826 catalog,
16827 &tables,
16828 *table_id,
16829 commit_epoch,
16830 record.seq.0,
16831 )?;
16832 }
16833 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
16834 Op::Ddl(DdlOp::ResetExternalTableState {
16835 name,
16836 generation_epoch,
16837 }) => {
16838 if *generation_epoch != commit_epoch {
16839 return Err(MongrelError::CorruptWal {
16840 offset: record.seq.0,
16841 reason: format!(
16842 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
16843 ),
16844 });
16845 }
16846 validate_recovered_external_name(name)?;
16847 }
16848 Op::TxnCommit { added_runs, .. } => {
16849 for added in added_runs {
16850 let Some(table) = validate_recovery_data_table_plan(
16851 catalog,
16852 &tables,
16853 added.table_id,
16854 commit_epoch,
16855 record.seq.0,
16856 )?
16857 else {
16858 continue;
16859 };
16860 if added.run_id >= u64::MAX as u128
16861 || !run_ids.insert((added.table_id, added.run_id))
16862 {
16863 return Err(MongrelError::CorruptWal {
16864 offset: record.seq.0,
16865 reason: format!(
16866 "duplicate or invalid recovered run {} for table {}",
16867 added.run_id, added.table_id
16868 ),
16869 });
16870 }
16871 if commit_epoch <= table.flushed_epoch {
16872 continue;
16873 }
16874 validate_planned_spilled_run(
16875 durable_root,
16876 record.txn_id,
16877 commit_epoch,
16878 added,
16879 &table.schema,
16880 kek.clone(),
16881 )?;
16882 }
16883 }
16884 _ => {}
16885 }
16886 }
16887 Ok(())
16888}
16889
16890fn validate_recovery_data_table_plan<'a>(
16891 catalog: &Catalog,
16892 tables: &'a HashMap<u64, RecoveryValidationTable>,
16893 table_id: u64,
16894 commit_epoch: u64,
16895 offset: u64,
16896) -> Result<Option<&'a RecoveryValidationTable>> {
16897 let entry = catalog
16898 .tables
16899 .iter()
16900 .find(|entry| entry.table_id == table_id)
16901 .ok_or_else(|| MongrelError::CorruptWal {
16902 offset,
16903 reason: format!("committed record references unknown table {table_id}"),
16904 })?;
16905 if commit_epoch < entry.created_epoch {
16906 return Err(MongrelError::CorruptWal {
16907 offset,
16908 reason: format!(
16909 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
16910 entry.created_epoch
16911 ),
16912 });
16913 }
16914 match entry.state {
16915 TableState::Dropped { at_epoch } => {
16916 let abandoned =
16917 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
16918 if commit_epoch >= at_epoch && !abandoned {
16919 return Err(MongrelError::CorruptWal {
16920 offset,
16921 reason: format!(
16922 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
16923 ),
16924 });
16925 }
16926 Ok(None)
16927 }
16928 TableState::Live => {
16929 tables
16930 .get(&table_id)
16931 .map(Some)
16932 .ok_or_else(|| MongrelError::CorruptWal {
16933 offset,
16934 reason: format!("live table {table_id} has no recovery plan"),
16935 })
16936 }
16937 TableState::Building { .. } => Err(MongrelError::CorruptWal {
16938 offset,
16939 reason: format!("building table {table_id} was not normalized before recovery"),
16940 }),
16941 }
16942}
16943
16944fn validate_planned_spilled_run(
16945 root: &crate::durable_file::DurableRoot,
16946 txn_id: u64,
16947 commit_epoch: u64,
16948 added: &crate::wal::AddedRun,
16949 schema: &Schema,
16950 kek: Option<Arc<crate::encryption::Kek>>,
16951) -> Result<()> {
16952 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
16953 let destination = table
16954 .join(crate::engine::RUNS_DIR)
16955 .join(format!("r-{}.sr", added.run_id as u64));
16956 let pending = table
16957 .join("_txn")
16958 .join(txn_id.to_string())
16959 .join(format!("r-{}.sr", added.run_id as u64));
16960 let file = match root.open_regular(&destination) {
16961 Ok(file) => file,
16962 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
16963 root.open_regular(&pending).map_err(|pending_error| {
16964 if pending_error.kind() == std::io::ErrorKind::NotFound {
16965 MongrelError::CorruptWal {
16966 offset: commit_epoch,
16967 reason: format!(
16968 "committed spilled run {} for transaction {txn_id} is missing",
16969 added.run_id
16970 ),
16971 }
16972 } else {
16973 pending_error.into()
16974 }
16975 })?
16976 }
16977 Err(error) => return Err(error.into()),
16978 };
16979 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
16980 let header = reader.header();
16981 if header.run_id != added.run_id
16982 || header.content_hash != added.content_hash
16983 || header.row_count != added.row_count
16984 || header.level != added.level
16985 || header.min_row_id != added.min_row_id
16986 || header.max_row_id != added.max_row_id
16987 || header.schema_id != schema.schema_id
16988 || !header.is_uniform_epoch()
16989 || header.epoch_created != 0
16990 {
16991 return Err(MongrelError::CorruptWal {
16992 offset: commit_epoch,
16993 reason: format!(
16994 "committed spilled run {} metadata differs from WAL",
16995 added.run_id
16996 ),
16997 });
16998 }
16999 reader.validate_all_pages()?;
17000 Ok(())
17001}
17002
17003#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17023pub enum StagedTxnWrite {
17024 Put {
17029 table_id: u64,
17031 rows: Vec<u8>,
17033 },
17034 Delete {
17036 table_id: u64,
17038 row_ids: Vec<u64>,
17040 },
17041}
17042
17043impl StagedTxnWrite {
17044 pub fn encode(&self) -> Result<Vec<u8>> {
17046 Ok(bincode::serialize(self)?)
17047 }
17048
17049 pub fn decode(bytes: &[u8]) -> Result<Self> {
17051 Ok(bincode::deserialize(bytes)?)
17052 }
17053}
17054
17055pub fn translate_records_for_replication(
17092 records: &[crate::wal::Record],
17093) -> Result<Vec<crate::wal::Record>> {
17094 use crate::wal::Op;
17095
17096 let txn_id = records.first().map(|record| record.txn_id).ok_or_else(|| {
17099 MongrelError::InvalidArgument("replicated transaction payload is empty".into())
17100 })?;
17101 if records.iter().any(|record| record.txn_id != txn_id) {
17102 return Err(MongrelError::InvalidArgument(
17103 "replicated transaction payload mixes transaction ids".into(),
17104 ));
17105 }
17106 let commits = records
17107 .iter()
17108 .filter(|record| matches!(record.op, Op::TxnCommit { .. }))
17109 .count();
17110 if commits != 1 || !matches!(records.last().map(|r| &r.op), Some(Op::TxnCommit { .. })) {
17111 return Err(MongrelError::InvalidArgument(
17112 "replicated transaction payload must end in exactly one commit marker".into(),
17113 ));
17114 }
17115
17116 let mut spilled_rows: HashMap<u64, Vec<crate::memtable::Row>> = HashMap::new();
17120 for record in records {
17121 if let Op::SpilledRows { table_id, rows } = &record.op {
17122 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(rows).map_err(|error| {
17123 MongrelError::InvalidArgument(format!(
17124 "spilled row payload for table {table_id} cannot decode for replication: \
17125 {error}"
17126 ))
17127 })?;
17128 spilled_rows.entry(*table_id).or_default().extend(chunk);
17129 }
17130 }
17131
17132 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
17136 unreachable!("one trailing commit marker validated above");
17137 };
17138 for run in added_runs {
17139 let rows = spilled_rows.get(&run.table_id).ok_or_else(|| {
17140 MongrelError::InvalidArgument(format!(
17141 "commit links spilled run {} for table {} but carries no logical row records \
17142 for it",
17143 run.run_id, run.table_id
17144 ))
17145 })?;
17146 let covered = rows
17147 .iter()
17148 .filter(|row| row.row_id.0 >= run.min_row_id && row.row_id.0 <= run.max_row_id)
17149 .count() as u64;
17150 if covered != run.row_count {
17151 return Err(MongrelError::InvalidArgument(format!(
17152 "commit links spilled run {} for table {} ({} rows in [{}, {}]) but the logical \
17153 row records cover {} rows",
17154 run.run_id, run.table_id, run.row_count, run.min_row_id, run.max_row_id, covered
17155 )));
17156 }
17157 }
17158
17159 let translated = records
17162 .iter()
17163 .map(|record| {
17164 let op = match &record.op {
17165 Op::SpilledRows { table_id, rows } => Op::Put {
17166 table_id: *table_id,
17167 rows: rows.clone(),
17168 },
17169 Op::TxnCommit { epoch, .. } => Op::TxnCommit {
17170 epoch: *epoch,
17171 added_runs: Vec::new(),
17172 },
17173 op => op.clone(),
17174 };
17175 crate::wal::Record::new(record.seq, record.txn_id, op)
17176 })
17177 .collect();
17178 Ok(translated)
17179}
17180
17181fn recover_shared_wal(
17182 durable_root: &crate::durable_file::DurableRoot,
17183 tables: &HashMap<u64, TableHandle>,
17184 catalog: &Catalog,
17185 epoch: &EpochAuthority,
17186 records: &[crate::wal::Record],
17187) -> Result<()> {
17188 use crate::memtable::Row;
17189 use crate::wal::{DdlOp, Op};
17190
17191 let mut committed: HashMap<u64, u64> = HashMap::new();
17193 let mut commit_ts_by_txn: HashMap<u64, mongreldb_types::hlc::HlcTimestamp> = HashMap::new();
17195 let mut spilled_to_link: Vec<(
17196 u64, u64, Vec<crate::wal::AddedRun>,
17199 )> = Vec::new();
17200 for r in records {
17201 match &r.op {
17202 Op::CommitTimestamp { unix_nanos } => {
17203 commit_ts_by_txn.insert(
17204 r.txn_id,
17205 mongreldb_types::hlc::HlcTimestamp {
17206 physical_micros: unix_nanos / 1_000,
17207 logical: 0,
17208 node_tiebreaker: 0,
17209 },
17210 );
17211 }
17212 Op::TxnCommit {
17213 epoch: ce,
17214 ref added_runs,
17215 } => {
17216 committed.insert(r.txn_id, *ce);
17217 if !added_runs.is_empty() {
17218 spilled_to_link.push((r.txn_id, *ce, added_runs.clone()));
17219 }
17220 }
17221 _ => {}
17222 }
17223 }
17224 for record in records {
17225 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
17226 continue;
17227 };
17228 match &record.op {
17229 Op::Put { table_id, .. }
17230 | Op::Delete { table_id, .. }
17231 | Op::TruncateTable { table_id } => {
17232 validate_recovered_data_table(
17233 catalog,
17234 tables,
17235 *table_id,
17236 commit_epoch,
17237 record.seq.0,
17238 )?;
17239 }
17240 Op::TxnCommit { added_runs, .. } => {
17241 for run in added_runs {
17242 validate_recovered_data_table(
17243 catalog,
17244 tables,
17245 run.table_id,
17246 commit_epoch,
17247 record.seq.0,
17248 )?;
17249 }
17250 }
17251 _ => {}
17252 }
17253 }
17254 let truncated_transactions: HashSet<(u64, u64)> = records
17255 .iter()
17256 .filter_map(|record| {
17257 committed.get(&record.txn_id)?;
17258 match record.op {
17259 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
17260 _ => None,
17261 }
17262 })
17263 .collect();
17264
17265 enum ExternalRecoveryAction {
17267 Write { name: String, state: Vec<u8> },
17268 Reset { name: String },
17269 }
17270 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
17271 let mut external_actions = Vec::new();
17272 let mut max_epoch = epoch.visible().0;
17273 for r in records.iter().cloned() {
17274 let Some(&ce) = committed.get(&r.txn_id) else {
17275 continue; };
17277 let commit_epoch = Epoch(ce);
17278 max_epoch = max_epoch.max(ce);
17279 match r.op {
17280 Op::Put { table_id, rows } => {
17281 let skip = tables
17283 .get(&table_id)
17284 .map(|h| h.lock().flushed_epoch() >= ce)
17285 .unwrap_or(true);
17286 if skip {
17287 continue;
17288 }
17289 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
17290 MongrelError::CorruptWal {
17291 offset: r.seq.0,
17292 reason: format!(
17293 "committed Put payload for transaction {} could not be decoded: {error}",
17294 r.txn_id
17295 ),
17296 }
17297 })?;
17298 let txn_commit_ts = commit_ts_by_txn.get(&r.txn_id).copied();
17303 let rows: Vec<Row> = rows
17304 .into_iter()
17305 .map(|mut row| {
17306 row.committed_epoch = commit_epoch;
17307 if row.commit_ts.is_none() {
17308 row.commit_ts = txn_commit_ts;
17309 }
17310 row
17311 })
17312 .collect();
17313 let entry = stage
17314 .entry(table_id)
17315 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17316 entry.0.extend(rows);
17317 entry.3 = commit_epoch;
17318 }
17319 Op::Delete { table_id, row_ids } => {
17320 let skip = tables
17321 .get(&table_id)
17322 .map(|h| h.lock().flushed_epoch() >= ce)
17323 .unwrap_or(true);
17324 if skip {
17325 continue;
17326 }
17327 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
17328 let entry = stage
17329 .entry(table_id)
17330 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
17331 entry.1.extend(dels);
17332 entry.3 = commit_epoch;
17333 }
17334 Op::TruncateTable { table_id } => {
17335 let skip = tables
17336 .get(&table_id)
17337 .map(|h| h.lock().flushed_epoch() >= ce)
17338 .unwrap_or(true);
17339 if skip {
17340 continue;
17341 }
17342 stage.insert(
17343 table_id,
17344 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
17345 );
17346 }
17347 Op::ExternalTableState { name, state } => {
17348 let current_generation = catalog
17349 .external_tables
17350 .iter()
17351 .find(|entry| entry.name == name)
17352 .map(|entry| entry.created_epoch);
17353 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
17354 validate_recovered_external_name(&name)?;
17355 external_actions.push(ExternalRecoveryAction::Write { name, state });
17356 }
17357 }
17358 Op::Ddl(DdlOp::ResetExternalTableState {
17359 name,
17360 generation_epoch,
17361 }) => {
17362 if generation_epoch != ce {
17363 return Err(MongrelError::CorruptWal {
17364 offset: r.seq.0,
17365 reason: format!(
17366 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
17367 ),
17368 });
17369 }
17370 validate_recovered_external_name(&name)?;
17371 external_actions.push(ExternalRecoveryAction::Reset { name });
17372 }
17373 Op::Flush { .. }
17374 | Op::TxnCommit { .. }
17375 | Op::TxnAbort
17376 | Op::Ddl(_)
17377 | Op::BeforeImage { .. }
17378 | Op::CommitTimestamp { .. }
17379 | Op::SpilledRows { .. } => {}
17380 }
17381 }
17382 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
17383 added_runs.retain(|added| {
17384 tables
17385 .get(&added.table_id)
17386 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
17387 });
17388 }
17389 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
17390 validate_recovery_table_stages(tables, &stage)?;
17391 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
17392
17393 for action in external_actions {
17397 match action {
17398 ExternalRecoveryAction::Write { name, state } => {
17399 write_external_state_file(durable_root, &name, &state)?;
17400 }
17401 ExternalRecoveryAction::Reset { name } => {
17402 durable_root.create_directory_all(VTAB_DIR)?;
17403 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
17404 }
17405 }
17406 }
17407 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
17408 let Some(handle) = tables.get(&table_id) else {
17409 continue;
17410 };
17411 let mut t = handle.lock();
17412 if let Some(epoch) = truncate_epoch {
17413 t.apply_truncate(epoch);
17414 }
17415 t.recover_apply(rows, deletes)?;
17416 let rows = t.visible_rows(Snapshot::unbounded())?;
17420 t.live_count = rows.len() as u64;
17421 t.persist_manifest(table_epoch.max(epoch.visible()))?;
17425 }
17426
17427 for (txn_id, ce, added_runs) in &spilled_to_link {
17431 for ar in added_runs {
17432 let Some(handle) = tables.get(&ar.table_id) else {
17433 continue;
17434 };
17435 let mut t = handle.lock();
17436 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
17437 let destination = table_dir
17438 .join(crate::engine::RUNS_DIR)
17439 .join(format!("r-{}.sr", ar.run_id));
17440 match durable_root.open_regular(&destination) {
17441 Ok(_) => {}
17442 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
17443 let pending = table_dir
17444 .join("_txn")
17445 .join(txn_id.to_string())
17446 .join(format!("r-{}.sr", ar.run_id));
17447 durable_root.rename_file_new(&pending, &destination)?;
17448 }
17449 Err(error) => return Err(error.into()),
17450 }
17451 let linked = t.recover_spilled_run(crate::manifest::RunRef {
17457 run_id: ar.run_id,
17458 level: ar.level,
17459 epoch_created: *ce,
17460 row_count: ar.row_count,
17461 });
17462 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
17463 if replaced {
17464 t.set_flushed_epoch(Epoch(*ce));
17465 }
17466 if linked || replaced {
17467 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
17468 }
17469 }
17470 }
17471
17472 epoch.advance_recovered(Epoch(max_epoch));
17473 Ok(())
17474}
17475
17476fn reconcile_recovered_table_metadata(
17477 tables: &HashMap<u64, TableHandle>,
17478 epoch: Epoch,
17479) -> Result<()> {
17480 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
17481 table_ids.sort_unstable();
17482 let mut plans = Vec::with_capacity(table_ids.len());
17483 for table_id in &table_ids {
17484 let handle = tables.get(table_id).ok_or_else(|| {
17485 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17486 })?;
17487 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
17488 }
17489 for (table_id, plan) in plans {
17492 let handle = tables.get(&table_id).ok_or_else(|| {
17493 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
17494 })?;
17495 handle.lock().apply_recovered_metadata(plan, epoch)?;
17496 }
17497 Ok(())
17498}
17499
17500fn validate_recovered_external_name(name: &str) -> Result<()> {
17501 if name.is_empty()
17502 || !name.chars().all(|character| {
17503 character.is_ascii_alphanumeric() || character == '_' || character == '-'
17504 })
17505 {
17506 return Err(MongrelError::CorruptWal {
17507 offset: 0,
17508 reason: format!("unsafe recovered external-table name {name:?}"),
17509 });
17510 }
17511 Ok(())
17512}
17513
17514fn validate_recovery_table_stages(
17515 tables: &HashMap<u64, TableHandle>,
17516 stages: &HashMap<u64, RecoveryTableStage>,
17517) -> Result<()> {
17518 for (table_id, (rows, _, _, _)) in stages {
17519 let handle = tables
17520 .get(table_id)
17521 .ok_or_else(|| MongrelError::CorruptWal {
17522 offset: *table_id,
17523 reason: format!("recovery stage references unmounted table {table_id}"),
17524 })?;
17525 let table = handle.lock();
17526 table.visible_rows(Snapshot::unbounded())?;
17529 for row in rows {
17530 validate_recovered_row(table.schema(), row)?;
17531 }
17532 }
17533 Ok(())
17534}
17535
17536fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
17537 if row.deleted || row.row_id.0 == u64::MAX {
17538 return Err(MongrelError::CorruptWal {
17539 offset: row.row_id.0,
17540 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
17541 });
17542 }
17543 let cells = row
17544 .columns
17545 .iter()
17546 .map(|(column, value)| (*column, value.clone()))
17547 .collect::<Vec<_>>();
17548 schema
17549 .validate_persisted_values(&cells)
17550 .map_err(|error| MongrelError::CorruptWal {
17551 offset: row.row_id.0,
17552 reason: format!("recovered row violates table schema: {error}"),
17553 })?;
17554 if schema.auto_increment_column().is_some_and(|column| {
17555 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
17556 }) {
17557 return Err(MongrelError::CorruptWal {
17558 offset: row.row_id.0,
17559 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
17560 });
17561 }
17562 Ok(())
17563}
17564
17565fn validate_recovery_spilled_runs(
17566 root: &crate::durable_file::DurableRoot,
17567 tables: &HashMap<u64, TableHandle>,
17568 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
17569) -> Result<()> {
17570 let mut identities = HashSet::new();
17571 for (txn_id, commit_epoch, added_runs) in spilled {
17572 for added in added_runs {
17573 if added.run_id >= u64::MAX as u128 {
17574 return Err(MongrelError::CorruptWal {
17575 offset: *commit_epoch,
17576 reason: format!(
17577 "recovered run id {} exceeds the on-disk namespace",
17578 added.run_id
17579 ),
17580 });
17581 }
17582 let Some(handle) = tables.get(&added.table_id) else {
17583 continue;
17584 };
17585 if !identities.insert((added.table_id, added.run_id)) {
17586 return Err(MongrelError::CorruptWal {
17587 offset: *commit_epoch,
17588 reason: format!(
17589 "duplicate recovered run {} for table {}",
17590 added.run_id, added.table_id
17591 ),
17592 });
17593 }
17594 let table = handle.lock();
17595 validate_planned_spilled_run(
17596 root,
17597 *txn_id,
17598 *commit_epoch,
17599 added,
17600 table.schema(),
17601 table.kek(),
17602 )?;
17603 }
17604 }
17605 Ok(())
17606}
17607
17608fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
17609 match condition {
17610 ProcedureCondition::Pk { .. } => {
17611 if schema.primary_key().is_none() {
17612 return Err(MongrelError::InvalidArgument(
17613 "procedure condition Pk references a table without a primary key".into(),
17614 ));
17615 }
17616 }
17617 ProcedureCondition::BitmapEq { column_id, .. }
17618 | ProcedureCondition::BitmapIn { column_id, .. }
17619 | ProcedureCondition::Range { column_id, .. }
17620 | ProcedureCondition::RangeF64 { column_id, .. }
17621 | ProcedureCondition::IsNull { column_id }
17622 | ProcedureCondition::IsNotNull { column_id }
17623 | ProcedureCondition::FmContains { column_id, .. } => {
17624 validate_column_id(*column_id, schema)?;
17625 }
17626 }
17627 Ok(())
17628}
17629
17630fn bind_procedure_args(
17631 procedure: &StoredProcedure,
17632 mut args: HashMap<String, crate::Value>,
17633) -> Result<HashMap<String, crate::Value>> {
17634 let mut out = HashMap::new();
17635 for param in &procedure.params {
17636 let value = match args.remove(¶m.name) {
17637 Some(value) => value,
17638 None => param.default.clone().ok_or_else(|| {
17639 MongrelError::InvalidArgument(format!(
17640 "missing required procedure parameter {:?}",
17641 param.name
17642 ))
17643 })?,
17644 };
17645 if !param.nullable && matches!(value, crate::Value::Null) {
17646 return Err(MongrelError::InvalidArgument(format!(
17647 "procedure parameter {:?} must not be NULL",
17648 param.name
17649 )));
17650 }
17651 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
17652 return Err(MongrelError::InvalidArgument(format!(
17653 "procedure parameter {:?} has wrong type",
17654 param.name
17655 )));
17656 }
17657 out.insert(param.name.clone(), value);
17658 }
17659 if let Some(extra) = args.keys().next() {
17660 return Err(MongrelError::InvalidArgument(format!(
17661 "unknown procedure parameter {extra:?}"
17662 )));
17663 }
17664 Ok(out)
17665}
17666
17667fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
17668 matches!(
17669 (value, ty),
17670 (crate::Value::Bool(_), crate::TypeId::Bool)
17671 | (crate::Value::Int64(_), crate::TypeId::Int8)
17672 | (crate::Value::Int64(_), crate::TypeId::Int16)
17673 | (crate::Value::Int64(_), crate::TypeId::Int32)
17674 | (crate::Value::Int64(_), crate::TypeId::Int64)
17675 | (crate::Value::Int64(_), crate::TypeId::UInt8)
17676 | (crate::Value::Int64(_), crate::TypeId::UInt16)
17677 | (crate::Value::Int64(_), crate::TypeId::UInt32)
17678 | (crate::Value::Int64(_), crate::TypeId::UInt64)
17679 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
17680 | (crate::Value::Int64(_), crate::TypeId::Date32)
17681 | (crate::Value::Float64(_), crate::TypeId::Float32)
17682 | (crate::Value::Float64(_), crate::TypeId::Float64)
17683 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
17684 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
17685 | (
17686 crate::Value::GeneratedEmbedding(_),
17687 crate::TypeId::Embedding { .. }
17688 )
17689 )
17690}
17691
17692fn eval_cells(
17693 cells: &[crate::procedure::ProcedureCell],
17694 args: &HashMap<String, crate::Value>,
17695 outputs: &HashMap<String, ProcedureCallOutput>,
17696) -> Result<Vec<(u16, crate::Value)>> {
17697 cells
17698 .iter()
17699 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
17700 .collect()
17701}
17702
17703fn eval_condition(
17704 condition: &ProcedureCondition,
17705 args: &HashMap<String, crate::Value>,
17706 outputs: &HashMap<String, ProcedureCallOutput>,
17707) -> Result<crate::Condition> {
17708 Ok(match condition {
17709 ProcedureCondition::Pk { value } => {
17710 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
17711 }
17712 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
17713 column_id: *column_id,
17714 value: eval_value(value, args, outputs)?.encode_key(),
17715 },
17716 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
17717 column_id: *column_id,
17718 values: values
17719 .iter()
17720 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
17721 .collect::<Result<Vec<_>>>()?,
17722 },
17723 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
17724 column_id: *column_id,
17725 lo: expect_i64(eval_value(lo, args, outputs)?)?,
17726 hi: expect_i64(eval_value(hi, args, outputs)?)?,
17727 },
17728 ProcedureCondition::RangeF64 {
17729 column_id,
17730 lo,
17731 lo_inclusive,
17732 hi,
17733 hi_inclusive,
17734 } => crate::Condition::RangeF64 {
17735 column_id: *column_id,
17736 lo: expect_f64(eval_value(lo, args, outputs)?)?,
17737 lo_inclusive: *lo_inclusive,
17738 hi: expect_f64(eval_value(hi, args, outputs)?)?,
17739 hi_inclusive: *hi_inclusive,
17740 },
17741 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
17742 column_id: *column_id,
17743 },
17744 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
17745 column_id: *column_id,
17746 },
17747 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
17748 column_id: *column_id,
17749 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
17750 },
17751 })
17752}
17753
17754fn eval_value(
17755 value: &ProcedureValue,
17756 args: &HashMap<String, crate::Value>,
17757 outputs: &HashMap<String, ProcedureCallOutput>,
17758) -> Result<crate::Value> {
17759 match value {
17760 ProcedureValue::Literal(value) => Ok(value.clone()),
17761 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
17762 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17763 }),
17764 ProcedureValue::StepScalar(id) => match outputs.get(id) {
17765 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
17766 _ => Err(MongrelError::InvalidArgument(format!(
17767 "procedure step {id:?} did not return a scalar"
17768 ))),
17769 },
17770 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
17771 Err(MongrelError::InvalidArgument(
17772 "row-valued procedure reference cannot be used as a scalar".into(),
17773 ))
17774 }
17775 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
17776 "structured procedure value cannot be used as a scalar cell".into(),
17777 )),
17778 }
17779}
17780
17781fn eval_return_output(
17782 value: &ProcedureValue,
17783 args: &HashMap<String, crate::Value>,
17784 outputs: &HashMap<String, ProcedureCallOutput>,
17785) -> Result<ProcedureCallOutput> {
17786 match value {
17787 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
17788 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
17789 args.get(name).cloned().ok_or_else(|| {
17790 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
17791 })?,
17792 )),
17793 ProcedureValue::StepRows(id)
17794 | ProcedureValue::StepRow(id)
17795 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
17796 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
17797 }),
17798 ProcedureValue::Object(fields) => {
17799 let mut out = Vec::with_capacity(fields.len());
17800 for (name, value) in fields {
17801 out.push((name.clone(), eval_return_output(value, args, outputs)?));
17802 }
17803 Ok(ProcedureCallOutput::Object(out))
17804 }
17805 ProcedureValue::Array(values) => {
17806 let mut out = Vec::with_capacity(values.len());
17807 for value in values {
17808 out.push(eval_return_output(value, args, outputs)?);
17809 }
17810 Ok(ProcedureCallOutput::Array(out))
17811 }
17812 }
17813}
17814
17815fn expect_i64(value: crate::Value) -> Result<i64> {
17816 match value {
17817 crate::Value::Int64(value) => Ok(value),
17818 _ => Err(MongrelError::InvalidArgument(
17819 "procedure value must be Int64".into(),
17820 )),
17821 }
17822}
17823
17824fn expect_f64(value: crate::Value) -> Result<f64> {
17825 match value {
17826 crate::Value::Float64(value) => Ok(value),
17827 _ => Err(MongrelError::InvalidArgument(
17828 "procedure value must be Float64".into(),
17829 )),
17830 }
17831}
17832
17833fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
17834 match value {
17835 crate::Value::Bytes(value) => Ok(value),
17836 _ => Err(MongrelError::InvalidArgument(
17837 "procedure value must be Bytes".into(),
17838 )),
17839 }
17840}
17841
17842fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
17843 if schema.columns.iter().any(|c| c.id == column_id) {
17844 Ok(())
17845 } else {
17846 Err(MongrelError::InvalidArgument(format!(
17847 "unknown column id {column_id}"
17848 )))
17849 }
17850}
17851
17852fn trigger_matches_event(
17853 trigger: &StoredTrigger,
17854 event: &WriteEvent,
17855 cat: &Catalog,
17856) -> Result<bool> {
17857 if trigger.event != event.kind {
17858 return Ok(false);
17859 }
17860 let TriggerTarget::Table(target) = &trigger.target else {
17861 return Ok(false);
17862 };
17863 if target != &event.table {
17864 return Ok(false);
17865 }
17866 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
17867 let schema = &cat
17868 .live(target)
17869 .ok_or_else(|| {
17870 MongrelError::InvalidArgument(format!(
17871 "trigger {:?} references unknown table {target:?}",
17872 trigger.name
17873 ))
17874 })?
17875 .schema;
17876 let mut watched = Vec::with_capacity(trigger.update_of.len());
17877 for name in &trigger.update_of {
17878 let col = schema.column(name).ok_or_else(|| {
17879 MongrelError::InvalidArgument(format!(
17880 "trigger {:?} references unknown UPDATE OF column {name:?}",
17881 trigger.name
17882 ))
17883 })?;
17884 watched.push(col.id);
17885 }
17886 if !event
17887 .changed_columns
17888 .iter()
17889 .any(|column_id| watched.contains(column_id))
17890 {
17891 return Ok(false);
17892 }
17893 }
17894 Ok(true)
17895}
17896
17897fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
17898 let mut ids = std::collections::BTreeSet::new();
17899 if let Some(old) = old {
17900 ids.extend(old.columns.keys().copied());
17901 }
17902 if let Some(new) = new {
17903 ids.extend(new.columns.keys().copied());
17904 }
17905 ids.into_iter()
17906 .filter(|id| {
17907 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
17908 })
17909 .collect()
17910}
17911
17912fn eval_trigger_cells(
17913 cells: &[crate::trigger::TriggerCell],
17914 event: &WriteEvent,
17915 selected: Option<&TriggerRowImage>,
17916) -> Result<Vec<(u16, Value)>> {
17917 cells
17918 .iter()
17919 .map(|cell| {
17920 Ok((
17921 cell.column_id,
17922 eval_trigger_value(&cell.value, event, selected)?,
17923 ))
17924 })
17925 .collect()
17926}
17927
17928fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
17929 match expr {
17930 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
17931 Value::Bool(value) => Ok(value),
17932 Value::Null => Ok(false),
17933 other => Err(MongrelError::InvalidArgument(format!(
17934 "trigger WHEN value must be boolean, got {other:?}"
17935 ))),
17936 },
17937 TriggerExpr::Eq { left, right } => Ok(values_equal(
17938 &eval_trigger_value(left, event, None)?,
17939 &eval_trigger_value(right, event, None)?,
17940 )),
17941 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
17942 &eval_trigger_value(left, event, None)?,
17943 &eval_trigger_value(right, event, None)?,
17944 )),
17945 TriggerExpr::Lt { left, right } => match value_order(
17946 &eval_trigger_value(left, event, None)?,
17947 &eval_trigger_value(right, event, None)?,
17948 ) {
17949 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
17950 None => Ok(false),
17951 },
17952 TriggerExpr::Lte { left, right } => match value_order(
17953 &eval_trigger_value(left, event, None)?,
17954 &eval_trigger_value(right, event, None)?,
17955 ) {
17956 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
17957 None => Ok(false),
17958 },
17959 TriggerExpr::Gt { left, right } => match value_order(
17960 &eval_trigger_value(left, event, None)?,
17961 &eval_trigger_value(right, event, None)?,
17962 ) {
17963 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
17964 None => Ok(false),
17965 },
17966 TriggerExpr::Gte { left, right } => match value_order(
17967 &eval_trigger_value(left, event, None)?,
17968 &eval_trigger_value(right, event, None)?,
17969 ) {
17970 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
17971 None => Ok(false),
17972 },
17973 TriggerExpr::IsNull(value) => Ok(matches!(
17974 eval_trigger_value(value, event, None)?,
17975 Value::Null
17976 )),
17977 TriggerExpr::IsNotNull(value) => Ok(!matches!(
17978 eval_trigger_value(value, event, None)?,
17979 Value::Null
17980 )),
17981 TriggerExpr::And { left, right } => {
17982 if !eval_trigger_expr(left, event)? {
17983 Ok(false)
17984 } else {
17985 Ok(eval_trigger_expr(right, event)?)
17986 }
17987 }
17988 TriggerExpr::Or { left, right } => {
17989 if eval_trigger_expr(left, event)? {
17990 Ok(true)
17991 } else {
17992 Ok(eval_trigger_expr(right, event)?)
17993 }
17994 }
17995 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
17996 }
17997}
17998
17999fn eval_trigger_condition(
18000 condition: &TriggerCondition,
18001 event: &WriteEvent,
18002 selected: &TriggerRowImage,
18003 schema: &Schema,
18004) -> Result<bool> {
18005 match condition {
18006 TriggerCondition::Pk { value } => {
18007 let pk = schema.primary_key().ok_or_else(|| {
18008 MongrelError::InvalidArgument(
18009 "trigger condition Pk references a table without a primary key".into(),
18010 )
18011 })?;
18012 let lhs = eval_trigger_value(value, event, Some(selected))?;
18013 Ok(values_equal(
18014 &lhs,
18015 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
18016 ))
18017 }
18018 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
18019 selected.columns.get(column_id).unwrap_or(&Value::Null),
18020 &eval_trigger_value(value, event, Some(selected))?,
18021 )),
18022 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
18023 selected.columns.get(column_id).unwrap_or(&Value::Null),
18024 &eval_trigger_value(value, event, Some(selected))?,
18025 )),
18026 TriggerCondition::Lt { column_id, value } => match value_order(
18027 selected.columns.get(column_id).unwrap_or(&Value::Null),
18028 &eval_trigger_value(value, event, Some(selected))?,
18029 ) {
18030 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
18031 None => Ok(false),
18032 },
18033 TriggerCondition::Lte { column_id, value } => match value_order(
18034 selected.columns.get(column_id).unwrap_or(&Value::Null),
18035 &eval_trigger_value(value, event, Some(selected))?,
18036 ) {
18037 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
18038 None => Ok(false),
18039 },
18040 TriggerCondition::Gt { column_id, value } => match value_order(
18041 selected.columns.get(column_id).unwrap_or(&Value::Null),
18042 &eval_trigger_value(value, event, Some(selected))?,
18043 ) {
18044 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
18045 None => Ok(false),
18046 },
18047 TriggerCondition::Gte { column_id, value } => match value_order(
18048 selected.columns.get(column_id).unwrap_or(&Value::Null),
18049 &eval_trigger_value(value, event, Some(selected))?,
18050 ) {
18051 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
18052 None => Ok(false),
18053 },
18054 TriggerCondition::IsNull { column_id } => Ok(matches!(
18055 selected.columns.get(column_id),
18056 None | Some(Value::Null)
18057 )),
18058 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
18059 selected.columns.get(column_id),
18060 None | Some(Value::Null)
18061 )),
18062 TriggerCondition::And { left, right } => {
18063 if !eval_trigger_condition(left, event, selected, schema)? {
18064 Ok(false)
18065 } else {
18066 Ok(eval_trigger_condition(right, event, selected, schema)?)
18067 }
18068 }
18069 TriggerCondition::Or { left, right } => {
18070 if eval_trigger_condition(left, event, selected, schema)? {
18071 Ok(true)
18072 } else {
18073 Ok(eval_trigger_condition(right, event, selected, schema)?)
18074 }
18075 }
18076 TriggerCondition::Not(condition) => {
18077 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
18078 }
18079 }
18080}
18081
18082fn eval_trigger_value(
18083 value: &TriggerValue,
18084 event: &WriteEvent,
18085 selected: Option<&TriggerRowImage>,
18086) -> Result<Value> {
18087 match value {
18088 TriggerValue::Literal(value) => Ok(value.clone()),
18089 TriggerValue::NewColumn(column_id) => event
18090 .new
18091 .as_ref()
18092 .and_then(|row| row.columns.get(column_id))
18093 .cloned()
18094 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
18095 TriggerValue::OldColumn(column_id) => event
18096 .old
18097 .as_ref()
18098 .and_then(|row| row.columns.get(column_id))
18099 .cloned()
18100 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
18101 TriggerValue::SelectedColumn(column_id) => selected
18102 .and_then(|row| row.columns.get(column_id))
18103 .cloned()
18104 .ok_or_else(|| {
18105 MongrelError::InvalidArgument("SELECTED column is not available".into())
18106 }),
18107 }
18108}
18109
18110fn values_equal(left: &Value, right: &Value) -> bool {
18111 match (left, right) {
18112 (Value::Null, Value::Null) => true,
18113 (Value::Bool(a), Value::Bool(b)) => a == b,
18114 (Value::Int64(a), Value::Int64(b)) => a == b,
18115 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
18116 (Value::Bytes(a), Value::Bytes(b)) => a == b,
18117 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => {
18118 let a = a.as_embedding().unwrap();
18119 let b = b.as_embedding().unwrap();
18120 a.len() == b.len()
18121 && a.iter()
18122 .zip(b.iter())
18123 .all(|(a, b)| a.to_bits() == b.to_bits())
18124 }
18125 _ => false,
18126 }
18127}
18128
18129fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
18130 match (left, right) {
18131 (Value::Null, _) | (_, Value::Null) => None,
18132 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
18133 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
18134 (Value::Int64(a), Value::Float64(b)) => {
18137 let af = *a as f64;
18138 Some(af.total_cmp(b))
18139 }
18140 (Value::Float64(a), Value::Int64(b)) => {
18143 let bf = *b as f64;
18144 Some(a.total_cmp(&bf))
18145 }
18146 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
18147 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
18148 (a, b) if a.as_embedding().is_some() && b.as_embedding().is_some() => None,
18149 _ => None,
18150 }
18151}
18152
18153fn trigger_message(value: Value) -> String {
18154 match value {
18155 Value::Null => "NULL".into(),
18156 Value::Bool(value) => value.to_string(),
18157 Value::Int64(value) => value.to_string(),
18158 Value::Float64(value) => value.to_string(),
18159 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
18160 Value::Embedding(value) => format!("{value:?}"),
18161 Value::GeneratedEmbedding(value) => format!("{:?}", value.vector),
18162 Value::Decimal(value) => value.to_string(),
18163 Value::Interval {
18164 months,
18165 days,
18166 nanos,
18167 } => format!("{months}m {days}d {nanos}ns"),
18168 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
18169 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
18170 }
18171}
18172
18173fn validate_trigger_step<'a>(
18174 step: &TriggerStep,
18175 cat: &'a Catalog,
18176 target_schema: &Schema,
18177 event: TriggerEvent,
18178 select_schemas: &mut HashMap<String, &'a Schema>,
18179) -> Result<()> {
18180 match step {
18181 TriggerStep::SetNew { cells } => {
18182 if event == TriggerEvent::Delete {
18183 return Err(MongrelError::InvalidArgument(
18184 "SetNew trigger step is not valid for DELETE triggers".into(),
18185 ));
18186 }
18187 for cell in cells {
18188 validate_column_id(cell.column_id, target_schema)?;
18189 validate_trigger_value(&cell.value, target_schema, event)?;
18190 }
18191 }
18192 TriggerStep::Insert { table, cells } => {
18193 let schema = trigger_write_schema(cat, table, "insert")?;
18194 for cell in cells {
18195 validate_column_id(cell.column_id, schema)?;
18196 validate_trigger_value(&cell.value, target_schema, event)?;
18197 }
18198 }
18199 TriggerStep::UpdateByPk { table, pk, cells } => {
18200 let schema = trigger_write_schema(cat, table, "update")?;
18201 if schema.primary_key().is_none() {
18202 return Err(MongrelError::InvalidArgument(format!(
18203 "trigger update_by_pk references table {table:?} without a primary key"
18204 )));
18205 }
18206 validate_trigger_value(pk, target_schema, event)?;
18207 for cell in cells {
18208 validate_column_id(cell.column_id, schema)?;
18209 validate_trigger_value(&cell.value, target_schema, event)?;
18210 }
18211 }
18212 TriggerStep::DeleteByPk { table, pk } => {
18213 let schema = trigger_write_schema(cat, table, "delete")?;
18214 if schema.primary_key().is_none() {
18215 return Err(MongrelError::InvalidArgument(format!(
18216 "trigger delete_by_pk references table {table:?} without a primary key"
18217 )));
18218 }
18219 validate_trigger_value(pk, target_schema, event)?;
18220 }
18221 TriggerStep::Select {
18222 id,
18223 table,
18224 conditions,
18225 } => {
18226 let schema = trigger_read_schema(cat, table)?;
18227 for condition in conditions {
18228 validate_trigger_condition(condition, schema, target_schema, event)?;
18229 }
18230 if select_schemas.contains_key(id) {
18231 return Err(MongrelError::InvalidArgument(format!(
18232 "duplicate select id {id:?} in trigger program"
18233 )));
18234 }
18235 select_schemas.insert(id.clone(), schema);
18236 }
18237 TriggerStep::Foreach { id, steps } => {
18238 if !select_schemas.contains_key(id) {
18239 return Err(MongrelError::InvalidArgument(format!(
18240 "foreach references unknown select id {id:?}"
18241 )));
18242 }
18243 let mut inner_select_schemas = select_schemas.clone();
18244 for step in steps {
18245 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
18246 }
18247 }
18248 TriggerStep::DeleteWhere { table, conditions } => {
18249 let schema = trigger_write_schema(cat, table, "delete")?;
18250 for condition in conditions {
18251 validate_trigger_condition(condition, schema, target_schema, event)?;
18252 }
18253 }
18254 TriggerStep::UpdateWhere {
18255 table,
18256 conditions,
18257 cells,
18258 } => {
18259 let schema = trigger_write_schema(cat, table, "update")?;
18260 for condition in conditions {
18261 validate_trigger_condition(condition, schema, target_schema, event)?;
18262 }
18263 for cell in cells {
18264 validate_column_id(cell.column_id, schema)?;
18265 validate_trigger_value(&cell.value, target_schema, event)?;
18266 }
18267 }
18268 TriggerStep::Raise { message, .. } => {
18269 validate_trigger_value(message, target_schema, event)?
18270 }
18271 }
18272 Ok(())
18273}
18274
18275fn trigger_validation_error(error: MongrelError) -> MongrelError {
18276 match error {
18277 MongrelError::TriggerValidation(_) => error,
18278 MongrelError::InvalidArgument(message)
18279 | MongrelError::Conflict(message)
18280 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
18281 error => error,
18282 }
18283}
18284
18285fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
18286 if let Some(entry) = cat.live(table) {
18287 return Ok(&entry.schema);
18288 }
18289 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18290 let allowed = match op {
18291 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
18292 "update" | "delete" => entry.capabilities.writable,
18293 _ => false,
18294 };
18295 if !allowed {
18296 return Err(MongrelError::InvalidArgument(format!(
18297 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
18298 entry.module
18299 )));
18300 }
18301 if !entry.capabilities.transaction_safe {
18302 return Err(MongrelError::InvalidArgument(format!(
18303 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
18304 entry.module
18305 )));
18306 }
18307 return Ok(&entry.declared_schema);
18308 }
18309 Err(MongrelError::InvalidArgument(format!(
18310 "trigger references unknown table {table:?}"
18311 )))
18312}
18313
18314fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
18315 if let Some(entry) = cat.live(table) {
18316 return Ok(&entry.schema);
18317 }
18318 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
18319 if entry.capabilities.trigger_safe {
18320 return Ok(&entry.declared_schema);
18321 }
18322 return Err(MongrelError::InvalidArgument(format!(
18323 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
18324 entry.module
18325 )));
18326 }
18327 Err(MongrelError::InvalidArgument(format!(
18328 "trigger references unknown table {table:?}"
18329 )))
18330}
18331
18332fn validate_trigger_condition(
18333 condition: &TriggerCondition,
18334 schema: &Schema,
18335 target_schema: &Schema,
18336 event: TriggerEvent,
18337) -> Result<()> {
18338 match condition {
18339 TriggerCondition::Pk { value } => {
18340 if schema.primary_key().is_none() {
18341 return Err(MongrelError::InvalidArgument(
18342 "trigger condition Pk references a table without a primary key".into(),
18343 ));
18344 }
18345 validate_trigger_value(value, target_schema, event)
18346 }
18347 TriggerCondition::Eq { column_id, value }
18348 | TriggerCondition::NotEq { column_id, value }
18349 | TriggerCondition::Lt { column_id, value }
18350 | TriggerCondition::Lte { column_id, value }
18351 | TriggerCondition::Gt { column_id, value }
18352 | TriggerCondition::Gte { column_id, value } => {
18353 validate_column_id(*column_id, schema)?;
18354 validate_trigger_value(value, target_schema, event)
18355 }
18356 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
18357 validate_column_id(*column_id, schema)
18358 }
18359 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
18360 validate_trigger_condition(left, schema, target_schema, event)?;
18361 validate_trigger_condition(right, schema, target_schema, event)
18362 }
18363 TriggerCondition::Not(condition) => {
18364 validate_trigger_condition(condition, schema, target_schema, event)
18365 }
18366 }
18367}
18368
18369fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
18370 match expr {
18371 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
18372 validate_trigger_value(value, schema, event)
18373 }
18374 TriggerExpr::Eq { left, right }
18375 | TriggerExpr::NotEq { left, right }
18376 | TriggerExpr::Lt { left, right }
18377 | TriggerExpr::Lte { left, right }
18378 | TriggerExpr::Gt { left, right }
18379 | TriggerExpr::Gte { left, right } => {
18380 validate_trigger_value(left, schema, event)?;
18381 validate_trigger_value(right, schema, event)
18382 }
18383 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
18384 validate_trigger_expr(left, schema, event)?;
18385 validate_trigger_expr(right, schema, event)
18386 }
18387 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
18388 }
18389}
18390
18391fn validate_trigger_value(
18392 value: &TriggerValue,
18393 schema: &Schema,
18394 event: TriggerEvent,
18395) -> Result<()> {
18396 match value {
18397 TriggerValue::Literal(_) => Ok(()),
18398 TriggerValue::NewColumn(id) => {
18399 if event == TriggerEvent::Delete {
18400 return Err(MongrelError::InvalidArgument(
18401 "DELETE triggers cannot reference NEW".into(),
18402 ));
18403 }
18404 validate_column_id(*id, schema)
18405 }
18406 TriggerValue::OldColumn(id) => {
18407 if event == TriggerEvent::Insert {
18408 return Err(MongrelError::InvalidArgument(
18409 "INSERT triggers cannot reference OLD".into(),
18410 ));
18411 }
18412 validate_column_id(*id, schema)
18413 }
18414 TriggerValue::SelectedColumn(_) => Ok(()),
18418 }
18419}
18420
18421const COMMIT_TS_LEDGER_CAP: usize = 10_000;
18426
18427fn commit_ts_ledger_from_recovery(
18435 records: &[crate::wal::Record],
18436) -> std::collections::BTreeMap<u64, mongreldb_types::hlc::HlcTimestamp> {
18437 use crate::wal::Op;
18438 let mut commits = HashMap::new();
18439 let mut timestamps = HashMap::new();
18440 for record in records {
18441 match &record.op {
18442 Op::TxnCommit { epoch, .. } => {
18443 commits.insert(record.txn_id, *epoch);
18444 }
18445 Op::CommitTimestamp { unix_nanos } => {
18446 timestamps.insert(record.txn_id, *unix_nanos);
18447 }
18448 _ => {}
18449 }
18450 }
18451 let mut ledger = std::collections::BTreeMap::new();
18452 for (txn_id, epoch) in commits {
18453 let Some(unix_nanos) = timestamps.get(&txn_id) else {
18454 continue;
18455 };
18456 ledger.insert(
18457 epoch,
18458 mongreldb_types::hlc::HlcTimestamp {
18459 physical_micros: unix_nanos / 1_000,
18460 logical: 0,
18461 node_tiebreaker: 0,
18462 },
18463 );
18464 }
18465 while ledger.len() > COMMIT_TS_LEDGER_CAP {
18466 ledger.pop_first();
18467 }
18468 ledger
18469}
18470
18471fn recover_ddl_from_wal(
18477 root: &Path,
18478 durable_root: Option<&crate::durable_file::DurableRoot>,
18479 target_catalog: &mut Catalog,
18480 meta_dek: Option<&[u8; META_DEK_LEN]>,
18481 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
18482 apply: bool,
18483 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18484) -> Result<()> {
18485 use crate::wal::SharedWal;
18486 let records = match durable_root {
18487 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
18488 None => SharedWal::replay_with_dek(root, wal_dek)?,
18489 };
18490 recover_ddl_from_records(
18491 root,
18492 durable_root,
18493 target_catalog,
18494 meta_dek,
18495 apply,
18496 table_roots,
18497 &records,
18498 )
18499}
18500
18501fn recover_ddl_from_records(
18502 root: &Path,
18503 durable_root: Option<&crate::durable_file::DurableRoot>,
18504 target_catalog: &mut Catalog,
18505 meta_dek: Option<&[u8; META_DEK_LEN]>,
18506 apply: bool,
18507 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
18508 records: &[crate::wal::Record],
18509) -> Result<()> {
18510 use crate::wal::{DdlOp, Op};
18511
18512 let original_catalog = target_catalog.clone();
18513 let mut recovered_catalog = original_catalog.clone();
18514 let cat = &mut recovered_catalog;
18515 let mut created_table_ids = HashSet::<u64>::new();
18516 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
18517
18518 let mut committed: HashMap<u64, u64> = HashMap::new();
18519 for r in records {
18520 if let Op::TxnCommit { epoch: ce, .. } = r.op {
18521 committed.insert(r.txn_id, ce);
18522 }
18523 }
18524 let catalog_snapshot_txns = records
18525 .iter()
18526 .filter_map(|record| {
18527 (committed.contains_key(&record.txn_id)
18528 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
18529 .then_some(record.txn_id)
18530 })
18531 .collect::<HashSet<_>>();
18532
18533 let mut changed = false;
18534 let mut applied_catalog_epoch = cat.db_epoch;
18535 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
18536 for r in records.iter().cloned() {
18537 let Some(&ce) = committed.get(&r.txn_id) else {
18538 continue;
18539 };
18540 let txn_id = r.txn_id;
18541 match r.op {
18542 Op::Ddl(DdlOp::CreateTable {
18543 table_id,
18544 ref name,
18545 ref schema_json,
18546 }) => {
18547 if cat.tables.iter().any(|t| t.table_id == table_id) {
18548 continue;
18549 }
18550 let schema = DdlOp::decode_schema(schema_json)?;
18551 validate_recovered_schema(&schema)?;
18552 created_table_ids.insert(table_id);
18553 cat.tables.push(CatalogEntry {
18554 table_id,
18555 name: name.clone(),
18556 schema,
18557 state: TableState::Live,
18558 created_epoch: ce,
18559 });
18560 cat.next_table_id =
18561 cat.next_table_id
18562 .max(table_id.checked_add(1).ok_or_else(|| {
18563 MongrelError::Full("table id namespace exhausted".into())
18564 })?);
18565 changed = true;
18566 }
18567 Op::Ddl(DdlOp::CreateBuildingTable {
18568 table_id,
18569 ref build_name,
18570 ref intended_name,
18571 ref query_id,
18572 created_at_unix_nanos,
18573 ref schema_json,
18574 }) => {
18575 if cat.tables.iter().any(|table| table.table_id == table_id) {
18576 continue;
18577 }
18578 let schema = DdlOp::decode_schema(schema_json)?;
18579 validate_recovered_schema(&schema)?;
18580 created_table_ids.insert(table_id);
18581 cat.tables.push(CatalogEntry {
18582 table_id,
18583 name: build_name.clone(),
18584 schema,
18585 state: TableState::Building {
18586 intended_name: intended_name.clone(),
18587 query_id: query_id.clone(),
18588 created_at_unix_nanos,
18589 replaces_table_id: None,
18590 },
18591 created_epoch: ce,
18592 });
18593 cat.next_table_id =
18594 cat.next_table_id
18595 .max(table_id.checked_add(1).ok_or_else(|| {
18596 MongrelError::Full("table id namespace exhausted".into())
18597 })?);
18598 changed = true;
18599 }
18600 Op::Ddl(DdlOp::CreateRebuildingTable {
18601 table_id,
18602 ref build_name,
18603 ref intended_name,
18604 ref query_id,
18605 created_at_unix_nanos,
18606 replaces_table_id,
18607 ref schema_json,
18608 }) => {
18609 if cat.tables.iter().any(|table| table.table_id == table_id) {
18610 continue;
18611 }
18612 let schema = DdlOp::decode_schema(schema_json)?;
18613 validate_recovered_schema(&schema)?;
18614 created_table_ids.insert(table_id);
18615 cat.tables.push(CatalogEntry {
18616 table_id,
18617 name: build_name.clone(),
18618 schema,
18619 state: TableState::Building {
18620 intended_name: intended_name.clone(),
18621 query_id: query_id.clone(),
18622 created_at_unix_nanos,
18623 replaces_table_id: Some(replaces_table_id),
18624 },
18625 created_epoch: ce,
18626 });
18627 cat.next_table_id =
18628 cat.next_table_id
18629 .max(table_id.checked_add(1).ok_or_else(|| {
18630 MongrelError::Full("table id namespace exhausted".into())
18631 })?);
18632 changed = true;
18633 }
18634 Op::Ddl(DdlOp::DropTable { table_id }) => {
18635 let mut dropped_name = None;
18636 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18637 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
18638 dropped_name = Some(entry.name.clone());
18639 entry.state = TableState::Dropped { at_epoch: ce };
18640 changed = true;
18641 }
18642 }
18643 if let Some(name) = dropped_name {
18644 let before = cat.materialized_views.len();
18645 cat.materialized_views
18646 .retain(|definition| definition.name != name);
18647 changed |= before != cat.materialized_views.len();
18648 cat.security.rls_tables.retain(|table| table != &name);
18649 cat.security.policies.retain(|policy| policy.table != name);
18650 cat.security.masks.retain(|mask| mask.table != name);
18651 for role in &mut cat.roles {
18652 role.permissions
18653 .retain(|permission| permission_table(permission) != Some(&name));
18654 }
18655 if !catalog_snapshot_txns.contains(&txn_id) {
18656 advance_security_version(cat)?;
18657 }
18658 }
18659 }
18660 Op::Ddl(DdlOp::PublishBuildingTable {
18661 table_id,
18662 ref new_name,
18663 }) => {
18664 if let Some(entry) = cat
18665 .tables
18666 .iter_mut()
18667 .find(|table| table.table_id == table_id)
18668 {
18669 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
18670 entry.name = new_name.clone();
18671 entry.state = TableState::Live;
18672 changed = true;
18673 }
18674 }
18675 }
18676 Op::Ddl(DdlOp::ReplaceBuildingTable {
18677 table_id,
18678 replaced_table_id,
18679 ref new_name,
18680 }) => {
18681 changed |=
18682 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
18683 }
18684 Op::Ddl(DdlOp::RenameTable {
18685 table_id,
18686 ref new_name,
18687 }) => {
18688 let mut old_name = None;
18689 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18690 if entry.name != *new_name {
18691 old_name = Some(entry.name.clone());
18692 entry.name = new_name.clone();
18693 changed = true;
18694 }
18695 }
18696 if let Some(old_name) = old_name {
18697 if let Some(definition) = cat
18698 .materialized_views
18699 .iter_mut()
18700 .find(|definition| definition.name == old_name)
18701 {
18702 definition.name = new_name.clone();
18703 }
18704 for table in &mut cat.security.rls_tables {
18705 if *table == old_name {
18706 *table = new_name.clone();
18707 }
18708 }
18709 for policy in &mut cat.security.policies {
18710 if policy.table == old_name {
18711 policy.table = new_name.clone();
18712 }
18713 }
18714 for mask in &mut cat.security.masks {
18715 if mask.table == old_name {
18716 mask.table = new_name.clone();
18717 }
18718 }
18719 for role in &mut cat.roles {
18720 for permission in &mut role.permissions {
18721 rename_permission_table(permission, &old_name, new_name);
18722 }
18723 }
18724 if !catalog_snapshot_txns.contains(&txn_id) {
18725 advance_security_version(cat)?;
18726 }
18727 }
18728 }
18732 Op::Ddl(DdlOp::AlterTable {
18733 table_id,
18734 ref column_json,
18735 }) => {
18736 let column = DdlOp::decode_column(column_json)?;
18737 let mut renamed = None;
18738 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
18739 renamed = entry
18740 .schema
18741 .columns
18742 .iter()
18743 .find(|existing| existing.id == column.id && existing.name != column.name)
18744 .map(|existing| {
18745 (
18746 entry.name.clone(),
18747 existing.name.clone(),
18748 column.name.clone(),
18749 )
18750 });
18751 if apply_recovered_column_def(&mut entry.schema, column)? {
18752 validate_recovered_schema(&entry.schema)?;
18753 changed = true;
18754 }
18755 }
18756 if let Some((table, old_name, new_name)) = renamed {
18757 for role in &mut cat.roles {
18758 for permission in &mut role.permissions {
18759 rename_permission_column(permission, &table, &old_name, &new_name);
18760 }
18761 }
18762 if !catalog_snapshot_txns.contains(&txn_id) {
18763 advance_security_version(cat)?;
18764 }
18765 }
18766 }
18767 Op::Ddl(DdlOp::SetTtl {
18768 table_id,
18769 ref policy_json,
18770 }) => {
18771 let policy = DdlOp::decode_ttl(policy_json)?;
18772 let entry = cat
18773 .tables
18774 .iter()
18775 .find(|entry| entry.table_id == table_id)
18776 .ok_or_else(|| {
18777 MongrelError::Schema(format!(
18778 "recovered TTL references unknown table id {table_id}"
18779 ))
18780 })?;
18781 if let Some(policy) = policy {
18782 let valid = entry
18783 .schema
18784 .columns
18785 .iter()
18786 .find(|column| column.id == policy.column_id)
18787 .is_some_and(|column| {
18788 column.ty == TypeId::TimestampNanos
18789 && policy.duration_nanos > 0
18790 && policy.duration_nanos <= i64::MAX as u64
18791 });
18792 if !valid {
18793 return Err(MongrelError::Schema(format!(
18794 "invalid recovered TTL policy for table id {table_id}"
18795 )));
18796 }
18797 }
18798 ttl_updates.insert(table_id, (policy, ce));
18799 }
18800 Op::Ddl(DdlOp::SetMaterializedView {
18801 ref name,
18802 ref definition_json,
18803 }) => {
18804 let definition = DdlOp::decode_materialized_view(definition_json)?;
18805 if definition.name != *name {
18806 return Err(MongrelError::Schema(format!(
18807 "materialized view WAL name mismatch: {name:?}"
18808 )));
18809 }
18810 if cat.live(name).is_some() {
18811 if let Some(existing) = cat
18812 .materialized_views
18813 .iter_mut()
18814 .find(|existing| existing.name == *name)
18815 {
18816 if *existing != definition {
18817 *existing = definition;
18818 changed = true;
18819 }
18820 } else {
18821 cat.materialized_views.push(definition);
18822 changed = true;
18823 }
18824 }
18825 }
18826 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
18827 let security = DdlOp::decode_security(security_json)?;
18828 validate_security_catalog(cat, &security)?;
18829 if cat.security != security {
18830 cat.security = security;
18831 if !catalog_snapshot_txns.contains(&txn_id) {
18832 advance_security_version(cat)?;
18833 }
18834 changed = true;
18835 }
18836 }
18837 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
18838 let target = match key.as_str() {
18839 "user_version" => &mut cat.user_version,
18840 "application_id" => &mut cat.application_id,
18841 _ => {
18842 return Err(MongrelError::InvalidArgument(format!(
18843 "unsupported recovered SQL pragma {key:?}"
18844 )))
18845 }
18846 };
18847 if *target != Some(value) {
18848 *target = Some(value);
18849 cat.db_epoch = cat.db_epoch.max(ce);
18850 changed = true;
18851 }
18852 }
18853 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
18854 if ce <= applied_catalog_epoch {
18855 continue;
18856 }
18857 let snapshot = DdlOp::decode_catalog(catalog_json)?;
18858 if snapshot.db_epoch != ce {
18859 return Err(MongrelError::Schema(format!(
18860 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
18861 snapshot.db_epoch
18862 )));
18863 }
18864 validate_recovered_catalog(&snapshot)?;
18865 validate_catalog_transition(cat, &snapshot)?;
18866 *cat = snapshot;
18867 applied_catalog_epoch = ce;
18868 changed = true;
18869 }
18870 _ => {}
18871 }
18872 }
18873
18874 if cat.db_epoch < max_committed_epoch {
18875 cat.db_epoch = max_committed_epoch;
18876 changed = true;
18877 }
18878 changed |= repair_catalog_allocator_counters(cat)?;
18879
18880 validate_recovered_catalog(cat)?;
18881 let storage_reconciliation = validate_recovered_storage_plan(
18882 root,
18883 durable_root,
18884 cat,
18885 &created_table_ids,
18886 &ttl_updates,
18887 meta_dek,
18888 )?;
18889
18890 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
18891 if apply && (changed || needs_storage_apply) {
18892 for table_id in storage_reconciliation {
18893 let entry = cat
18894 .tables
18895 .iter()
18896 .find(|entry| entry.table_id == table_id)
18897 .ok_or_else(|| MongrelError::CorruptWal {
18898 offset: table_id,
18899 reason: "recovery storage plan lost its catalog table".into(),
18900 })?;
18901 ensure_recovered_table_storage(
18902 table_roots
18903 .and_then(|roots| roots.get(&table_id))
18904 .map(Arc::as_ref),
18905 durable_root,
18906 &root.join(TABLES_DIR).join(table_id.to_string()),
18907 table_id,
18908 &entry.schema,
18909 meta_dek,
18910 )?;
18911 }
18912 for (table_id, (policy, ttl_epoch)) in ttl_updates {
18913 let Some(entry) = cat.tables.iter().find(|entry| {
18914 entry.table_id == table_id
18915 && matches!(entry.state, TableState::Live | TableState::Building { .. })
18916 }) else {
18917 continue;
18918 };
18919 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
18920 {
18921 root.try_clone()?
18922 } else if let Some(root) = durable_root {
18923 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
18924 } else {
18925 crate::durable_file::DurableRoot::open(
18926 root.join(TABLES_DIR).join(table_id.to_string()),
18927 )?
18928 };
18929 let table_dir = table_root.io_path()?;
18930 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
18931 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
18932 manifest.ttl = policy;
18933 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
18934 manifest.schema_id = entry.schema.schema_id;
18935 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18936 }
18937 }
18938 if changed {
18939 match durable_root {
18940 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
18941 None => catalog::write_atomic(root, cat, meta_dek)?,
18942 }
18943 }
18944 }
18945 *target_catalog = recovered_catalog;
18946 Ok(())
18947}
18948
18949fn ensure_recovered_table_storage(
18950 pinned_table: Option<&crate::durable_file::DurableRoot>,
18951 durable_root: Option<&crate::durable_file::DurableRoot>,
18952 fallback_table_dir: &Path,
18953 table_id: u64,
18954 schema: &Schema,
18955 meta_dek: Option<&[u8; META_DEK_LEN]>,
18956) -> Result<()> {
18957 let table_root = if let Some(root) = pinned_table {
18958 root.try_clone()?
18959 } else if let Some(root) = durable_root {
18960 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
18961 match root.open_directory(&relative) {
18962 Ok(table) => table,
18963 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
18964 root.create_directory_all_pinned(relative)?
18965 }
18966 Err(error) => return Err(error.into()),
18967 }
18968 } else {
18969 crate::durable_file::create_directory_all(fallback_table_dir)?;
18970 crate::durable_file::DurableRoot::open(fallback_table_dir)?
18971 };
18972 let table_dir = table_root.io_path()?;
18973 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
18974 Ok(manifest) => {
18975 if manifest.table_id != table_id {
18976 return Err(MongrelError::Conflict(format!(
18977 "recovered table directory id mismatch: expected {table_id}, found {}",
18978 manifest.table_id
18979 )));
18980 }
18981 Some(manifest)
18982 }
18983 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
18984 Err(error) => return Err(error),
18985 };
18986
18987 table_root.create_directory_all(crate::engine::WAL_DIR)?;
18988 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
18989 crate::engine::write_schema(&table_dir, schema)?;
18990
18991 if let Some(mut manifest) = existing_manifest.take() {
18992 if manifest.schema_id != schema.schema_id {
18993 manifest.schema_id = schema.schema_id;
18994 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
18995 }
18996 } else {
18997 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
18999 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
19000 }
19001 Ok(())
19002}
19003
19004fn validate_recovered_schema(schema: &Schema) -> Result<()> {
19005 schema.validate_auto_increment()?;
19006 schema.validate_defaults()?;
19007 schema.validate_ai()?;
19008 let mut column_ids = HashSet::new();
19009 let mut column_names = HashSet::new();
19010 for column in &schema.columns {
19011 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
19012 return Err(MongrelError::Schema(
19013 "recovered schema contains duplicate columns".into(),
19014 ));
19015 }
19016 match &column.ty {
19017 TypeId::Decimal128 { precision, scale }
19018 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
19019 {
19020 return Err(MongrelError::Schema(format!(
19021 "column {:?} has invalid decimal precision or scale",
19022 column.name
19023 )));
19024 }
19025 TypeId::Enum { variants }
19026 if variants.is_empty()
19027 || variants.iter().any(String::is_empty)
19028 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
19029 {
19030 return Err(MongrelError::Schema(format!(
19031 "column {:?} has invalid enum variants",
19032 column.name
19033 )));
19034 }
19035 _ => {}
19036 }
19037 }
19038 let mut index_names = HashSet::new();
19039 for index in &schema.indexes {
19040 index.validate_options()?;
19041 if index.name.is_empty()
19042 || !index_names.insert(index.name.as_str())
19043 || schema
19044 .columns
19045 .iter()
19046 .all(|column| column.id != index.column_id)
19047 {
19048 return Err(MongrelError::Schema(format!(
19049 "recovered index {:?} references missing column {}",
19050 index.name, index.column_id
19051 )));
19052 }
19053 }
19054 let mut colocated = HashSet::new();
19055 for group in &schema.colocation {
19056 if group.is_empty()
19057 || group.iter().any(|id| !column_ids.contains(id))
19058 || group.iter().any(|id| !colocated.insert(*id))
19059 {
19060 return Err(MongrelError::Schema(
19061 "recovered schema contains invalid column co-location groups".into(),
19062 ));
19063 }
19064 }
19065
19066 let mut constraint_ids = HashSet::new();
19067 let mut constraint_names = HashSet::<String>::new();
19068 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
19069 if name.is_empty()
19070 || !constraint_ids.insert(id)
19071 || !constraint_names.insert(name.to_owned())
19072 {
19073 return Err(MongrelError::Schema(
19074 "recovered schema contains duplicate or empty constraint identities".into(),
19075 ));
19076 }
19077 Ok(())
19078 };
19079 for unique in &schema.constraints.uniques {
19080 validate_constraint_identity(unique.id, &unique.name)?;
19081 if unique.columns.is_empty()
19082 || unique.columns.iter().any(|id| !column_ids.contains(id))
19083 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
19084 {
19085 return Err(MongrelError::Schema(format!(
19086 "unique constraint {:?} has invalid columns",
19087 unique.name
19088 )));
19089 }
19090 }
19091 for foreign_key in &schema.constraints.foreign_keys {
19092 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
19093 if foreign_key.ref_table.is_empty()
19094 || foreign_key.columns.is_empty()
19095 || foreign_key.columns.len() != foreign_key.ref_columns.len()
19096 || foreign_key
19097 .columns
19098 .iter()
19099 .any(|id| !column_ids.contains(id))
19100 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
19101 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
19102 != foreign_key.ref_columns.len()
19103 {
19104 return Err(MongrelError::Schema(format!(
19105 "foreign key {:?} has invalid columns",
19106 foreign_key.name
19107 )));
19108 }
19109 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
19110 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
19111 && foreign_key.columns.iter().any(|id| {
19112 schema
19113 .columns
19114 .iter()
19115 .find(|column| column.id == *id)
19116 .is_none_or(|column| {
19117 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
19118 })
19119 })
19120 {
19121 return Err(MongrelError::Schema(format!(
19122 "foreign key {:?} uses SET NULL on a non-nullable column",
19123 foreign_key.name
19124 )));
19125 }
19126 }
19127 for check in &schema.constraints.checks {
19128 validate_constraint_identity(check.id, &check.name)?;
19129 check.expr.validate()?;
19130 validate_check_columns(&check.expr, &column_ids)?;
19131 }
19132 Ok(())
19133}
19134
19135fn validate_check_columns(
19136 expression: &crate::constraint::CheckExpr,
19137 column_ids: &HashSet<u16>,
19138) -> Result<()> {
19139 use crate::constraint::CheckExpr;
19140 match expression {
19141 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
19142 if column_ids.contains(id) {
19143 Ok(())
19144 } else {
19145 Err(MongrelError::Schema(format!(
19146 "check constraint references unknown column {id}"
19147 )))
19148 }
19149 }
19150 CheckExpr::Regex { col, .. } => {
19151 if column_ids.contains(col) {
19152 Ok(())
19153 } else {
19154 Err(MongrelError::Schema(format!(
19155 "check constraint references unknown column {col}"
19156 )))
19157 }
19158 }
19159 CheckExpr::Add(left, right)
19160 | CheckExpr::Sub(left, right)
19161 | CheckExpr::Mul(left, right)
19162 | CheckExpr::Div(left, right)
19163 | CheckExpr::Mod(left, right)
19164 | CheckExpr::Eq(left, right)
19165 | CheckExpr::Ne(left, right)
19166 | CheckExpr::Lt(left, right)
19167 | CheckExpr::Le(left, right)
19168 | CheckExpr::Gt(left, right)
19169 | CheckExpr::Ge(left, right)
19170 | CheckExpr::And(left, right)
19171 | CheckExpr::Or(left, right) => {
19172 validate_check_columns(left, column_ids)?;
19173 validate_check_columns(right, column_ids)
19174 }
19175 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
19176 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
19177 }
19178}
19179
19180fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
19181 for (name, prior, candidate) in [
19182 ("db_epoch", current.db_epoch, next.db_epoch),
19183 ("next_table_id", current.next_table_id, next.next_table_id),
19184 (
19185 "next_segment_no",
19186 current.next_segment_no,
19187 next.next_segment_no,
19188 ),
19189 ("next_user_id", current.next_user_id, next.next_user_id),
19190 (
19191 "security_version",
19192 current.security_version,
19193 next.security_version,
19194 ),
19195 ] {
19196 if candidate < prior {
19197 return Err(MongrelError::Schema(format!(
19198 "catalog snapshot rolls back {name} from {prior} to {candidate}"
19199 )));
19200 }
19201 }
19202 for prior in ¤t.tables {
19203 let Some(candidate) = next
19204 .tables
19205 .iter()
19206 .find(|entry| entry.table_id == prior.table_id)
19207 else {
19208 return Err(MongrelError::Schema(format!(
19209 "catalog snapshot removes table identity {}",
19210 prior.table_id
19211 )));
19212 };
19213 if candidate.created_epoch != prior.created_epoch
19214 || candidate.schema.schema_id < prior.schema.schema_id
19215 || matches!(prior.state, TableState::Dropped { .. })
19216 && !matches!(candidate.state, TableState::Dropped { .. })
19217 {
19218 return Err(MongrelError::Schema(format!(
19219 "catalog snapshot rolls back table identity {}",
19220 prior.table_id
19221 )));
19222 }
19223 }
19224 for prior in ¤t.users {
19225 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
19226 if candidate.username != prior.username
19227 || candidate.created_epoch != prior.created_epoch
19228 {
19229 return Err(MongrelError::Schema(format!(
19230 "catalog snapshot reuses user identity {}",
19231 prior.id
19232 )));
19233 }
19234 }
19235 }
19236 Ok(())
19237}
19238
19239fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
19240 let mut table_ids = HashSet::new();
19241 let mut active_names = HashSet::new();
19242 let mut max_table_id = None::<u64>;
19243 for entry in &catalog.tables {
19244 if !table_ids.insert(entry.table_id) {
19245 return Err(MongrelError::Schema(format!(
19246 "catalog contains duplicate table id {}",
19247 entry.table_id
19248 )));
19249 }
19250 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
19251 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
19252 return Err(MongrelError::Schema(format!(
19253 "catalog table {} has invalid name or creation epoch",
19254 entry.table_id
19255 )));
19256 }
19257 validate_recovered_schema(&entry.schema)?;
19258 match &entry.state {
19259 TableState::Live => {
19260 if !active_names.insert(entry.name.as_str()) {
19261 return Err(MongrelError::Schema(format!(
19262 "catalog contains duplicate active table name {:?}",
19263 entry.name
19264 )));
19265 }
19266 }
19267 TableState::Dropped { at_epoch } => {
19268 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
19269 return Err(MongrelError::Schema(format!(
19270 "catalog table {} has invalid drop epoch {at_epoch}",
19271 entry.table_id
19272 )));
19273 }
19274 }
19275 TableState::Building {
19276 intended_name,
19277 query_id,
19278 replaces_table_id,
19279 ..
19280 } => {
19281 if intended_name.is_empty() || query_id.is_empty() {
19282 return Err(MongrelError::Schema(format!(
19283 "building table {} has empty identity fields",
19284 entry.table_id
19285 )));
19286 }
19287 if !active_names.insert(entry.name.as_str()) {
19288 return Err(MongrelError::Schema(format!(
19289 "catalog contains duplicate active/building table name {:?}",
19290 entry.name
19291 )));
19292 }
19293 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
19294 return Err(MongrelError::Schema(
19295 "building table cannot replace itself".into(),
19296 ));
19297 }
19298 }
19299 }
19300 }
19301 if let Some(maximum) = max_table_id {
19302 let required = maximum
19303 .checked_add(1)
19304 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19305 if catalog.next_table_id < required {
19306 return Err(MongrelError::Schema(format!(
19307 "catalog next_table_id {} precedes required {required}",
19308 catalog.next_table_id
19309 )));
19310 }
19311 }
19312 for entry in &catalog.tables {
19313 if let TableState::Building {
19314 replaces_table_id: Some(replaced),
19315 ..
19316 } = entry.state
19317 {
19318 if !table_ids.contains(&replaced) {
19319 return Err(MongrelError::Schema(format!(
19320 "building table {} replaces unknown table {replaced}",
19321 entry.table_id
19322 )));
19323 }
19324 }
19325 }
19326 for entry in &catalog.tables {
19327 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19328 validate_foreign_key_targets(catalog, &entry.schema)?;
19329 }
19330 }
19331
19332 let mut external_names = HashSet::new();
19333 for entry in &catalog.external_tables {
19334 entry.validate()?;
19335 validate_recovered_schema(&entry.declared_schema)?;
19336 if !entry.declared_schema.constraints.is_empty() {
19337 return Err(MongrelError::Schema(format!(
19338 "external table {:?} cannot carry engine-enforced constraints",
19339 entry.name
19340 )));
19341 }
19342 if entry.created_epoch > catalog.db_epoch
19343 || !external_names.insert(entry.name.as_str())
19344 || active_names.contains(entry.name.as_str())
19345 {
19346 return Err(MongrelError::Schema(format!(
19347 "invalid or duplicate external table {:?}",
19348 entry.name
19349 )));
19350 }
19351 }
19352
19353 let mut procedure_names = HashSet::new();
19354 for entry in &catalog.procedures {
19355 entry.procedure.validate()?;
19356 if entry.procedure.created_epoch > entry.procedure.updated_epoch
19357 || entry.procedure.updated_epoch > catalog.db_epoch
19358 || !procedure_names.insert(entry.procedure.name.as_str())
19359 {
19360 return Err(MongrelError::Schema(format!(
19361 "invalid or duplicate procedure {:?}",
19362 entry.procedure.name
19363 )));
19364 }
19365 validate_recovered_procedure_references(catalog, &entry.procedure)?;
19366 }
19367
19368 let mut trigger_names = HashSet::new();
19369 for entry in &catalog.triggers {
19370 entry.trigger.validate()?;
19371 if entry.trigger.created_epoch > entry.trigger.updated_epoch
19372 || entry.trigger.updated_epoch > catalog.db_epoch
19373 || !trigger_names.insert(entry.trigger.name.as_str())
19374 {
19375 return Err(MongrelError::Schema(format!(
19376 "invalid or duplicate trigger {:?}",
19377 entry.trigger.name
19378 )));
19379 }
19380 validate_recovered_trigger_references(catalog, &entry.trigger)?;
19381 }
19382
19383 let mut views = HashSet::new();
19384 for view in &catalog.materialized_views {
19385 let target = catalog.live(&view.name).ok_or_else(|| {
19386 MongrelError::Schema(format!(
19387 "materialized view {:?} has no live table",
19388 view.name
19389 ))
19390 })?;
19391 if view.name.is_empty()
19392 || view.query.trim().is_empty()
19393 || view.last_refresh_epoch > catalog.db_epoch
19394 || !views.insert(view.name.as_str())
19395 {
19396 return Err(MongrelError::Schema(format!(
19397 "materialized view {:?} has no unique live table",
19398 view.name
19399 )));
19400 }
19401 if let Some(incremental) = &view.incremental {
19402 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
19403 MongrelError::Schema(format!(
19404 "materialized view {:?} references missing source {:?}",
19405 view.name, incremental.source_table
19406 ))
19407 })?;
19408 if source.table_id != incremental.source_table_id
19409 || source
19410 .schema
19411 .columns
19412 .iter()
19413 .all(|column| column.id != incremental.group_column)
19414 {
19415 return Err(MongrelError::Schema(format!(
19416 "materialized view {:?} has invalid incremental source",
19417 view.name
19418 )));
19419 }
19420 let target_ids = target
19421 .schema
19422 .columns
19423 .iter()
19424 .map(|column| column.id)
19425 .collect::<HashSet<_>>();
19426 let mut output_ids = HashSet::new();
19427 let count_outputs = incremental
19428 .outputs
19429 .iter()
19430 .filter(|output| {
19431 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19432 })
19433 .count();
19434 if incremental.checkpoint_event_id.is_empty()
19435 || !target_ids.contains(&incremental.group_output_column)
19436 || !target_ids.contains(&incremental.count_output_column)
19437 || incremental.outputs.is_empty()
19438 || count_outputs != 1
19439 || incremental.outputs.iter().any(|output| {
19440 !target_ids.contains(&output.output_column)
19441 || output.output_column == incremental.group_output_column
19442 || !output_ids.insert(output.output_column)
19443 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
19444 && output.output_column != incremental.count_output_column
19445 || match output.kind {
19446 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
19447 source
19448 .schema
19449 .columns
19450 .iter()
19451 .all(|column| column.id != source_column)
19452 }
19453 crate::catalog::IncrementalAggregateKind::Count => false,
19454 }
19455 })
19456 {
19457 return Err(MongrelError::Schema(format!(
19458 "materialized view {:?} has invalid incremental outputs",
19459 view.name
19460 )));
19461 }
19462 }
19463 }
19464
19465 validate_security_catalog(catalog, &catalog.security)?;
19466 validate_recovered_auth_catalog(catalog)?;
19467 Ok(())
19468}
19469
19470fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
19471 let mut changed = false;
19472 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
19473 let required = maximum
19474 .checked_add(1)
19475 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
19476 if catalog.next_table_id < required {
19477 catalog.next_table_id = required;
19478 changed = true;
19479 }
19480 }
19481 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
19482 let required = maximum
19483 .checked_add(1)
19484 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
19485 if catalog.next_user_id < required {
19486 catalog.next_user_id = required;
19487 changed = true;
19488 }
19489 }
19490 Ok(changed)
19491}
19492
19493fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
19494 for foreign_key in &schema.constraints.foreign_keys {
19495 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
19496 MongrelError::Schema(format!(
19497 "foreign key {:?} references unknown live table {:?}",
19498 foreign_key.name, foreign_key.ref_table
19499 ))
19500 })?;
19501 let referenced_unique = parent
19502 .schema
19503 .constraints
19504 .uniques
19505 .iter()
19506 .any(|unique| unique.columns == foreign_key.ref_columns)
19507 || foreign_key.ref_columns.len() == 1
19508 && parent
19509 .schema
19510 .primary_key()
19511 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
19512 if !referenced_unique {
19513 return Err(MongrelError::Schema(format!(
19514 "foreign key {:?} does not reference a unique key",
19515 foreign_key.name
19516 )));
19517 }
19518 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
19519 let local = schema.columns.iter().find(|column| column.id == *local_id);
19520 let referenced = parent
19521 .schema
19522 .columns
19523 .iter()
19524 .find(|column| column.id == *parent_id);
19525 if local
19526 .zip(referenced)
19527 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
19528 {
19529 return Err(MongrelError::Schema(format!(
19530 "foreign key {:?} has missing or incompatible columns",
19531 foreign_key.name
19532 )));
19533 }
19534 }
19535 }
19536 Ok(())
19537}
19538
19539fn validate_recovered_procedure_references(
19540 catalog: &Catalog,
19541 procedure: &StoredProcedure,
19542) -> Result<()> {
19543 for step in &procedure.body.steps {
19544 let Some(table_name) = step.table() else {
19545 continue;
19546 };
19547 let schema = &catalog
19548 .live(table_name)
19549 .ok_or_else(|| {
19550 MongrelError::Schema(format!(
19551 "procedure {:?} references unknown table {table_name:?}",
19552 procedure.name
19553 ))
19554 })?
19555 .schema;
19556 match step {
19557 ProcedureStep::NativeQuery {
19558 conditions,
19559 projection,
19560 ..
19561 } => {
19562 for condition in conditions {
19563 validate_condition_columns(condition, schema)?;
19564 }
19565 for id in projection.iter().flatten() {
19566 validate_column_id(*id, schema)?;
19567 }
19568 }
19569 ProcedureStep::Put { cells, .. } => {
19570 for cell in cells {
19571 validate_column_id(cell.column_id, schema)?;
19572 }
19573 }
19574 ProcedureStep::Upsert {
19575 cells,
19576 update_cells,
19577 ..
19578 } => {
19579 for cell in cells.iter().chain(update_cells.iter().flatten()) {
19580 validate_column_id(cell.column_id, schema)?;
19581 }
19582 }
19583 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
19584 return Err(MongrelError::Schema(format!(
19585 "procedure {:?} deletes by primary key on table without one",
19586 procedure.name
19587 )));
19588 }
19589 ProcedureStep::DeleteByPk { .. }
19590 | ProcedureStep::DeleteRows { .. }
19591 | ProcedureStep::SqlQuery { .. } => {}
19592 }
19593 }
19594 Ok(())
19595}
19596
19597fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
19598 let target_schema = match &trigger.target {
19599 TriggerTarget::Table(name) => catalog
19600 .live(name)
19601 .ok_or_else(|| {
19602 MongrelError::Schema(format!(
19603 "trigger {:?} references unknown table {name:?}",
19604 trigger.name
19605 ))
19606 })?
19607 .schema
19608 .clone(),
19609 TriggerTarget::View(_) => Schema {
19610 columns: trigger.target_columns.clone(),
19611 ..Schema::default()
19612 },
19613 };
19614 for column in &trigger.update_of {
19615 if target_schema.column(column).is_none() {
19616 return Err(MongrelError::Schema(format!(
19617 "trigger {:?} references unknown UPDATE OF column {column:?}",
19618 trigger.name
19619 )));
19620 }
19621 }
19622 if let Some(expr) = &trigger.when {
19623 validate_trigger_expr(expr, &target_schema, trigger.event)?;
19624 }
19625 let mut selects = HashMap::new();
19626 for step in &trigger.program.steps {
19627 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
19628 return Err(MongrelError::Schema(
19629 "SetNew is only valid in BEFORE triggers".into(),
19630 ));
19631 }
19632 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
19633 }
19634 Ok(())
19635}
19636
19637fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
19638 let mut role_names = HashSet::new();
19639 for role in &catalog.roles {
19640 if role.name.is_empty()
19641 || role.created_epoch > catalog.db_epoch
19642 || !role_names.insert(role.name.as_str())
19643 {
19644 return Err(MongrelError::Schema(format!(
19645 "invalid or duplicate role {:?}",
19646 role.name
19647 )));
19648 }
19649 for permission in &role.permissions {
19650 if let Some(table) = permission_table(permission) {
19651 let schema = catalog
19652 .live(table)
19653 .map(|entry| &entry.schema)
19654 .or_else(|| {
19655 catalog
19656 .external_tables
19657 .iter()
19658 .find(|entry| entry.name == table)
19659 .map(|entry| &entry.declared_schema)
19660 })
19661 .ok_or_else(|| {
19662 MongrelError::Schema(format!(
19663 "role {:?} references unknown table {table:?}",
19664 role.name
19665 ))
19666 })?;
19667 let columns = match permission {
19668 crate::auth::Permission::SelectColumns { columns, .. }
19669 | crate::auth::Permission::InsertColumns { columns, .. }
19670 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
19671 _ => None,
19672 };
19673 if columns.is_some_and(|columns| {
19674 columns.is_empty()
19675 || columns.iter().any(|column| schema.column(column).is_none())
19676 }) {
19677 return Err(MongrelError::Schema(format!(
19678 "role {:?} contains invalid column permissions",
19679 role.name
19680 )));
19681 }
19682 }
19683 }
19684 }
19685 let mut user_ids = HashSet::new();
19686 let mut usernames = HashSet::new();
19687 let mut maximum_user_id = 0;
19688 for user in &catalog.users {
19689 maximum_user_id = maximum_user_id.max(user.id);
19690 if user.id == 0
19691 || user.username.is_empty()
19692 || user.password_hash.is_empty()
19693 || user.created_epoch > catalog.db_epoch
19694 || !user_ids.insert(user.id)
19695 || !usernames.insert(user.username.as_str())
19696 || user
19697 .roles
19698 .iter()
19699 .any(|role| !role_names.contains(role.as_str()))
19700 {
19701 return Err(MongrelError::Schema(format!(
19702 "invalid or duplicate user {:?}",
19703 user.username
19704 )));
19705 }
19706 }
19707 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
19708 return Err(MongrelError::Schema(
19709 "catalog next_user_id does not advance beyond existing user ids".into(),
19710 ));
19711 }
19712 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
19713 return Err(MongrelError::Schema(
19714 "authenticated catalog has no administrator".into(),
19715 ));
19716 }
19717 Ok(())
19718}
19719
19720fn validate_recovered_storage_plan(
19721 root: &Path,
19722 durable_root: Option<&crate::durable_file::DurableRoot>,
19723 catalog: &Catalog,
19724 created_table_ids: &HashSet<u64>,
19725 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
19726 meta_dek: Option<&[u8; META_DEK_LEN]>,
19727) -> Result<Vec<u64>> {
19728 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
19729 let mut reconcile = Vec::new();
19730 for entry in &catalog.tables {
19731 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19732 continue;
19733 }
19734 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19735 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
19736 let table_exists = match durable_root {
19737 Some(root) => match root.open_directory(&relative_dir) {
19738 Ok(_) => true,
19739 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19740 Err(error) => return Err(error.into()),
19741 },
19742 None => table_dir.is_dir(),
19743 };
19744 if !table_exists {
19745 if created_table_ids.contains(&entry.table_id) {
19746 reconcile.push(entry.table_id);
19747 continue;
19748 }
19749 return Err(MongrelError::NotFound(format!(
19750 "catalog table {} storage is missing",
19751 entry.table_id
19752 )));
19753 }
19754 let manifest_result = match durable_root {
19755 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
19756 None => crate::manifest::read(&table_dir, meta_dek),
19757 };
19758 let manifest = match manifest_result {
19759 Ok(manifest) => manifest,
19760 Err(MongrelError::Io(error))
19761 if created_table_ids.contains(&entry.table_id)
19762 && error.kind() == std::io::ErrorKind::NotFound =>
19763 {
19764 reconcile.push(entry.table_id);
19765 continue;
19766 }
19767 Err(error) => return Err(error),
19768 };
19769 if manifest.table_id != entry.table_id {
19770 return Err(MongrelError::Conflict(format!(
19771 "catalog table {} storage identity mismatch",
19772 entry.table_id
19773 )));
19774 }
19775 let schema_result = match durable_root {
19776 Some(root) => root
19777 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
19778 .map_err(MongrelError::from),
19779 None => crate::durable_file::open_regular_nofollow(
19780 &table_dir.join(crate::engine::SCHEMA_FILENAME),
19781 ),
19782 };
19783 let file = match schema_result {
19784 Ok(file) => file,
19785 Err(MongrelError::Io(error))
19786 if created_table_ids.contains(&entry.table_id)
19787 && error.kind() == std::io::ErrorKind::NotFound =>
19788 {
19789 reconcile.push(entry.table_id);
19790 continue;
19791 }
19792 Err(error) => return Err(error),
19793 };
19794 let length = file.metadata()?.len();
19795 if length > MAX_SCHEMA_BYTES {
19796 return Err(MongrelError::ResourceLimitExceeded {
19797 resource: "recovered schema bytes",
19798 requested: usize::try_from(length).unwrap_or(usize::MAX),
19799 limit: MAX_SCHEMA_BYTES as usize,
19800 });
19801 }
19802 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
19803 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
19804 if manifest.schema_id != entry.schema.schema_id
19805 || crate::wal::DdlOp::encode_schema(&disk_schema)?
19806 != crate::wal::DdlOp::encode_schema(&entry.schema)?
19807 {
19808 reconcile.push(entry.table_id);
19809 }
19810 }
19811 for table_id in ttl_updates.keys() {
19812 if !catalog.tables.iter().any(|entry| {
19813 entry.table_id == *table_id
19814 && matches!(entry.state, TableState::Live | TableState::Building { .. })
19815 }) {
19816 continue;
19817 }
19818 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
19819 let table_exists = match durable_root {
19820 Some(root) => match root.open_directory(&relative_dir) {
19821 Ok(_) => true,
19822 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
19823 Err(error) => return Err(error.into()),
19824 },
19825 None => root.join(&relative_dir).is_dir(),
19826 };
19827 if !table_exists && !created_table_ids.contains(table_id) {
19828 return Err(MongrelError::NotFound(format!(
19829 "TTL recovery table {table_id} storage is missing"
19830 )));
19831 }
19832 }
19833 reconcile.sort_unstable();
19834 reconcile.dedup();
19835 Ok(reconcile)
19836}
19837
19838fn validate_catalog_table_storage(
19839 root: &crate::durable_file::DurableRoot,
19840 catalog: &Catalog,
19841 meta_dek: Option<&[u8; META_DEK_LEN]>,
19842) -> Result<()> {
19843 for entry in &catalog.tables {
19844 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
19845 continue;
19846 }
19847 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
19848 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
19849 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
19850 return Err(MongrelError::Conflict(format!(
19851 "catalog table {} storage identity mismatch",
19852 entry.table_id
19853 )));
19854 }
19855 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
19856 }
19857 Ok(())
19858}
19859
19860fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
19861 match schema.columns.iter_mut().find(|c| c.id == column.id) {
19862 Some(existing) if *existing == column => Ok(false),
19863 Some(existing) => {
19864 *existing = column;
19865 schema.schema_id = schema
19866 .schema_id
19867 .checked_add(1)
19868 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19869 Ok(true)
19870 }
19871 None => {
19872 schema.columns.push(column);
19873 schema.schema_id = schema
19874 .schema_id
19875 .checked_add(1)
19876 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
19877 Ok(true)
19878 }
19879 }
19880}
19881
19882fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
19883 use crate::auth::Permission;
19884 match permission {
19885 Permission::Select { table }
19886 | Permission::Insert { table }
19887 | Permission::Update { table }
19888 | Permission::Delete { table }
19889 | Permission::SelectColumns { table, .. }
19890 | Permission::InsertColumns { table, .. }
19891 | Permission::UpdateColumns { table, .. } => Some(table),
19892 Permission::All | Permission::Ddl | Permission::Admin => None,
19893 }
19894}
19895
19896fn apply_rebuilding_publish(
19897 catalog: &mut Catalog,
19898 table_id: u64,
19899 replaced_table_id: u64,
19900 new_name: &str,
19901 epoch: u64,
19902) -> Result<bool> {
19903 let already_published = catalog.tables.iter().any(|entry| {
19904 entry.table_id == table_id
19905 && entry.name == new_name
19906 && matches!(entry.state, TableState::Live)
19907 }) && catalog.tables.iter().any(|entry| {
19908 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
19909 });
19910 if already_published {
19911 return Ok(false);
19912 }
19913 let schema = catalog
19914 .tables
19915 .iter()
19916 .find(|entry| entry.table_id == table_id)
19917 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
19918 .schema
19919 .clone();
19920 let replaced = catalog
19921 .tables
19922 .iter_mut()
19923 .find(|entry| entry.table_id == replaced_table_id)
19924 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
19925 replaced.state = TableState::Dropped { at_epoch: epoch };
19926 let replacement = catalog
19927 .tables
19928 .iter_mut()
19929 .find(|entry| entry.table_id == table_id)
19930 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
19931 replacement.name = new_name.to_string();
19932 replacement.state = TableState::Live;
19933
19934 for role in &mut catalog.roles {
19935 role.permissions.retain_mut(|permission| {
19936 retain_rebuilt_permission_columns(permission, new_name, &schema)
19937 });
19938 }
19939 for definition in &mut catalog.materialized_views {
19940 if let Some(incremental) = definition.incremental.as_mut() {
19941 if incremental.source_table == new_name
19942 && incremental.source_table_id == replaced_table_id
19943 {
19944 incremental.source_table_id = table_id;
19945 }
19946 }
19947 }
19948 advance_security_version(catalog)?;
19949 Ok(true)
19950}
19951
19952fn retain_rebuilt_permission_columns(
19953 permission: &mut crate::auth::Permission,
19954 target_table: &str,
19955 schema: &Schema,
19956) -> bool {
19957 use crate::auth::Permission;
19958 let columns = match permission {
19959 Permission::SelectColumns { table, columns }
19960 | Permission::InsertColumns { table, columns }
19961 | Permission::UpdateColumns { table, columns }
19962 if table == target_table =>
19963 {
19964 Some(columns)
19965 }
19966 _ => None,
19967 };
19968 if let Some(columns) = columns {
19969 columns.retain(|column| schema.column(column).is_some());
19970 !columns.is_empty()
19971 } else {
19972 true
19973 }
19974}
19975
19976fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
19977 use crate::auth::Permission;
19978 let table = match permission {
19979 Permission::Select { table }
19980 | Permission::Insert { table }
19981 | Permission::Update { table }
19982 | Permission::Delete { table }
19983 | Permission::SelectColumns { table, .. }
19984 | Permission::InsertColumns { table, .. }
19985 | Permission::UpdateColumns { table, .. } => Some(table),
19986 Permission::All | Permission::Ddl | Permission::Admin => None,
19987 };
19988 if let Some(table) = table.filter(|table| table.as_str() == old) {
19989 *table = new.to_string();
19990 }
19991}
19992
19993fn rename_permission_column(
19994 permission: &mut crate::auth::Permission,
19995 target_table: &str,
19996 old: &str,
19997 new: &str,
19998) {
19999 use crate::auth::Permission;
20000 let columns = match permission {
20001 Permission::SelectColumns { table, columns }
20002 | Permission::InsertColumns { table, columns }
20003 | Permission::UpdateColumns { table, columns }
20004 if table == target_table =>
20005 {
20006 Some(columns)
20007 }
20008 _ => None,
20009 };
20010 if let Some(column) = columns
20011 .into_iter()
20012 .flatten()
20013 .find(|column| column.as_str() == old)
20014 {
20015 *column = new.to_string();
20016 }
20017}
20018
20019pub(crate) fn validate_security_catalog(
20020 catalog: &Catalog,
20021 security: &crate::security::SecurityCatalog,
20022) -> Result<()> {
20023 let mut policy_names = HashSet::new();
20024 for table in &security.rls_tables {
20025 if catalog.live(table).is_none() {
20026 return Err(MongrelError::NotFound(format!(
20027 "RLS table {table:?} not found"
20028 )));
20029 }
20030 }
20031 for policy in &security.policies {
20032 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
20033 return Err(MongrelError::InvalidArgument(format!(
20034 "duplicate policy {:?} on {:?}",
20035 policy.name, policy.table
20036 )));
20037 }
20038 let schema = &catalog
20039 .live(&policy.table)
20040 .ok_or_else(|| {
20041 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
20042 })?
20043 .schema;
20044 if let Some(expression) = &policy.using {
20045 validate_security_expression(expression, schema)?;
20046 }
20047 if let Some(expression) = &policy.with_check {
20048 validate_security_expression(expression, schema)?;
20049 }
20050 }
20051 let mut mask_names = HashSet::new();
20052 for mask in &security.masks {
20053 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
20054 return Err(MongrelError::InvalidArgument(format!(
20055 "duplicate mask {:?} on {:?}",
20056 mask.name, mask.table
20057 )));
20058 }
20059 let column = catalog
20060 .live(&mask.table)
20061 .and_then(|entry| {
20062 entry
20063 .schema
20064 .columns
20065 .iter()
20066 .find(|column| column.id == mask.column)
20067 })
20068 .ok_or_else(|| {
20069 MongrelError::NotFound(format!(
20070 "mask column {} on {:?} not found",
20071 mask.column, mask.table
20072 ))
20073 })?;
20074 if matches!(
20075 mask.strategy,
20076 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
20077 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
20078 {
20079 return Err(MongrelError::InvalidArgument(format!(
20080 "mask {:?} requires a string/bytes column",
20081 mask.name
20082 )));
20083 }
20084 }
20085 Ok(())
20086}
20087
20088fn validate_security_expression(
20089 expression: &crate::security::SecurityExpr,
20090 schema: &Schema,
20091) -> Result<()> {
20092 use crate::security::SecurityExpr;
20093 match expression {
20094 SecurityExpr::True => Ok(()),
20095 SecurityExpr::ColumnEqCurrentUser { column }
20096 | SecurityExpr::ColumnEqValue { column, .. } => {
20097 if schema
20098 .columns
20099 .iter()
20100 .any(|candidate| candidate.id == *column)
20101 {
20102 Ok(())
20103 } else {
20104 Err(MongrelError::InvalidArgument(format!(
20105 "security expression references unknown column id {column}"
20106 )))
20107 }
20108 }
20109 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
20110 validate_security_expression(left, schema)?;
20111 validate_security_expression(right, schema)
20112 }
20113 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
20114 }
20115}
20116
20117fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
20119 let referenced = cat
20120 .tables
20121 .iter()
20122 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
20123 .map(|entry| entry.table_id)
20124 .collect::<HashSet<_>>();
20125 let tables_dir = root.join(TABLES_DIR);
20126 let entries = match std::fs::read_dir(&tables_dir) {
20127 Ok(entries) => entries,
20128 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
20129 Err(error) => return Err(error.into()),
20130 };
20131 for entry in entries {
20132 let entry = entry?;
20133 if !entry.file_type()?.is_dir() {
20134 continue;
20135 }
20136 let file_name = entry.file_name();
20137 let Some(name) = file_name.to_str() else {
20138 continue;
20139 };
20140 let Ok(table_id) = name.parse::<u64>() else {
20141 continue;
20142 };
20143 if name != table_id.to_string() {
20144 continue;
20145 }
20146 if !referenced.contains(&table_id) {
20147 crate::durable_file::remove_directory_all(&entry.path())?;
20148 }
20149 }
20150 Ok(())
20151}
20152
20153fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
20158 for entry in &cat.tables {
20159 let txn_dir = root
20160 .join(TABLES_DIR)
20161 .join(entry.table_id.to_string())
20162 .join("_txn");
20163 if txn_dir.exists() {
20164 let _ = std::fs::remove_dir_all(&txn_dir);
20165 }
20166 }
20167}
20168
20169#[cfg(test)]
20170mod write_permission_tests {
20171 use super::*;
20172 use crate::txn::Staged;
20173
20174 struct NoopExternalBridge;
20175
20176 impl ExternalTriggerBridge for NoopExternalBridge {
20177 fn apply_trigger_external_write(
20178 &self,
20179 _entry: &ExternalTableEntry,
20180 base_state: Vec<u8>,
20181 _op: ExternalTriggerWrite,
20182 ) -> Result<ExternalTriggerWriteResult> {
20183 Ok(ExternalTriggerWriteResult::new(base_state))
20184 }
20185 }
20186
20187 fn assert_txn_namespace_full<T>(result: Result<T>) {
20188 assert!(matches!(result, Err(MongrelError::Full(_))));
20189 }
20190
20191 #[test]
20192 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
20193 let directory = tempfile::tempdir().unwrap();
20194 let database = Database::create(directory.path()).unwrap();
20195 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
20196 *database.next_txn_id.lock() = generation << 32;
20197 let before = crate::wal::SharedWal::replay(directory.path())
20198 .unwrap()
20199 .len();
20200 let bridge = NoopExternalBridge;
20201
20202 assert_txn_namespace_full(database.begin().commit());
20203 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
20204 assert_txn_namespace_full(
20205 database
20206 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
20207 .commit(),
20208 );
20209 assert_txn_namespace_full(
20210 database
20211 .begin_with_external_trigger_bridge(&bridge)
20212 .commit(),
20213 );
20214 assert_txn_namespace_full(
20215 database
20216 .begin_with_external_trigger_bridge_as(&bridge, None)
20217 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
20218 );
20219
20220 assert_eq!(
20221 crate::wal::SharedWal::replay(directory.path())
20222 .unwrap()
20223 .len(),
20224 before
20225 );
20226 drop(database);
20227 Database::open(directory.path()).unwrap();
20228 }
20229
20230 #[test]
20231 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
20232 let directory = tempfile::tempdir().unwrap();
20233 let table_dir = directory.path().join("7");
20234 crate::durable_file::create_directory_all(&table_dir).unwrap();
20235 let original_schema = test_schema();
20236 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
20237 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
20238 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
20239 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
20240 let original_bytes = std::fs::read(&schema_path).unwrap();
20241
20242 let mut replacement_schema = original_schema;
20243 replacement_schema.schema_id += 1;
20244 assert!(matches!(
20245 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
20246 Err(MongrelError::Conflict(_))
20247 ));
20248
20249 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
20250 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
20251 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
20252 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
20253 }
20254
20255 #[test]
20256 fn catalog_table_missing_storage_fails_without_recreating_it() {
20257 let directory = tempfile::tempdir().unwrap();
20258 let table_dir = {
20259 let database = Database::create(directory.path()).unwrap();
20260 database.create_table("docs", test_schema()).unwrap();
20261 directory
20262 .path()
20263 .join(TABLES_DIR)
20264 .join(database.table_id("docs").unwrap().to_string())
20265 };
20266 std::fs::remove_dir_all(&table_dir).unwrap();
20267
20268 assert!(matches!(
20269 Database::open(directory.path()),
20270 Err(MongrelError::NotFound(_))
20271 ));
20272 assert!(!table_dir.exists());
20273 }
20274
20275 #[test]
20276 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
20277 let directory = tempfile::tempdir().unwrap();
20278 let database = std::sync::Arc::new(
20279 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
20280 );
20281 database.create_user("alice", "old-password").unwrap();
20282 let old_identity = database.user_identity("alice").unwrap();
20283 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
20284 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
20285 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
20286 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
20287
20288 std::thread::scope(|scope| {
20289 let authenticate = {
20290 let database = std::sync::Arc::clone(&database);
20291 scope.spawn(move || {
20292 database.authenticate_principal_inner("alice", "old-password", || {
20293 verified_tx.send(()).unwrap();
20294 resume_rx.recv().unwrap();
20295 })
20296 })
20297 };
20298 verified_rx.recv().unwrap();
20299 let mutate = {
20300 let database = std::sync::Arc::clone(&database);
20301 scope.spawn(move || {
20302 mutation_started_tx.send(()).unwrap();
20303 database.drop_user("alice").unwrap();
20304 database.create_user("alice", "new-password").unwrap();
20305 mutation_done_tx.send(()).unwrap();
20306 })
20307 };
20308 mutation_started_rx.recv().unwrap();
20309 assert!(mutation_done_rx
20310 .recv_timeout(std::time::Duration::from_millis(50))
20311 .is_err());
20312 resume_tx.send(()).unwrap();
20313 let principal = authenticate.join().unwrap().unwrap().unwrap();
20314 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
20315 mutate.join().unwrap();
20316 });
20317
20318 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
20319 assert!(database
20320 .authenticate_principal("alice", "old-password")
20321 .unwrap()
20322 .is_none());
20323 assert!(database
20324 .authenticate_principal("alice", "new-password")
20325 .unwrap()
20326 .is_some());
20327 }
20328
20329 #[test]
20330 fn homogeneous_batch_summarizes_to_one_permission_decision() {
20331 let staging = (0..10_050)
20332 .map(|_| {
20333 (
20334 7,
20335 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
20336 )
20337 })
20338 .collect::<Vec<_>>();
20339
20340 let needs = summarize_write_permissions(&staging);
20341 let table = needs.get(&7).unwrap();
20342 assert_eq!(needs.len(), 1);
20343 assert!(table.insert);
20344 assert_eq!(table.insert_columns, [1, 2]);
20345 assert!(!table.update);
20346 assert!(!table.delete);
20347 assert!(!table.truncate);
20348 }
20349
20350 #[test]
20351 fn mixed_writes_union_columns_and_preserve_empty_operations() {
20352 let staging = vec![
20353 (7, Staged::Put(vec![(2, Value::Int64(2))])),
20354 (7, Staged::Put(vec![(1, Value::Int64(1))])),
20355 (
20356 7,
20357 Staged::Update {
20358 row_id: RowId(1),
20359 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
20360 changed_columns: vec![2],
20361 },
20362 ),
20363 (7, Staged::Delete(RowId(2))),
20364 (8, Staged::Truncate),
20365 ];
20366
20367 let needs = summarize_write_permissions(&staging);
20368 let table = needs.get(&7).unwrap();
20369 assert_eq!(table.insert_columns, [1, 2]);
20370 assert!(table.update);
20371 assert_eq!(table.update_columns, [2]);
20372 assert!(table.delete);
20373 assert!(needs.get(&8).unwrap().truncate);
20374 }
20375
20376 #[test]
20377 fn final_permission_decisions_do_not_scale_with_rows() {
20378 let credentialless_dir = tempfile::tempdir().unwrap();
20379 let credentialless = Database::create(credentialless_dir.path()).unwrap();
20380 credentialless.create_table("docs", test_schema()).unwrap();
20381 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20382 credentialless
20383 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
20384 .unwrap();
20385 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
20386
20387 let authenticated_dir = tempfile::tempdir().unwrap();
20388 let authenticated =
20389 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
20390 .unwrap();
20391 authenticated.create_table("docs", test_schema()).unwrap();
20392 let admin = authenticated.resolve_principal("admin").unwrap();
20393 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20394 authenticated
20395 .validate_write_permissions(
20396 &puts(authenticated.table_id("docs").unwrap()),
20397 Some(&admin),
20398 None,
20399 )
20400 .unwrap();
20401 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20402 }
20403
20404 #[test]
20405 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
20406 let dir = tempfile::tempdir().unwrap();
20407 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20408 db.create_table("docs", test_schema()).unwrap();
20409 let admin = db.resolve_principal("admin").unwrap();
20410 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20411
20412 let mut transaction = db.begin_as(Some(admin));
20413 transaction
20414 .delete_batch("docs", (0..100).map(RowId).collect())
20415 .unwrap();
20416 transaction.commit().unwrap();
20417
20418 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
20419 }
20420
20421 #[test]
20422 fn truncate_validation_checks_admin_once_for_all_tables() {
20423 let dir = tempfile::tempdir().unwrap();
20424 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
20425 db.create_table("first", test_schema()).unwrap();
20426 db.create_table("second", test_schema()).unwrap();
20427 let admin = db.resolve_principal("admin").unwrap();
20428 let staging = vec![
20429 (db.table_id("first").unwrap(), Staged::Truncate),
20430 (db.table_id("second").unwrap(), Staged::Truncate),
20431 ];
20432
20433 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
20434 db.validate_write_permissions(&staging, Some(&admin), None)
20435 .unwrap();
20436 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
20437 }
20438
20439 #[test]
20440 fn one_table_commit_batches_structural_work() {
20441 let dir = tempfile::tempdir().unwrap();
20442 let db = Database::create(dir.path()).unwrap();
20443 db.create_table("docs", test_schema()).unwrap();
20444 let table_id = db.table_id("docs").unwrap();
20445
20446 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
20447 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20448 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20449 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20450 db.transaction(|transaction| {
20451 for id in 0..100 {
20452 transaction.put("docs", vec![(1, Value::Int64(id))])?;
20453 }
20454 Ok(())
20455 })
20456 .unwrap();
20457
20458 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
20459 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20460 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20461 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20462
20463 let puts = crate::wal::SharedWal::replay(dir.path())
20464 .unwrap()
20465 .into_iter()
20466 .filter_map(|record| match record.op {
20467 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
20468 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
20469 .unwrap()
20470 .len(),
20471 ),
20472 _ => None,
20473 })
20474 .collect::<Vec<_>>();
20475 assert_eq!(puts, [100]);
20476
20477 let row_ids = db
20478 .table("docs")
20479 .unwrap()
20480 .lock()
20481 .visible_rows(db.snapshot().0)
20482 .unwrap()
20483 .into_iter()
20484 .take(2)
20485 .map(|row| row.row_id)
20486 .collect::<Vec<_>>();
20487 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
20488 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
20489 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
20490 db.transaction(|transaction| {
20491 for row_id in row_ids {
20492 transaction.delete("docs", row_id)?;
20493 }
20494 Ok(())
20495 })
20496 .unwrap();
20497 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20498 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
20499 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
20500
20501 let deletes = crate::wal::SharedWal::replay(dir.path())
20502 .unwrap()
20503 .into_iter()
20504 .filter_map(|record| match record.op {
20505 crate::wal::Op::Delete {
20506 table_id: id,
20507 row_ids,
20508 } if id == table_id => Some(row_ids.len()),
20509 _ => None,
20510 })
20511 .collect::<Vec<_>>();
20512 assert_eq!(deletes, [2]);
20513 }
20514
20515 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
20516 (0..10_050)
20517 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
20518 .collect()
20519 }
20520
20521 fn test_schema() -> Schema {
20522 Schema {
20523 columns: vec![ColumnDef {
20524 id: 1,
20525 name: "id".into(),
20526 ty: TypeId::Int64,
20527 flags: crate::schema::ColumnFlags::empty()
20528 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20529 default_value: None,
20530 embedding_source: None,
20531 }],
20532 ..Schema::default()
20533 }
20534 }
20535}
20536
20537#[cfg(test)]
20538mod cdc_bounds_tests {
20539 use super::*;
20540
20541 #[test]
20542 fn retained_byte_limit_rejects_without_allocating_payload() {
20543 let mut retained = 0;
20544 let error = charge_cdc_bytes(
20545 &mut retained,
20546 CDC_MAX_RETAINED_BYTES.saturating_add(1),
20547 "CDC retained bytes",
20548 )
20549 .unwrap_err();
20550 assert!(matches!(
20551 error,
20552 MongrelError::ResourceLimitExceeded {
20553 resource: "CDC retained bytes",
20554 ..
20555 }
20556 ));
20557 }
20558
20559 #[test]
20560 fn row_json_estimate_accounts_for_byte_array_expansion() {
20561 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
20562 .with_column(1, Value::Bytes(vec![0; 1024]));
20563 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
20564 }
20565}
20566
20567#[cfg(test)]
20568mod generation_metrics_tests {
20569 use super::*;
20570 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
20571
20572 #[test]
20573 fn legacy_cow_fallback_is_measured() {
20574 let dir = tempfile::tempdir().unwrap();
20575 let table = Table::create(
20576 dir.path(),
20577 Schema {
20578 columns: vec![ColumnDef {
20579 id: 1,
20580 name: "id".into(),
20581 ty: TypeId::Int64,
20582 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
20583 default_value: None,
20584 embedding_source: None,
20585 }],
20586 ..Schema::default()
20587 },
20588 1,
20589 )
20590 .unwrap();
20591 let handle = TableHandle::from_table(table);
20592 let held = match &handle.inner {
20593 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
20594 TableHandleInner::Direct(_) => unreachable!(),
20595 };
20596
20597 handle.lock().set_sync_byte_threshold(1);
20598
20599 let stats = handle.generation_stats();
20600 assert_eq!(stats.cow_clone_count, 1);
20601 assert!(stats.estimated_cow_clone_bytes > 0);
20602 drop(held);
20603 }
20604}
20605
20606#[cfg(test)]
20607mod trigger_engine_tests {
20608 use super::*;
20609
20610 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
20611 WriteEvent {
20612 table: "test".into(),
20613 kind: TriggerEvent::Insert,
20614 new: Some(TriggerRowImage {
20615 columns: new_cells.iter().cloned().collect(),
20616 }),
20617 old: Some(TriggerRowImage {
20618 columns: old_cells.iter().cloned().collect(),
20619 }),
20620 changed_columns: Vec::new(),
20621 op_indices: Vec::new(),
20622 put_idx: None,
20623 trigger_stack: Vec::new(),
20624 }
20625 }
20626
20627 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
20628 WriteEvent {
20629 table: "test".into(),
20630 kind: TriggerEvent::Insert,
20631 new: Some(TriggerRowImage {
20632 columns: new_cells.iter().cloned().collect(),
20633 }),
20634 old: None,
20635 changed_columns: Vec::new(),
20636 op_indices: Vec::new(),
20637 put_idx: None,
20638 trigger_stack: Vec::new(),
20639 }
20640 }
20641
20642 #[test]
20643 fn value_order_int64_vs_float64() {
20644 assert_eq!(
20645 value_order(&Value::Int64(5), &Value::Float64(5.0)),
20646 Some(std::cmp::Ordering::Equal)
20647 );
20648 assert_eq!(
20649 value_order(&Value::Int64(5), &Value::Float64(3.0)),
20650 Some(std::cmp::Ordering::Greater)
20651 );
20652 assert_eq!(
20653 value_order(&Value::Int64(2), &Value::Float64(3.0)),
20654 Some(std::cmp::Ordering::Less)
20655 );
20656 }
20657
20658 #[test]
20659 fn value_order_null_returns_none() {
20660 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
20661 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
20662 assert_eq!(value_order(&Value::Null, &Value::Null), None);
20663 }
20664
20665 #[test]
20666 fn value_order_cross_group_returns_none() {
20667 assert_eq!(
20668 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
20669 None
20670 );
20671 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
20672 assert_eq!(
20673 value_order(
20674 &Value::Embedding(vec![1.0, 2.0]),
20675 &Value::Embedding(vec![1.0, 2.0])
20676 ),
20677 None
20678 );
20679 }
20680
20681 #[test]
20682 fn eval_trigger_expr_ranges_and_booleans() {
20683 let expr = TriggerExpr::And {
20684 left: Box::new(TriggerExpr::Gt {
20685 left: TriggerValue::NewColumn(1),
20686 right: TriggerValue::Literal(Value::Int64(0)),
20687 }),
20688 right: Box::new(TriggerExpr::Lte {
20689 left: TriggerValue::NewColumn(1),
20690 right: TriggerValue::Literal(Value::Int64(100)),
20691 }),
20692 };
20693 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
20694 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
20695 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
20696
20697 let or_expr = TriggerExpr::Or {
20698 left: Box::new(TriggerExpr::Lt {
20699 left: TriggerValue::NewColumn(1),
20700 right: TriggerValue::Literal(Value::Int64(0)),
20701 }),
20702 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
20703 TriggerValue::OldColumn(2),
20704 )))),
20705 };
20706 assert!(eval_trigger_expr(
20707 &or_expr,
20708 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
20709 )
20710 .unwrap());
20711 assert!(!eval_trigger_expr(
20712 &or_expr,
20713 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
20714 )
20715 .unwrap());
20716
20717 assert!(eval_trigger_expr(
20718 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
20719 &event_insert(&[])
20720 )
20721 .unwrap());
20722 assert!(!eval_trigger_expr(
20723 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
20724 &event_insert(&[])
20725 )
20726 .unwrap());
20727 assert!(!eval_trigger_expr(
20728 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
20729 &event_insert(&[])
20730 )
20731 .unwrap());
20732 }
20733}
20734
20735#[cfg(test)]
20736mod core_resource_tests {
20737 use super::*;
20738
20739 fn int_pk_schema() -> Schema {
20740 Schema {
20741 columns: vec![ColumnDef {
20742 id: 1,
20743 name: "id".into(),
20744 ty: TypeId::Int64,
20745 flags: crate::schema::ColumnFlags::empty()
20746 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20747 default_value: None,
20748 embedding_source: None,
20749 }],
20750 ..Schema::default()
20751 }
20752 }
20753
20754 #[test]
20755 fn open_constructs_governor_spill_and_jobs_with_documented_defaults() {
20756 let dir = tempfile::tempdir().unwrap();
20757 let db = Database::create(dir.path()).unwrap();
20758 assert_eq!(
20759 db.memory_governor().max_bytes(),
20760 DEFAULT_MEMORY_BUDGET_BYTES
20761 );
20762 assert_eq!(
20763 db.spill_manager().config().global_bytes,
20764 DEFAULT_TEMP_DISK_BUDGET_BYTES
20765 );
20766 assert!(db.job_registry().list().is_empty());
20767 assert_eq!(
20769 db.resource_groups().len(),
20770 crate::resource::WorkloadClass::ALL.len()
20771 );
20772 assert!(db.resource_groups().get("control").is_some());
20773 assert!(db.embedding_providers().list_ids().is_empty());
20774 let err = db
20776 .embedding_providers()
20777 .embed(
20778 &crate::embedding::EmbeddingSource::SuppliedByApplication,
20779 &["text"],
20780 4,
20781 )
20782 .unwrap_err();
20783 assert!(matches!(
20784 err,
20785 crate::embedding::EmbeddingError::SuppliedByApplication
20786 ));
20787 }
20788
20789 #[test]
20790 fn lock_rows_for_update_acquires_exclusive_row_locks() {
20791 use crate::locks::LockKey;
20792 use crate::rowid::RowId;
20793
20794 let dir = tempfile::tempdir().unwrap();
20795 let db = Database::create(dir.path()).unwrap();
20796 let txn_id = db.allocate_lock_txn_id().unwrap();
20797 let rid = RowId(42);
20798 db.lock_rows_for_update(txn_id, 7, &[rid], None).unwrap();
20799 assert!(db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20800 db.release_txn_locks(txn_id);
20801 assert!(!db.lock_manager().holds(txn_id, &LockKey::row(7, rid)));
20802 }
20803
20804 #[test]
20805 fn open_with_options_sizes_the_core_budgets() {
20806 let dir = tempfile::tempdir().unwrap();
20807 let db = Database::create(dir.path()).unwrap();
20808 drop(db);
20809 let db = Database::open_with_options(
20810 dir.path(),
20811 OpenOptions::default()
20812 .with_memory_budget_bytes(256 * 1024 * 1024)
20813 .with_temp_disk_budget_bytes(16 * 1024 * 1024),
20814 )
20815 .unwrap();
20816 assert_eq!(db.memory_governor().max_bytes(), 256 * 1024 * 1024);
20817 assert_eq!(db.spill_manager().config().global_bytes, 16 * 1024 * 1024);
20818 }
20819
20820 #[test]
20821 fn zero_budgets_are_rejected() {
20822 let dir = tempfile::tempdir().unwrap();
20823 let db = Database::create(dir.path()).unwrap();
20824 drop(db);
20825 let result = Database::open_with_options(
20826 dir.path(),
20827 OpenOptions::default().with_memory_budget_bytes(0),
20828 );
20829 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20830 let result = Database::open_with_options(
20831 dir.path(),
20832 OpenOptions::default().with_temp_disk_budget_bytes(0),
20833 );
20834 assert!(matches!(result, Err(MongrelError::InvalidArgument(_))));
20835 }
20836
20837 #[test]
20838 fn page_caches_reserve_under_the_governor() {
20839 let dir = tempfile::tempdir().unwrap();
20840 let db = Database::create(dir.path()).unwrap();
20841 db.create_table("t", int_pk_schema()).unwrap();
20842 let mut txn = db.begin();
20843 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20844 txn.commit().unwrap();
20845 let stats = db.memory_governor().stats();
20849 assert_eq!(
20850 stats.usage_for(crate::memory::MemoryClass::PageCache),
20851 db.page_cache.used_bytes()
20852 );
20853 assert_eq!(
20854 stats.usage_for(crate::memory::MemoryClass::DecodedCache),
20855 db.decoded_cache.used_bytes()
20856 );
20857 assert_eq!(
20858 db.memory_governor().reclaimable_bytes(),
20859 db.page_cache.used_bytes() + db.decoded_cache.used_bytes()
20860 );
20861 let _ = db.memory_governor().evict_reclaimable(1024 * 1024);
20863 }
20864
20865 #[test]
20866 fn job_registry_persists_across_reopen() {
20867 let dir = tempfile::tempdir().unwrap();
20868 let db = Database::create(dir.path()).unwrap();
20869 db.create_table("t", int_pk_schema()).unwrap();
20870 let job_id = db
20871 .job_registry()
20872 .submit(
20873 crate::jobs::JobKind::IndexBuild,
20874 crate::jobs::JobTarget {
20875 table: "t".to_string(),
20876 index: Some("idx".to_string()),
20877 },
20878 )
20879 .unwrap();
20880 drop(db);
20881 let db = Database::open(dir.path()).unwrap();
20882 let record = db.job_registry().get(job_id).expect("job survives reopen");
20883 assert_eq!(record.state, crate::jobs::JobState::Pending);
20884 }
20885
20886 #[test]
20887 fn spill_manager_open_sweeps_stale_temp_tree() {
20888 let dir = tempfile::tempdir().unwrap();
20889 let db = Database::create(dir.path()).unwrap();
20890 let stale = dir.path().join("temp").join("spill").join("q-deadbeef");
20891 std::fs::create_dir_all(&stale).unwrap();
20892 std::fs::write(stale.join("chunk-0"), b"stale").unwrap();
20893 drop(db);
20894 let db = Database::open(dir.path()).unwrap();
20895 assert!(
20896 !stale.exists(),
20897 "the startup sweep removes stale spill files (S1E-004)"
20898 );
20899 let session = db
20901 .spill_manager()
20902 .begin_query(
20903 mongreldb_types::ids::QueryId::from_bytes([7u8; 16]),
20904 1024 * 1024,
20905 )
20906 .unwrap();
20907 assert_eq!(session.used(), 0);
20908 }
20909}
20910
20911#[cfg(test)]
20912mod version_pin_tests {
20913 use super::*;
20914
20915 fn int_pk_schema() -> Schema {
20916 Schema {
20917 columns: vec![ColumnDef {
20918 id: 1,
20919 name: "id".into(),
20920 ty: TypeId::Int64,
20921 flags: crate::schema::ColumnFlags::empty()
20922 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
20923 default_value: None,
20924 embedding_source: None,
20925 }],
20926 ..Schema::default()
20927 }
20928 }
20929
20930 fn pins_for(
20931 report: &[TablePinsReport],
20932 table: &str,
20933 source: crate::retention::PinSource,
20934 ) -> Option<crate::retention::PinInfo> {
20935 report
20936 .iter()
20937 .find(|entry| entry.table == table)
20938 .and_then(|entry| entry.pins.get(source).cloned())
20939 }
20940
20941 #[test]
20942 fn backup_boundary_registers_backup_pitr_pin() {
20943 let source = tempfile::tempdir().unwrap();
20944 let destination_parent = tempfile::tempdir().unwrap();
20945 let destination = destination_parent.path().join("backup");
20946 let db = Arc::new(Database::create(source.path()).unwrap());
20947 db.create_table("t", int_pk_schema()).unwrap();
20948 let mut txn = db.begin();
20949 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
20950 let boundary_epoch = txn.commit().unwrap();
20951
20952 let hold = Arc::new(std::sync::Barrier::new(2));
20953 let resume = Arc::new(std::sync::Barrier::new(2));
20954 db.__set_backup_hook({
20955 let hold = Arc::clone(&hold);
20956 let resume = Arc::clone(&resume);
20957 move || {
20958 hold.wait();
20959 resume.wait();
20960 }
20961 });
20962
20963 let backup = {
20964 let db = Arc::clone(&db);
20965 let destination = destination.clone();
20966 std::thread::spawn(move || db.hot_backup(destination))
20967 };
20968 hold.wait();
20969 let report = db.version_pins_report();
20972 let pin = pins_for(&report, "t", crate::retention::PinSource::BackupPitr)
20973 .expect("backup boundary must register a BackupPitr pin");
20974 assert_eq!(pin.oldest_epoch, boundary_epoch);
20975 assert!(pin.pin_count >= 1);
20976 resume.wait();
20977 backup.join().unwrap().unwrap();
20978
20979 let report = db.version_pins_report();
20980 assert!(
20981 pins_for(&report, "t", crate::retention::PinSource::BackupPitr).is_none(),
20982 "the BackupPitr pin releases when the backup finishes"
20983 );
20984 }
20985
20986 #[test]
20987 fn snapshot_and_read_generation_pins_surface_in_report() {
20988 let dir = tempfile::tempdir().unwrap();
20989 let db = Database::create(dir.path()).unwrap();
20990 db.create_table("t", int_pk_schema()).unwrap();
20991 let mut txn = db.begin();
20992 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
20993 txn.commit().unwrap();
20994
20995 let (_snapshot, guard) = db.snapshot();
20996 let report = db.version_pins_report();
20997 assert!(
20998 pins_for(
20999 &report,
21000 "t",
21001 crate::retention::PinSource::TransactionSnapshot
21002 )
21003 .is_some(),
21004 "a database snapshot projects the TransactionSnapshot source"
21005 );
21006 drop(guard);
21007
21008 let handle = db.table("t").unwrap();
21009 let (generation, _snapshot) = handle.read_generation_with_context(None).unwrap();
21010 let report = db.version_pins_report();
21011 assert!(
21012 pins_for(&report, "t", crate::retention::PinSource::ReadGeneration).is_some(),
21013 "a cloned read generation registers a ReadGeneration pin"
21014 );
21015 drop(generation);
21016
21017 let report = db.version_pins_report();
21018 let entry = report.iter().find(|entry| entry.table == "t").unwrap();
21019 assert!(
21020 entry
21021 .pins
21022 .get(crate::retention::PinSource::BackupPitr)
21023 .is_none()
21024 && entry
21025 .pins
21026 .get(crate::retention::PinSource::Replication)
21027 .is_none()
21028 && entry
21029 .pins
21030 .get(crate::retention::PinSource::OnlineIndexBuild)
21031 .is_none(),
21032 "untaken sources stay absent from the report"
21033 );
21034 }
21035}
21036
21037#[cfg(test)]
21038mod lock_manager_tests {
21039 use super::*;
21040 use crate::locks::LockKey;
21041
21042 fn col(id: u16, name: &str, ty: TypeId, flags: crate::schema::ColumnFlags) -> ColumnDef {
21043 ColumnDef {
21044 id,
21045 name: name.into(),
21046 ty,
21047 flags,
21048 default_value: None,
21049 embedding_source: None,
21050 }
21051 }
21052
21053 fn unique_schema() -> Schema {
21054 let mut constraints = crate::constraint::TableConstraints::default();
21055 constraints
21056 .uniques
21057 .push(crate::constraint::UniqueConstraint {
21058 id: 1,
21059 name: "users_email_unique".into(),
21060 columns: vec![1],
21061 });
21062 Schema {
21063 columns: vec![
21064 col(
21065 0,
21066 "id",
21067 TypeId::Int64,
21068 crate::schema::ColumnFlags::empty()
21069 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21070 ),
21071 col(
21072 1,
21073 "email",
21074 TypeId::Bytes,
21075 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
21076 ),
21077 ],
21078 constraints,
21079 ..Schema::default()
21080 }
21081 }
21082
21083 fn parent_schema() -> Schema {
21084 Schema {
21085 columns: vec![col(
21086 0,
21087 "id",
21088 TypeId::Int64,
21089 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::PRIMARY_KEY),
21090 )],
21091 ..Schema::default()
21092 }
21093 }
21094
21095 fn child_schema() -> Schema {
21096 let mut constraints = crate::constraint::TableConstraints::default();
21097 constraints
21098 .foreign_keys
21099 .push(crate::constraint::ForeignKey {
21100 id: 1,
21101 name: "child_parent_fk".into(),
21102 columns: vec![1],
21103 ref_table: "parent".into(),
21104 ref_columns: vec![0],
21105 on_delete: crate::constraint::FkAction::Restrict,
21106 on_update: crate::constraint::FkAction::Restrict,
21107 });
21108 Schema {
21109 columns: vec![
21110 col(
21111 0,
21112 "id",
21113 TypeId::Int64,
21114 crate::schema::ColumnFlags::empty()
21115 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21116 ),
21117 col(
21118 1,
21119 "pid",
21120 TypeId::Int64,
21121 crate::schema::ColumnFlags::empty().with(crate::schema::ColumnFlags::NULLABLE),
21122 ),
21123 ],
21124 constraints,
21125 ..Schema::default()
21126 }
21127 }
21128
21129 fn auto_inc_schema() -> Schema {
21130 Schema {
21131 columns: vec![col(
21132 0,
21133 "id",
21134 TypeId::Int64,
21135 crate::schema::ColumnFlags::empty()
21136 .with(crate::schema::ColumnFlags::PRIMARY_KEY)
21137 .with(crate::schema::ColumnFlags::AUTO_INCREMENT),
21138 )],
21139 ..Schema::default()
21140 }
21141 }
21142
21143 fn pk_lock_key(table_id: u64, value: i64) -> LockKey {
21144 let mut key = b"pk:".to_vec();
21145 key.extend_from_slice(&Value::Int64(value).encode_key());
21146 LockKey::key(table_id, key)
21147 }
21148
21149 #[test]
21150 fn unique_claims_serialize_concurrent_commits() {
21151 let dir = tempfile::tempdir().unwrap();
21152 let db = Arc::new(Database::create(dir.path()).unwrap());
21153 let table_id = db.create_table("users", unique_schema()).unwrap();
21154 let pk_key = pk_lock_key(table_id, 1);
21155 let entered = Arc::new(std::sync::Barrier::new(2));
21156 let resume = Arc::new(std::sync::Barrier::new(2));
21157 let parked = Arc::new(AtomicBool::new(false));
21158 db.__set_catalog_commit_hook({
21159 let entered = Arc::clone(&entered);
21160 let resume = Arc::clone(&resume);
21161 let parked = Arc::clone(&parked);
21162 move || {
21163 if !parked.swap(true, Ordering::SeqCst) {
21166 entered.wait();
21167 resume.wait();
21168 }
21169 }
21170 });
21171
21172 let mut txn_a = db.begin();
21173 txn_a
21174 .put(
21175 "users",
21176 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21177 )
21178 .unwrap();
21179 let a_id = txn_a.txn_id();
21180 let (a_tx, a_rx) = std::sync::mpsc::channel();
21181 let (b_tx, b_rx) = std::sync::mpsc::channel();
21182 std::thread::scope(|scope| {
21183 scope.spawn(|| {
21184 a_tx.send(txn_a.commit()).unwrap();
21185 });
21186 entered.wait();
21187 assert!(
21189 db.lock_manager().holds(a_id, &pk_key),
21190 "primary-key claim must be held until the commit ends"
21191 );
21192 let mut uq_key = format!("uq{}:", 1).into_bytes();
21193 let cells_map: HashMap<u16, Value> = [(1u16, Value::Bytes(b"a@x".to_vec()))]
21194 .into_iter()
21195 .collect();
21196 uq_key.extend_from_slice(
21197 &crate::constraint::encode_composite_key(&[1], &cells_map).unwrap(),
21198 );
21199 assert!(
21200 db.lock_manager()
21201 .holds(a_id, &LockKey::key(table_id, uq_key)),
21202 "declared-unique claim must be held until the commit ends"
21203 );
21204
21205 let mut txn_b = db.begin();
21206 txn_b
21207 .put(
21208 "users",
21209 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"b@x".to_vec()))],
21210 )
21211 .unwrap();
21212 scope.spawn(|| {
21213 b_tx.send(txn_b.commit()).unwrap();
21214 });
21215 std::thread::sleep(std::time::Duration::from_millis(100));
21216 assert!(
21217 b_rx.try_recv().is_err(),
21218 "the concurrent claim must block until A ends its transaction"
21219 );
21220 resume.wait();
21221 assert!(a_rx.recv().unwrap().is_ok());
21222 let b_result = b_rx.recv().unwrap();
21223 assert!(
21224 matches!(b_result, Err(MongrelError::Conflict(_))),
21225 "the loser surfaces a conflict after serializing: {b_result:?}"
21226 );
21227 });
21228 assert!(
21229 !db.lock_manager().holds(a_id, &pk_key),
21230 "no phantom holds remain after the commit"
21231 );
21232 }
21233
21234 #[test]
21235 fn ddl_waits_for_inflight_dml_commit_on_schema_barrier() {
21236 let dir = tempfile::tempdir().unwrap();
21237 let db = Arc::new(Database::create(dir.path()).unwrap());
21238 db.create_table("parent", parent_schema()).unwrap();
21239 db.create_table("child", child_schema()).unwrap();
21240 let mut seed = db.begin();
21241 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21242 seed.commit().unwrap();
21243
21244 let entered = Arc::new(std::sync::Barrier::new(2));
21245 let resume = Arc::new(std::sync::Barrier::new(2));
21246 db.__set_fk_lock_hook({
21247 let entered = Arc::clone(&entered);
21248 let resume = Arc::clone(&resume);
21249 move || {
21250 entered.wait();
21251 resume.wait();
21252 }
21253 });
21254
21255 let mut txn_a = db.begin();
21256 txn_a
21257 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(1))])
21258 .unwrap();
21259 let a_id = txn_a.txn_id();
21260 let (a_tx, a_rx) = std::sync::mpsc::channel();
21261 let (ddl_tx, ddl_rx) = std::sync::mpsc::channel();
21262 std::thread::scope(|scope| {
21263 scope.spawn(|| {
21264 a_tx.send(txn_a.commit()).unwrap();
21265 });
21266 entered.wait();
21267 assert!(
21269 db.lock_manager().holds(a_id, &LockKey::schema_barrier()),
21270 "DML holds the schema barrier Shared for its commit"
21271 );
21272 let db = Arc::clone(&db);
21273 scope.spawn(move || {
21274 ddl_tx.send(db.drop_table("parent")).unwrap();
21275 });
21276 std::thread::sleep(std::time::Duration::from_millis(100));
21277 assert!(
21278 ddl_rx.try_recv().is_err(),
21279 "DDL must wait on the Exclusive schema barrier while DML is in flight"
21280 );
21281 resume.wait();
21282 let a_result = a_rx.recv().unwrap();
21288 match &a_result {
21289 Ok(_) => {}
21290 Err(MongrelError::Conflict(message)) => {
21291 assert!(
21292 message.contains("security policy changed during write"),
21293 "unexpected commit conflict: {message}"
21294 );
21295 }
21296 other => panic!("unexpected commit outcome: {other:?}"),
21297 }
21298 assert!(ddl_rx.recv().unwrap().is_ok());
21299 });
21300 assert!(!db.lock_manager().holds(a_id, &LockKey::schema_barrier()));
21301 }
21302
21303 #[test]
21304 fn auto_increment_sequence_barrier_held_until_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 let entered = Arc::new(std::sync::Barrier::new(2));
21310 let resume = Arc::new(std::sync::Barrier::new(2));
21311 let parked = Arc::new(AtomicBool::new(false));
21312 db.__set_catalog_commit_hook({
21313 let entered = Arc::clone(&entered);
21314 let resume = Arc::clone(&resume);
21315 let parked = Arc::clone(&parked);
21316 move || {
21317 if !parked.swap(true, Ordering::SeqCst) {
21318 entered.wait();
21319 resume.wait();
21320 }
21321 }
21322 });
21323
21324 let mut txn_a = db.begin();
21325 txn_a.put("seq_t", vec![(0, Value::Null)]).unwrap();
21326 let a_id = txn_a.txn_id();
21327 assert!(
21329 db.lock_manager().holds(a_id, &barrier_key),
21330 "sequence allocation takes the barrier at stage time"
21331 );
21332 let (a_tx, a_rx) = std::sync::mpsc::channel();
21333 std::thread::scope(|scope| {
21334 scope.spawn(|| {
21335 a_tx.send(txn_a.commit()).unwrap();
21336 });
21337 entered.wait();
21338 assert!(
21339 db.lock_manager().holds(a_id, &barrier_key),
21340 "the barrier is held through the commit"
21341 );
21342 resume.wait();
21343 assert!(a_rx.recv().unwrap().is_ok());
21344 });
21345 assert!(
21346 !db.lock_manager().holds(a_id, &barrier_key),
21347 "the barrier releases when the commit ends"
21348 );
21349 }
21350
21351 #[test]
21352 fn fk_wait_for_cycle_surfaces_deadlock_victim() {
21353 let dir = tempfile::tempdir().unwrap();
21354 let db = Database::create(dir.path()).unwrap();
21355 db.create_table("parent", parent_schema()).unwrap();
21356 db.create_table("child", child_schema()).unwrap();
21357 let mut seed = db.begin();
21358 seed.put("parent", vec![(0, Value::Int64(1))]).unwrap();
21359 seed.put("parent", vec![(0, Value::Int64(2))]).unwrap();
21360 seed.commit().unwrap();
21361 let (rid1, rid2) = {
21362 let handle = db.table("parent").unwrap();
21363 let table = handle.lock();
21364 let rid = |pk: i64| {
21365 table
21366 .lookup_pk(&Value::Int64(pk).encode_key())
21367 .expect("seeded parent row")
21368 };
21369 (rid(1), rid(2))
21370 };
21371
21372 let rendezvous = Arc::new(std::sync::Barrier::new(2));
21377 let calls = Arc::new(AtomicUsize::new(0));
21378 db.__set_fk_lock_hook({
21379 let rendezvous = Arc::clone(&rendezvous);
21380 let calls = Arc::clone(&calls);
21381 move || {
21382 if calls.fetch_add(1, Ordering::SeqCst) < 2 {
21383 rendezvous.wait();
21384 }
21385 }
21386 });
21387
21388 let mut txn_a = db.begin();
21391 txn_a.delete("parent", rid1).unwrap();
21392 txn_a
21393 .put("child", vec![(0, Value::Int64(100)), (1, Value::Int64(2))])
21394 .unwrap();
21395 let mut txn_b = db.begin();
21396 txn_b.delete("parent", rid2).unwrap();
21397 txn_b
21398 .put("child", vec![(0, Value::Int64(101)), (1, Value::Int64(1))])
21399 .unwrap();
21400 let b_id = txn_b.txn_id();
21401
21402 let (a_tx, a_rx) = std::sync::mpsc::channel();
21403 let (b_tx, b_rx) = std::sync::mpsc::channel();
21404 std::thread::scope(|scope| {
21405 scope.spawn(|| {
21406 a_tx.send(txn_a.commit()).unwrap();
21407 });
21408 scope.spawn(|| {
21409 b_tx.send(txn_b.commit()).unwrap();
21410 });
21411 let a_result = a_rx.recv().unwrap();
21412 let b_result = b_rx.recv().unwrap();
21413 assert!(
21414 a_result.is_ok(),
21415 "the survivor commits once the victim releases: {a_result:?}"
21416 );
21417 match b_result {
21418 Err(MongrelError::Deadlock { victim, .. }) => {
21419 assert_eq!(victim, b_id, "the youngest transaction is the victim");
21420 }
21421 other => panic!("the victim must surface a deadlock, got {other:?}"),
21422 }
21423 });
21424 let fk_key = |table: &str, pk: i64| {
21426 let table_id = db.table_id(table).unwrap();
21427 let mut key = b"fk:".to_vec();
21428 key.extend_from_slice(&Value::Int64(pk).encode_key());
21429 LockKey::key(table_id, key)
21430 };
21431 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 2)));
21432 assert!(!db.lock_manager().holds(b_id, &fk_key("parent", 1)));
21433 }
21434
21435 #[test]
21436 fn locks_release_after_commit_rollback_and_failed_commit() {
21437 let dir = tempfile::tempdir().unwrap();
21438 let db = Database::create(dir.path()).unwrap();
21439 let table_id = db.create_table("seq_t", auto_inc_schema()).unwrap();
21440 let barrier_key = LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str());
21441
21442 let mut txn = db.begin();
21444 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21445 let committed_id = txn.txn_id();
21446 txn.commit().unwrap();
21447 assert!(!db.lock_manager().holds(committed_id, &barrier_key));
21448
21449 let mut txn = db.begin();
21451 txn.put("seq_t", vec![(0, Value::Null)]).unwrap();
21452 let rolled_back_id = txn.txn_id();
21453 assert!(db.lock_manager().holds(rolled_back_id, &barrier_key));
21454 txn.rollback();
21455 assert!(
21456 !db.lock_manager().holds(rolled_back_id, &barrier_key),
21457 "rollback must not leave phantom holds"
21458 );
21459
21460 db.create_table("users", unique_schema()).unwrap();
21463 let users_id = db.table_id("users").unwrap();
21464 let mut seed = db.begin();
21465 seed.put(
21466 "users",
21467 vec![(0, Value::Int64(1)), (1, Value::Bytes(b"a@x".to_vec()))],
21468 )
21469 .unwrap();
21470 seed.commit().unwrap();
21471 let mut txn = db.begin();
21472 txn.put(
21475 "users",
21476 vec![(0, Value::Int64(2)), (1, Value::Bytes(b"a@x".to_vec()))],
21477 )
21478 .unwrap();
21479 let failed_id = txn.txn_id();
21480 let result = txn.commit();
21481 assert!(matches!(result, Err(MongrelError::Conflict(_))));
21482 assert!(
21483 !db.lock_manager()
21484 .holds(failed_id, &pk_lock_key(users_id, 2)),
21485 "a failed commit must not leave phantom holds"
21486 );
21487 }
21488}
21489
21490#[cfg(test)]
21491mod lifecycle_tests {
21492 use super::*;
21493
21494 fn int_pk_schema() -> Schema {
21495 Schema {
21496 columns: vec![ColumnDef {
21497 id: 1,
21498 name: "id".into(),
21499 ty: TypeId::Int64,
21500 flags: crate::schema::ColumnFlags::empty()
21501 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21502 default_value: None,
21503 embedding_source: None,
21504 }],
21505 ..Schema::default()
21506 }
21507 }
21508
21509 #[test]
21510 fn poisoned_core_rejects_operations_with_typed_errors() {
21511 let dir = tempfile::tempdir().unwrap();
21512 let db = Database::create(dir.path()).unwrap();
21513 db.create_table("t", int_pk_schema()).unwrap();
21514 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Open);
21515
21516 db.poisoned.store(true, Ordering::Relaxed);
21521 db.lifecycle.poison();
21522 assert_eq!(db.lifecycle_state(), crate::core::LifecycleState::Poisoned);
21523
21524 let error = db.gc().unwrap_err();
21527 assert!(
21528 matches!(error, MongrelError::Conflict(_)),
21529 "gc must reject on a poisoned core: {error:?}"
21530 );
21531 let error = db.compact().unwrap_err();
21532 assert!(
21533 matches!(error, MongrelError::Conflict(_)),
21534 "compact must reject on a poisoned core: {error:?}"
21535 );
21536 assert!(db.operation_guard().is_err());
21537 let error = db.create_table("t2", int_pk_schema()).unwrap_err();
21540 assert!(
21541 error.to_string().contains("database poisoned"),
21542 "the legacy poison error still wins where it existed: {error:?}"
21543 );
21544 let mut txn = db.begin();
21545 txn.put("t", vec![(1, Value::Int64(2))]).unwrap();
21546 assert!(txn
21547 .commit()
21548 .unwrap_err()
21549 .to_string()
21550 .contains("database poisoned"));
21551 }
21552
21553 #[test]
21554 fn shutdown_waits_for_operation_guards_to_drain() {
21555 let dir = tempfile::tempdir().unwrap();
21556 let db = Arc::new(Database::create(dir.path()).unwrap());
21557 db.create_table("t", int_pk_schema()).unwrap();
21558 let guard = db.operation_guard().unwrap();
21561 let (started_tx, started_rx) = std::sync::mpsc::channel();
21562 let (done_tx, done_rx) = std::sync::mpsc::channel();
21563 let shutdown_thread = std::thread::spawn(move || {
21564 started_tx.send(()).unwrap();
21565 let result = db.shutdown();
21566 let _ = done_tx.send(result);
21567 });
21568 started_rx.recv().unwrap();
21569 std::thread::sleep(std::time::Duration::from_millis(100));
21570 assert!(
21571 done_rx.try_recv().is_err(),
21572 "shutdown must wait for the outstanding guard to drain"
21573 );
21574 drop(guard);
21575 shutdown_thread.join().unwrap();
21576 assert!(
21577 done_rx.recv().unwrap().is_ok(),
21578 "shutdown completes once the guard drops"
21579 );
21580 }
21581}
21582
21583#[cfg(test)]
21584mod commit_ts_ledger_tests {
21585 use super::*;
21586 use crate::memtable::Row;
21587
21588 fn int_pk_schema() -> Schema {
21589 Schema {
21590 columns: vec![ColumnDef {
21591 id: 1,
21592 name: "id".into(),
21593 ty: TypeId::Int64,
21594 flags: crate::schema::ColumnFlags::empty()
21595 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
21596 default_value: None,
21597 embedding_source: None,
21598 }],
21599 ..Schema::default()
21600 }
21601 }
21602
21603 fn commit_one(db: &Database) -> (Epoch, mongreldb_types::hlc::HlcTimestamp) {
21604 let mut txn = db.begin();
21605 let handle = txn.state_handle();
21606 txn.put("t", vec![(1, Value::Int64(1))]).unwrap();
21607 let epoch = txn.commit().unwrap();
21608 let crate::txn::TransactionState::Committed(receipt) = handle.state() else {
21609 panic!("expected Committed, got {:?}", handle.state());
21610 };
21611 (epoch, receipt.commit_ts)
21612 }
21613
21614 #[test]
21615 fn commit_ts_for_epoch_returns_the_exact_receipt_within_one_open() {
21616 let dir = tempfile::tempdir().unwrap();
21617 let db = Database::create(dir.path()).unwrap();
21618 db.create_table("t", int_pk_schema()).unwrap();
21619
21620 let (epoch, commit_ts) = commit_one(&db);
21621 assert_eq!(db.commit_ts_for_epoch(epoch), Some(commit_ts));
21622 assert_eq!(db.commit_ts_for_epoch(Epoch(epoch.0 + 100)), None);
21624 }
21625
21626 #[test]
21627 fn commit_ts_for_epoch_survives_reopen_with_the_physical_component() {
21628 let dir = tempfile::tempdir().unwrap();
21629 let (epoch, commit_ts) = {
21630 let db = Database::create(dir.path()).unwrap();
21631 db.create_table("t", int_pk_schema()).unwrap();
21632 commit_one(&db)
21633 };
21634
21635 let db = Database::open(dir.path()).unwrap();
21636 let reconstructed = db
21637 .commit_ts_for_epoch(epoch)
21638 .expect("the durable WAL CommitTimestamp ledger reconstructs the epoch");
21639 assert_eq!(reconstructed.physical_micros, commit_ts.physical_micros);
21640 assert_eq!(reconstructed.logical, 0);
21643 assert_eq!(reconstructed.node_tiebreaker, 0);
21644 }
21645
21646 #[test]
21647 fn recovery_ledger_keeps_only_newest_epochs_and_ignores_aborted_txns() {
21648 use crate::wal::Op;
21649 let records = vec![
21650 crate::wal::Record::new(Epoch(1), 7, Op::CommitTimestamp { unix_nanos: 1_000 }),
21651 crate::wal::Record::new(
21652 Epoch(2),
21653 7,
21654 Op::TxnCommit {
21655 epoch: 41,
21656 added_runs: vec![],
21657 },
21658 ),
21659 crate::wal::Record::new(
21661 Epoch(3),
21662 8,
21663 Op::TxnCommit {
21664 epoch: 42,
21665 added_runs: vec![],
21666 },
21667 ),
21668 crate::wal::Record::new(Epoch(4), 9, Op::CommitTimestamp { unix_nanos: 9_000 }),
21670 ];
21671 let ledger = commit_ts_ledger_from_recovery(&records);
21672 assert_eq!(ledger.len(), 1);
21673 assert_eq!(
21674 ledger.get(&41),
21675 Some(&mongreldb_types::hlc::HlcTimestamp {
21676 physical_micros: 1,
21677 logical: 0,
21678 node_tiebreaker: 0,
21679 })
21680 );
21681 }
21682
21683 #[test]
21684 fn new_writes_always_have_some_commit_ts() {
21685 let dir = tempfile::tempdir().unwrap();
21686 let db = Database::create(dir.path()).unwrap();
21687 db.create_table("t", int_pk_schema()).unwrap();
21688 let mut txn = db.begin();
21689 let state = txn.state_handle();
21690 txn.put("t", vec![(1, Value::Int64(7))]).unwrap();
21691 let epoch = txn.commit().unwrap();
21692 let crate::txn::TransactionState::Committed(receipt) = state.state() else {
21693 panic!("expected Committed receipt");
21694 };
21695 let handle = db.table("t").unwrap();
21696 let table = handle.lock();
21697 let (snap, _g) = db.snapshot();
21700 let rows = table.visible_rows(snap).expect("visible rows");
21701 assert_eq!(
21702 rows.len(),
21703 1,
21704 "committed put must be visible under HLC snapshot"
21705 );
21706 assert_eq!(rows[0].commit_ts, Some(receipt.commit_ts));
21707 assert_eq!(db.commit_ts_for_epoch(epoch), Some(receipt.commit_ts));
21708 }
21709
21710 #[test]
21711 fn same_transaction_identical_hlc_on_apply() {
21712 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21713
21714 let dir = tempfile::tempdir().unwrap();
21715 let db = Database::create_cluster_replica(
21716 dir.path(),
21717 ClusterId::from_bytes([1; 16]),
21718 NodeId::from_bytes([2; 16]),
21719 DatabaseId::from_bytes([3; 16]),
21720 )
21721 .unwrap();
21722 db.apply_replicated_catalog_command(&crate::catalog_cmds::CatalogCommandRecord {
21723 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
21724 catalog_version: 1,
21725 command: crate::catalog_cmds::CatalogCommand::CreateTable {
21726 name: "t".into(),
21727 schema: int_pk_schema(),
21728 created_epoch: 1,
21729 },
21730 })
21731 .unwrap();
21732 let table_id = db.table_id("t").unwrap();
21733 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
21734 physical_micros: 42_000,
21735 logical: 3,
21736 node_tiebreaker: 9,
21737 };
21738 let staged = vec![StagedTxnWrite::Put {
21741 table_id,
21742 rows: bincode::serialize(&vec![
21743 Row::new(crate::RowId(1), Epoch(0)).with_column(1, Value::Int64(1))
21744 ])
21745 .unwrap(),
21746 }
21747 .encode()
21748 .unwrap()];
21749 assert!(db
21750 .apply_committed_transaction(1 << 63, commit_ts, &staged)
21751 .unwrap());
21752 let staged2 = vec![StagedTxnWrite::Put {
21753 table_id,
21754 rows: bincode::serialize(&vec![
21755 Row::new(crate::RowId(2), Epoch(0)).with_column(1, Value::Int64(2))
21756 ])
21757 .unwrap(),
21758 }
21759 .encode()
21760 .unwrap()];
21761 assert!(db
21762 .apply_committed_transaction((1 << 63) + 1, commit_ts, &staged2)
21763 .unwrap());
21764 let handle = db.table("t").unwrap();
21765 let table = handle.lock();
21766 let row1 = table
21767 .get(crate::RowId(1), Snapshot::unbounded())
21768 .expect("applied row 1");
21769 let row2 = table
21770 .get(crate::RowId(2), Snapshot::unbounded())
21771 .expect("applied row 2");
21772 let recovery_form = mongreldb_types::hlc::HlcTimestamp {
21777 physical_micros: commit_ts.physical_micros,
21778 logical: 0,
21779 node_tiebreaker: 0,
21780 };
21781 assert_eq!(row1.commit_ts, Some(recovery_form));
21782 assert_eq!(row2.commit_ts, Some(recovery_form));
21783 assert_eq!(row1.commit_ts, row2.commit_ts);
21784 drop(table);
21785 let epoch = db.visible_epoch();
21788 assert_eq!(
21789 db.commit_ts_for_epoch(epoch).map(|ts| ts.physical_micros),
21790 Some(commit_ts.physical_micros)
21791 );
21792 }
21793
21794 #[test]
21795 fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
21796 use mongreldb_types::hlc::HlcTimestamp;
21797 let early = HlcTimestamp {
21798 physical_micros: 100,
21799 logical: 0,
21800 node_tiebreaker: 1,
21801 };
21802 let late = HlcTimestamp {
21803 physical_micros: 200,
21804 logical: 0,
21805 node_tiebreaker: 1,
21806 };
21807 let snap = Snapshot::at_hlc(Epoch(99), early);
21809 assert!(!snap.observes_version(Epoch(1), Some(late)));
21810 assert!(snap.observes_version(Epoch(1), Some(early)));
21811 let dir = tempfile::tempdir().unwrap();
21813 let db = Database::create(dir.path()).unwrap();
21814 let (snap, _g) = db.snapshot();
21815 assert_ne!(
21816 snap.commit_ts,
21817 HlcTimestamp::ZERO,
21818 "Database::snapshot must pin live HLC via at_hlc"
21819 );
21820 }
21821
21822 #[test]
21823 fn hlc_stamped_row_visible_at_hlc_snapshot_not_epoch_only() {
21824 let dir = tempfile::tempdir().unwrap();
21825 let db = Database::create(dir.path()).unwrap();
21826 db.create_table("t", int_pk_schema()).unwrap();
21827 let (_epoch, commit_ts) = commit_one(&db);
21828 assert_ne!(commit_ts, mongreldb_types::hlc::HlcTimestamp::ZERO);
21829
21830 let handle = db.table("t").unwrap();
21831 let table = handle.lock();
21832 let hlc_snap = Snapshot::at_hlc(Epoch(u64::MAX), commit_ts);
21834 let rows = table.visible_rows(hlc_snap).expect("visible");
21835 assert_eq!(rows.len(), 1);
21836 assert_eq!(rows[0].commit_ts, Some(commit_ts));
21837 assert!(table.get(rows[0].row_id, hlc_snap).is_some());
21838
21839 let legacy = Snapshot::at(Epoch(u64::MAX));
21841 assert!(!legacy.uses_hlc_authority());
21842 assert_eq!(
21843 table.visible_rows(legacy).expect("visible").len(),
21844 1,
21845 "epoch pin sees HLC-stamped rows by epoch during dual-model migration"
21846 );
21847 assert!(table.get(rows[0].row_id, legacy).is_some());
21848 }
21849
21850 #[test]
21851 fn hlc_gc_floor_reports_named_sources() {
21852 let dir = tempfile::tempdir().unwrap();
21853 let db = Database::create(dir.path()).unwrap();
21854 db.create_table("t", int_pk_schema()).unwrap();
21855 let (epoch, commit_ts) = commit_one(&db);
21856
21857 let empty = db.hlc_gc_floor();
21859 assert_eq!(empty.floor(), mongreldb_types::hlc::HlcTimestamp::ZERO);
21860 assert_eq!(empty.sources().len(), 6);
21861
21862 let (_snap, guard) = db.snapshot();
21864 let with_pin = db.hlc_gc_floor();
21865 let projected = db.commit_ts_for_epoch(epoch);
21867 if let Some(ts) = projected {
21868 assert_eq!(with_pin.transaction_snapshot, ts);
21870 assert_eq!(with_pin.floor(), ts);
21871 assert_eq!(ts.physical_micros, commit_ts.physical_micros);
21872 } else {
21873 assert_eq!(
21874 with_pin.transaction_snapshot,
21875 mongreldb_types::hlc::HlcTimestamp::ZERO
21876 );
21877 }
21878 drop(guard);
21879 }
21880}
21881
21882#[cfg(test)]
21883mod stage2e_storage_mode_tests {
21884 use super::*;
21885 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
21886 use crate::storage_mode::{StorageMode, STORAGE_MODE_FILENAME};
21887 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
21888
21889 fn identity(seed: u8) -> (ClusterId, NodeId, DatabaseId) {
21890 (
21891 ClusterId::from_bytes([seed; 16]),
21892 NodeId::from_bytes([seed + 1; 16]),
21893 DatabaseId::from_bytes([seed + 2; 16]),
21894 )
21895 }
21896
21897 fn marker(root: &Path) -> Option<StorageMode> {
21898 let durable = crate::durable_file::DurableRoot::open(root).unwrap();
21899 crate::storage_mode::read(&durable).unwrap()
21900 }
21901
21902 fn simple_schema() -> Schema {
21903 Schema {
21904 columns: vec![ColumnDef {
21905 id: 1,
21906 name: "id".into(),
21907 ty: TypeId::Int64,
21908 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
21909 default_value: None,
21910 embedding_source: None,
21911 }],
21912 ..Schema::default()
21913 }
21914 }
21915
21916 #[test]
21917 fn standalone_create_writes_marker_and_reopens() {
21918 let dir = tempfile::tempdir().unwrap();
21919 let root = dir.path().join("db");
21920 let db = Database::create(&root).unwrap();
21921 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21922 assert_eq!(db.storage_mode().unwrap(), Some(StorageMode::Standalone));
21923 drop(db);
21924 let db = Database::open(&root).unwrap();
21925 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21926 drop(db);
21927 }
21928
21929 #[test]
21930 fn legacy_database_without_marker_opens_and_gains_marker() {
21931 let dir = tempfile::tempdir().unwrap();
21932 let root = dir.path().join("db");
21933 let db = Database::create(&root).unwrap();
21934 drop(db);
21935 std::fs::remove_file(root.join(META_DIR).join(STORAGE_MODE_FILENAME)).unwrap();
21937 assert_eq!(marker(&root), None);
21938 let db = Database::open(&root).unwrap();
21939 assert_eq!(marker(&root), Some(StorageMode::Standalone));
21940 drop(db);
21941 }
21942
21943 #[test]
21944 fn server_owned_standalone_opens_embedded() {
21945 let dir = tempfile::tempdir().unwrap();
21946 let root = dir.path().join("db");
21947 let db = Database::create(&root).unwrap();
21948 drop(db);
21949 let durable = crate::durable_file::DurableRoot::open(&root).unwrap();
21950 crate::storage_mode::rewrite(&durable, &StorageMode::ServerOwnedStandalone).unwrap();
21951 let db = Database::open(&root).unwrap();
21952 assert_eq!(marker(&root), Some(StorageMode::ServerOwnedStandalone));
21953 drop(db);
21954 }
21955
21956 #[test]
21957 fn cluster_replica_is_rejected_by_normal_opens() {
21958 let dir = tempfile::tempdir().unwrap();
21959 let root = dir.path().join("db");
21960 let (cluster_id, node_id, database_id) = identity(10);
21961 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
21962 assert_eq!(
21963 marker(&root),
21964 Some(StorageMode::ClusterReplica {
21965 cluster_id,
21966 node_id,
21967 database_id,
21968 })
21969 );
21970 drop(db);
21971
21972 let error = Database::open(&root).unwrap_err();
21973 let message = error.to_string();
21974 assert!(
21975 matches!(error, MongrelError::InvalidArgument(_)),
21976 "unexpected error: {message}"
21977 );
21978 assert!(message.contains("cluster node runtime"), "{message}");
21979 assert!(message.contains(&cluster_id.to_hex()), "{message}");
21980 assert!(message.contains(&node_id.to_hex()), "{message}");
21981 assert!(message.contains(&database_id.to_hex()), "{message}");
21982
21983 let error = Database::open_with_options(&root, OpenOptions::default()).unwrap_err();
21984 assert!(error.to_string().contains("cluster node runtime"));
21985 assert_eq!(
21987 marker(&root),
21988 Some(StorageMode::ClusterReplica {
21989 cluster_id,
21990 node_id,
21991 database_id,
21992 })
21993 );
21994 }
21995
21996 #[test]
21997 fn offline_validation_opens_cluster_replica_read_only() {
21998 let dir = tempfile::tempdir().unwrap();
21999 let root = dir.path().join("db");
22000 let (cluster_id, node_id, database_id) = identity(20);
22001 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
22002 drop(db);
22003
22004 let options = OpenOptions::default().with_offline_validation(true);
22005 let db = Database::open_with_options(&root, options).unwrap();
22006 assert!(db.is_read_only_replica());
22007 let error = db.create_table("t", simple_schema()).unwrap_err();
22008 assert!(matches!(error, MongrelError::ReadOnlyReplica));
22009 drop(db);
22010 assert_eq!(
22012 marker(&root),
22013 Some(StorageMode::ClusterReplica {
22014 cluster_id,
22015 node_id,
22016 database_id,
22017 })
22018 );
22019 }
22020
22021 #[test]
22022 fn cluster_runtime_open_requires_exact_identity() {
22023 let dir = tempfile::tempdir().unwrap();
22024 let root = dir.path().join("db");
22025 let (cluster_id, node_id, database_id) = identity(30);
22026 let db = Database::create_cluster_replica(&root, cluster_id, node_id, database_id).unwrap();
22027 drop(db);
22028
22029 let error = Database::open_cluster_replica(&root, &StorageMode::Standalone).unwrap_err();
22031 assert!(matches!(error, MongrelError::InvalidArgument(_)));
22032 let wrong = StorageMode::ClusterReplica {
22034 cluster_id,
22035 node_id,
22036 database_id: DatabaseId::from_bytes([99; 16]),
22037 };
22038 let error = Database::open_cluster_replica(&root, &wrong).unwrap_err();
22039 assert!(error.to_string().contains("identity mismatch"), "{error}");
22040 let legacy = dir.path().join("legacy");
22042 let legacy_db = Database::create(&legacy).unwrap();
22043 drop(legacy_db);
22044 let expected = StorageMode::ClusterReplica {
22045 cluster_id,
22046 node_id,
22047 database_id,
22048 };
22049 let error = Database::open_cluster_replica(&legacy, &expected).unwrap_err();
22050 assert!(error.to_string().contains("identity mismatch"), "{error}");
22051
22052 let db = Database::open_cluster_replica(&root, &expected).unwrap();
22055 assert!(db.is_read_only_replica());
22056 let error = db.create_table("t", simple_schema()).unwrap_err();
22057 assert!(matches!(error, MongrelError::ReadOnlyReplica));
22058 drop(db);
22059 }
22060}
22061
22062#[cfg(test)]
22063mod stage2e_replicated_apply_tests {
22064 use super::*;
22065 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord, CatalogDelta};
22066 use crate::memtable::{Row, Value};
22067 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
22068 use crate::wal::{Op, Record};
22069 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
22070 use std::sync::Arc;
22071
22072 fn ids() -> (ClusterId, NodeId, DatabaseId) {
22073 (
22074 ClusterId::from_bytes([1; 16]),
22075 NodeId::from_bytes([2; 16]),
22076 DatabaseId::from_bytes([3; 16]),
22077 )
22078 }
22079
22080 fn expected_mode() -> crate::storage_mode::StorageMode {
22081 let (cluster_id, node_id, database_id) = ids();
22082 crate::storage_mode::StorageMode::ClusterReplica {
22083 cluster_id,
22084 node_id,
22085 database_id,
22086 }
22087 }
22088
22089 fn simple_schema() -> Schema {
22090 Schema {
22091 columns: vec![ColumnDef {
22092 id: 1,
22093 name: "id".into(),
22094 ty: TypeId::Int64,
22095 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
22096 default_value: None,
22097 embedding_source: None,
22098 }],
22099 ..Schema::default()
22100 }
22101 }
22102
22103 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
22104 CatalogCommandRecord {
22105 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
22106 catalog_version,
22107 command: CatalogCommand::CreateTable {
22108 name: name.to_string(),
22109 schema: simple_schema(),
22110 created_epoch: 1,
22111 },
22112 }
22113 }
22114
22115 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
22116 let rows: Vec<Row> = values
22117 .iter()
22118 .map(|value| {
22119 Row::new(crate::RowId(*value as u64), Epoch(epoch))
22122 .with_column(1, Value::Int64(*value))
22123 })
22124 .collect();
22125 vec![
22126 Record::new(
22127 Epoch(0),
22128 txn_id,
22129 Op::Put {
22130 table_id,
22131 rows: bincode::serialize(&rows).unwrap(),
22132 },
22133 ),
22134 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
22135 Record::new(
22136 Epoch(0),
22137 txn_id,
22138 Op::TxnCommit {
22139 epoch,
22140 added_runs: Vec::new(),
22141 },
22142 ),
22143 ]
22144 }
22145
22146 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22147 let handle = db.table(table).unwrap();
22148 let snap = db.visible_snapshot();
22150 let rows = handle.lock().visible_rows(snap).unwrap();
22151 let mut values: Vec<i64> = rows
22152 .iter()
22153 .map(|row| match row.columns.get(&1) {
22154 Some(Value::Int64(value)) => *value,
22155 other => panic!("unexpected column: {other:?}"),
22156 })
22157 .collect();
22158 values.sort_unstable();
22159 values
22160 }
22161
22162 #[test]
22163 fn catalog_command_mounts_table_and_replays_as_noop() {
22164 let dir = tempfile::tempdir().unwrap();
22165 let (cluster_id, node_id, database_id) = ids();
22166 let db =
22167 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22168
22169 let record = create_table_record("items", 1);
22170 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22171 assert!(matches!(delta, CatalogDelta::TableCreated { .. }));
22172 assert_eq!(db.table_names(), vec!["items".to_string()]);
22173 assert_eq!(db.catalog_version(), 1);
22174
22175 let delta = db.apply_replicated_catalog_command(&record).unwrap();
22177 assert!(matches!(delta, CatalogDelta::NoOp));
22178 assert_eq!(db.table_names().len(), 1);
22179 drop(db);
22180
22181 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22183 assert_eq!(db.table_names(), vec!["items".to_string()]);
22184 assert_eq!(db.catalog_version(), 1);
22185 }
22186
22187 #[test]
22188 fn records_apply_rows_and_skip_replays_across_restart() {
22189 let dir = tempfile::tempdir().unwrap();
22190 let (cluster_id, node_id, database_id) = ids();
22191 let db =
22192 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22193 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22194 .unwrap();
22195
22196 let records = put_records(1, 0, 2, &[10, 20, 30]);
22197 assert!(db.apply_replicated_records(&records).unwrap());
22198 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22199 assert_eq!(db.visible_epoch(), Epoch(2));
22200
22201 assert!(!db.apply_replicated_records(&records).unwrap());
22204 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30]);
22205
22206 let later = put_records(2, 0, 3, &[40]);
22208 assert!(db.apply_replicated_records(&later).unwrap());
22209 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22210 let db = Arc::new(db);
22211 db.shutdown().unwrap();
22212
22213 let db = Database::open_cluster_replica(dir.path(), &expected_mode()).unwrap();
22216 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22217 assert!(!db.apply_replicated_records(&later).unwrap());
22218 assert!(!db.apply_replicated_records(&records).unwrap());
22219 assert_eq!(visible_ids(&db, "items"), vec![10, 20, 30, 40]);
22220 }
22221
22222 #[test]
22223 fn spilled_run_commits_fail_closed_this_wave() {
22224 let dir = tempfile::tempdir().unwrap();
22225 let (cluster_id, node_id, database_id) = ids();
22226 let db =
22227 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22228 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22229 .unwrap();
22230 let mut records = put_records(1, 0, 2, &[10]);
22231 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22232 panic!("put_records ends in TxnCommit");
22233 };
22234 added_runs.push(crate::wal::AddedRun {
22235 table_id: 0,
22236 run_id: 7,
22237 row_count: 1,
22238 level: 0,
22239 min_row_id: 1,
22240 max_row_id: 1,
22241 content_hash: [0; 32],
22242 });
22243 let error = db.apply_replicated_records(&records).unwrap_err();
22244 assert!(
22245 error.to_string().contains("spilled-run"),
22246 "unexpected error: {error}"
22247 );
22248 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22249 }
22250
22251 #[test]
22252 fn records_without_commit_marker_fail_closed() {
22253 let dir = tempfile::tempdir().unwrap();
22254 let (cluster_id, node_id, database_id) = ids();
22255 let db =
22256 Database::create_cluster_replica(dir.path(), cluster_id, node_id, database_id).unwrap();
22257 db.apply_replicated_catalog_command(&create_table_record("items", 1))
22258 .unwrap();
22259 let mut records = put_records(1, 0, 2, &[10]);
22260 records.pop(); let error = db.apply_replicated_records(&records).unwrap_err();
22262 assert!(matches!(error, MongrelError::InvalidArgument(_)));
22263 assert!(db.apply_replicated_records(&[]).is_err());
22264 assert_eq!(visible_ids(&db, "items"), Vec::<i64>::new());
22265 }
22266}
22267
22268#[cfg(test)]
22269mod stage2c_spill_translation_tests {
22270 use super::*;
22271 use crate::catalog_cmds::{CatalogCommand, CatalogCommandRecord};
22272 use crate::memtable::{Row, Value};
22273 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
22274 use crate::wal::{Op, Record};
22275 use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
22276
22277 fn simple_schema() -> Schema {
22278 Schema {
22279 columns: vec![ColumnDef {
22280 id: 1,
22281 name: "id".into(),
22282 ty: TypeId::Int64,
22283 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
22284 default_value: None,
22285 embedding_source: None,
22286 }],
22287 ..Schema::default()
22288 }
22289 }
22290
22291 fn create_table_record(name: &str, catalog_version: u64) -> CatalogCommandRecord {
22292 CatalogCommandRecord {
22293 version: crate::catalog_cmds::CATALOG_COMMAND_FORMAT_VERSION,
22294 catalog_version,
22295 command: CatalogCommand::CreateTable {
22296 name: name.to_string(),
22297 schema: simple_schema(),
22298 created_epoch: 1,
22299 },
22300 }
22301 }
22302
22303 fn visible_ids(db: &Database, table: &str) -> Vec<i64> {
22304 let handle = db.table(table).unwrap();
22305 let snap = db.visible_snapshot();
22307 let rows = handle.lock().visible_rows(snap).unwrap();
22308 let mut values: Vec<i64> = rows
22309 .iter()
22310 .map(|row| match row.columns.get(&1) {
22311 Some(Value::Int64(value)) => *value,
22312 other => panic!("unexpected column: {other:?}"),
22313 })
22314 .collect();
22315 values.sort_unstable();
22316 values
22317 }
22318
22319 fn put_records(txn_id: u64, table_id: u64, epoch: u64, values: &[i64]) -> Vec<Record> {
22320 let rows: Vec<Row> = values
22321 .iter()
22322 .map(|value| {
22323 Row::new(crate::RowId(*value as u64), Epoch(epoch))
22324 .with_column(1, Value::Int64(*value))
22325 })
22326 .collect();
22327 vec![
22328 Record::new(
22329 Epoch(0),
22330 txn_id,
22331 Op::Put {
22332 table_id,
22333 rows: bincode::serialize(&rows).unwrap(),
22334 },
22335 ),
22336 Record::new(Epoch(0), txn_id, Op::CommitTimestamp { unix_nanos: 1_000 }),
22337 Record::new(
22338 Epoch(0),
22339 txn_id,
22340 Op::TxnCommit {
22341 epoch,
22342 added_runs: Vec::new(),
22343 },
22344 ),
22345 ]
22346 }
22347
22348 fn added_run(
22349 table_id: u64,
22350 row_count: u64,
22351 min_row_id: u64,
22352 max_row_id: u64,
22353 ) -> crate::wal::AddedRun {
22354 crate::wal::AddedRun {
22355 table_id,
22356 run_id: 7,
22357 row_count,
22358 level: 0,
22359 min_row_id,
22360 max_row_id,
22361 content_hash: [0; 32],
22362 }
22363 }
22364
22365 fn spilled_commit_records(db: &Database) -> Vec<Record> {
22368 let records = crate::wal::SharedWal::replay_with_dek(&db.root, None).unwrap();
22369 let txn_id = records
22370 .iter()
22371 .find_map(|record| match &record.op {
22372 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty() => Some(record.txn_id),
22373 _ => None,
22374 })
22375 .expect("a spilled commit is present in the WAL");
22376 records
22377 .into_iter()
22378 .filter(|record| record.txn_id == txn_id)
22379 .collect()
22380 }
22381
22382 #[test]
22383 fn non_spilled_records_translate_byte_identical() {
22384 let records = put_records(1, 0, 2, &[10, 20, 30]);
22385 let translated = translate_records_for_replication(&records).unwrap();
22386 assert_eq!(
22387 bincode::serialize(&translated).unwrap(),
22388 bincode::serialize(&records).unwrap(),
22389 "a commit without spill links must pass through byte-identical"
22390 );
22391 }
22392
22393 #[test]
22394 fn translation_rejects_uncovered_or_malformed_spills() {
22395 let mut records = put_records(1, 0, 2, &[10]);
22397 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22398 panic!("put_records ends in TxnCommit");
22399 };
22400 added_runs.push(added_run(0, 1, 10, 10));
22401 let error = translate_records_for_replication(&records).unwrap_err();
22402 assert!(
22403 error.to_string().contains("no logical row records"),
22404 "unexpected error: {error}"
22405 );
22406
22407 let mut records = put_records(1, 0, 2, &[10]);
22409 let spilled: Vec<Row> = (0..3_u64)
22410 .map(|value| {
22411 Row::new(crate::RowId(value), Epoch(2)).with_column(1, Value::Int64(value as i64))
22412 })
22413 .collect();
22414 records.insert(
22415 0,
22416 Record::new(
22417 Epoch(0),
22418 1,
22419 Op::SpilledRows {
22420 table_id: 0,
22421 rows: bincode::serialize(&spilled).unwrap(),
22422 },
22423 ),
22424 );
22425 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22426 panic!("put_records ends in TxnCommit");
22427 };
22428 added_runs.push(added_run(0, 4, 0, 3));
22429 let error = translate_records_for_replication(&records).unwrap_err();
22430 assert!(
22431 error.to_string().contains("cover 3 rows"),
22432 "unexpected error: {error}"
22433 );
22434
22435 let mut records = put_records(1, 0, 2, &[10]);
22437 records.insert(
22438 0,
22439 Record::new(
22440 Epoch(0),
22441 1,
22442 Op::SpilledRows {
22443 table_id: 0,
22444 rows: vec![0xFF, 0x01, 0x02],
22445 },
22446 ),
22447 );
22448 let Some(Op::TxnCommit { added_runs, .. }) = records.last_mut().map(|r| &mut r.op) else {
22449 panic!("put_records ends in TxnCommit");
22450 };
22451 added_runs.push(added_run(0, 1, 0, 0));
22452 assert!(translate_records_for_replication(&records).is_err());
22453
22454 assert!(translate_records_for_replication(&[]).is_err());
22456 let mut mixed = put_records(1, 0, 2, &[10]);
22457 mixed[0].txn_id = 99;
22458 assert!(translate_records_for_replication(&mixed).is_err());
22459 let mut no_commit = put_records(1, 0, 2, &[10]);
22460 no_commit.pop();
22461 assert!(translate_records_for_replication(&no_commit).is_err());
22462 }
22463
22464 #[test]
22465 fn spilled_commit_translates_to_logical_rows_and_applies_on_replica() {
22466 let leader_dir = tempfile::tempdir().unwrap();
22468 let leader = Database::create(leader_dir.path()).unwrap();
22469 leader.create_table("t", simple_schema()).unwrap();
22470 leader.set_spill_threshold(1);
22471 let table_id = leader.table_id("t").unwrap();
22472 let values: Vec<i64> = (0..60).collect();
22473 leader
22474 .transaction(|txn| {
22475 for value in &values {
22476 txn.put("t", vec![(1, Value::Int64(*value))])?;
22477 }
22478 Ok(())
22479 })
22480 .unwrap();
22481 assert_eq!(visible_ids(&leader, "t"), values);
22482
22483 let records = spilled_commit_records(&leader);
22486 assert!(records
22487 .iter()
22488 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22489 let Some(Op::TxnCommit { added_runs, epoch }) = records.last().map(|r| &r.op) else {
22490 panic!("a commit sequence ends in TxnCommit");
22491 };
22492 assert!(!added_runs.is_empty());
22493 let commit_epoch = *epoch;
22494
22495 let translated = translate_records_for_replication(&records).unwrap();
22498 assert!(records
22499 .iter()
22500 .any(|record| matches!(record.op, Op::SpilledRows { .. })));
22501 let Some(Op::TxnCommit { added_runs, .. }) = records.last().map(|r| &r.op) else {
22502 panic!("a commit sequence ends in TxnCommit");
22503 };
22504 assert!(!added_runs.is_empty(), "input records must be unchanged");
22505 assert_eq!(translated.len(), records.len());
22506 assert!(translated
22507 .iter()
22508 .all(|record| !matches!(record.op, Op::SpilledRows { .. })));
22509 assert!(translated
22510 .iter()
22511 .any(|record| matches!(record.op, Op::Put { .. })));
22512 let Some(Op::TxnCommit { added_runs, epoch }) = translated.last().map(|r| &r.op) else {
22513 panic!("a commit sequence ends in TxnCommit");
22514 };
22515 assert!(added_runs.is_empty(), "no added_runs may reach a replica");
22516 assert_eq!(*epoch, commit_epoch);
22517
22518 let replica_dir = tempfile::tempdir().unwrap();
22520 let replica = Database::create_cluster_replica(
22521 replica_dir.path(),
22522 ClusterId::from_bytes([1; 16]),
22523 NodeId::from_bytes([2; 16]),
22524 DatabaseId::from_bytes([3; 16]),
22525 )
22526 .unwrap();
22527 replica
22528 .apply_replicated_catalog_command(&create_table_record("t", 1))
22529 .unwrap();
22530 assert_eq!(replica.table_id("t").unwrap(), table_id);
22531 assert!(replica.apply_replicated_records(&translated).unwrap());
22532 assert_eq!(visible_ids(&replica, "t"), values);
22533 assert_eq!(visible_ids(&replica, "t"), visible_ids(&leader, "t"));
22534
22535 drop(leader);
22538 let leader = Database::open(leader_dir.path()).unwrap();
22539 assert_eq!(visible_ids(&leader, "t"), values);
22540 }
22541
22542 #[test]
22543 fn staged_txn_writes_validate_and_apply() {
22544 let dir = tempfile::tempdir().unwrap();
22545 let db = Database::create_cluster_replica(
22546 dir.path(),
22547 ClusterId::from_bytes([1; 16]),
22548 NodeId::from_bytes([2; 16]),
22549 DatabaseId::from_bytes([3; 16]),
22550 )
22551 .unwrap();
22552 db.apply_replicated_catalog_command(&create_table_record("t", 1))
22553 .unwrap();
22554 let table_id = db.table_id("t").unwrap();
22555
22556 assert!(db.validate_staged_txn_writes(&[vec![0xFF]]).is_err());
22558 let unknown_table = StagedTxnWrite::Put {
22559 table_id: 99,
22560 rows: bincode::serialize(&Vec::<Row>::new()).unwrap(),
22561 }
22562 .encode()
22563 .unwrap();
22564 assert!(db.validate_staged_txn_writes(&[unknown_table]).is_err());
22565 let good: Vec<Vec<u8>> = [10_i64, 20, 30]
22566 .iter()
22567 .map(|value| {
22568 let rows = vec![Row::new(crate::RowId(*value as u64), Epoch(0))
22569 .with_column(1, Value::Int64(*value))];
22570 StagedTxnWrite::Put {
22571 table_id,
22572 rows: bincode::serialize(&rows).unwrap(),
22573 }
22574 .encode()
22575 .unwrap()
22576 })
22577 .collect();
22578 db.validate_staged_txn_writes(&good).unwrap();
22579
22580 let commit_ts = mongreldb_types::hlc::HlcTimestamp {
22583 physical_micros: 5_000,
22584 logical: 0,
22585 node_tiebreaker: 0,
22586 };
22587 assert!(db
22588 .apply_staged_txn_writes(1 << 63, &good, commit_ts)
22589 .unwrap());
22590 assert_eq!(visible_ids(&db, "t"), vec![10, 20, 30]);
22591 let delete = StagedTxnWrite::Delete {
22592 table_id,
22593 row_ids: vec![20],
22594 }
22595 .encode()
22596 .unwrap();
22597 assert!(db
22598 .apply_staged_txn_writes((1 << 63) + 1, &[delete], commit_ts)
22599 .unwrap());
22600 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22601
22602 let db = Arc::new(db);
22605 db.shutdown().unwrap();
22606 let expected = crate::storage_mode::StorageMode::ClusterReplica {
22607 cluster_id: ClusterId::from_bytes([1; 16]),
22608 node_id: NodeId::from_bytes([2; 16]),
22609 database_id: DatabaseId::from_bytes([3; 16]),
22610 };
22611 let db = Database::open_cluster_replica(dir.path(), &expected).unwrap();
22612 assert_eq!(visible_ids(&db, "t"), vec![10, 30]);
22613 }
22614}