1use crate::catalog::{self, Catalog, CatalogEntry, TableState, META_DEK_LEN};
10use crate::engine::{SharedCtx, Table};
11use crate::epoch::{Epoch, EpochAuthority, Snapshot};
12use crate::error::{MongrelError, Result};
13use crate::external_table::ExternalTableEntry;
14use crate::memtable::Value;
15use crate::procedure::{
16 ProcedureCallOutput, ProcedureCallResult, ProcedureCallRow, ProcedureCondition, ProcedureEntry,
17 ProcedureStep, ProcedureValue, StoredProcedure,
18};
19use crate::retention::{OwnedSnapshotGuard, SnapshotGuard, SnapshotRegistry};
20use crate::rowid::RowId;
21use crate::schema::{AlterColumn, ColumnDef, Schema};
22use crate::trigger::{
23 StoredTrigger, TriggerCondition, TriggerConfig, TriggerEntry, TriggerEvent, TriggerExpr,
24 TriggerRaiseAction, TriggerStep, TriggerTarget, TriggerTiming, TriggerValue,
25};
26use parking_lot::{Mutex, RwLock};
27use std::collections::HashMap;
28use std::io::Write;
29use std::path::{Path, PathBuf};
30use std::sync::atomic::{AtomicBool, AtomicU32};
31use std::sync::Arc;
32
33pub const TABLES_DIR: &str = "tables";
34pub const VTAB_DIR: &str = "_vtab";
35pub const META_DIR: &str = "_meta";
36pub const KEYS_FILENAME: &str = "keys";
37
38pub const WAL_TABLE_ID: u64 = u64::MAX;
42pub const EXTERNAL_TABLE_ID: u64 = u64::MAX - 1;
45
46#[derive(Debug, Clone)]
47pub enum ExternalTriggerWrite {
48 Insert {
49 table: String,
50 cells: Vec<(u16, Value)>,
51 },
52 UpdateByPk {
53 table: String,
54 pk: Value,
55 cells: Vec<(u16, Value)>,
56 },
57 DeleteByPk {
58 table: String,
59 pk: Value,
60 },
61}
62
63impl ExternalTriggerWrite {
64 fn table(&self) -> &str {
65 match self {
66 Self::Insert { table, .. }
67 | Self::UpdateByPk { table, .. }
68 | Self::DeleteByPk { table, .. } => table,
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub enum ExternalTriggerBaseWrite {
75 Put {
76 table: String,
77 cells: Vec<(u16, Value)>,
78 },
79 Delete {
80 table: String,
81 row_id: RowId,
82 },
83}
84
85#[derive(Debug, Clone, PartialEq)]
86pub struct ExternalTriggerWriteResult {
87 pub state: Vec<u8>,
88 pub base_writes: Vec<ExternalTriggerBaseWrite>,
89}
90
91impl ExternalTriggerWriteResult {
92 pub fn new(state: Vec<u8>) -> Self {
93 Self {
94 state,
95 base_writes: Vec::new(),
96 }
97 }
98}
99
100pub trait ExternalTriggerBridge {
101 fn apply_trigger_external_write(
102 &self,
103 entry: &ExternalTableEntry,
104 base_state: Vec<u8>,
105 op: ExternalTriggerWrite,
106 ) -> Result<ExternalTriggerWriteResult>;
107}
108
109struct SpilledRun {
111 table_id: u64,
112 run_id: u128,
113 pending_path: PathBuf,
114 rows: Vec<crate::memtable::Row>,
115 row_count: u64,
116 min_rid: u64,
117 max_rid: u64,
118}
119
120#[derive(Debug, Clone)]
121struct TriggerRowImage {
122 columns: HashMap<u16, Value>,
123}
124
125impl TriggerRowImage {
126 fn from_row(row: crate::memtable::Row) -> Self {
127 Self {
128 columns: row.columns,
129 }
130 }
131
132 fn from_cells(cells: &[(u16, Value)]) -> Self {
133 Self {
134 columns: cells.iter().cloned().collect(),
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
140struct WriteEvent {
141 table: String,
142 kind: TriggerEvent,
143 old: Option<TriggerRowImage>,
144 new: Option<TriggerRowImage>,
145 changed_columns: Vec<u16>,
146 op_indices: Vec<usize>,
147 put_idx: Option<usize>,
148 trigger_stack: Vec<String>,
149}
150
151#[derive(Default)]
152struct TriggerExpansion {
153 before: Vec<(u64, crate::txn::Staged)>,
154 before_stacks: Vec<Vec<String>>,
155 before_external: Vec<ExternalTriggerWrite>,
156 after: Vec<(u64, crate::txn::Staged)>,
157 after_stacks: Vec<Vec<String>>,
158 after_external: Vec<ExternalTriggerWrite>,
159 ignored_indices: std::collections::BTreeSet<usize>,
160}
161
162struct TriggerProgramOutput<'a> {
163 added: &'a mut Vec<(u64, crate::txn::Staged)>,
164 added_stacks: &'a mut Vec<Vec<String>>,
165 added_external: &'a mut Vec<ExternalTriggerWrite>,
166 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170enum TriggerProgramOutcome {
171 Continue,
172 Ignore,
173}
174
175#[derive(Debug, Clone)]
177pub struct CheckIssue {
178 pub table_id: u64,
179 pub table_name: String,
180 pub severity: String,
181 pub description: String,
182}
183
184pub type TableHandle = Arc<Mutex<Table>>;
188
189pub struct Database {
192 root: PathBuf,
193 catalog: RwLock<Catalog>,
194 epoch: Arc<EpochAuthority>,
195 snapshots: Arc<SnapshotRegistry>,
196 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
197 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
198 commit_lock: Arc<Mutex<()>>,
199 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
204 next_txn_id: Arc<Mutex<u64>>,
208 tables: RwLock<HashMap<u64, TableHandle>>,
209 kek: Option<Arc<crate::encryption::Kek>>,
210 ddl_lock: Mutex<()>,
213 meta_dek: Option<[u8; META_DEK_LEN]>,
214 spill_threshold: std::sync::atomic::AtomicU64,
217 conflicts: crate::txn::ConflictIndex,
220 active_txns: crate::txn::ActiveTxns,
223 poisoned: Arc<std::sync::atomic::AtomicBool>,
226 group: Arc<crate::txn::GroupCommit>,
230 active_spills: Arc<crate::retention::ActiveSpills>,
233 #[doc(hidden)]
237 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
238 trigger_recursive: AtomicBool,
239 trigger_max_depth: AtomicU32,
240}
241
242impl Database {
243 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
245 Self::create_inner(root, None)
246 }
247
248 #[cfg(feature = "encryption")]
251 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
252 let root = root.as_ref();
253 std::fs::create_dir_all(root)?;
254 std::fs::create_dir_all(root.join(META_DIR))?;
255 let salt = crate::encryption::random_salt();
256 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
257 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
258 Self::create_inner(root, Some(kek))
259 }
260
261 #[cfg(feature = "encryption")]
264 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
265 let root = root.as_ref();
266 std::fs::create_dir_all(root)?;
267 std::fs::create_dir_all(root.join(META_DIR))?;
268 let salt = crate::encryption::random_salt();
269 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
270 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
271 Self::create_inner(root, Some(kek))
272 }
273
274 fn create_inner(
275 root: impl AsRef<Path>,
276 kek: Option<Arc<crate::encryption::Kek>>,
277 ) -> Result<Self> {
278 let root = root.as_ref().to_path_buf();
279 std::fs::create_dir_all(&root)?;
280 std::fs::create_dir_all(root.join(TABLES_DIR))?;
281 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
282 let cat = Catalog::empty();
283 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
284 Self::finish_open(root, cat, kek, meta_dek, false)
285 }
286
287 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
289 Self::open_inner(root, None, None)
290 }
291
292 #[cfg(feature = "encryption")]
294 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
295 let root = root.as_ref();
296 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
297 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
298 let mut salt = [0u8; crate::encryption::SALT_LEN];
299 salt.copy_from_slice(&salt_bytes);
300 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
301 Self::open_inner(root, Some(kek), None)
302 }
303
304 #[cfg(feature = "encryption")]
306 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
307 let root = root.as_ref();
308 let salt_path = root.join(META_DIR).join(KEYS_FILENAME);
309 let salt_bytes = std::fs::read(&salt_path).map_err(|e| {
310 MongrelError::NotFound(format!(
311 "encryption salt file {:?}: {e} (database not encrypted, or corrupted)",
312 salt_path
313 ))
314 })?;
315 if salt_bytes.len() != crate::encryption::SALT_LEN {
316 return Err(MongrelError::InvalidArgument(format!(
317 "salt file is {} bytes, expected {}",
318 salt_bytes.len(),
319 crate::encryption::SALT_LEN
320 )));
321 }
322 let mut salt = [0u8; crate::encryption::SALT_LEN];
323 salt.copy_from_slice(&salt_bytes);
324 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
325 Self::open_inner(root, Some(kek), None)
326 }
327
328 fn open_inner(
329 root: impl AsRef<Path>,
330 kek: Option<Arc<crate::encryption::Kek>>,
331 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
332 ) -> Result<Self> {
333 let root = root.as_ref().to_path_buf();
334 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
335 let cat = catalog::read(&root, meta_dek.as_ref())?
336 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
337 Self::finish_open(root, cat, kek, meta_dek, true)
338 }
339
340 fn finish_open(
341 root: PathBuf,
342 cat: Catalog,
343 kek: Option<Arc<crate::encryption::Kek>>,
344 meta_dek: Option<[u8; META_DEK_LEN]>,
345 existing: bool,
346 ) -> Result<Self> {
347 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
348 let snapshots = Arc::new(SnapshotRegistry::new());
349 let page_cache = Arc::new(crate::cache::Sharded::new(
350 crate::cache::CACHE_SHARDS,
351 || {
352 crate::cache::PageCache::new(
353 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
354 )
355 },
356 ));
357 let decoded_cache = Arc::new(crate::cache::Sharded::new(
358 crate::cache::CACHE_SHARDS,
359 || {
360 crate::cache::DecodedPageCache::new(
361 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
362 )
363 },
364 ));
365 let commit_lock = Arc::new(Mutex::new(()));
366 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
367 let shared_wal = Arc::new(Mutex::new(if existing {
368 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
369 } else {
370 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
371 }));
372 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
376 let group = Arc::new(crate::txn::GroupCommit::new(
377 shared_wal.lock().durable_seq(),
378 ));
379 let txn_ids = Arc::new(Mutex::new(1u64));
383
384 let mut cat = cat;
390 if existing {
391 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
392 }
393
394 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
400 for entry in &cat.tables {
401 if !matches!(entry.state, TableState::Live) {
402 continue;
403 }
404 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
405 let ctx = SharedCtx {
406 epoch: Arc::clone(&epoch),
407 page_cache: Arc::clone(&page_cache),
408 decoded_cache: Arc::clone(&decoded_cache),
409 snapshots: Arc::clone(&snapshots),
410 kek: kek.clone(),
411 commit_lock: Arc::clone(&commit_lock),
412 shared: Some(crate::engine::SharedWalCtx {
413 wal: Arc::clone(&shared_wal),
414 group: Arc::clone(&group),
415 poisoned: Arc::clone(&poisoned),
416 txn_ids: Arc::clone(&txn_ids),
417 }),
418 };
419 let t = Table::open_in(&tdir, ctx)?;
420 tables.insert(entry.table_id, Arc::new(Mutex::new(t)));
421 }
422
423 if existing {
429 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
430 sweep_pending_txn_dirs(&root, &cat);
433 }
434
435 if existing {
439 cat.open_generation = cat.open_generation.wrapping_add(1);
440 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
441 }
442 let next_txn_id = (cat.open_generation << 32) | 1;
443 *txn_ids.lock() = next_txn_id;
445
446 Ok(Self {
447 root,
448 catalog: RwLock::new(cat),
449 epoch,
450 snapshots,
451 page_cache,
452 decoded_cache,
453 commit_lock,
454 shared_wal,
455 next_txn_id: txn_ids,
456 tables: RwLock::new(tables),
457 kek,
458 ddl_lock: Mutex::new(()),
459 meta_dek,
460 conflicts: crate::txn::ConflictIndex::new(),
461 active_txns: crate::txn::ActiveTxns::new(),
462 poisoned,
463 group,
464 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
465 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
466 spill_hook: Mutex::new(None),
467 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
468 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
469 })
470 }
471
472 pub fn visible_epoch(&self) -> Epoch {
474 self.epoch.visible()
475 }
476
477 pub fn catalog_snapshot(&self) -> Catalog {
479 self.catalog.read().clone()
480 }
481
482 pub fn root(&self) -> &Path {
484 &self.root
485 }
486
487 pub fn table_id(&self, name: &str) -> Result<u64> {
490 let cat = self.catalog.read();
491 cat.live(name)
492 .map(|e| e.table_id)
493 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
494 }
495
496 pub fn procedures(&self) -> Vec<StoredProcedure> {
497 self.catalog
498 .read()
499 .procedures
500 .iter()
501 .map(|p| p.procedure.clone())
502 .collect()
503 }
504
505 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
506 self.catalog
507 .read()
508 .procedures
509 .iter()
510 .find(|p| p.procedure.name == name)
511 .map(|p| p.procedure.clone())
512 }
513
514 pub fn create_procedure(&self, mut procedure: StoredProcedure) -> Result<StoredProcedure> {
515 let _g = self.ddl_lock.lock();
516 procedure.validate()?;
517 self.validate_procedure_references(&procedure)?;
518 {
519 let cat = self.catalog.read();
520 if cat
521 .procedures
522 .iter()
523 .any(|p| p.procedure.name == procedure.name)
524 {
525 return Err(MongrelError::InvalidArgument(format!(
526 "procedure {:?} already exists",
527 procedure.name
528 )));
529 }
530 }
531 let commit_lock = Arc::clone(&self.commit_lock);
532 let _c = commit_lock.lock();
533 let epoch = self.epoch.bump_assigned();
534 procedure.created_epoch = epoch.0;
535 procedure.updated_epoch = epoch.0;
536 {
537 let mut cat = self.catalog.write();
538 cat.procedures.push(ProcedureEntry::from(procedure.clone()));
539 cat.db_epoch = epoch.0;
540 }
541 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
542 self.advance_visible(epoch);
543 Ok(procedure)
544 }
545
546 pub fn create_or_replace_procedure(
547 &self,
548 procedure: StoredProcedure,
549 ) -> Result<StoredProcedure> {
550 let _g = self.ddl_lock.lock();
551 procedure.validate()?;
552 self.validate_procedure_references(&procedure)?;
553 let commit_lock = Arc::clone(&self.commit_lock);
554 let _c = commit_lock.lock();
555 let epoch = self.epoch.bump_assigned();
556 let replaced = {
557 let mut cat = self.catalog.write();
558 let next = match cat
559 .procedures
560 .iter()
561 .position(|p| p.procedure.name == procedure.name)
562 {
563 Some(idx) => {
564 let next = cat.procedures[idx]
565 .procedure
566 .replaced(procedure.clone(), epoch.0)?;
567 cat.procedures[idx] = ProcedureEntry::from(next.clone());
568 next
569 }
570 None => {
571 let mut next = procedure;
572 next.created_epoch = epoch.0;
573 next.updated_epoch = epoch.0;
574 cat.procedures.push(ProcedureEntry::from(next.clone()));
575 next
576 }
577 };
578 cat.db_epoch = epoch.0;
579 next
580 };
581 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
582 self.advance_visible(epoch);
583 Ok(replaced)
584 }
585
586 pub fn drop_procedure(&self, name: &str) -> Result<()> {
587 let _g = self.ddl_lock.lock();
588 let commit_lock = Arc::clone(&self.commit_lock);
589 let _c = commit_lock.lock();
590 let epoch = self.epoch.bump_assigned();
591 {
592 let mut cat = self.catalog.write();
593 let before = cat.procedures.len();
594 cat.procedures.retain(|p| p.procedure.name != name);
595 if cat.procedures.len() == before {
596 return Err(MongrelError::NotFound(format!(
597 "procedure {name:?} not found"
598 )));
599 }
600 cat.db_epoch = epoch.0;
601 }
602 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
603 self.advance_visible(epoch);
604 Ok(())
605 }
606
607 pub fn triggers(&self) -> Vec<StoredTrigger> {
608 self.catalog
609 .read()
610 .triggers
611 .iter()
612 .map(|t| t.trigger.clone())
613 .collect()
614 }
615
616 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
617 self.catalog
618 .read()
619 .triggers
620 .iter()
621 .find(|t| t.trigger.name == name)
622 .map(|t| t.trigger.clone())
623 }
624
625 pub fn create_trigger(&self, mut trigger: StoredTrigger) -> Result<StoredTrigger> {
626 let _g = self.ddl_lock.lock();
627 trigger.validate()?;
628 self.validate_trigger_references(&trigger)?;
629 {
630 let cat = self.catalog.read();
631 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
632 return Err(MongrelError::InvalidArgument(format!(
633 "trigger {:?} already exists",
634 trigger.name
635 )));
636 }
637 }
638 let commit_lock = Arc::clone(&self.commit_lock);
639 let _c = commit_lock.lock();
640 let epoch = self.epoch.bump_assigned();
641 trigger.created_epoch = epoch.0;
642 trigger.updated_epoch = epoch.0;
643 {
644 let mut cat = self.catalog.write();
645 cat.triggers.push(TriggerEntry::from(trigger.clone()));
646 cat.db_epoch = epoch.0;
647 }
648 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
649 self.advance_visible(epoch);
650 Ok(trigger)
651 }
652
653 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
654 let _g = self.ddl_lock.lock();
655 trigger.validate()?;
656 self.validate_trigger_references(&trigger)?;
657 let commit_lock = Arc::clone(&self.commit_lock);
658 let _c = commit_lock.lock();
659 let epoch = self.epoch.bump_assigned();
660 let replaced = {
661 let mut cat = self.catalog.write();
662 let next = match cat
663 .triggers
664 .iter()
665 .position(|t| t.trigger.name == trigger.name)
666 {
667 Some(idx) => {
668 let next = cat.triggers[idx]
669 .trigger
670 .replaced(trigger.clone(), epoch.0)?;
671 cat.triggers[idx] = TriggerEntry::from(next.clone());
672 next
673 }
674 None => {
675 let mut next = trigger;
676 next.created_epoch = epoch.0;
677 next.updated_epoch = epoch.0;
678 cat.triggers.push(TriggerEntry::from(next.clone()));
679 next
680 }
681 };
682 cat.db_epoch = epoch.0;
683 next
684 };
685 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
686 self.advance_visible(epoch);
687 Ok(replaced)
688 }
689
690 pub fn drop_trigger(&self, name: &str) -> Result<()> {
691 let _g = self.ddl_lock.lock();
692 let commit_lock = Arc::clone(&self.commit_lock);
693 let _c = commit_lock.lock();
694 let epoch = self.epoch.bump_assigned();
695 {
696 let mut cat = self.catalog.write();
697 let before = cat.triggers.len();
698 cat.triggers.retain(|t| t.trigger.name != name);
699 if cat.triggers.len() == before {
700 return Err(MongrelError::NotFound(format!(
701 "trigger {name:?} not found"
702 )));
703 }
704 cat.db_epoch = epoch.0;
705 }
706 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
707 self.advance_visible(epoch);
708 Ok(())
709 }
710
711 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
712 self.catalog.read().external_tables.clone()
713 }
714
715 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
716 self.catalog
717 .read()
718 .external_tables
719 .iter()
720 .find(|t| t.name == name)
721 .cloned()
722 }
723
724 pub fn create_external_table(
725 &self,
726 mut entry: ExternalTableEntry,
727 ) -> Result<ExternalTableEntry> {
728 let _g = self.ddl_lock.lock();
729 entry.validate()?;
730 {
731 let cat = self.catalog.read();
732 if cat.live(&entry.name).is_some()
733 || cat.external_tables.iter().any(|t| t.name == entry.name)
734 {
735 return Err(MongrelError::InvalidArgument(format!(
736 "table {:?} already exists",
737 entry.name
738 )));
739 }
740 }
741 let commit_lock = Arc::clone(&self.commit_lock);
742 let _c = commit_lock.lock();
743 let epoch = self.epoch.bump_assigned();
744 entry.created_epoch = epoch.0;
745 {
746 let mut cat = self.catalog.write();
747 cat.external_tables.push(entry.clone());
748 cat.db_epoch = epoch.0;
749 }
750 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
751 self.advance_visible(epoch);
752 Ok(entry)
753 }
754
755 pub fn drop_external_table(&self, name: &str) -> Result<()> {
756 let _g = self.ddl_lock.lock();
757 let commit_lock = Arc::clone(&self.commit_lock);
758 let _c = commit_lock.lock();
759 let epoch = self.epoch.bump_assigned();
760 {
761 let mut cat = self.catalog.write();
762 let before = cat.external_tables.len();
763 cat.external_tables.retain(|t| t.name != name);
764 if cat.external_tables.len() == before {
765 return Err(MongrelError::NotFound(format!(
766 "external table {name:?} not found"
767 )));
768 }
769 cat.db_epoch = epoch.0;
770 }
771 let state_dir = self.root.join(VTAB_DIR).join(name);
772 if state_dir.exists() {
773 std::fs::remove_dir_all(state_dir)?;
774 }
775 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
776 self.advance_visible(epoch);
777 Ok(())
778 }
779
780 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
781 let txn_id = self.alloc_txn_id();
782 self.commit_transaction_with_external_states(
783 txn_id,
784 self.epoch.visible(),
785 Vec::new(),
786 vec![(name.to_string(), state.to_vec())],
787 None,
788 )
789 }
790
791 pub fn trigger_config(&self) -> TriggerConfig {
792 use std::sync::atomic::Ordering;
793 TriggerConfig {
794 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
795 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
796 }
797 }
798
799 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
800 use std::sync::atomic::Ordering;
801 if config.max_depth == 0 {
802 return Err(MongrelError::InvalidArgument(
803 "trigger max_depth must be greater than 0".into(),
804 ));
805 }
806 self.trigger_recursive
807 .store(config.recursive_triggers, Ordering::Relaxed);
808 self.trigger_max_depth
809 .store(config.max_depth, Ordering::Relaxed);
810 Ok(())
811 }
812
813 pub fn set_recursive_triggers(&self, recursive: bool) {
814 use std::sync::atomic::Ordering;
815 self.trigger_recursive.store(recursive, Ordering::Relaxed);
816 }
817
818 pub fn call_procedure(
819 &self,
820 name: &str,
821 args: HashMap<String, crate::Value>,
822 ) -> Result<ProcedureCallResult> {
823 let procedure = self
824 .procedure(name)
825 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
826 let args = bind_procedure_args(&procedure, args)?;
827 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
828 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
829 if has_writes {
830 let mut tx = self.begin();
831 let run = (|| {
832 for step in &procedure.body.steps {
833 let output =
834 self.execute_procedure_step(step, &args, &outputs, Some(&mut tx))?;
835 outputs.insert(step.id().to_string(), output);
836 }
837 eval_return_output(&procedure.body.return_value, &args, &outputs)
838 })();
839 match run {
840 Ok(output) => {
841 let epoch = tx.commit()?.0;
842 Ok(ProcedureCallResult {
843 epoch: Some(epoch),
844 output,
845 })
846 }
847 Err(e) => {
848 tx.rollback();
849 Err(e)
850 }
851 }
852 } else {
853 for step in &procedure.body.steps {
854 let output = self.execute_procedure_step(step, &args, &outputs, None)?;
855 outputs.insert(step.id().to_string(), output);
856 }
857 Ok(ProcedureCallResult {
858 epoch: None,
859 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
860 })
861 }
862 }
863
864 fn execute_procedure_step(
865 &self,
866 step: &ProcedureStep,
867 args: &HashMap<String, crate::Value>,
868 outputs: &HashMap<String, ProcedureCallOutput>,
869 tx: Option<&mut crate::txn::Transaction<'_>>,
870 ) -> Result<ProcedureCallOutput> {
871 match step {
872 ProcedureStep::NativeQuery {
873 table,
874 conditions,
875 projection,
876 limit,
877 ..
878 } => {
879 let mut q = crate::Query::new();
880 for condition in conditions {
881 q = q.and(eval_condition(condition, args, outputs)?);
882 }
883 let handle = self.table(table)?;
884 let mut rows = handle.lock().query(&q)?;
885 if let Some(limit) = limit {
886 rows.truncate(*limit);
887 }
888 let projection = projection.as_ref();
889 Ok(ProcedureCallOutput::Rows(
890 rows.into_iter()
891 .map(|row| ProcedureCallRow {
892 row_id: Some(row.row_id),
893 columns: match projection {
894 Some(ids) => row
895 .columns
896 .into_iter()
897 .filter(|(id, _)| ids.contains(id))
898 .collect(),
899 None => row.columns,
900 },
901 })
902 .collect(),
903 ))
904 }
905 ProcedureStep::Put {
906 table,
907 cells,
908 returning,
909 ..
910 } => {
911 let tx = tx.ok_or_else(|| {
912 MongrelError::InvalidArgument(
913 "write procedure step requires a transaction".into(),
914 )
915 })?;
916 let cells = eval_cells(cells, args, outputs)?;
917 if *returning {
918 let out = tx.put_returning(table, cells)?;
919 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
920 row_id: None,
921 columns: out.row.columns.into_iter().collect(),
922 }))
923 } else {
924 tx.put(table, cells)?;
925 Ok(ProcedureCallOutput::Null)
926 }
927 }
928 ProcedureStep::Upsert {
929 table,
930 cells,
931 update_cells,
932 returning,
933 ..
934 } => {
935 let tx = tx.ok_or_else(|| {
936 MongrelError::InvalidArgument(
937 "write procedure step requires a transaction".into(),
938 )
939 })?;
940 let cells = eval_cells(cells, args, outputs)?;
941 let action = match update_cells {
942 Some(update_cells) => {
943 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
944 }
945 None => crate::UpsertAction::DoNothing,
946 };
947 let out = tx.upsert(table, cells, action)?;
948 if *returning {
949 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
950 row_id: None,
951 columns: out.row.columns.into_iter().collect(),
952 }))
953 } else {
954 Ok(ProcedureCallOutput::Null)
955 }
956 }
957 ProcedureStep::DeleteByPk { table, pk, .. } => {
958 let tx = tx.ok_or_else(|| {
959 MongrelError::InvalidArgument(
960 "write procedure step requires a transaction".into(),
961 )
962 })?;
963 let pk = eval_value(pk, args, outputs)?;
964 let handle = self.table(table)?;
965 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
966 MongrelError::NotFound("procedure delete_by_pk target not found".into())
967 })?;
968 tx.delete(table, row_id)?;
969 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
970 }
971 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
972 "DeleteRows procedure step is not supported by the core executor yet".into(),
973 )),
974 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
975 "SqlQuery procedure step must be executed by mongreldb-query".into(),
976 )),
977 }
978 }
979
980 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
981 let cat = self.catalog.read();
982 for step in &procedure.body.steps {
983 let Some(table_name) = step.table() else {
984 continue;
985 };
986 let schema = &cat
987 .live(table_name)
988 .ok_or_else(|| {
989 MongrelError::InvalidArgument(format!(
990 "procedure {:?} references unknown table {table_name:?}",
991 procedure.name
992 ))
993 })?
994 .schema;
995 match step {
996 ProcedureStep::NativeQuery {
997 conditions,
998 projection,
999 ..
1000 } => {
1001 for condition in conditions {
1002 validate_condition_columns(condition, schema)?;
1003 }
1004 if let Some(projection) = projection {
1005 for id in projection {
1006 validate_column_id(*id, schema)?;
1007 }
1008 }
1009 }
1010 ProcedureStep::Put { cells, .. } => {
1011 for cell in cells {
1012 validate_column_id(cell.column_id, schema)?;
1013 }
1014 }
1015 ProcedureStep::Upsert {
1016 cells,
1017 update_cells,
1018 ..
1019 } => {
1020 for cell in cells {
1021 validate_column_id(cell.column_id, schema)?;
1022 }
1023 if let Some(update_cells) = update_cells {
1024 for cell in update_cells {
1025 validate_column_id(cell.column_id, schema)?;
1026 }
1027 }
1028 }
1029 ProcedureStep::DeleteByPk { .. } => {
1030 if schema.primary_key().is_none() {
1031 return Err(MongrelError::InvalidArgument(format!(
1032 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
1033 procedure.name
1034 )));
1035 }
1036 }
1037 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
1038 }
1039 }
1040 Ok(())
1041 }
1042
1043 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
1044 let cat = self.catalog.read();
1045 let target_schema = match &trigger.target {
1046 TriggerTarget::Table(target_name) => cat
1047 .live(target_name)
1048 .ok_or_else(|| {
1049 MongrelError::InvalidArgument(format!(
1050 "trigger {:?} references unknown target table {target_name:?}",
1051 trigger.name
1052 ))
1053 })?
1054 .schema
1055 .clone(),
1056 TriggerTarget::View(_) => Schema {
1057 columns: trigger.target_columns.clone(),
1058 ..Schema::default()
1059 },
1060 };
1061 for col in &trigger.update_of {
1062 if target_schema.column(col).is_none() {
1063 return Err(MongrelError::InvalidArgument(format!(
1064 "trigger {:?} UPDATE OF references unknown column {col:?}",
1065 trigger.name
1066 )));
1067 }
1068 }
1069 if let Some(expr) = &trigger.when {
1070 validate_trigger_expr(expr, &target_schema, trigger.event)?;
1071 }
1072 for step in &trigger.program.steps {
1073 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
1074 {
1075 return Err(MongrelError::InvalidArgument(
1076 "SetNew trigger steps are only valid in BEFORE triggers".into(),
1077 ));
1078 }
1079 validate_trigger_step(step, &cat, &target_schema, trigger.event)?;
1080 }
1081 Ok(())
1082 }
1083
1084 pub fn begin(&self) -> crate::txn::Transaction<'_> {
1086 let txn_id = self.alloc_txn_id();
1087 let read = Snapshot::at(self.epoch.visible());
1088 crate::txn::Transaction::new(self, txn_id, read)
1089 }
1090
1091 pub fn begin_with_external_trigger_bridge<'a>(
1094 &'a self,
1095 bridge: &'a dyn ExternalTriggerBridge,
1096 ) -> crate::txn::Transaction<'a> {
1097 let txn_id = self.alloc_txn_id();
1098 let read = Snapshot::at(self.epoch.visible());
1099 crate::txn::Transaction::new(self, txn_id, read).with_external_trigger_bridge(bridge)
1100 }
1101
1102 pub fn transaction<T>(
1104 &self,
1105 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
1106 ) -> Result<T> {
1107 let mut tx = self.begin();
1108 match f(&mut tx) {
1109 Ok(out) => {
1110 tx.commit()?;
1111 Ok(out)
1112 }
1113 Err(e) => {
1114 tx.rollback();
1115 Err(e)
1116 }
1117 }
1118 }
1119
1120 pub fn transaction_with_external_trigger_bridge<'a, T>(
1123 &'a self,
1124 bridge: &'a dyn ExternalTriggerBridge,
1125 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
1126 ) -> Result<T> {
1127 let mut tx = self.begin_with_external_trigger_bridge(bridge);
1128 match f(&mut tx) {
1129 Ok(out) => {
1130 tx.commit()?;
1131 Ok(out)
1132 }
1133 Err(e) => {
1134 tx.rollback();
1135 Err(e)
1136 }
1137 }
1138 }
1139
1140 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
1143 self.active_txns.register(epoch)
1144 }
1145
1146 fn fill_auto_increment_for_staging(
1147 &self,
1148 staging: &mut [(u64, crate::txn::Staged)],
1149 ) -> Result<()> {
1150 let tables = self.tables.read();
1151 for (table_id, staged) in staging {
1152 if let crate::txn::Staged::Put(cells) = staged {
1153 if let Some(handle) = tables.get(table_id) {
1154 let mut t = handle.lock();
1155 t.fill_auto_inc(cells)?;
1156 }
1157 }
1158 }
1159 Ok(())
1160 }
1161
1162 fn expand_table_triggers(
1163 &self,
1164 staging: &mut Vec<(u64, crate::txn::Staged)>,
1165 read_epoch: Epoch,
1166 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
1167 external_states: &mut Vec<(String, Vec<u8>)>,
1168 ) -> Result<()> {
1169 let mut external_writes = Vec::new();
1170 let config = self.trigger_config();
1171 if config.recursive_triggers {
1172 let chunk = std::mem::take(staging);
1173 let stacks = vec![Vec::new(); chunk.len()];
1174 *staging = self.expand_trigger_chunk(
1175 chunk,
1176 stacks,
1177 read_epoch,
1178 0,
1179 config.max_depth,
1180 &mut external_writes,
1181 )?;
1182 self.apply_external_trigger_writes(
1183 external_writes,
1184 external_trigger_bridge,
1185 external_states,
1186 staging,
1187 )?;
1188 return Ok(());
1189 }
1190
1191 let mut expansion = self.expand_table_triggers_once(staging, read_epoch, None)?;
1192 if !expansion.before.is_empty() {
1193 let mut final_staging = expansion.before;
1194 final_staging.extend(filter_ignored_staging(
1195 std::mem::take(staging),
1196 &expansion.ignored_indices,
1197 ));
1198 *staging = final_staging;
1199 } else if !expansion.ignored_indices.is_empty() {
1200 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
1201 }
1202 staging.append(&mut expansion.after);
1203 external_writes.append(&mut expansion.before_external);
1204 external_writes.append(&mut expansion.after_external);
1205 self.apply_external_trigger_writes(
1206 external_writes,
1207 external_trigger_bridge,
1208 external_states,
1209 staging,
1210 )?;
1211 Ok(())
1212 }
1213
1214 fn expand_trigger_chunk(
1215 &self,
1216 mut chunk: Vec<(u64, crate::txn::Staged)>,
1217 stacks: Vec<Vec<String>>,
1218 read_epoch: Epoch,
1219 depth: u32,
1220 max_depth: u32,
1221 external_writes: &mut Vec<ExternalTriggerWrite>,
1222 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
1223 if chunk.is_empty() {
1224 return Ok(Vec::new());
1225 }
1226 self.fill_auto_increment_for_staging(&mut chunk)?;
1227 let expansion = self.expand_table_triggers_once(&mut chunk, read_epoch, Some(&stacks))?;
1228 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
1229 let stack = expansion
1230 .before_stacks
1231 .first()
1232 .or_else(|| expansion.after_stacks.first())
1233 .cloned()
1234 .unwrap_or_default();
1235 return Err(MongrelError::Conflict(format!(
1236 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
1237 Self::format_trigger_stack(&stack)
1238 )));
1239 }
1240
1241 let mut out = Vec::new();
1242 external_writes.extend(expansion.before_external);
1243 out.extend(self.expand_trigger_chunk(
1244 expansion.before,
1245 expansion.before_stacks,
1246 read_epoch,
1247 depth + 1,
1248 max_depth,
1249 external_writes,
1250 )?);
1251 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
1252 external_writes.extend(expansion.after_external);
1253 out.extend(self.expand_trigger_chunk(
1254 expansion.after,
1255 expansion.after_stacks,
1256 read_epoch,
1257 depth + 1,
1258 max_depth,
1259 external_writes,
1260 )?);
1261 Ok(out)
1262 }
1263
1264 fn apply_external_trigger_writes(
1265 &self,
1266 writes: Vec<ExternalTriggerWrite>,
1267 bridge: Option<&dyn ExternalTriggerBridge>,
1268 external_states: &mut Vec<(String, Vec<u8>)>,
1269 staging: &mut Vec<(u64, crate::txn::Staged)>,
1270 ) -> Result<()> {
1271 if writes.is_empty() {
1272 return Ok(());
1273 }
1274 let bridge = bridge.ok_or_else(|| {
1275 MongrelError::InvalidArgument(
1276 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
1277 )
1278 })?;
1279 for write in writes {
1280 let table = write.table().to_string();
1281 let entry = self.external_table(&table).ok_or_else(|| {
1282 MongrelError::NotFound(format!("external table {table:?} not found"))
1283 })?;
1284 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
1285 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
1286 external_states.push((table, result.state));
1287 for base_write in result.base_writes {
1288 match base_write {
1289 ExternalTriggerBaseWrite::Put { table, cells } => {
1290 let table_id = self.table_id(&table)?;
1291 staging.push((table_id, crate::txn::Staged::Put(cells)));
1292 }
1293 ExternalTriggerBaseWrite::Delete { table, row_id } => {
1294 let table_id = self.table_id(&table)?;
1295 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
1296 }
1297 }
1298 }
1299 }
1300 dedup_external_states_in_place(external_states);
1301 Ok(())
1302 }
1303
1304 fn expand_table_triggers_once(
1305 &self,
1306 staging: &mut Vec<(u64, crate::txn::Staged)>,
1307 read_epoch: Epoch,
1308 trigger_stacks: Option<&[Vec<String>]>,
1309 ) -> Result<TriggerExpansion> {
1310 let triggers: Vec<StoredTrigger> = self
1311 .catalog
1312 .read()
1313 .triggers
1314 .iter()
1315 .filter(|entry| {
1316 entry.trigger.enabled
1317 && matches!(
1318 entry.trigger.timing,
1319 TriggerTiming::Before | TriggerTiming::After
1320 )
1321 && matches!(entry.trigger.target, TriggerTarget::Table(_))
1322 })
1323 .map(|entry| entry.trigger.clone())
1324 .collect();
1325 if triggers.is_empty() || staging.is_empty() {
1326 return Ok(TriggerExpansion::default());
1327 }
1328
1329 let before_triggers = triggers
1330 .iter()
1331 .filter(|trigger| trigger.timing == TriggerTiming::Before)
1332 .cloned()
1333 .collect::<Vec<_>>();
1334 let after_triggers = triggers
1335 .iter()
1336 .filter(|trigger| trigger.timing == TriggerTiming::After)
1337 .cloned()
1338 .collect::<Vec<_>>();
1339
1340 let mut before_added = Vec::new();
1341 let mut before_stacks = Vec::new();
1342 let mut before_external = Vec::new();
1343 let mut ignored_indices = std::collections::BTreeSet::new();
1344 if !before_triggers.is_empty() {
1345 let before_events =
1346 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?;
1347 let mut out = TriggerProgramOutput {
1348 added: &mut before_added,
1349 added_stacks: &mut before_stacks,
1350 added_external: &mut before_external,
1351 ignored_indices: &mut ignored_indices,
1352 };
1353 self.execute_triggers_for_events(
1354 &before_triggers,
1355 &before_events,
1356 Some(staging),
1357 &mut out,
1358 )?;
1359 }
1360
1361 let after_events = if after_triggers.is_empty() {
1362 Vec::new()
1363 } else {
1364 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?
1365 .into_iter()
1366 .filter(|event| {
1367 !event
1368 .op_indices
1369 .iter()
1370 .any(|idx| ignored_indices.contains(idx))
1371 })
1372 .collect()
1373 };
1374
1375 let mut after_added = Vec::new();
1376 let mut after_stacks = Vec::new();
1377 let mut after_external = Vec::new();
1378 let mut out = TriggerProgramOutput {
1379 added: &mut after_added,
1380 added_stacks: &mut after_stacks,
1381 added_external: &mut after_external,
1382 ignored_indices: &mut ignored_indices,
1383 };
1384 self.execute_triggers_for_events(&after_triggers, &after_events, None, &mut out)?;
1385 Ok(TriggerExpansion {
1386 before: before_added,
1387 before_stacks,
1388 before_external,
1389 after: after_added,
1390 after_stacks,
1391 after_external,
1392 ignored_indices,
1393 })
1394 }
1395
1396 fn execute_triggers_for_events(
1397 &self,
1398 triggers: &[StoredTrigger],
1399 events: &[WriteEvent],
1400 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
1401 out: &mut TriggerProgramOutput<'_>,
1402 ) -> Result<()> {
1403 for event in events {
1404 for trigger in triggers {
1405 if event
1406 .op_indices
1407 .iter()
1408 .any(|idx| out.ignored_indices.contains(idx))
1409 {
1410 break;
1411 }
1412 let matches = {
1413 let cat = self.catalog.read();
1414 trigger_matches_event(trigger, event, &cat)?
1415 };
1416 if !matches {
1417 continue;
1418 }
1419 if let Some(when) = &trigger.when {
1420 if !eval_trigger_expr(when, event)? {
1421 continue;
1422 }
1423 }
1424 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
1425 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
1426 return Err(MongrelError::Conflict(format!(
1427 "trigger recursion cycle detected; trigger stack: {}",
1428 Self::format_trigger_stack(&trigger_stack)
1429 )));
1430 }
1431 let outcome = match staging.as_mut() {
1432 Some(staging) => self.execute_trigger_program(
1433 trigger,
1434 event,
1435 Some(&mut **staging),
1436 out,
1437 &trigger_stack,
1438 )?,
1439 None => {
1440 self.execute_trigger_program(trigger, event, None, out, &trigger_stack)?
1441 }
1442 };
1443 if outcome == TriggerProgramOutcome::Ignore {
1444 out.ignored_indices.extend(event.op_indices.iter().copied());
1445 break;
1446 }
1447 }
1448 }
1449 Ok(())
1450 }
1451
1452 fn trigger_events_for_staging(
1453 &self,
1454 staging: &[(u64, crate::txn::Staged)],
1455 read_epoch: Epoch,
1456 trigger_stacks: Option<&[Vec<String>]>,
1457 ) -> Result<Vec<WriteEvent>> {
1458 use crate::txn::Staged;
1459 use std::collections::{HashMap, VecDeque};
1460
1461 let snapshot = Snapshot::at(read_epoch);
1462 let cat = self.catalog.read();
1463 let mut table_names = HashMap::new();
1464 let mut table_schemas = HashMap::new();
1465 for entry in cat
1466 .tables
1467 .iter()
1468 .filter(|entry| matches!(entry.state, TableState::Live))
1469 {
1470 table_names.insert(entry.table_id, entry.name.clone());
1471 table_schemas.insert(entry.table_id, entry.schema.clone());
1472 }
1473 drop(cat);
1474
1475 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
1476 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
1477 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
1478
1479 for (idx, (table_id, staged)) in staging.iter().enumerate() {
1480 let Some(schema) = table_schemas.get(table_id) else {
1481 continue;
1482 };
1483 let Some(pk) = schema.primary_key() else {
1484 continue;
1485 };
1486 match staged {
1487 Staged::Delete(row_id) => {
1488 let handle = self.table_by_id(*table_id)?;
1489 let Some(row) = handle.lock().get(*row_id, snapshot) else {
1490 continue;
1491 };
1492 let Some(pk_value) = row.columns.get(&pk.id) else {
1493 continue;
1494 };
1495 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
1496 delete_by_key
1497 .entry((*table_id, pk_value.encode_key()))
1498 .or_default()
1499 .push_back(idx);
1500 }
1501 Staged::Put(cells) => {
1502 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
1503 put_by_key
1504 .entry((*table_id, value.encode_key()))
1505 .or_default()
1506 .push_back(idx);
1507 }
1508 }
1509 Staged::Truncate => {}
1510 }
1511 }
1512
1513 let mut paired_delete = std::collections::HashSet::new();
1514 let mut paired_put = std::collections::HashSet::new();
1515 let mut events = Vec::new();
1516
1517 for (key, deletes) in delete_by_key.iter_mut() {
1518 let Some(puts) = put_by_key.get_mut(key) else {
1519 continue;
1520 };
1521 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
1522 paired_delete.insert(delete_idx);
1523 paired_put.insert(put_idx);
1524 let (table_id, _) = &staging[put_idx];
1525 let Some(table_name) = table_names.get(table_id).cloned() else {
1526 continue;
1527 };
1528 let old = old_rows.get(&delete_idx).cloned();
1529 let new = match &staging[put_idx].1 {
1530 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
1531 _ => None,
1532 };
1533 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
1534 events.push(WriteEvent {
1535 table: table_name,
1536 kind: TriggerEvent::Update,
1537 old,
1538 new,
1539 changed_columns,
1540 op_indices: vec![delete_idx, put_idx],
1541 put_idx: Some(put_idx),
1542 trigger_stack: Self::trigger_stack_for_indices(
1543 trigger_stacks,
1544 &[delete_idx, put_idx],
1545 ),
1546 });
1547 }
1548 }
1549
1550 for (idx, (table_id, staged)) in staging.iter().enumerate() {
1551 let Some(table_name) = table_names.get(table_id).cloned() else {
1552 continue;
1553 };
1554 match staged {
1555 Staged::Put(cells) if !paired_put.contains(&idx) => {
1556 let new = Some(TriggerRowImage::from_cells(cells));
1557 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
1558 events.push(WriteEvent {
1559 table: table_name,
1560 kind: TriggerEvent::Insert,
1561 old: None,
1562 new,
1563 changed_columns,
1564 op_indices: vec![idx],
1565 put_idx: Some(idx),
1566 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
1567 });
1568 }
1569 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
1570 let old = match old_rows.get(&idx).cloned() {
1571 Some(old) => Some(old),
1572 None => {
1573 let handle = self.table_by_id(*table_id)?;
1574 let row = handle.lock().get(*row_id, snapshot);
1575 row.map(TriggerRowImage::from_row)
1576 }
1577 };
1578 let Some(old) = old else {
1579 continue;
1580 };
1581 let changed_columns = old.columns.keys().copied().collect();
1582 events.push(WriteEvent {
1583 table: table_name,
1584 kind: TriggerEvent::Delete,
1585 old: Some(old),
1586 new: None,
1587 changed_columns,
1588 op_indices: vec![idx],
1589 put_idx: None,
1590 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
1591 });
1592 }
1593 Staged::Truncate => {}
1594 _ => {}
1595 }
1596 }
1597
1598 Ok(events)
1599 }
1600
1601 fn execute_trigger_program(
1602 &self,
1603 trigger: &StoredTrigger,
1604 event: &WriteEvent,
1605 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
1606 out: &mut TriggerProgramOutput<'_>,
1607 trigger_stack: &[String],
1608 ) -> Result<TriggerProgramOutcome> {
1609 let mut event = event.clone();
1610 for step in &trigger.program.steps {
1611 match step {
1612 TriggerStep::SetNew { cells } => {
1613 if trigger.timing != TriggerTiming::Before {
1614 return Err(MongrelError::InvalidArgument(
1615 "SetNew trigger step is only valid in BEFORE triggers".into(),
1616 ));
1617 }
1618 let put_idx = event.put_idx.ok_or_else(|| {
1619 MongrelError::InvalidArgument(
1620 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
1621 )
1622 })?;
1623 let staging = staging.as_deref_mut().ok_or_else(|| {
1624 MongrelError::InvalidArgument(
1625 "SetNew trigger step requires mutable trigger staging".into(),
1626 )
1627 })?;
1628 let Some((_, crate::txn::Staged::Put(row_cells))) = staging.get_mut(put_idx)
1629 else {
1630 return Err(MongrelError::InvalidArgument(
1631 "SetNew trigger step target row is not mutable".into(),
1632 ));
1633 };
1634 for (column_id, value) in eval_trigger_cells(cells, &event)? {
1635 row_cells.retain(|(id, _)| *id != column_id);
1636 row_cells.push((column_id, value.clone()));
1637 if let Some(new) = &mut event.new {
1638 new.columns.insert(column_id, value);
1639 }
1640 }
1641 row_cells.sort_by_key(|(id, _)| *id);
1642 }
1643 TriggerStep::Insert { table, cells } => {
1644 let cells = eval_trigger_cells(cells, &event)?;
1645 if let Ok(table_id) = self.table_id(table) {
1646 out.added.push((table_id, crate::txn::Staged::Put(cells)));
1647 out.added_stacks.push(trigger_stack.to_vec());
1648 } else if self.external_table(table).is_some() {
1649 out.added_external.push(ExternalTriggerWrite::Insert {
1650 table: table.clone(),
1651 cells,
1652 });
1653 } else {
1654 return Err(MongrelError::NotFound(format!(
1655 "trigger {:?} insert target {table:?} not found",
1656 trigger.name
1657 )));
1658 }
1659 }
1660 TriggerStep::UpdateByPk { table, pk, cells } => {
1661 let pk = eval_trigger_value(pk, &event)?;
1662 let cells = eval_trigger_cells(cells, &event)?;
1663 if self.external_table(table).is_some() {
1664 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
1665 table: table.clone(),
1666 pk,
1667 cells,
1668 });
1669 } else {
1670 let row_id = self
1671 .table(table)?
1672 .lock()
1673 .lookup_pk(&pk.encode_key())
1674 .ok_or_else(|| {
1675 MongrelError::NotFound(format!(
1676 "trigger {:?} update target not found",
1677 trigger.name
1678 ))
1679 })?;
1680 let handle = self.table(table)?;
1681 let snapshot = Snapshot::at(self.epoch.visible());
1682 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
1683 MongrelError::NotFound(format!(
1684 "trigger {:?} update target not visible",
1685 trigger.name
1686 ))
1687 })?;
1688 let mut merged = old.columns;
1689 for (column_id, value) in cells {
1690 merged.insert(column_id, value);
1691 }
1692 out.added
1693 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
1694 out.added_stacks.push(trigger_stack.to_vec());
1695 out.added.push((
1696 self.table_id(table)?,
1697 crate::txn::Staged::Put(merged.into_iter().collect()),
1698 ));
1699 out.added_stacks.push(trigger_stack.to_vec());
1700 }
1701 }
1702 TriggerStep::DeleteByPk { table, pk } => {
1703 let pk = eval_trigger_value(pk, &event)?;
1704 if self.external_table(table).is_some() {
1705 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
1706 table: table.clone(),
1707 pk,
1708 });
1709 } else {
1710 let row_id = self
1711 .table(table)?
1712 .lock()
1713 .lookup_pk(&pk.encode_key())
1714 .ok_or_else(|| {
1715 MongrelError::NotFound(format!(
1716 "trigger {:?} delete target not found",
1717 trigger.name
1718 ))
1719 })?;
1720 out.added
1721 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
1722 out.added_stacks.push(trigger_stack.to_vec());
1723 }
1724 }
1725 TriggerStep::Select { .. } => {}
1726 TriggerStep::Raise { action, message } => match action {
1727 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
1728 TriggerRaiseAction::Abort
1729 | TriggerRaiseAction::Fail
1730 | TriggerRaiseAction::Rollback => {
1731 let message = eval_trigger_value(message, &event)?;
1732 return Err(MongrelError::Conflict(format!(
1733 "trigger {:?} raised: {}; trigger stack: {}",
1734 trigger.name,
1735 trigger_message(message),
1736 Self::format_trigger_stack(trigger_stack)
1737 )));
1738 }
1739 },
1740 }
1741 }
1742 Ok(TriggerProgramOutcome::Continue)
1743 }
1744
1745 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
1746 let Some(stacks) = stacks else {
1747 return Vec::new();
1748 };
1749 let mut out = Vec::new();
1750 for idx in indices {
1751 let Some(stack) = stacks.get(*idx) else {
1752 continue;
1753 };
1754 for name in stack {
1755 if !out.iter().any(|existing| existing == name) {
1756 out.push(name.clone());
1757 }
1758 }
1759 }
1760 out
1761 }
1762
1763 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
1764 let mut out = stack.to_vec();
1765 out.push(trigger_name.to_string());
1766 out
1767 }
1768
1769 fn format_trigger_stack(stack: &[String]) -> String {
1770 if stack.is_empty() {
1771 "<root>".into()
1772 } else {
1773 stack.join(" -> ")
1774 }
1775 }
1776
1777 fn validate_constraints(
1792 &self,
1793 staging: &mut Vec<(u64, crate::txn::Staged)>,
1794 read_epoch: Epoch,
1795 ) -> Result<()> {
1796 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
1797 use crate::memtable::Row;
1798 use crate::txn::Staged;
1799 use std::collections::HashSet;
1800
1801 let snapshot = Snapshot::at(read_epoch);
1802 let cat = self.catalog.read();
1803
1804 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
1806 .tables
1807 .iter()
1808 .filter(|e| matches!(e.state, TableState::Live))
1809 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
1810 .collect();
1811
1812 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
1814 if !any_constraints {
1815 return Ok(());
1816 }
1817
1818 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
1820 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
1821 if let Some(r) = rows_cache.get(&table_id) {
1822 return Ok(r.clone());
1823 }
1824 let handle = self.table_by_id(table_id)?;
1825 let rows = handle.lock().visible_rows(snapshot)?;
1826 rows_cache.insert(table_id, rows.clone());
1827 Ok(rows)
1828 };
1829
1830 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
1835 loop {
1836 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
1837 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
1838 .iter()
1839 .filter_map(|(t, op)| match op {
1840 Staged::Delete(rid) => Some((*t, *rid)),
1841 _ => None,
1842 })
1843 .collect();
1844 for (table_id, rid) in deletes {
1845 if !cascaded.insert((table_id, rid.0)) {
1846 continue;
1847 }
1848 let Some(tname) = live
1849 .iter()
1850 .find(|(t, _, _)| *t == table_id)
1851 .map(|(_, n, _)| *n)
1852 else {
1853 continue;
1854 };
1855 let parent_handle = self.table_by_id(table_id)?;
1856 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
1857 continue;
1858 };
1859 for (child_id, _child_name, child_schema) in &live {
1860 for fk in &child_schema.constraints.foreign_keys {
1861 if fk.ref_table != tname {
1862 continue;
1863 }
1864 let Some(parent_key) =
1865 encode_composite_key(&fk.ref_columns, &parent_row.columns)
1866 else {
1867 continue;
1868 };
1869 match fk.on_delete {
1870 FkAction::Restrict => continue,
1871 FkAction::Cascade => {
1872 let child_rows = load_rows(*child_id)?;
1873 for cr in &child_rows {
1874 if !cascaded.contains(&(*child_id, cr.row_id.0))
1875 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
1876 == Some(parent_key.as_slice())
1877 {
1878 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
1879 }
1880 }
1881 }
1882 FkAction::SetNull => {
1883 let child_rows = load_rows(*child_id)?;
1884 for cr in &child_rows {
1885 if !cascaded.contains(&(*child_id, cr.row_id.0))
1886 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
1887 == Some(parent_key.as_slice())
1888 {
1889 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
1892 .columns
1893 .iter()
1894 .map(|(k, v)| (*k, v.clone()))
1895 .collect();
1896 for cid in &fk.columns {
1897 cells.retain(|(k, _)| k != cid);
1898 cells.push((*cid, crate::memtable::Value::Null));
1899 }
1900 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
1901 new_ops.push((*child_id, Staged::Put(cells)));
1902 }
1903 }
1904 }
1905 }
1906 }
1907 }
1908 }
1909 if new_ops.is_empty() {
1910 break;
1911 }
1912 staging.extend(new_ops);
1913 }
1914
1915 let staged_deletes: HashSet<(u64, u64)> = staging
1919 .iter()
1920 .filter_map(|(t, op)| match op {
1921 Staged::Delete(rid) => Some((*t, rid.0)),
1922 _ => None,
1923 })
1924 .collect();
1925
1926 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
1928
1929 for (table_id, op) in staging.iter() {
1931 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
1932 else {
1933 continue;
1934 };
1935 let cells_map: HashMap<u16, crate::memtable::Value>;
1936 match op {
1937 Staged::Put(cells) => {
1938 cells_map = cells.iter().cloned().collect();
1939
1940 if !schema.constraints.checks.is_empty() {
1942 validate_checks(&schema.constraints.checks, &cells_map)?;
1943 }
1944
1945 for uc in &schema.constraints.uniques {
1947 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
1948 continue; };
1950 let marker = (*table_id, uc.id, key.clone());
1951 if !seen_unique.insert(marker) {
1952 return Err(MongrelError::Conflict(format!(
1953 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
1954 uc.name
1955 )));
1956 }
1957 let rows = load_rows(*table_id)?;
1958 for r in &rows {
1959 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
1962 continue;
1963 }
1964 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
1965 if theirs == key {
1966 return Err(MongrelError::Conflict(format!(
1967 "UNIQUE constraint '{}' on table '{tname}' violated",
1968 uc.name
1969 )));
1970 }
1971 }
1972 }
1973 }
1974
1975 for fk in &schema.constraints.foreign_keys {
1977 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
1978 continue; };
1980 let Some(parent_id) = cat
1981 .tables
1982 .iter()
1983 .find(|t| t.name == fk.ref_table)
1984 .map(|t| t.table_id)
1985 else {
1986 return Err(MongrelError::InvalidArgument(format!(
1987 "FOREIGN KEY '{}' references unknown table '{}'",
1988 fk.name, fk.ref_table
1989 )));
1990 };
1991 let parent_rows = load_rows(parent_id)?;
1992 let mut found = false;
1993 for r in &parent_rows {
1994 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
1995 continue;
1996 }
1997 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
1998 if pkey == child_key {
1999 found = true;
2000 break;
2001 }
2002 }
2003 }
2004 if !found {
2005 return Err(MongrelError::Conflict(format!(
2006 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
2007 fk.name, fk.ref_table
2008 )));
2009 }
2010 }
2011 }
2012 Staged::Delete(rid) => {
2013 let parent_handle = self.table_by_id(*table_id)?;
2017 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
2018 continue;
2019 };
2020 for (child_id, child_name, child_schema) in &live {
2021 for fk in &child_schema.constraints.foreign_keys {
2022 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
2023 continue;
2024 }
2025 let Some(parent_key) =
2026 encode_composite_key(&fk.ref_columns, &parent_row.columns)
2027 else {
2028 continue;
2029 };
2030 let child_rows = load_rows(*child_id)?;
2031 for r in &child_rows {
2032 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
2035 continue;
2036 }
2037 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
2038 if ck == parent_key {
2039 return Err(MongrelError::Conflict(format!(
2040 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
2041 fk.name
2042 )));
2043 }
2044 }
2045 }
2046 }
2047 }
2048 }
2049 Staged::Truncate => {
2050 for (child_id, child_name, child_schema) in &live {
2054 for fk in &child_schema.constraints.foreign_keys {
2055 if fk.ref_table != tname {
2056 continue;
2057 }
2058 let child_rows = load_rows(*child_id)?;
2059 if child_rows
2060 .iter()
2061 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
2062 {
2063 return Err(MongrelError::Conflict(format!(
2064 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
2065 fk.name
2066 )));
2067 }
2068 }
2069 }
2070 }
2071 }
2072 }
2073 Ok(())
2074 }
2075
2076 pub(crate) fn commit_transaction_with_external_states(
2083 &self,
2084 txn_id: u64,
2085 read_epoch: Epoch,
2086 mut staging: Vec<(u64, crate::txn::Staged)>,
2087 external_states: Vec<(String, Vec<u8>)>,
2088 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
2089 ) -> Result<Epoch> {
2090 use crate::memtable::Row;
2091 use crate::txn::{Staged, StagedOp, WriteKey};
2092 use crate::wal::Op;
2093 use std::collections::hash_map::DefaultHasher;
2094 use std::hash::{Hash, Hasher};
2095 use std::sync::atomic::Ordering;
2096
2097 if self.poisoned.load(Ordering::Relaxed) {
2098 return Err(MongrelError::Other(
2099 "database poisoned by fsync error".into(),
2100 ));
2101 }
2102 let mut external_states = dedup_external_states(external_states);
2103 if !external_states.is_empty() {
2104 let cat = self.catalog.read();
2105 for (name, _) in &external_states {
2106 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
2107 return Err(MongrelError::NotFound(format!(
2108 "external table {name:?} not found"
2109 )));
2110 }
2111 }
2112 }
2113
2114 self.fill_auto_increment_for_staging(&mut staging)?;
2117 self.expand_table_triggers(
2118 &mut staging,
2119 read_epoch,
2120 external_trigger_bridge,
2121 &mut external_states,
2122 )?;
2123 self.fill_auto_increment_for_staging(&mut staging)?;
2124 external_states = dedup_external_states(external_states);
2125
2126 self.validate_constraints(&mut staging, read_epoch)?;
2131
2132 let write_keys = {
2133 let cat = self.catalog.read();
2134 let mut keys: Vec<WriteKey> = Vec::new();
2135 for (table_id, staged) in &staging {
2136 match staged {
2137 Staged::Put(cells) => {
2138 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
2139 for col in &entry.schema.columns {
2140 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
2141 if let Some((_, val)) =
2142 cells.iter().find(|(id, _)| *id == col.id)
2143 {
2144 let mut h = DefaultHasher::new();
2145 val.encode_key().hash(&mut h);
2146 keys.push(WriteKey::Unique {
2147 table_id: *table_id,
2148 index_id: 0,
2149 key_hash: h.finish(),
2150 });
2151 }
2152 }
2153 }
2154 for uc in &entry.schema.constraints.uniques {
2161 if let Some(key_bytes) = crate::constraint::encode_composite_key(
2162 &uc.columns,
2163 &cells.iter().cloned().collect(),
2164 ) {
2165 let mut h = DefaultHasher::new();
2166 key_bytes.hash(&mut h);
2167 keys.push(WriteKey::Unique {
2168 table_id: *table_id,
2169 index_id: uc.id | 0x8000,
2170 key_hash: h.finish(),
2171 });
2172 }
2173 }
2174 }
2175 }
2176 Staged::Delete(rid) => keys.push(WriteKey::Row {
2177 table_id: *table_id,
2178 row_id: rid.0,
2179 }),
2180 Staged::Truncate => keys.push(WriteKey::Table {
2181 table_id: *table_id,
2182 }),
2183 }
2184 }
2185 for (name, _) in &external_states {
2186 let mut h = DefaultHasher::new();
2187 name.hash(&mut h);
2188 keys.push(WriteKey::Unique {
2189 table_id: EXTERNAL_TABLE_ID,
2190 index_id: 0,
2191 key_hash: h.finish(),
2192 });
2193 }
2194 keys
2195 };
2196
2197 let min_active = self.active_txns.min_read_epoch();
2199 if min_active < u64::MAX {
2200 self.conflicts.prune_below(Epoch(min_active));
2201 }
2202
2203 if self.conflicts.conflicts(&write_keys, read_epoch) {
2207 return Err(MongrelError::Conflict(
2208 "write-write conflict (pre-validate, first-committer-wins)".into(),
2209 ));
2210 }
2211 let pre_validate_version = self.conflicts.version();
2212
2213 let mut spilled: Vec<SpilledRun> = Vec::new();
2217 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
2218 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
2222 {
2223 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
2224 for (table_id, staged) in &staging {
2225 if let Staged::Put(cells) = staged {
2226 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
2227 }
2228 }
2229 let tables = self.tables.read();
2230 for (&table_id, &bytes) in &table_bytes {
2231 if bytes as u64
2232 <= self
2233 .spill_threshold
2234 .load(std::sync::atomic::Ordering::Relaxed)
2235 {
2236 continue;
2237 }
2238 let Some(handle) = tables.get(&table_id) else {
2239 continue;
2240 };
2241 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
2242 let mut t = handle.lock();
2243 let tdir = t.table_dir().to_path_buf();
2244 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
2245 std::fs::create_dir_all(&txn_dir)?;
2246 let run_id = t.alloc_run_id() as u128;
2247 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
2248
2249 let mut rows: Vec<Row> = Vec::new();
2250 for (tid, staged) in &staging {
2251 if *tid != table_id {
2252 continue;
2253 }
2254 if let Staged::Put(cells) = staged {
2255 t.validate_cells_not_null(cells)?;
2256 let row_id = t.alloc_row_id();
2257 let mut row = Row::new(row_id, Epoch(0));
2258 for (c, v) in cells {
2259 row.columns.insert(*c, v.clone());
2260 }
2261 rows.push(row);
2262 }
2263 }
2264 let schema = t.schema_ref().clone();
2265 let kek = t.kek_ref().cloned();
2266 let specs = t.indexable_column_specs();
2267 drop(t);
2268
2269 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
2270 .uniform_epoch(true);
2271 if let Some(ref kek) = kek {
2272 writer = writer.with_encryption(kek.as_ref(), specs);
2273 }
2274 let header = writer.write(&pending_path, &rows)?;
2275 let row_count = header.row_count;
2276 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
2277 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
2278
2279 spilled.push(SpilledRun {
2280 table_id,
2281 run_id,
2282 pending_path,
2283 rows,
2284 row_count,
2285 min_rid,
2286 max_rid,
2287 });
2288 spilled_tables.insert(table_id);
2289 }
2290 }
2291
2292 if spill_guard.is_some() {
2294 if let Some(hook) = self.spill_hook.lock().as_ref() {
2295 hook();
2296 }
2297 }
2298
2299 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
2310 {
2311 let tables = self.tables.read();
2312 for (table_id, staged) in &staging {
2313 match staged {
2314 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
2315 let handle = tables.get(table_id).ok_or_else(|| {
2316 MongrelError::NotFound(format!("table {table_id} not mounted"))
2317 })?;
2318 let mut t = handle.lock();
2319 t.validate_cells_not_null(cells)?;
2320 let row_id = t.alloc_row_id();
2321 drop(t);
2322 let mut row = Row::new(row_id, Epoch(0));
2323 for (c, v) in cells {
2324 row.columns.insert(*c, v.clone());
2325 }
2326 prebuilt.push(Some(row));
2327 }
2328 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
2329 }
2330 }
2331 }
2332
2333 let mut prepared_external = Vec::with_capacity(external_states.len());
2334 for (name, state) in &external_states {
2335 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
2336 prepared_external.push((name.clone(), state.clone(), pending));
2337 }
2338
2339 let added_runs: Vec<crate::wal::AddedRun> = spilled
2341 .iter()
2342 .map(|s| crate::wal::AddedRun {
2343 table_id: s.table_id,
2344 run_id: s.run_id,
2345 row_count: s.row_count,
2346 level: 0,
2347 min_row_id: s.min_rid,
2348 max_row_id: s.max_rid,
2349 content_hash: [0u8; 32],
2350 })
2351 .collect();
2352 let (new_epoch, applies, commit_seq) = {
2353 let mut wal = self.shared_wal.lock();
2354
2355 if self.conflicts.version() != pre_validate_version
2360 && self.conflicts.conflicts(&write_keys, read_epoch)
2361 {
2362 drop(wal);
2366 for s in &spilled {
2367 if let Some(parent) = s.pending_path.parent() {
2368 let _ = std::fs::remove_dir_all(parent);
2369 }
2370 }
2371 for (_, _, pending) in &prepared_external {
2372 let _ = std::fs::remove_file(pending);
2373 }
2374 return Err(MongrelError::Conflict(
2375 "write-write conflict (sequencer delta re-check)".into(),
2376 ));
2377 }
2378
2379 let new_epoch = self.epoch.bump_assigned();
2380 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
2381
2382 for (idx, (table_id, staged)) in staging.iter().enumerate() {
2383 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
2386 continue;
2387 }
2388 let mut ops = Vec::new();
2389 match staged {
2390 Staged::Put(_) => {
2391 let mut row = prebuilt[idx].take().expect("prebuilt put row");
2393 row.committed_epoch = new_epoch;
2394 let payload = bincode::serialize(&vec![row.clone()])
2395 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
2396 wal.append(
2397 txn_id,
2398 *table_id,
2399 Op::Put {
2400 table_id: *table_id,
2401 rows: payload,
2402 },
2403 )?;
2404 ops.push(StagedOp::Put(row));
2405 }
2406 Staged::Delete(rid) => {
2407 wal.append(
2408 txn_id,
2409 *table_id,
2410 Op::Delete {
2411 table_id: *table_id,
2412 row_ids: vec![*rid],
2413 },
2414 )?;
2415 ops.push(StagedOp::Delete(*rid));
2416 }
2417 Staged::Truncate => {
2418 wal.append(
2419 txn_id,
2420 *table_id,
2421 Op::TruncateTable {
2422 table_id: *table_id,
2423 },
2424 )?;
2425 ops.push(StagedOp::Truncate);
2426 }
2427 }
2428 applies.push((*table_id, ops));
2429 }
2430
2431 for (name, state, _) in &prepared_external {
2432 wal.append(
2433 txn_id,
2434 EXTERNAL_TABLE_ID,
2435 Op::ExternalTableState {
2436 name: name.clone(),
2437 state: state.clone(),
2438 },
2439 )?;
2440 }
2441
2442 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
2443
2444 self.conflicts.record(&write_keys, new_epoch);
2449 (new_epoch, applies, commit_seq)
2450 };
2451
2452 self.group
2454 .await_durable(&self.shared_wal, commit_seq)
2455 .inspect_err(|_| {
2456 self.poisoned.store(true, Ordering::Relaxed);
2457 })?;
2458
2459 {
2461 let tables = self.tables.read();
2462 for s in &spilled {
2464 if let Some(handle) = tables.get(&s.table_id) {
2465 let mut t = handle.lock();
2466 let dest = t.run_path(s.run_id as u64);
2467 std::fs::rename(&s.pending_path, &dest)?;
2468 if let Some(parent) = s.pending_path.parent() {
2470 let _ = std::fs::remove_dir_all(parent);
2471 }
2472 t.link_run(crate::manifest::RunRef {
2473 run_id: s.run_id,
2474 level: 0,
2475 epoch_created: new_epoch.0,
2476 row_count: s.row_count,
2477 });
2478 t.apply_run_metadata(&s.rows)?;
2483 t.invalidate_pending_cache();
2484 t.persist_manifest(new_epoch)?;
2485 }
2486 }
2487 for (table_id, ops) in applies {
2489 if let Some(handle) = tables.get(&table_id) {
2490 let mut t = handle.lock();
2491 for op in ops {
2492 match op {
2493 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
2494 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
2495 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
2496 }
2497 }
2498 t.invalidate_pending_cache();
2499 t.persist_manifest(new_epoch)?;
2500 }
2501 }
2502 }
2503 for (name, _, pending) in &prepared_external {
2504 publish_external_state_file(&self.root, name, pending)?;
2505 }
2506
2507 self.advance_visible(new_epoch);
2508 Ok(new_epoch)
2509 }
2510
2511 fn advance_visible(&self, published: Epoch) {
2517 self.epoch.publish_in_order(published);
2518 }
2519
2520 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
2523 let e = self.epoch.visible();
2524 let g = self.snapshots.register(e);
2525 (Snapshot::at(e), g)
2526 }
2527
2528 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
2531 let e = self.epoch.visible();
2532 let g = self.snapshots.register_owned(e);
2533 (Snapshot::at(e), g)
2534 }
2535
2536 pub fn table_names(&self) -> Vec<String> {
2538 self.catalog
2539 .read()
2540 .tables
2541 .iter()
2542 .filter(|t| matches!(t.state, TableState::Live))
2543 .map(|t| t.name.clone())
2544 .collect()
2545 }
2546
2547 pub fn close(&self) -> Result<()> {
2553 for name in self.table_names() {
2554 if let Ok(handle) = self.table(&name) {
2555 if let Err(e) = handle.lock().close() {
2556 eprintln!("[close] flush failed for {name}: {e}");
2557 }
2558 }
2559 }
2560 Ok(())
2561 }
2562
2563 pub fn compact(&self) -> Result<(usize, usize)> {
2569 let mut compacted = 0;
2570 let mut skipped = 0;
2571 for name in self.table_names() {
2572 let Ok(handle) = self.table(&name) else {
2573 continue;
2574 };
2575 {
2576 let mut t = handle.lock();
2577 let before = t.run_count();
2578 if before < 2 {
2579 skipped += 1;
2580 continue;
2581 }
2582 match t.compact() {
2583 Ok(()) => {
2584 let after = t.run_count();
2585 compacted += 1;
2586 eprintln!("[compact] {name}: {before} -> {after} runs");
2587 }
2588 Err(e) => {
2589 eprintln!("[compact] {name}: compaction failed: {e}");
2590 skipped += 1;
2591 }
2592 }
2593 }
2594 }
2595 Ok((compacted, skipped))
2596 }
2597
2598 pub fn compact_table(&self, name: &str) -> Result<bool> {
2601 let handle = self.table(name)?;
2602 let mut t = handle.lock();
2603 let before = t.run_count();
2604 if before < 2 {
2605 return Ok(false);
2606 }
2607 t.compact()?;
2608 Ok(t.run_count() < before)
2609 }
2610
2611 pub fn table(&self, name: &str) -> Result<TableHandle> {
2613 let cat = self.catalog.read();
2614 let entry = cat
2615 .live(name)
2616 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
2617 let id = entry.table_id;
2618 drop(cat);
2619 self.tables
2620 .read()
2621 .get(&id)
2622 .cloned()
2623 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
2624 }
2625
2626 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
2629 self.tables
2630 .read()
2631 .get(&id)
2632 .cloned()
2633 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
2634 }
2635
2636 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
2642 use crate::wal::DdlOp;
2643 use std::sync::atomic::Ordering;
2644
2645 if self.poisoned.load(Ordering::Relaxed) {
2646 return Err(MongrelError::Other(
2647 "database poisoned by fsync error".into(),
2648 ));
2649 }
2650
2651 let _g = self.ddl_lock.lock();
2652 {
2653 let cat = self.catalog.read();
2654 if cat.live(name).is_some() {
2655 return Err(MongrelError::InvalidArgument(format!(
2656 "table {name:?} already exists"
2657 )));
2658 }
2659 }
2660
2661 let commit_lock = Arc::clone(&self.commit_lock);
2664 let _c = commit_lock.lock();
2665 let table_id = {
2666 let mut cat = self.catalog.write();
2667 let id = cat.next_table_id;
2668 cat.next_table_id += 1;
2669 id
2670 };
2671 let epoch = self.epoch.bump_assigned();
2672 let txn_id = self.alloc_txn_id();
2673
2674 let mut schema = schema;
2678 schema.schema_id = table_id;
2679 schema.validate_auto_increment()?;
2686
2687 let schema_json = DdlOp::encode_schema(&schema)?;
2690 let commit_seq = {
2691 let mut wal = self.shared_wal.lock();
2692 wal.append(
2693 txn_id,
2694 table_id,
2695 crate::wal::Op::Ddl(DdlOp::CreateTable {
2696 table_id,
2697 name: name.to_string(),
2698 schema_json,
2699 }),
2700 )?;
2701 wal.append_commit(txn_id, epoch, &[])?
2702 };
2703 self.group
2704 .await_durable(&self.shared_wal, commit_seq)
2705 .inspect_err(|_| {
2706 self.poisoned.store(true, Ordering::Relaxed);
2707 })?;
2708
2709 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
2711 std::fs::create_dir_all(&tdir)?;
2712 let ctx = SharedCtx {
2713 epoch: Arc::clone(&self.epoch),
2714 page_cache: Arc::clone(&self.page_cache),
2715 decoded_cache: Arc::clone(&self.decoded_cache),
2716 snapshots: Arc::clone(&self.snapshots),
2717 kek: self.kek.clone(),
2718 commit_lock: Arc::clone(&self.commit_lock),
2719 shared: Some(crate::engine::SharedWalCtx {
2720 wal: Arc::clone(&self.shared_wal),
2721 group: Arc::clone(&self.group),
2722 poisoned: Arc::clone(&self.poisoned),
2723 txn_ids: Arc::clone(&self.next_txn_id),
2724 }),
2725 };
2726 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
2727
2728 {
2731 let mut cat = self.catalog.write();
2732 cat.tables.push(CatalogEntry {
2733 table_id,
2734 name: name.to_string(),
2735 schema,
2736 state: TableState::Live,
2737 created_epoch: epoch.0,
2738 });
2739 }
2740 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2741 self.tables
2742 .write()
2743 .insert(table_id, Arc::new(Mutex::new(table)));
2744
2745 self.advance_visible(epoch);
2746 Ok(table_id)
2747 }
2748
2749 pub fn drop_table(&self, name: &str) -> Result<()> {
2751 use crate::wal::DdlOp;
2752 use std::sync::atomic::Ordering;
2753
2754 if self.poisoned.load(Ordering::Relaxed) {
2755 return Err(MongrelError::Other(
2756 "database poisoned by fsync error".into(),
2757 ));
2758 }
2759
2760 let _g = self.ddl_lock.lock();
2761 let table_id = {
2762 let cat = self.catalog.read();
2763 cat.live(name)
2764 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
2765 .table_id
2766 };
2767
2768 let commit_lock = Arc::clone(&self.commit_lock);
2769 let _c = commit_lock.lock();
2770 let epoch = self.epoch.bump_assigned();
2771 let txn_id = self.alloc_txn_id();
2772 let commit_seq = {
2773 let mut wal = self.shared_wal.lock();
2774 wal.append(
2775 txn_id,
2776 table_id,
2777 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
2778 )?;
2779 wal.append_commit(txn_id, epoch, &[])?
2780 };
2781 self.group
2782 .await_durable(&self.shared_wal, commit_seq)
2783 .inspect_err(|_| {
2784 self.poisoned.store(true, Ordering::Relaxed);
2785 })?;
2786
2787 {
2788 let mut cat = self.catalog.write();
2789 let entry = cat
2790 .tables
2791 .iter_mut()
2792 .find(|t| t.table_id == table_id)
2793 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
2794 entry.state = TableState::Dropped { at_epoch: epoch.0 };
2795 cat.triggers.retain(|trigger| {
2796 !matches!(
2797 &trigger.trigger.target,
2798 TriggerTarget::Table(target) if target == name
2799 )
2800 });
2801 }
2802 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2803 self.tables.write().remove(&table_id);
2804
2805 self.advance_visible(epoch);
2806 Ok(())
2807 }
2808
2809 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
2818 use crate::wal::DdlOp;
2819 use std::sync::atomic::Ordering;
2820
2821 if self.poisoned.load(Ordering::Relaxed) {
2822 return Err(MongrelError::Other(
2823 "database poisoned by fsync error".into(),
2824 ));
2825 }
2826
2827 if name == new_name {
2830 return Ok(());
2831 }
2832 if new_name.is_empty() {
2833 return Err(MongrelError::InvalidArgument(
2834 "rename_table: new name must not be empty".into(),
2835 ));
2836 }
2837
2838 let _g = self.ddl_lock.lock();
2839 let table_id = {
2840 let cat = self.catalog.read();
2841 let src = cat
2842 .live(name)
2843 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
2844 if cat.live(new_name).is_some() {
2848 return Err(MongrelError::InvalidArgument(format!(
2849 "rename_table: a table named {new_name:?} already exists"
2850 )));
2851 }
2852 src.table_id
2853 };
2854
2855 let commit_lock = Arc::clone(&self.commit_lock);
2856 let _c = commit_lock.lock();
2857 let epoch = self.epoch.bump_assigned();
2858 let txn_id = self.alloc_txn_id();
2859 let commit_seq = {
2860 let mut wal = self.shared_wal.lock();
2861 wal.append(
2862 txn_id,
2863 table_id,
2864 crate::wal::Op::Ddl(DdlOp::RenameTable {
2865 table_id,
2866 new_name: new_name.to_string(),
2867 }),
2868 )?;
2869 wal.append_commit(txn_id, epoch, &[])?
2870 };
2871 self.group
2872 .await_durable(&self.shared_wal, commit_seq)
2873 .inspect_err(|_| {
2874 self.poisoned.store(true, Ordering::Relaxed);
2875 })?;
2876
2877 {
2878 let mut cat = self.catalog.write();
2879 let entry = cat
2880 .tables
2881 .iter_mut()
2882 .find(|t| t.table_id == table_id)
2883 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
2884 entry.name = new_name.to_string();
2885 for trigger in &mut cat.triggers {
2886 if matches!(
2887 &trigger.trigger.target,
2888 TriggerTarget::Table(target) if target == name
2889 ) {
2890 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
2891 }
2892 }
2893 }
2894 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2895 self.advance_visible(epoch);
2898 Ok(())
2899 }
2900
2901 pub fn alter_column(
2902 &self,
2903 table_name: &str,
2904 column_name: &str,
2905 change: AlterColumn,
2906 ) -> Result<ColumnDef> {
2907 use crate::wal::DdlOp;
2908 use std::sync::atomic::Ordering;
2909
2910 if self.poisoned.load(Ordering::Relaxed) {
2911 return Err(MongrelError::Other(
2912 "database poisoned by fsync error".into(),
2913 ));
2914 }
2915
2916 let _g = self.ddl_lock.lock();
2917 let table_id = {
2918 let cat = self.catalog.read();
2919 cat.live(table_name)
2920 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
2921 .table_id
2922 };
2923 let handle =
2924 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
2925 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
2926 })?;
2927 let mut table = handle.lock();
2928 let column = table.prepare_alter_column(column_name, &change)?;
2929 let renamed_column = (column.name != column_name).then(|| column.name.clone());
2930 if table
2931 .schema()
2932 .columns
2933 .iter()
2934 .find(|c| c.id == column.id)
2935 .is_some_and(|c| c == &column)
2936 {
2937 return Ok(column);
2938 }
2939
2940 let commit_lock = Arc::clone(&self.commit_lock);
2941 let _c = commit_lock.lock();
2942 let epoch = self.epoch.bump_assigned();
2943 let txn_id = self.alloc_txn_id();
2944 let column_json = DdlOp::encode_column(&column)?;
2945 let commit_seq = {
2946 let mut wal = self.shared_wal.lock();
2947 wal.append(
2948 txn_id,
2949 table_id,
2950 crate::wal::Op::Ddl(DdlOp::AlterTable {
2951 table_id,
2952 column_json,
2953 }),
2954 )?;
2955 wal.append_commit(txn_id, epoch, &[])?
2956 };
2957 self.group
2958 .await_durable(&self.shared_wal, commit_seq)
2959 .inspect_err(|_| {
2960 self.poisoned.store(true, Ordering::Relaxed);
2961 })?;
2962
2963 table.apply_altered_column(column.clone())?;
2964 let schema = table.schema().clone();
2965 drop(table);
2966
2967 {
2968 let mut cat = self.catalog.write();
2969 let entry = cat
2970 .tables
2971 .iter_mut()
2972 .find(|t| t.table_id == table_id)
2973 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
2974 entry.schema = schema;
2975 if let Some(new_column_name) = renamed_column {
2976 for trigger in &mut cat.triggers {
2977 if matches!(
2978 &trigger.trigger.target,
2979 TriggerTarget::Table(target) if target == table_name
2980 ) {
2981 trigger.trigger = trigger.trigger.renamed_update_column(
2982 column_name,
2983 new_column_name.clone(),
2984 epoch.0,
2985 )?;
2986 }
2987 }
2988 }
2989 }
2990 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2991
2992 self.advance_visible(epoch);
2993 Ok(column)
2994 }
2995
2996 pub fn gc(&self) -> Result<usize> {
3002 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
3003 let mut reclaimed = 0;
3004
3005 let cat = self.catalog.read();
3007 for entry in &cat.tables {
3008 if let TableState::Dropped { at_epoch } = entry.state {
3009 if at_epoch <= min_active {
3010 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
3011 if tdir.exists() {
3012 std::fs::remove_dir_all(&tdir)?;
3013 reclaimed += 1;
3014 }
3015 }
3016 }
3017 }
3018 drop(cat);
3019
3020 let cat = self.catalog.read();
3025 for entry in &cat.tables {
3026 if !matches!(entry.state, TableState::Live) {
3027 continue;
3028 }
3029 let txn_dir = self
3030 .root
3031 .join(TABLES_DIR)
3032 .join(entry.table_id.to_string())
3033 .join("_txn");
3034 if !txn_dir.exists() {
3035 continue;
3036 }
3037 for sub in std::fs::read_dir(&txn_dir)? {
3038 let sub = sub?;
3039 let name = sub.file_name();
3040 let Some(name) = name.to_str() else { continue };
3041 let is_active = name
3043 .parse::<u64>()
3044 .map(|id| self.active_spills.is_active(id))
3045 .unwrap_or(false);
3046 if is_active {
3047 continue;
3048 }
3049 std::fs::remove_dir_all(sub.path())?;
3050 reclaimed += 1;
3051 }
3052 }
3053 drop(cat);
3054
3055 let external_names = {
3056 let cat = self.catalog.read();
3057 cat.external_tables
3058 .iter()
3059 .map(|entry| entry.name.clone())
3060 .collect::<std::collections::HashSet<_>>()
3061 };
3062 let vtab_dir = self.root.join(VTAB_DIR);
3063 if vtab_dir.exists() {
3064 for entry in std::fs::read_dir(&vtab_dir)? {
3065 let entry = entry?;
3066 let name = entry.file_name();
3067 let Some(name) = name.to_str() else { continue };
3068 if external_names.contains(name) {
3069 continue;
3070 }
3071 let path = entry.path();
3072 if path.is_dir() {
3073 std::fs::remove_dir_all(path)?;
3074 } else {
3075 std::fs::remove_file(path)?;
3076 }
3077 reclaimed += 1;
3078 }
3079 }
3080
3081 let tables = self.tables.read();
3085 for handle in tables.values() {
3086 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
3087 }
3088
3089 let all_durable = self.active_spills.is_idle()
3097 && tables.values().all(|h| {
3098 let g = h.lock();
3099 g.memtable_len() == 0 && g.mutable_run_len() == 0
3100 });
3101 drop(tables);
3102 if all_durable {
3103 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
3104 }
3105
3106 Ok(reclaimed)
3107 }
3108 fn alloc_txn_id(&self) -> u64 {
3109 let mut g = self.next_txn_id.lock();
3110 let id = *g;
3111 *g = g.wrapping_add(1);
3112 id
3113 }
3114
3115 pub fn set_spill_threshold(&self, bytes: u64) {
3119 self.spill_threshold
3120 .store(bytes, std::sync::atomic::Ordering::Relaxed);
3121 }
3122
3123 #[doc(hidden)]
3127 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
3128 *self.spill_hook.lock() = Some(Box::new(f));
3129 }
3130
3131 #[doc(hidden)]
3135 pub fn __wal_group_sync_count(&self) -> u64 {
3136 self.shared_wal.lock().group_sync_count()
3137 }
3138
3139 #[doc(hidden)]
3142 pub fn __poison(&self) {
3143 self.poisoned
3144 .store(true, std::sync::atomic::Ordering::Relaxed);
3145 }
3146
3147 pub fn check(&self) -> Vec<CheckIssue> {
3160 let mut issues = Vec::new();
3161 let cat = self.catalog.read();
3162 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
3163 for entry in &cat.tables {
3164 if !matches!(entry.state, TableState::Live) {
3165 continue;
3166 }
3167 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
3168 let mut err = |sev: &str, desc: String| {
3169 issues.push(CheckIssue {
3170 table_id: entry.table_id,
3171 table_name: entry.name.clone(),
3172 severity: sev.into(),
3173 description: desc,
3174 });
3175 };
3176 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
3177 Ok(m) => m,
3178 Err(e) => {
3179 err("error", format!("manifest read failed: {e}"));
3180 continue;
3181 }
3182 };
3183 if m.flushed_epoch > m.current_epoch {
3184 err(
3185 "error",
3186 format!(
3187 "flushed_epoch {} exceeds current_epoch {} (impossible)",
3188 m.flushed_epoch, m.current_epoch
3189 ),
3190 );
3191 }
3192
3193 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
3194 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
3195 for rr in &m.runs {
3196 referenced.insert(rr.run_id);
3197 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
3198 if !run_path.exists() {
3199 err("error", format!("missing run file: r-{}.sr", rr.run_id));
3200 continue;
3201 }
3202 match crate::sorted_run::RunReader::open(
3203 &run_path,
3204 entry.schema.clone(),
3205 self.kek.clone(),
3206 ) {
3207 Ok(reader) => {
3208 if reader.row_count() as u64 != rr.row_count {
3209 err(
3210 "error",
3211 format!(
3212 "run r-{} row count mismatch: manifest {} vs run {}",
3213 rr.run_id,
3214 rr.row_count,
3215 reader.row_count()
3216 ),
3217 );
3218 }
3219 }
3220 Err(e) => {
3221 err(
3222 "error",
3223 format!("run r-{} integrity check failed: {e}", rr.run_id),
3224 );
3225 }
3226 }
3227 }
3228
3229 for r in &m.retiring {
3233 referenced.insert(r.run_id);
3234 }
3235
3236 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
3238 for ent in rd.flatten() {
3239 let p = ent.path();
3240 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
3241 continue;
3242 }
3243 let run_id = p
3244 .file_stem()
3245 .and_then(|s| s.to_str())
3246 .and_then(|s| s.strip_prefix("r-"))
3247 .and_then(|s| s.parse::<u128>().ok());
3248 if let Some(id) = run_id {
3249 if !referenced.contains(&id) {
3250 err(
3251 "warning",
3252 format!("orphan run file r-{id}.sr not referenced by the manifest"),
3253 );
3254 }
3255 }
3256 }
3257 }
3258 }
3259
3260 let external_names = cat
3261 .external_tables
3262 .iter()
3263 .map(|entry| entry.name.clone())
3264 .collect::<std::collections::HashSet<_>>();
3265 let vtab_dir = self.root.join(VTAB_DIR);
3266 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
3267 for entry in entries.flatten() {
3268 let name = entry.file_name();
3269 let Some(name) = name.to_str() else { continue };
3270 if !external_names.contains(name) {
3271 issues.push(CheckIssue {
3272 table_id: EXTERNAL_TABLE_ID,
3273 table_name: name.to_string(),
3274 severity: "warning".into(),
3275 description: format!(
3276 "orphan external table state entry {:?} not referenced by the catalog",
3277 entry.path()
3278 ),
3279 });
3280 }
3281 }
3282 }
3283
3284 for (seg, msg) in self.shared_wal.lock().verify_segments() {
3291 issues.push(CheckIssue {
3292 table_id: WAL_TABLE_ID,
3293 table_name: "<wal>".into(),
3294 severity: "error".into(),
3295 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
3296 });
3297 }
3298 issues
3299 }
3300
3301 pub fn doctor(&self) -> Result<Vec<u64>> {
3305 let _ddl = self.ddl_lock.lock();
3308 let issues = self.check();
3309 let bad_tables: std::collections::HashSet<u64> = issues
3314 .iter()
3315 .filter(|i| {
3316 i.severity == "error"
3317 && i.table_id != WAL_TABLE_ID
3318 && i.table_id != EXTERNAL_TABLE_ID
3319 })
3320 .map(|i| i.table_id)
3321 .collect();
3322 if bad_tables.is_empty() {
3323 return Ok(Vec::new());
3324 }
3325
3326 let qdir = self.root.join("_quarantine");
3327 std::fs::create_dir_all(&qdir)?;
3328 let mut quarantined = Vec::new();
3329 for &table_id in &bad_tables {
3330 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
3331 if tdir.exists() {
3332 let dest = qdir.join(table_id.to_string());
3333 std::fs::rename(&tdir, &dest)?;
3334 }
3335 {
3336 let mut cat = self.catalog.write();
3337 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
3338 entry.state = TableState::Dropped {
3339 at_epoch: self.epoch.visible().0,
3340 };
3341 }
3342 }
3343 self.tables.write().remove(&table_id);
3345 quarantined.push(table_id);
3346 }
3347 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3348 Ok(quarantined)
3349 }
3350
3351 #[allow(dead_code)]
3353 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
3354 self.kek.as_ref()
3355 }
3356
3357 #[allow(dead_code)]
3359 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
3360 &self.epoch
3361 }
3362
3363 #[allow(dead_code)]
3365 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
3366 &self.snapshots
3367 }
3368}
3369
3370fn external_state_dir(root: &Path, name: &str) -> PathBuf {
3371 root.join(VTAB_DIR).join(name)
3372}
3373
3374fn filter_ignored_staging(
3375 staging: Vec<(u64, crate::txn::Staged)>,
3376 ignored_indices: &std::collections::BTreeSet<usize>,
3377) -> Vec<(u64, crate::txn::Staged)> {
3378 if ignored_indices.is_empty() {
3379 return staging;
3380 }
3381 staging
3382 .into_iter()
3383 .enumerate()
3384 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
3385 .collect()
3386}
3387
3388fn external_state_file(root: &Path, name: &str) -> PathBuf {
3389 external_state_dir(root, name).join("state.json")
3390}
3391
3392fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
3393 let path = external_state_file(root, name);
3394 match std::fs::read(path) {
3395 Ok(bytes) => Ok(bytes),
3396 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
3397 Err(e) => Err(e.into()),
3398 }
3399}
3400
3401fn current_external_state_bytes(
3402 root: &Path,
3403 external_states: &[(String, Vec<u8>)],
3404 name: &str,
3405) -> Result<Vec<u8>> {
3406 for (table, state) in external_states.iter().rev() {
3407 if table == name {
3408 return Ok(state.clone());
3409 }
3410 }
3411 read_external_state_file(root, name)
3412}
3413
3414fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
3415 let mut out = external_states;
3416 dedup_external_states_in_place(&mut out);
3417 out
3418}
3419
3420fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
3421 let mut seen = std::collections::HashSet::new();
3422 let mut out = Vec::with_capacity(external_states.len());
3423 for (name, state) in std::mem::take(external_states).into_iter().rev() {
3424 if seen.insert(name.clone()) {
3425 out.push((name, state));
3426 }
3427 }
3428 out.reverse();
3429 *external_states = out;
3430}
3431
3432fn prepare_external_state_file(
3433 root: &Path,
3434 name: &str,
3435 state: &[u8],
3436 txn_id: u64,
3437) -> Result<PathBuf> {
3438 let dir = external_state_dir(root, name);
3439 std::fs::create_dir_all(&dir)?;
3440 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
3441 {
3442 let mut file = std::fs::File::create(&pending)?;
3443 file.write_all(state)?;
3444 file.sync_all()?;
3445 }
3446 Ok(pending)
3447}
3448
3449fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
3450 let path = external_state_file(root, name);
3451 std::fs::rename(pending, &path)?;
3452 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
3453 let _ = dir.sync_all();
3454 }
3455 Ok(())
3456}
3457
3458fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
3459 let pending = prepare_external_state_file(root, name, state, 0)?;
3460 publish_external_state_file(root, name, &pending)
3461}
3462
3463fn recover_shared_wal(
3472 root: &Path,
3473 tables: &HashMap<u64, TableHandle>,
3474 epoch: &EpochAuthority,
3475 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
3476) -> Result<()> {
3477 use crate::memtable::Row;
3478 use crate::rowid::RowId;
3479 use crate::wal::{Op, SharedWal};
3480
3481 let records = SharedWal::replay_with_dek(root, wal_dek)?;
3482
3483 let mut committed: HashMap<u64, u64> = HashMap::new();
3485 let mut spilled_to_link: Vec<(
3486 u64, u64, Vec<crate::wal::AddedRun>,
3489 )> = Vec::new();
3490 for r in &records {
3491 if let Op::TxnCommit {
3492 epoch: ce,
3493 ref added_runs,
3494 } = r.op
3495 {
3496 committed.insert(r.txn_id, ce);
3497 if !added_runs.is_empty() {
3498 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
3499 }
3500 }
3501 }
3502
3503 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
3505 let mut stage: HashMap<u64, TableStage> = HashMap::new();
3506 let mut max_epoch = epoch.visible().0;
3507 for r in records {
3508 let Some(&ce) = committed.get(&r.txn_id) else {
3509 continue; };
3511 let commit_epoch = Epoch(ce);
3512 max_epoch = max_epoch.max(ce);
3513 match r.op {
3514 Op::Put { table_id, rows } => {
3515 let skip = tables
3517 .get(&table_id)
3518 .map(|h| h.lock().flushed_epoch() >= ce)
3519 .unwrap_or(true);
3520 if skip {
3521 continue;
3522 }
3523 let rows: Vec<Row> = match bincode::deserialize(&rows) {
3524 Ok(v) => v,
3525 Err(_) => continue,
3526 };
3527 let rows: Vec<Row> = rows
3530 .into_iter()
3531 .map(|mut row| {
3532 row.committed_epoch = commit_epoch;
3533 row
3534 })
3535 .collect();
3536 let entry = stage
3537 .entry(table_id)
3538 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
3539 entry.0.extend(rows);
3540 entry.3 = commit_epoch;
3541 }
3542 Op::Delete { table_id, row_ids } => {
3543 let skip = tables
3544 .get(&table_id)
3545 .map(|h| h.lock().flushed_epoch() >= ce)
3546 .unwrap_or(true);
3547 if skip {
3548 continue;
3549 }
3550 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
3551 let entry = stage
3552 .entry(table_id)
3553 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
3554 entry.1.extend(dels);
3555 entry.3 = commit_epoch;
3556 }
3557 Op::TruncateTable { table_id } => {
3558 let skip = tables
3559 .get(&table_id)
3560 .map(|h| h.lock().flushed_epoch() >= ce)
3561 .unwrap_or(true);
3562 if skip {
3563 continue;
3564 }
3565 stage.insert(
3566 table_id,
3567 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
3568 );
3569 }
3570 Op::ExternalTableState { name, state } => {
3571 write_external_state_file(root, &name, &state)?;
3572 }
3573 Op::Flush { .. } | Op::TxnCommit { .. } | Op::TxnAbort | Op::Ddl(_) => {}
3574 }
3575 }
3576 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
3577 let Some(handle) = tables.get(&table_id) else {
3578 continue;
3579 };
3580 let mut t = handle.lock();
3581 if let Some(epoch) = truncate_epoch {
3582 t.apply_truncate(epoch)?;
3583 }
3584 t.recover_apply(rows, deletes)?;
3585 if truncate_epoch.is_some() {
3586 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
3587 t.live_count = rows.len() as u64;
3588 t.persist_manifest(table_epoch)?;
3589 }
3590 }
3591
3592 for (txn_id, ce, added_runs) in &spilled_to_link {
3596 for ar in added_runs {
3597 let Some(handle) = tables.get(&ar.table_id) else {
3598 continue;
3599 };
3600 let mut t = handle.lock();
3601 let dest = t.run_path(ar.run_id as u64);
3602 if !dest.exists() {
3603 let pending = root
3604 .join(TABLES_DIR)
3605 .join(ar.table_id.to_string())
3606 .join("_txn")
3607 .join(txn_id.to_string())
3608 .join(format!("r-{}.sr", ar.run_id));
3609 if pending.exists() {
3610 if let Some(parent) = pending.parent() {
3611 std::fs::rename(&pending, &dest)?;
3612 let _ = std::fs::remove_dir_all(parent);
3613 }
3614 }
3615 }
3616 if t.run_path(ar.run_id as u64).exists() {
3622 t.recover_spilled_run(crate::manifest::RunRef {
3623 run_id: ar.run_id,
3624 level: ar.level,
3625 epoch_created: *ce,
3626 row_count: ar.row_count,
3627 });
3628 }
3629 }
3630 }
3631
3632 epoch.advance_recovered(Epoch(max_epoch));
3633 Ok(())
3634}
3635
3636fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
3637 match condition {
3638 ProcedureCondition::Pk { .. } => {
3639 if schema.primary_key().is_none() {
3640 return Err(MongrelError::InvalidArgument(
3641 "procedure condition Pk references a table without a primary key".into(),
3642 ));
3643 }
3644 }
3645 ProcedureCondition::BitmapEq { column_id, .. }
3646 | ProcedureCondition::BitmapIn { column_id, .. }
3647 | ProcedureCondition::Range { column_id, .. }
3648 | ProcedureCondition::RangeF64 { column_id, .. }
3649 | ProcedureCondition::IsNull { column_id }
3650 | ProcedureCondition::IsNotNull { column_id }
3651 | ProcedureCondition::FmContains { column_id, .. } => {
3652 validate_column_id(*column_id, schema)?;
3653 }
3654 }
3655 Ok(())
3656}
3657
3658fn bind_procedure_args(
3659 procedure: &StoredProcedure,
3660 mut args: HashMap<String, crate::Value>,
3661) -> Result<HashMap<String, crate::Value>> {
3662 let mut out = HashMap::new();
3663 for param in &procedure.params {
3664 let value = match args.remove(¶m.name) {
3665 Some(value) => value,
3666 None => param.default.clone().ok_or_else(|| {
3667 MongrelError::InvalidArgument(format!(
3668 "missing required procedure parameter {:?}",
3669 param.name
3670 ))
3671 })?,
3672 };
3673 if !param.nullable && matches!(value, crate::Value::Null) {
3674 return Err(MongrelError::InvalidArgument(format!(
3675 "procedure parameter {:?} must not be NULL",
3676 param.name
3677 )));
3678 }
3679 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty) {
3680 return Err(MongrelError::InvalidArgument(format!(
3681 "procedure parameter {:?} has wrong type",
3682 param.name
3683 )));
3684 }
3685 out.insert(param.name.clone(), value);
3686 }
3687 if let Some(extra) = args.keys().next() {
3688 return Err(MongrelError::InvalidArgument(format!(
3689 "unknown procedure parameter {extra:?}"
3690 )));
3691 }
3692 Ok(out)
3693}
3694
3695fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
3696 matches!(
3697 (value, ty),
3698 (crate::Value::Bool(_), crate::TypeId::Bool)
3699 | (crate::Value::Int64(_), crate::TypeId::Int8)
3700 | (crate::Value::Int64(_), crate::TypeId::Int16)
3701 | (crate::Value::Int64(_), crate::TypeId::Int32)
3702 | (crate::Value::Int64(_), crate::TypeId::Int64)
3703 | (crate::Value::Int64(_), crate::TypeId::UInt8)
3704 | (crate::Value::Int64(_), crate::TypeId::UInt16)
3705 | (crate::Value::Int64(_), crate::TypeId::UInt32)
3706 | (crate::Value::Int64(_), crate::TypeId::UInt64)
3707 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
3708 | (crate::Value::Int64(_), crate::TypeId::Date32)
3709 | (crate::Value::Float64(_), crate::TypeId::Float32)
3710 | (crate::Value::Float64(_), crate::TypeId::Float64)
3711 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
3712 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
3713 )
3714}
3715
3716fn eval_cells(
3717 cells: &[crate::procedure::ProcedureCell],
3718 args: &HashMap<String, crate::Value>,
3719 outputs: &HashMap<String, ProcedureCallOutput>,
3720) -> Result<Vec<(u16, crate::Value)>> {
3721 cells
3722 .iter()
3723 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
3724 .collect()
3725}
3726
3727fn eval_condition(
3728 condition: &ProcedureCondition,
3729 args: &HashMap<String, crate::Value>,
3730 outputs: &HashMap<String, ProcedureCallOutput>,
3731) -> Result<crate::Condition> {
3732 Ok(match condition {
3733 ProcedureCondition::Pk { value } => {
3734 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
3735 }
3736 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
3737 column_id: *column_id,
3738 value: eval_value(value, args, outputs)?.encode_key(),
3739 },
3740 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
3741 column_id: *column_id,
3742 values: values
3743 .iter()
3744 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
3745 .collect::<Result<Vec<_>>>()?,
3746 },
3747 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
3748 column_id: *column_id,
3749 lo: expect_i64(eval_value(lo, args, outputs)?)?,
3750 hi: expect_i64(eval_value(hi, args, outputs)?)?,
3751 },
3752 ProcedureCondition::RangeF64 {
3753 column_id,
3754 lo,
3755 lo_inclusive,
3756 hi,
3757 hi_inclusive,
3758 } => crate::Condition::RangeF64 {
3759 column_id: *column_id,
3760 lo: expect_f64(eval_value(lo, args, outputs)?)?,
3761 lo_inclusive: *lo_inclusive,
3762 hi: expect_f64(eval_value(hi, args, outputs)?)?,
3763 hi_inclusive: *hi_inclusive,
3764 },
3765 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
3766 column_id: *column_id,
3767 },
3768 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
3769 column_id: *column_id,
3770 },
3771 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
3772 column_id: *column_id,
3773 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
3774 },
3775 })
3776}
3777
3778fn eval_value(
3779 value: &ProcedureValue,
3780 args: &HashMap<String, crate::Value>,
3781 outputs: &HashMap<String, ProcedureCallOutput>,
3782) -> Result<crate::Value> {
3783 match value {
3784 ProcedureValue::Literal(value) => Ok(value.clone()),
3785 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
3786 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
3787 }),
3788 ProcedureValue::StepScalar(id) => match outputs.get(id) {
3789 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
3790 _ => Err(MongrelError::InvalidArgument(format!(
3791 "procedure step {id:?} did not return a scalar"
3792 ))),
3793 },
3794 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
3795 Err(MongrelError::InvalidArgument(
3796 "row-valued procedure reference cannot be used as a scalar".into(),
3797 ))
3798 }
3799 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
3800 "structured procedure value cannot be used as a scalar cell".into(),
3801 )),
3802 }
3803}
3804
3805fn eval_return_output(
3806 value: &ProcedureValue,
3807 args: &HashMap<String, crate::Value>,
3808 outputs: &HashMap<String, ProcedureCallOutput>,
3809) -> Result<ProcedureCallOutput> {
3810 match value {
3811 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
3812 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
3813 args.get(name).cloned().ok_or_else(|| {
3814 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
3815 })?,
3816 )),
3817 ProcedureValue::StepRows(id)
3818 | ProcedureValue::StepRow(id)
3819 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
3820 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
3821 }),
3822 ProcedureValue::Object(fields) => {
3823 let mut out = Vec::with_capacity(fields.len());
3824 for (name, value) in fields {
3825 out.push((name.clone(), eval_return_output(value, args, outputs)?));
3826 }
3827 Ok(ProcedureCallOutput::Object(out))
3828 }
3829 ProcedureValue::Array(values) => {
3830 let mut out = Vec::with_capacity(values.len());
3831 for value in values {
3832 out.push(eval_return_output(value, args, outputs)?);
3833 }
3834 Ok(ProcedureCallOutput::Array(out))
3835 }
3836 }
3837}
3838
3839fn expect_i64(value: crate::Value) -> Result<i64> {
3840 match value {
3841 crate::Value::Int64(value) => Ok(value),
3842 _ => Err(MongrelError::InvalidArgument(
3843 "procedure value must be Int64".into(),
3844 )),
3845 }
3846}
3847
3848fn expect_f64(value: crate::Value) -> Result<f64> {
3849 match value {
3850 crate::Value::Float64(value) => Ok(value),
3851 _ => Err(MongrelError::InvalidArgument(
3852 "procedure value must be Float64".into(),
3853 )),
3854 }
3855}
3856
3857fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
3858 match value {
3859 crate::Value::Bytes(value) => Ok(value),
3860 _ => Err(MongrelError::InvalidArgument(
3861 "procedure value must be Bytes".into(),
3862 )),
3863 }
3864}
3865
3866fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
3867 if schema.columns.iter().any(|c| c.id == column_id) {
3868 Ok(())
3869 } else {
3870 Err(MongrelError::InvalidArgument(format!(
3871 "unknown column id {column_id}"
3872 )))
3873 }
3874}
3875
3876fn trigger_matches_event(
3877 trigger: &StoredTrigger,
3878 event: &WriteEvent,
3879 cat: &Catalog,
3880) -> Result<bool> {
3881 if trigger.event != event.kind {
3882 return Ok(false);
3883 }
3884 let TriggerTarget::Table(target) = &trigger.target else {
3885 return Ok(false);
3886 };
3887 if target != &event.table {
3888 return Ok(false);
3889 }
3890 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
3891 let schema = &cat
3892 .live(target)
3893 .ok_or_else(|| {
3894 MongrelError::InvalidArgument(format!(
3895 "trigger {:?} references unknown table {target:?}",
3896 trigger.name
3897 ))
3898 })?
3899 .schema;
3900 let mut watched = Vec::with_capacity(trigger.update_of.len());
3901 for name in &trigger.update_of {
3902 let col = schema.column(name).ok_or_else(|| {
3903 MongrelError::InvalidArgument(format!(
3904 "trigger {:?} references unknown UPDATE OF column {name:?}",
3905 trigger.name
3906 ))
3907 })?;
3908 watched.push(col.id);
3909 }
3910 if !event
3911 .changed_columns
3912 .iter()
3913 .any(|column_id| watched.contains(column_id))
3914 {
3915 return Ok(false);
3916 }
3917 }
3918 Ok(true)
3919}
3920
3921fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
3922 let mut ids = std::collections::BTreeSet::new();
3923 if let Some(old) = old {
3924 ids.extend(old.columns.keys().copied());
3925 }
3926 if let Some(new) = new {
3927 ids.extend(new.columns.keys().copied());
3928 }
3929 ids.into_iter()
3930 .filter(|id| {
3931 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
3932 })
3933 .collect()
3934}
3935
3936fn eval_trigger_cells(
3937 cells: &[crate::trigger::TriggerCell],
3938 event: &WriteEvent,
3939) -> Result<Vec<(u16, Value)>> {
3940 cells
3941 .iter()
3942 .map(|cell| Ok((cell.column_id, eval_trigger_value(&cell.value, event)?)))
3943 .collect()
3944}
3945
3946fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
3947 match expr {
3948 TriggerExpr::Value(value) => match eval_trigger_value(value, event)? {
3949 Value::Bool(value) => Ok(value),
3950 Value::Null => Ok(false),
3951 other => Err(MongrelError::InvalidArgument(format!(
3952 "trigger WHEN value must be boolean, got {other:?}"
3953 ))),
3954 },
3955 TriggerExpr::Eq { left, right } => Ok(values_equal(
3956 &eval_trigger_value(left, event)?,
3957 &eval_trigger_value(right, event)?,
3958 )),
3959 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
3960 &eval_trigger_value(left, event)?,
3961 &eval_trigger_value(right, event)?,
3962 )),
3963 TriggerExpr::IsNull(value) => Ok(matches!(eval_trigger_value(value, event)?, Value::Null)),
3964 TriggerExpr::IsNotNull(value) => {
3965 Ok(!matches!(eval_trigger_value(value, event)?, Value::Null))
3966 }
3967 }
3968}
3969
3970fn eval_trigger_value(value: &TriggerValue, event: &WriteEvent) -> Result<Value> {
3971 match value {
3972 TriggerValue::Literal(value) => Ok(value.clone()),
3973 TriggerValue::NewColumn(column_id) => event
3974 .new
3975 .as_ref()
3976 .and_then(|row| row.columns.get(column_id))
3977 .cloned()
3978 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
3979 TriggerValue::OldColumn(column_id) => event
3980 .old
3981 .as_ref()
3982 .and_then(|row| row.columns.get(column_id))
3983 .cloned()
3984 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
3985 }
3986}
3987
3988fn values_equal(left: &Value, right: &Value) -> bool {
3989 match (left, right) {
3990 (Value::Null, Value::Null) => true,
3991 (Value::Bool(a), Value::Bool(b)) => a == b,
3992 (Value::Int64(a), Value::Int64(b)) => a == b,
3993 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
3994 (Value::Bytes(a), Value::Bytes(b)) => a == b,
3995 (Value::Embedding(a), Value::Embedding(b)) => {
3996 a.len() == b.len()
3997 && a.iter()
3998 .zip(b.iter())
3999 .all(|(a, b)| a.to_bits() == b.to_bits())
4000 }
4001 _ => false,
4002 }
4003}
4004
4005fn trigger_message(value: Value) -> String {
4006 match value {
4007 Value::Null => "NULL".into(),
4008 Value::Bool(value) => value.to_string(),
4009 Value::Int64(value) => value.to_string(),
4010 Value::Float64(value) => value.to_string(),
4011 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
4012 Value::Embedding(value) => format!("{value:?}"),
4013 }
4014}
4015
4016fn validate_trigger_step(
4017 step: &TriggerStep,
4018 cat: &Catalog,
4019 target_schema: &Schema,
4020 event: TriggerEvent,
4021) -> Result<()> {
4022 match step {
4023 TriggerStep::SetNew { cells } => {
4024 if event == TriggerEvent::Delete {
4025 return Err(MongrelError::InvalidArgument(
4026 "SetNew trigger step is not valid for DELETE triggers".into(),
4027 ));
4028 }
4029 for cell in cells {
4030 validate_column_id(cell.column_id, target_schema)?;
4031 validate_trigger_value(&cell.value, target_schema, event)?;
4032 }
4033 }
4034 TriggerStep::Insert { table, cells } => {
4035 let schema = trigger_write_schema(cat, table, "insert")?;
4036 for cell in cells {
4037 validate_column_id(cell.column_id, schema)?;
4038 validate_trigger_value(&cell.value, target_schema, event)?;
4039 }
4040 }
4041 TriggerStep::UpdateByPk { table, pk, cells } => {
4042 let schema = trigger_write_schema(cat, table, "update")?;
4043 if schema.primary_key().is_none() {
4044 return Err(MongrelError::InvalidArgument(format!(
4045 "trigger update_by_pk references table {table:?} without a primary key"
4046 )));
4047 }
4048 validate_trigger_value(pk, target_schema, event)?;
4049 for cell in cells {
4050 validate_column_id(cell.column_id, schema)?;
4051 validate_trigger_value(&cell.value, target_schema, event)?;
4052 }
4053 }
4054 TriggerStep::DeleteByPk { table, pk } => {
4055 let schema = trigger_write_schema(cat, table, "delete")?;
4056 if schema.primary_key().is_none() {
4057 return Err(MongrelError::InvalidArgument(format!(
4058 "trigger delete_by_pk references table {table:?} without a primary key"
4059 )));
4060 }
4061 validate_trigger_value(pk, target_schema, event)?;
4062 }
4063 TriggerStep::Select {
4064 table, conditions, ..
4065 } => {
4066 let schema = trigger_read_schema(cat, table)?;
4067 for condition in conditions {
4068 validate_trigger_condition(condition, schema, target_schema, event)?;
4069 }
4070 }
4071 TriggerStep::Raise { message, .. } => {
4072 validate_trigger_value(message, target_schema, event)?
4073 }
4074 }
4075 Ok(())
4076}
4077
4078fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
4079 if let Some(entry) = cat.live(table) {
4080 return Ok(&entry.schema);
4081 }
4082 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
4083 let allowed = match op {
4084 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
4085 "update" | "delete" => entry.capabilities.writable,
4086 _ => false,
4087 };
4088 if !allowed {
4089 return Err(MongrelError::InvalidArgument(format!(
4090 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
4091 entry.module
4092 )));
4093 }
4094 if !entry.capabilities.transaction_safe {
4095 return Err(MongrelError::InvalidArgument(format!(
4096 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
4097 entry.module
4098 )));
4099 }
4100 return Ok(&entry.declared_schema);
4101 }
4102 Err(MongrelError::InvalidArgument(format!(
4103 "trigger references unknown table {table:?}"
4104 )))
4105}
4106
4107fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
4108 if let Some(entry) = cat.live(table) {
4109 return Ok(&entry.schema);
4110 }
4111 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
4112 if entry.capabilities.trigger_safe {
4113 return Ok(&entry.declared_schema);
4114 }
4115 return Err(MongrelError::InvalidArgument(format!(
4116 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
4117 entry.module
4118 )));
4119 }
4120 Err(MongrelError::InvalidArgument(format!(
4121 "trigger references unknown table {table:?}"
4122 )))
4123}
4124
4125fn validate_trigger_condition(
4126 condition: &TriggerCondition,
4127 schema: &Schema,
4128 target_schema: &Schema,
4129 event: TriggerEvent,
4130) -> Result<()> {
4131 match condition {
4132 TriggerCondition::Pk { value } => {
4133 if schema.primary_key().is_none() {
4134 return Err(MongrelError::InvalidArgument(
4135 "trigger condition Pk references a table without a primary key".into(),
4136 ));
4137 }
4138 validate_trigger_value(value, target_schema, event)
4139 }
4140 TriggerCondition::Eq { column_id, value } => {
4141 validate_column_id(*column_id, schema)?;
4142 validate_trigger_value(value, target_schema, event)
4143 }
4144 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
4145 validate_column_id(*column_id, schema)
4146 }
4147 }
4148}
4149
4150fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
4151 match expr {
4152 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
4153 validate_trigger_value(value, schema, event)
4154 }
4155 TriggerExpr::Eq { left, right } | TriggerExpr::NotEq { left, right } => {
4156 validate_trigger_value(left, schema, event)?;
4157 validate_trigger_value(right, schema, event)
4158 }
4159 }
4160}
4161
4162fn validate_trigger_value(
4163 value: &TriggerValue,
4164 schema: &Schema,
4165 event: TriggerEvent,
4166) -> Result<()> {
4167 match value {
4168 TriggerValue::Literal(_) => Ok(()),
4169 TriggerValue::NewColumn(id) => {
4170 if event == TriggerEvent::Delete {
4171 return Err(MongrelError::InvalidArgument(
4172 "DELETE triggers cannot reference NEW".into(),
4173 ));
4174 }
4175 validate_column_id(*id, schema)
4176 }
4177 TriggerValue::OldColumn(id) => {
4178 if event == TriggerEvent::Insert {
4179 return Err(MongrelError::InvalidArgument(
4180 "INSERT triggers cannot reference OLD".into(),
4181 ));
4182 }
4183 validate_column_id(*id, schema)
4184 }
4185 }
4186}
4187
4188fn recover_ddl_from_wal(
4194 root: &Path,
4195 cat: &mut Catalog,
4196 meta_dek: Option<&[u8; META_DEK_LEN]>,
4197 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
4198) -> Result<()> {
4199 use crate::wal::{DdlOp, Op, SharedWal};
4200
4201 let records = match SharedWal::replay_with_dek(root, wal_dek) {
4202 Ok(r) => r,
4203 Err(_) => return Ok(()),
4204 };
4205
4206 let mut committed: HashMap<u64, u64> = HashMap::new();
4207 for r in &records {
4208 if let Op::TxnCommit { epoch: ce, .. } = r.op {
4209 committed.insert(r.txn_id, ce);
4210 }
4211 }
4212
4213 let mut changed = false;
4214 for r in records {
4215 let Some(&ce) = committed.get(&r.txn_id) else {
4216 continue;
4217 };
4218 match r.op {
4219 Op::Ddl(DdlOp::CreateTable {
4220 table_id,
4221 ref name,
4222 ref schema_json,
4223 }) => {
4224 if cat.tables.iter().any(|t| t.table_id == table_id) {
4225 continue;
4226 }
4227 let schema = DdlOp::decode_schema(schema_json)?;
4228 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
4229 if !tdir.exists() {
4230 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
4231 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
4232 crate::engine::write_schema(&tdir, &schema)?;
4233 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
4239 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
4240 }
4241 cat.tables.push(CatalogEntry {
4242 table_id,
4243 name: name.clone(),
4244 schema,
4245 state: TableState::Live,
4246 created_epoch: ce,
4247 });
4248 cat.next_table_id = cat.next_table_id.max(table_id + 1);
4249 changed = true;
4250 }
4251 Op::Ddl(DdlOp::DropTable { table_id }) => {
4252 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4253 if matches!(entry.state, TableState::Live) {
4254 entry.state = TableState::Dropped { at_epoch: ce };
4255 changed = true;
4256 }
4257 }
4258 }
4259 Op::Ddl(DdlOp::RenameTable {
4260 table_id,
4261 ref new_name,
4262 }) => {
4263 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4264 if entry.name != *new_name {
4265 entry.name = new_name.clone();
4266 changed = true;
4267 }
4268 }
4269 }
4273 Op::Ddl(DdlOp::AlterTable {
4274 table_id,
4275 ref column_json,
4276 }) => {
4277 let column = DdlOp::decode_column(column_json)?;
4278 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4279 if apply_recovered_column_def(&mut entry.schema, column) {
4280 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
4281 if tdir.exists() {
4282 crate::engine::write_schema(&tdir, &entry.schema)?;
4283 }
4284 changed = true;
4285 }
4286 }
4287 }
4288 _ => {}
4289 }
4290 }
4291
4292 if changed {
4293 catalog::write_atomic(root, cat, meta_dek)?;
4294 }
4295 Ok(())
4296}
4297
4298fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
4299 match schema.columns.iter_mut().find(|c| c.id == column.id) {
4300 Some(existing) if *existing == column => false,
4301 Some(existing) => {
4302 *existing = column;
4303 schema.schema_id = schema.schema_id.saturating_add(1);
4304 true
4305 }
4306 None => {
4307 schema.columns.push(column);
4308 schema.schema_id = schema.schema_id.saturating_add(1);
4309 true
4310 }
4311 }
4312}
4313
4314fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
4319 for entry in &cat.tables {
4320 let txn_dir = root
4321 .join(TABLES_DIR)
4322 .join(entry.table_id.to_string())
4323 .join("_txn");
4324 if txn_dir.exists() {
4325 let _ = std::fs::remove_dir_all(&txn_dir);
4326 }
4327 }
4328}