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 trigger_max_loop_iterations: AtomicU32,
241 _lock: Option<std::fs::File>,
244 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
249}
250
251#[derive(Debug, Clone, serde::Serialize)]
253pub struct ChangeEvent {
254 pub channel: String,
255 pub table: String,
256 pub op: String,
257 pub epoch: u64,
258 pub message: Option<String>,
259}
260
261impl Database {
262 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
264 Self::create_inner(root, None)
265 }
266
267 #[cfg(feature = "encryption")]
270 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
271 let root = root.as_ref();
272 std::fs::create_dir_all(root)?;
273 std::fs::create_dir_all(root.join(META_DIR))?;
274 let salt = crate::encryption::random_salt();
275 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
276 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
277 Self::create_inner(root, Some(kek))
278 }
279
280 #[cfg(feature = "encryption")]
283 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
284 let root = root.as_ref();
285 std::fs::create_dir_all(root)?;
286 std::fs::create_dir_all(root.join(META_DIR))?;
287 let salt = crate::encryption::random_salt();
288 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
289 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
290 Self::create_inner(root, Some(kek))
291 }
292
293 fn create_inner(
294 root: impl AsRef<Path>,
295 kek: Option<Arc<crate::encryption::Kek>>,
296 ) -> Result<Self> {
297 let root = root.as_ref().to_path_buf();
298 std::fs::create_dir_all(&root)?;
299 std::fs::create_dir_all(root.join(TABLES_DIR))?;
300 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
301 let cat = Catalog::empty();
302 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
303 Self::finish_open(root, cat, kek, meta_dek, false)
304 }
305
306 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
308 Self::open_inner(root, None, None)
309 }
310
311 #[cfg(feature = "encryption")]
313 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
314 let root = root.as_ref();
315 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
316 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
317 let mut salt = [0u8; crate::encryption::SALT_LEN];
318 salt.copy_from_slice(&salt_bytes);
319 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
320 Self::open_inner(root, Some(kek), None)
321 }
322
323 #[cfg(feature = "encryption")]
325 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
326 let root = root.as_ref();
327 let salt_path = root.join(META_DIR).join(KEYS_FILENAME);
328 let salt_bytes = std::fs::read(&salt_path).map_err(|e| {
329 MongrelError::NotFound(format!(
330 "encryption salt file {:?}: {e} (database not encrypted, or corrupted)",
331 salt_path
332 ))
333 })?;
334 if salt_bytes.len() != crate::encryption::SALT_LEN {
335 return Err(MongrelError::InvalidArgument(format!(
336 "salt file is {} bytes, expected {}",
337 salt_bytes.len(),
338 crate::encryption::SALT_LEN
339 )));
340 }
341 let mut salt = [0u8; crate::encryption::SALT_LEN];
342 salt.copy_from_slice(&salt_bytes);
343 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
344 Self::open_inner(root, Some(kek), None)
345 }
346
347 fn open_inner(
348 root: impl AsRef<Path>,
349 kek: Option<Arc<crate::encryption::Kek>>,
350 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
351 ) -> Result<Self> {
352 let root = root.as_ref().to_path_buf();
353 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
354 let cat = catalog::read(&root, meta_dek.as_ref())?
355 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
356 Self::finish_open(root, cat, kek, meta_dek, true)
357 }
358
359 fn finish_open(
360 root: PathBuf,
361 cat: Catalog,
362 kek: Option<Arc<crate::encryption::Kek>>,
363 meta_dek: Option<[u8; META_DEK_LEN]>,
364 existing: bool,
365 ) -> Result<Self> {
366 std::fs::create_dir_all(root.join("_meta")).ok();
372 let lock_path = root.join("_meta").join(".lock");
373 let canonical = lock_path.canonicalize().unwrap_or(lock_path.clone());
374 let lock_file = {
375 static LOCKED_PATHS: std::sync::OnceLock<
376 std::sync::Mutex<std::collections::HashSet<PathBuf>>,
377 > = std::sync::OnceLock::new();
378 let locked = LOCKED_PATHS
379 .get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()));
380 let mut guard = locked.lock().unwrap();
381 if guard.contains(&canonical) {
382 None
384 } else {
385 let f = std::fs::OpenOptions::new()
386 .create(true)
387 .truncate(false)
388 .write(true)
389 .open(&lock_path)?;
390 use fs2::FileExt;
391 f.try_lock_exclusive().map_err(|e| {
392 MongrelError::Io(std::io::Error::other(format!(
393 "database at {} is locked by another process: {e}",
394 root.display()
395 )))
396 })?;
397 guard.insert(canonical.clone());
398 Some(f)
399 }
400 };
401
402 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
403 let snapshots = Arc::new(SnapshotRegistry::new());
404 let page_cache = Arc::new(crate::cache::Sharded::new(
405 crate::cache::CACHE_SHARDS,
406 || {
407 crate::cache::PageCache::new(
408 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
409 )
410 },
411 ));
412 let decoded_cache = Arc::new(crate::cache::Sharded::new(
413 crate::cache::CACHE_SHARDS,
414 || {
415 crate::cache::DecodedPageCache::new(
416 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
417 )
418 },
419 ));
420 let commit_lock = Arc::new(Mutex::new(()));
421 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
422 let shared_wal = Arc::new(Mutex::new(if existing {
423 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
424 } else {
425 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
426 }));
427 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
431 let group = Arc::new(crate::txn::GroupCommit::new(
432 shared_wal.lock().durable_seq(),
433 ));
434 let txn_ids = Arc::new(Mutex::new(1u64));
438
439 let mut cat = cat;
445 if existing {
446 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
447 }
448
449 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
455 for entry in &cat.tables {
456 if !matches!(entry.state, TableState::Live) {
457 continue;
458 }
459 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
460 let ctx = SharedCtx {
461 epoch: Arc::clone(&epoch),
462 page_cache: Arc::clone(&page_cache),
463 decoded_cache: Arc::clone(&decoded_cache),
464 snapshots: Arc::clone(&snapshots),
465 kek: kek.clone(),
466 commit_lock: Arc::clone(&commit_lock),
467 shared: Some(crate::engine::SharedWalCtx {
468 wal: Arc::clone(&shared_wal),
469 group: Arc::clone(&group),
470 poisoned: Arc::clone(&poisoned),
471 txn_ids: Arc::clone(&txn_ids),
472 }),
473 };
474 let t = Table::open_in(&tdir, ctx)?;
475 tables.insert(entry.table_id, Arc::new(Mutex::new(t)));
476 }
477
478 if existing {
484 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
485 sweep_pending_txn_dirs(&root, &cat);
488 }
489
490 if existing {
494 cat.open_generation = cat.open_generation.wrapping_add(1);
495 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
496 }
497 let next_txn_id = (cat.open_generation << 32) | 1;
498 *txn_ids.lock() = next_txn_id;
500
501 Ok(Self {
502 root,
503 catalog: RwLock::new(cat),
504 epoch,
505 snapshots,
506 page_cache,
507 decoded_cache,
508 commit_lock,
509 shared_wal,
510 next_txn_id: txn_ids,
511 tables: RwLock::new(tables),
512 kek,
513 ddl_lock: Mutex::new(()),
514 meta_dek,
515 conflicts: crate::txn::ConflictIndex::new(),
516 active_txns: crate::txn::ActiveTxns::new(),
517 poisoned,
518 group,
519 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
520 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
521 spill_hook: Mutex::new(None),
522 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
523 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
524 trigger_max_loop_iterations: AtomicU32::new(
525 TriggerConfig::default().max_loop_iterations,
526 ),
527 _lock: lock_file,
528 notify: {
529 let (tx, _rx) = tokio::sync::broadcast::channel(256);
530 tx
531 },
532 })
533 }
534
535 pub fn visible_epoch(&self) -> Epoch {
537 self.epoch.visible()
538 }
539
540 pub fn catalog_snapshot(&self) -> Catalog {
542 self.catalog.read().clone()
543 }
544
545 pub fn root(&self) -> &Path {
547 &self.root
548 }
549
550 pub fn table_id(&self, name: &str) -> Result<u64> {
553 let cat = self.catalog.read();
554 cat.live(name)
555 .map(|e| e.table_id)
556 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
557 }
558
559 pub fn procedures(&self) -> Vec<StoredProcedure> {
560 self.catalog
561 .read()
562 .procedures
563 .iter()
564 .map(|p| p.procedure.clone())
565 .collect()
566 }
567
568 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
569 self.catalog
570 .read()
571 .procedures
572 .iter()
573 .find(|p| p.procedure.name == name)
574 .map(|p| p.procedure.clone())
575 }
576
577 pub fn create_procedure(&self, mut procedure: StoredProcedure) -> Result<StoredProcedure> {
578 let _g = self.ddl_lock.lock();
579 procedure.validate()?;
580 self.validate_procedure_references(&procedure)?;
581 {
582 let cat = self.catalog.read();
583 if cat
584 .procedures
585 .iter()
586 .any(|p| p.procedure.name == procedure.name)
587 {
588 return Err(MongrelError::InvalidArgument(format!(
589 "procedure {:?} already exists",
590 procedure.name
591 )));
592 }
593 }
594 let commit_lock = Arc::clone(&self.commit_lock);
595 let _c = commit_lock.lock();
596 let epoch = self.epoch.bump_assigned();
597 procedure.created_epoch = epoch.0;
598 procedure.updated_epoch = epoch.0;
599 {
600 let mut cat = self.catalog.write();
601 cat.procedures.push(ProcedureEntry::from(procedure.clone()));
602 cat.db_epoch = epoch.0;
603 }
604 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
605 self.advance_visible(epoch);
606 Ok(procedure)
607 }
608
609 pub fn create_or_replace_procedure(
610 &self,
611 procedure: StoredProcedure,
612 ) -> Result<StoredProcedure> {
613 let _g = self.ddl_lock.lock();
614 procedure.validate()?;
615 self.validate_procedure_references(&procedure)?;
616 let commit_lock = Arc::clone(&self.commit_lock);
617 let _c = commit_lock.lock();
618 let epoch = self.epoch.bump_assigned();
619 let replaced = {
620 let mut cat = self.catalog.write();
621 let next = match cat
622 .procedures
623 .iter()
624 .position(|p| p.procedure.name == procedure.name)
625 {
626 Some(idx) => {
627 let next = cat.procedures[idx]
628 .procedure
629 .replaced(procedure.clone(), epoch.0)?;
630 cat.procedures[idx] = ProcedureEntry::from(next.clone());
631 next
632 }
633 None => {
634 let mut next = procedure;
635 next.created_epoch = epoch.0;
636 next.updated_epoch = epoch.0;
637 cat.procedures.push(ProcedureEntry::from(next.clone()));
638 next
639 }
640 };
641 cat.db_epoch = epoch.0;
642 next
643 };
644 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
645 self.advance_visible(epoch);
646 Ok(replaced)
647 }
648
649 pub fn drop_procedure(&self, name: &str) -> Result<()> {
650 let _g = self.ddl_lock.lock();
651 let commit_lock = Arc::clone(&self.commit_lock);
652 let _c = commit_lock.lock();
653 let epoch = self.epoch.bump_assigned();
654 {
655 let mut cat = self.catalog.write();
656 let before = cat.procedures.len();
657 cat.procedures.retain(|p| p.procedure.name != name);
658 if cat.procedures.len() == before {
659 return Err(MongrelError::NotFound(format!(
660 "procedure {name:?} not found"
661 )));
662 }
663 cat.db_epoch = epoch.0;
664 }
665 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
666 self.advance_visible(epoch);
667 Ok(())
668 }
669
670 pub fn triggers(&self) -> Vec<StoredTrigger> {
671 self.catalog
672 .read()
673 .triggers
674 .iter()
675 .map(|t| t.trigger.clone())
676 .collect()
677 }
678
679 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
680 self.catalog
681 .read()
682 .triggers
683 .iter()
684 .find(|t| t.trigger.name == name)
685 .map(|t| t.trigger.clone())
686 }
687
688 pub fn create_trigger(&self, mut trigger: StoredTrigger) -> Result<StoredTrigger> {
689 let _g = self.ddl_lock.lock();
690 trigger.validate()?;
691 self.validate_trigger_references(&trigger)?;
692 {
693 let cat = self.catalog.read();
694 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
695 return Err(MongrelError::InvalidArgument(format!(
696 "trigger {:?} already exists",
697 trigger.name
698 )));
699 }
700 }
701 let commit_lock = Arc::clone(&self.commit_lock);
702 let _c = commit_lock.lock();
703 let epoch = self.epoch.bump_assigned();
704 trigger.created_epoch = epoch.0;
705 trigger.updated_epoch = epoch.0;
706 {
707 let mut cat = self.catalog.write();
708 cat.triggers.push(TriggerEntry::from(trigger.clone()));
709 cat.db_epoch = epoch.0;
710 }
711 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
712 self.advance_visible(epoch);
713 Ok(trigger)
714 }
715
716 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
717 let _g = self.ddl_lock.lock();
718 trigger.validate()?;
719 self.validate_trigger_references(&trigger)?;
720 let commit_lock = Arc::clone(&self.commit_lock);
721 let _c = commit_lock.lock();
722 let epoch = self.epoch.bump_assigned();
723 let replaced = {
724 let mut cat = self.catalog.write();
725 let next = match cat
726 .triggers
727 .iter()
728 .position(|t| t.trigger.name == trigger.name)
729 {
730 Some(idx) => {
731 let next = cat.triggers[idx]
732 .trigger
733 .replaced(trigger.clone(), epoch.0)?;
734 cat.triggers[idx] = TriggerEntry::from(next.clone());
735 next
736 }
737 None => {
738 let mut next = trigger;
739 next.created_epoch = epoch.0;
740 next.updated_epoch = epoch.0;
741 cat.triggers.push(TriggerEntry::from(next.clone()));
742 next
743 }
744 };
745 cat.db_epoch = epoch.0;
746 next
747 };
748 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
749 self.advance_visible(epoch);
750 Ok(replaced)
751 }
752
753 pub fn drop_trigger(&self, name: &str) -> Result<()> {
754 let _g = self.ddl_lock.lock();
755 let commit_lock = Arc::clone(&self.commit_lock);
756 let _c = commit_lock.lock();
757 let epoch = self.epoch.bump_assigned();
758 {
759 let mut cat = self.catalog.write();
760 let before = cat.triggers.len();
761 cat.triggers.retain(|t| t.trigger.name != name);
762 if cat.triggers.len() == before {
763 return Err(MongrelError::NotFound(format!(
764 "trigger {name:?} not found"
765 )));
766 }
767 cat.db_epoch = epoch.0;
768 }
769 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
770 self.advance_visible(epoch);
771 Ok(())
772 }
773
774 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
775 self.catalog.read().external_tables.clone()
776 }
777
778 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
779 self.catalog
780 .read()
781 .external_tables
782 .iter()
783 .find(|t| t.name == name)
784 .cloned()
785 }
786
787 pub fn create_external_table(
788 &self,
789 mut entry: ExternalTableEntry,
790 ) -> Result<ExternalTableEntry> {
791 let _g = self.ddl_lock.lock();
792 entry.validate()?;
793 {
794 let cat = self.catalog.read();
795 if cat.live(&entry.name).is_some()
796 || cat.external_tables.iter().any(|t| t.name == entry.name)
797 {
798 return Err(MongrelError::InvalidArgument(format!(
799 "table {:?} already exists",
800 entry.name
801 )));
802 }
803 }
804 let commit_lock = Arc::clone(&self.commit_lock);
805 let _c = commit_lock.lock();
806 let epoch = self.epoch.bump_assigned();
807 entry.created_epoch = epoch.0;
808 {
809 let mut cat = self.catalog.write();
810 cat.external_tables.push(entry.clone());
811 cat.db_epoch = epoch.0;
812 }
813 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
814 self.advance_visible(epoch);
815 Ok(entry)
816 }
817
818 pub fn drop_external_table(&self, name: &str) -> Result<()> {
819 let _g = self.ddl_lock.lock();
820 let commit_lock = Arc::clone(&self.commit_lock);
821 let _c = commit_lock.lock();
822 let epoch = self.epoch.bump_assigned();
823 {
824 let mut cat = self.catalog.write();
825 let before = cat.external_tables.len();
826 cat.external_tables.retain(|t| t.name != name);
827 if cat.external_tables.len() == before {
828 return Err(MongrelError::NotFound(format!(
829 "external table {name:?} not found"
830 )));
831 }
832 cat.db_epoch = epoch.0;
833 }
834 let state_dir = self.root.join(VTAB_DIR).join(name);
835 if state_dir.exists() {
836 std::fs::remove_dir_all(state_dir)?;
837 }
838 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
839 self.advance_visible(epoch);
840 Ok(())
841 }
842
843 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
844 let txn_id = self.alloc_txn_id();
845 self.commit_transaction_with_external_states(
846 txn_id,
847 self.epoch.visible(),
848 Vec::new(),
849 vec![(name.to_string(), state.to_vec())],
850 None,
851 )
852 }
853
854 pub fn trigger_config(&self) -> TriggerConfig {
855 use std::sync::atomic::Ordering;
856 TriggerConfig {
857 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
858 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
859 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
860 }
861 }
862
863 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
864 use std::sync::atomic::Ordering;
865 if config.max_depth == 0 {
866 return Err(MongrelError::InvalidArgument(
867 "trigger max_depth must be greater than 0".into(),
868 ));
869 }
870 self.trigger_recursive
871 .store(config.recursive_triggers, Ordering::Relaxed);
872 self.trigger_max_depth
873 .store(config.max_depth, Ordering::Relaxed);
874 self.trigger_max_loop_iterations
875 .store(config.max_loop_iterations, Ordering::Relaxed);
876 Ok(())
877 }
878
879 pub fn set_recursive_triggers(&self, recursive: bool) {
880 use std::sync::atomic::Ordering;
881 self.trigger_recursive.store(recursive, Ordering::Relaxed);
882 }
883
884 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
887 self.notify.subscribe()
888 }
889
890 pub fn notify(&self, channel: &str, message: Option<String>) {
893 let _ = self.notify.send(ChangeEvent {
894 channel: channel.to_string(),
895 table: String::new(),
896 op: "notify".into(),
897 epoch: self.epoch.visible().0,
898 message,
899 });
900 }
901
902 pub fn call_procedure(
903 &self,
904 name: &str,
905 args: HashMap<String, crate::Value>,
906 ) -> Result<ProcedureCallResult> {
907 let procedure = self
908 .procedure(name)
909 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
910 let args = bind_procedure_args(&procedure, args)?;
911 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
912 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
913 if has_writes {
914 let mut tx = self.begin();
915 let run = (|| {
916 for step in &procedure.body.steps {
917 let output =
918 self.execute_procedure_step(step, &args, &outputs, Some(&mut tx))?;
919 outputs.insert(step.id().to_string(), output);
920 }
921 eval_return_output(&procedure.body.return_value, &args, &outputs)
922 })();
923 match run {
924 Ok(output) => {
925 let epoch = tx.commit()?.0;
926 Ok(ProcedureCallResult {
927 epoch: Some(epoch),
928 output,
929 })
930 }
931 Err(e) => {
932 tx.rollback();
933 Err(e)
934 }
935 }
936 } else {
937 for step in &procedure.body.steps {
938 let output = self.execute_procedure_step(step, &args, &outputs, None)?;
939 outputs.insert(step.id().to_string(), output);
940 }
941 Ok(ProcedureCallResult {
942 epoch: None,
943 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
944 })
945 }
946 }
947
948 fn execute_procedure_step(
949 &self,
950 step: &ProcedureStep,
951 args: &HashMap<String, crate::Value>,
952 outputs: &HashMap<String, ProcedureCallOutput>,
953 tx: Option<&mut crate::txn::Transaction<'_>>,
954 ) -> Result<ProcedureCallOutput> {
955 match step {
956 ProcedureStep::NativeQuery {
957 table,
958 conditions,
959 projection,
960 limit,
961 ..
962 } => {
963 let mut q = crate::Query::new();
964 for condition in conditions {
965 q = q.and(eval_condition(condition, args, outputs)?);
966 }
967 let handle = self.table(table)?;
968 let mut rows = handle.lock().query(&q)?;
969 if let Some(limit) = limit {
970 rows.truncate(*limit);
971 }
972 let projection = projection.as_ref();
973 Ok(ProcedureCallOutput::Rows(
974 rows.into_iter()
975 .map(|row| ProcedureCallRow {
976 row_id: Some(row.row_id),
977 columns: match projection {
978 Some(ids) => row
979 .columns
980 .into_iter()
981 .filter(|(id, _)| ids.contains(id))
982 .collect(),
983 None => row.columns,
984 },
985 })
986 .collect(),
987 ))
988 }
989 ProcedureStep::Put {
990 table,
991 cells,
992 returning,
993 ..
994 } => {
995 let tx = tx.ok_or_else(|| {
996 MongrelError::InvalidArgument(
997 "write procedure step requires a transaction".into(),
998 )
999 })?;
1000 let cells = eval_cells(cells, args, outputs)?;
1001 if *returning {
1002 let out = tx.put_returning(table, cells)?;
1003 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
1004 row_id: None,
1005 columns: out.row.columns.into_iter().collect(),
1006 }))
1007 } else {
1008 tx.put(table, cells)?;
1009 Ok(ProcedureCallOutput::Null)
1010 }
1011 }
1012 ProcedureStep::Upsert {
1013 table,
1014 cells,
1015 update_cells,
1016 returning,
1017 ..
1018 } => {
1019 let tx = tx.ok_or_else(|| {
1020 MongrelError::InvalidArgument(
1021 "write procedure step requires a transaction".into(),
1022 )
1023 })?;
1024 let cells = eval_cells(cells, args, outputs)?;
1025 let action = match update_cells {
1026 Some(update_cells) => {
1027 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
1028 }
1029 None => crate::UpsertAction::DoNothing,
1030 };
1031 let out = tx.upsert(table, cells, action)?;
1032 if *returning {
1033 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
1034 row_id: None,
1035 columns: out.row.columns.into_iter().collect(),
1036 }))
1037 } else {
1038 Ok(ProcedureCallOutput::Null)
1039 }
1040 }
1041 ProcedureStep::DeleteByPk { table, pk, .. } => {
1042 let tx = tx.ok_or_else(|| {
1043 MongrelError::InvalidArgument(
1044 "write procedure step requires a transaction".into(),
1045 )
1046 })?;
1047 let pk = eval_value(pk, args, outputs)?;
1048 let handle = self.table(table)?;
1049 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
1050 MongrelError::NotFound("procedure delete_by_pk target not found".into())
1051 })?;
1052 tx.delete(table, row_id)?;
1053 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
1054 }
1055 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
1056 "DeleteRows procedure step is not supported by the core executor yet".into(),
1057 )),
1058 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
1059 "SqlQuery procedure step must be executed by mongreldb-query".into(),
1060 )),
1061 }
1062 }
1063
1064 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
1065 let cat = self.catalog.read();
1066 for step in &procedure.body.steps {
1067 let Some(table_name) = step.table() else {
1068 continue;
1069 };
1070 let schema = &cat
1071 .live(table_name)
1072 .ok_or_else(|| {
1073 MongrelError::InvalidArgument(format!(
1074 "procedure {:?} references unknown table {table_name:?}",
1075 procedure.name
1076 ))
1077 })?
1078 .schema;
1079 match step {
1080 ProcedureStep::NativeQuery {
1081 conditions,
1082 projection,
1083 ..
1084 } => {
1085 for condition in conditions {
1086 validate_condition_columns(condition, schema)?;
1087 }
1088 if let Some(projection) = projection {
1089 for id in projection {
1090 validate_column_id(*id, schema)?;
1091 }
1092 }
1093 }
1094 ProcedureStep::Put { cells, .. } => {
1095 for cell in cells {
1096 validate_column_id(cell.column_id, schema)?;
1097 }
1098 }
1099 ProcedureStep::Upsert {
1100 cells,
1101 update_cells,
1102 ..
1103 } => {
1104 for cell in cells {
1105 validate_column_id(cell.column_id, schema)?;
1106 }
1107 if let Some(update_cells) = update_cells {
1108 for cell in update_cells {
1109 validate_column_id(cell.column_id, schema)?;
1110 }
1111 }
1112 }
1113 ProcedureStep::DeleteByPk { .. } => {
1114 if schema.primary_key().is_none() {
1115 return Err(MongrelError::InvalidArgument(format!(
1116 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
1117 procedure.name
1118 )));
1119 }
1120 }
1121 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
1122 }
1123 }
1124 Ok(())
1125 }
1126
1127 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
1128 let cat = self.catalog.read();
1129 let target_schema = match &trigger.target {
1130 TriggerTarget::Table(target_name) => cat
1131 .live(target_name)
1132 .ok_or_else(|| {
1133 MongrelError::InvalidArgument(format!(
1134 "trigger {:?} references unknown target table {target_name:?}",
1135 trigger.name
1136 ))
1137 })?
1138 .schema
1139 .clone(),
1140 TriggerTarget::View(_) => Schema {
1141 columns: trigger.target_columns.clone(),
1142 ..Schema::default()
1143 },
1144 };
1145 for col in &trigger.update_of {
1146 if target_schema.column(col).is_none() {
1147 return Err(MongrelError::InvalidArgument(format!(
1148 "trigger {:?} UPDATE OF references unknown column {col:?}",
1149 trigger.name
1150 )));
1151 }
1152 }
1153 if let Some(expr) = &trigger.when {
1154 validate_trigger_expr(expr, &target_schema, trigger.event)?;
1155 }
1156 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
1157 for step in &trigger.program.steps {
1158 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
1159 {
1160 return Err(MongrelError::InvalidArgument(
1161 "SetNew trigger steps are only valid in BEFORE triggers".into(),
1162 ));
1163 }
1164 validate_trigger_step(
1165 step,
1166 &cat,
1167 &target_schema,
1168 trigger.event,
1169 &mut select_schemas,
1170 )?;
1171 }
1172 Ok(())
1173 }
1174
1175 pub fn begin(&self) -> crate::txn::Transaction<'_> {
1177 self.begin_with_isolation(crate::txn::IsolationLevel::default())
1178 }
1179
1180 pub fn begin_with_isolation(
1182 &self,
1183 level: crate::txn::IsolationLevel,
1184 ) -> crate::txn::Transaction<'_> {
1185 let txn_id = self.alloc_txn_id();
1186 let epoch = match level {
1187 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
1188 _ => self.epoch.visible(),
1189 };
1190 let read = Snapshot::at(epoch);
1191 crate::txn::Transaction::new(self, txn_id, read)
1192 }
1193
1194 pub fn begin_with_external_trigger_bridge<'a>(
1197 &'a self,
1198 bridge: &'a dyn ExternalTriggerBridge,
1199 ) -> crate::txn::Transaction<'a> {
1200 let txn_id = self.alloc_txn_id();
1201 let read = Snapshot::at(self.epoch.visible());
1202 crate::txn::Transaction::new(self, txn_id, read).with_external_trigger_bridge(bridge)
1203 }
1204
1205 pub fn transaction<T>(
1207 &self,
1208 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
1209 ) -> Result<T> {
1210 let mut tx = self.begin();
1211 match f(&mut tx) {
1212 Ok(out) => {
1213 tx.commit()?;
1214 Ok(out)
1215 }
1216 Err(e) => {
1217 tx.rollback();
1218 Err(e)
1219 }
1220 }
1221 }
1222
1223 pub fn transaction_with_external_trigger_bridge<'a, T>(
1226 &'a self,
1227 bridge: &'a dyn ExternalTriggerBridge,
1228 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
1229 ) -> Result<T> {
1230 let mut tx = self.begin_with_external_trigger_bridge(bridge);
1231 match f(&mut tx) {
1232 Ok(out) => {
1233 tx.commit()?;
1234 Ok(out)
1235 }
1236 Err(e) => {
1237 tx.rollback();
1238 Err(e)
1239 }
1240 }
1241 }
1242
1243 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
1246 self.active_txns.register(epoch)
1247 }
1248
1249 fn fill_auto_increment_for_staging(
1250 &self,
1251 staging: &mut [(u64, crate::txn::Staged)],
1252 ) -> Result<()> {
1253 let tables = self.tables.read();
1254 for (table_id, staged) in staging {
1255 if let crate::txn::Staged::Put(cells) = staged {
1256 if let Some(handle) = tables.get(table_id) {
1257 let mut t = handle.lock();
1258 t.fill_auto_inc(cells)?;
1259 }
1260 }
1261 }
1262 Ok(())
1263 }
1264
1265 fn expand_table_triggers(
1266 &self,
1267 staging: &mut Vec<(u64, crate::txn::Staged)>,
1268 read_epoch: Epoch,
1269 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
1270 external_states: &mut Vec<(String, Vec<u8>)>,
1271 ) -> Result<()> {
1272 let mut external_writes = Vec::new();
1273 let config = self.trigger_config();
1274 if config.recursive_triggers {
1275 let chunk = std::mem::take(staging);
1276 let stacks = vec![Vec::new(); chunk.len()];
1277 *staging = self.expand_trigger_chunk(
1278 chunk,
1279 stacks,
1280 read_epoch,
1281 0,
1282 config.max_depth,
1283 &mut external_writes,
1284 &config,
1285 )?;
1286 self.apply_external_trigger_writes(
1287 external_writes,
1288 external_trigger_bridge,
1289 external_states,
1290 staging,
1291 )?;
1292 return Ok(());
1293 }
1294
1295 let mut expansion = self.expand_table_triggers_once(staging, read_epoch, None, &config)?;
1296 if !expansion.before.is_empty() {
1297 let mut final_staging = expansion.before;
1298 final_staging.extend(filter_ignored_staging(
1299 std::mem::take(staging),
1300 &expansion.ignored_indices,
1301 ));
1302 *staging = final_staging;
1303 } else if !expansion.ignored_indices.is_empty() {
1304 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
1305 }
1306 staging.append(&mut expansion.after);
1307 external_writes.append(&mut expansion.before_external);
1308 external_writes.append(&mut expansion.after_external);
1309 self.apply_external_trigger_writes(
1310 external_writes,
1311 external_trigger_bridge,
1312 external_states,
1313 staging,
1314 )?;
1315 Ok(())
1316 }
1317
1318 #[allow(clippy::too_many_arguments)]
1319 fn expand_trigger_chunk(
1320 &self,
1321 mut chunk: Vec<(u64, crate::txn::Staged)>,
1322 stacks: Vec<Vec<String>>,
1323 read_epoch: Epoch,
1324 depth: u32,
1325 max_depth: u32,
1326 external_writes: &mut Vec<ExternalTriggerWrite>,
1327 config: &TriggerConfig,
1328 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
1329 if chunk.is_empty() {
1330 return Ok(Vec::new());
1331 }
1332 self.fill_auto_increment_for_staging(&mut chunk)?;
1333 let expansion =
1334 self.expand_table_triggers_once(&mut chunk, read_epoch, Some(&stacks), config)?;
1335 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
1336 let stack = expansion
1337 .before_stacks
1338 .first()
1339 .or_else(|| expansion.after_stacks.first())
1340 .cloned()
1341 .unwrap_or_default();
1342 return Err(MongrelError::Conflict(format!(
1343 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
1344 Self::format_trigger_stack(&stack)
1345 )));
1346 }
1347
1348 let mut out = Vec::new();
1349 external_writes.extend(expansion.before_external);
1350 out.extend(self.expand_trigger_chunk(
1351 expansion.before,
1352 expansion.before_stacks,
1353 read_epoch,
1354 depth + 1,
1355 max_depth,
1356 external_writes,
1357 config,
1358 )?);
1359 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
1360 external_writes.extend(expansion.after_external);
1361 out.extend(self.expand_trigger_chunk(
1362 expansion.after,
1363 expansion.after_stacks,
1364 read_epoch,
1365 depth + 1,
1366 max_depth,
1367 external_writes,
1368 config,
1369 )?);
1370 Ok(out)
1371 }
1372
1373 fn apply_external_trigger_writes(
1374 &self,
1375 writes: Vec<ExternalTriggerWrite>,
1376 bridge: Option<&dyn ExternalTriggerBridge>,
1377 external_states: &mut Vec<(String, Vec<u8>)>,
1378 staging: &mut Vec<(u64, crate::txn::Staged)>,
1379 ) -> Result<()> {
1380 if writes.is_empty() {
1381 return Ok(());
1382 }
1383 let bridge = bridge.ok_or_else(|| {
1384 MongrelError::InvalidArgument(
1385 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
1386 )
1387 })?;
1388 for write in writes {
1389 let table = write.table().to_string();
1390 let entry = self.external_table(&table).ok_or_else(|| {
1391 MongrelError::NotFound(format!("external table {table:?} not found"))
1392 })?;
1393 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
1394 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
1395 external_states.push((table, result.state));
1396 for base_write in result.base_writes {
1397 match base_write {
1398 ExternalTriggerBaseWrite::Put { table, cells } => {
1399 let table_id = self.table_id(&table)?;
1400 staging.push((table_id, crate::txn::Staged::Put(cells)));
1401 }
1402 ExternalTriggerBaseWrite::Delete { table, row_id } => {
1403 let table_id = self.table_id(&table)?;
1404 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
1405 }
1406 }
1407 }
1408 }
1409 dedup_external_states_in_place(external_states);
1410 Ok(())
1411 }
1412
1413 fn expand_table_triggers_once(
1414 &self,
1415 staging: &mut Vec<(u64, crate::txn::Staged)>,
1416 read_epoch: Epoch,
1417 trigger_stacks: Option<&[Vec<String>]>,
1418 config: &TriggerConfig,
1419 ) -> Result<TriggerExpansion> {
1420 let triggers: Vec<StoredTrigger> = self
1421 .catalog
1422 .read()
1423 .triggers
1424 .iter()
1425 .filter(|entry| {
1426 entry.trigger.enabled
1427 && matches!(
1428 entry.trigger.timing,
1429 TriggerTiming::Before | TriggerTiming::After
1430 )
1431 && matches!(entry.trigger.target, TriggerTarget::Table(_))
1432 })
1433 .map(|entry| entry.trigger.clone())
1434 .collect();
1435 if triggers.is_empty() || staging.is_empty() {
1436 return Ok(TriggerExpansion::default());
1437 }
1438
1439 let before_triggers = triggers
1440 .iter()
1441 .filter(|trigger| trigger.timing == TriggerTiming::Before)
1442 .cloned()
1443 .collect::<Vec<_>>();
1444 let after_triggers = triggers
1445 .iter()
1446 .filter(|trigger| trigger.timing == TriggerTiming::After)
1447 .cloned()
1448 .collect::<Vec<_>>();
1449
1450 let mut before_added = Vec::new();
1451 let mut before_stacks = Vec::new();
1452 let mut before_external = Vec::new();
1453 let mut ignored_indices = std::collections::BTreeSet::new();
1454 if !before_triggers.is_empty() {
1455 let before_events =
1456 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?;
1457 let mut out = TriggerProgramOutput {
1458 added: &mut before_added,
1459 added_stacks: &mut before_stacks,
1460 added_external: &mut before_external,
1461 ignored_indices: &mut ignored_indices,
1462 };
1463 self.execute_triggers_for_events(
1464 &before_triggers,
1465 &before_events,
1466 Some(staging),
1467 &mut out,
1468 config,
1469 read_epoch,
1470 )?;
1471 }
1472
1473 let after_events = if after_triggers.is_empty() {
1474 Vec::new()
1475 } else {
1476 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?
1477 .into_iter()
1478 .filter(|event| {
1479 !event
1480 .op_indices
1481 .iter()
1482 .any(|idx| ignored_indices.contains(idx))
1483 })
1484 .collect()
1485 };
1486
1487 let mut after_added = Vec::new();
1488 let mut after_stacks = Vec::new();
1489 let mut after_external = Vec::new();
1490 let mut out = TriggerProgramOutput {
1491 added: &mut after_added,
1492 added_stacks: &mut after_stacks,
1493 added_external: &mut after_external,
1494 ignored_indices: &mut ignored_indices,
1495 };
1496 self.execute_triggers_for_events(
1497 &after_triggers,
1498 &after_events,
1499 None,
1500 &mut out,
1501 config,
1502 read_epoch,
1503 )?;
1504 Ok(TriggerExpansion {
1505 before: before_added,
1506 before_stacks,
1507 before_external,
1508 after: after_added,
1509 after_stacks,
1510 after_external,
1511 ignored_indices,
1512 })
1513 }
1514
1515 fn execute_triggers_for_events(
1516 &self,
1517 triggers: &[StoredTrigger],
1518 events: &[WriteEvent],
1519 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
1520 out: &mut TriggerProgramOutput<'_>,
1521 config: &TriggerConfig,
1522 read_epoch: Epoch,
1523 ) -> Result<()> {
1524 for event in events {
1525 for trigger in triggers {
1526 if event
1527 .op_indices
1528 .iter()
1529 .any(|idx| out.ignored_indices.contains(idx))
1530 {
1531 break;
1532 }
1533 let matches = {
1534 let cat = self.catalog.read();
1535 trigger_matches_event(trigger, event, &cat)?
1536 };
1537 if !matches {
1538 continue;
1539 }
1540 if let Some(when) = &trigger.when {
1541 if !eval_trigger_expr(when, event)? {
1542 continue;
1543 }
1544 }
1545 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
1546 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
1547 return Err(MongrelError::Conflict(format!(
1548 "trigger recursion cycle detected; trigger stack: {}",
1549 Self::format_trigger_stack(&trigger_stack)
1550 )));
1551 }
1552 let outcome = match staging.as_mut() {
1553 Some(staging) => self.execute_trigger_program(
1554 trigger,
1555 event,
1556 Some(&mut **staging),
1557 out,
1558 &trigger_stack,
1559 config,
1560 read_epoch,
1561 )?,
1562 None => self.execute_trigger_program(
1563 trigger,
1564 event,
1565 None,
1566 out,
1567 &trigger_stack,
1568 config,
1569 read_epoch,
1570 )?,
1571 };
1572 if outcome == TriggerProgramOutcome::Ignore {
1573 out.ignored_indices.extend(event.op_indices.iter().copied());
1574 break;
1575 }
1576 }
1577 }
1578 Ok(())
1579 }
1580
1581 fn trigger_events_for_staging(
1582 &self,
1583 staging: &[(u64, crate::txn::Staged)],
1584 read_epoch: Epoch,
1585 trigger_stacks: Option<&[Vec<String>]>,
1586 ) -> Result<Vec<WriteEvent>> {
1587 use crate::txn::Staged;
1588 use std::collections::{HashMap, VecDeque};
1589
1590 let snapshot = Snapshot::at(read_epoch);
1591 let cat = self.catalog.read();
1592 let mut table_names = HashMap::new();
1593 let mut table_schemas = HashMap::new();
1594 for entry in cat
1595 .tables
1596 .iter()
1597 .filter(|entry| matches!(entry.state, TableState::Live))
1598 {
1599 table_names.insert(entry.table_id, entry.name.clone());
1600 table_schemas.insert(entry.table_id, entry.schema.clone());
1601 }
1602 drop(cat);
1603
1604 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
1605 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
1606 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
1607
1608 for (idx, (table_id, staged)) in staging.iter().enumerate() {
1609 let Some(schema) = table_schemas.get(table_id) else {
1610 continue;
1611 };
1612 let Some(pk) = schema.primary_key() else {
1613 continue;
1614 };
1615 match staged {
1616 Staged::Delete(row_id) => {
1617 let handle = self.table_by_id(*table_id)?;
1618 let Some(row) = handle.lock().get(*row_id, snapshot) else {
1619 continue;
1620 };
1621 let Some(pk_value) = row.columns.get(&pk.id) else {
1622 continue;
1623 };
1624 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
1625 delete_by_key
1626 .entry((*table_id, pk_value.encode_key()))
1627 .or_default()
1628 .push_back(idx);
1629 }
1630 Staged::Put(cells) => {
1631 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
1632 put_by_key
1633 .entry((*table_id, value.encode_key()))
1634 .or_default()
1635 .push_back(idx);
1636 }
1637 }
1638 Staged::Truncate => {}
1639 }
1640 }
1641
1642 let mut paired_delete = std::collections::HashSet::new();
1643 let mut paired_put = std::collections::HashSet::new();
1644 let mut events = Vec::new();
1645
1646 for (key, deletes) in delete_by_key.iter_mut() {
1647 let Some(puts) = put_by_key.get_mut(key) else {
1648 continue;
1649 };
1650 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
1651 paired_delete.insert(delete_idx);
1652 paired_put.insert(put_idx);
1653 let (table_id, _) = &staging[put_idx];
1654 let Some(table_name) = table_names.get(table_id).cloned() else {
1655 continue;
1656 };
1657 let old = old_rows.get(&delete_idx).cloned();
1658 let new = match &staging[put_idx].1 {
1659 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
1660 _ => None,
1661 };
1662 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
1663 events.push(WriteEvent {
1664 table: table_name,
1665 kind: TriggerEvent::Update,
1666 old,
1667 new,
1668 changed_columns,
1669 op_indices: vec![delete_idx, put_idx],
1670 put_idx: Some(put_idx),
1671 trigger_stack: Self::trigger_stack_for_indices(
1672 trigger_stacks,
1673 &[delete_idx, put_idx],
1674 ),
1675 });
1676 }
1677 }
1678
1679 for (idx, (table_id, staged)) in staging.iter().enumerate() {
1680 let Some(table_name) = table_names.get(table_id).cloned() else {
1681 continue;
1682 };
1683 match staged {
1684 Staged::Put(cells) if !paired_put.contains(&idx) => {
1685 let new = Some(TriggerRowImage::from_cells(cells));
1686 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
1687 events.push(WriteEvent {
1688 table: table_name,
1689 kind: TriggerEvent::Insert,
1690 old: None,
1691 new,
1692 changed_columns,
1693 op_indices: vec![idx],
1694 put_idx: Some(idx),
1695 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
1696 });
1697 }
1698 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
1699 let old = match old_rows.get(&idx).cloned() {
1700 Some(old) => Some(old),
1701 None => {
1702 let handle = self.table_by_id(*table_id)?;
1703 let row = handle.lock().get(*row_id, snapshot);
1704 row.map(TriggerRowImage::from_row)
1705 }
1706 };
1707 let Some(old) = old else {
1708 continue;
1709 };
1710 let changed_columns = old.columns.keys().copied().collect();
1711 events.push(WriteEvent {
1712 table: table_name,
1713 kind: TriggerEvent::Delete,
1714 old: Some(old),
1715 new: None,
1716 changed_columns,
1717 op_indices: vec![idx],
1718 put_idx: None,
1719 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
1720 });
1721 }
1722 Staged::Truncate => {}
1723 _ => {}
1724 }
1725 }
1726
1727 Ok(events)
1728 }
1729
1730 #[allow(clippy::too_many_arguments)]
1731 fn execute_trigger_program(
1732 &self,
1733 trigger: &StoredTrigger,
1734 event: &WriteEvent,
1735 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
1736 out: &mut TriggerProgramOutput<'_>,
1737 trigger_stack: &[String],
1738 config: &TriggerConfig,
1739 read_epoch: Epoch,
1740 ) -> Result<TriggerProgramOutcome> {
1741 let mut event = event.clone();
1742 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
1743 self.execute_trigger_steps(
1744 trigger,
1745 &trigger.program.steps,
1746 &mut event,
1747 staging,
1748 out,
1749 trigger_stack,
1750 config,
1751 &mut select_results,
1752 0,
1753 None,
1754 read_epoch,
1755 )
1756 }
1757
1758 #[allow(clippy::too_many_arguments)]
1759 fn execute_trigger_steps(
1760 &self,
1761 trigger: &StoredTrigger,
1762 steps: &[TriggerStep],
1763 event: &mut WriteEvent,
1764 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
1765 out: &mut TriggerProgramOutput<'_>,
1766 trigger_stack: &[String],
1767 config: &TriggerConfig,
1768 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
1769 depth: u32,
1770 selected: Option<&TriggerRowImage>,
1771 read_epoch: Epoch,
1772 ) -> Result<TriggerProgramOutcome> {
1773 let _ = depth;
1774 for step in steps {
1775 match step {
1776 TriggerStep::SetNew { cells } => {
1777 if trigger.timing != TriggerTiming::Before {
1778 return Err(MongrelError::InvalidArgument(
1779 "SetNew trigger step is only valid in BEFORE triggers".into(),
1780 ));
1781 }
1782 let put_idx = event.put_idx.ok_or_else(|| {
1783 MongrelError::InvalidArgument(
1784 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
1785 )
1786 })?;
1787 let staging = staging.as_deref_mut().ok_or_else(|| {
1788 MongrelError::InvalidArgument(
1789 "SetNew trigger step requires mutable trigger staging".into(),
1790 )
1791 })?;
1792 let Some((_, crate::txn::Staged::Put(row_cells))) = staging.get_mut(put_idx)
1793 else {
1794 return Err(MongrelError::InvalidArgument(
1795 "SetNew trigger step target row is not mutable".into(),
1796 ));
1797 };
1798 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
1799 row_cells.retain(|(id, _)| *id != column_id);
1800 row_cells.push((column_id, value.clone()));
1801 if let Some(new) = &mut event.new {
1802 new.columns.insert(column_id, value);
1803 }
1804 }
1805 row_cells.sort_by_key(|(id, _)| *id);
1806 }
1807 TriggerStep::Insert { table, cells } => {
1808 let cells = eval_trigger_cells(cells, event, selected)?;
1809 if let Ok(table_id) = self.table_id(table) {
1810 out.added.push((table_id, crate::txn::Staged::Put(cells)));
1811 out.added_stacks.push(trigger_stack.to_vec());
1812 } else if self.external_table(table).is_some() {
1813 out.added_external.push(ExternalTriggerWrite::Insert {
1814 table: table.clone(),
1815 cells,
1816 });
1817 } else {
1818 return Err(MongrelError::NotFound(format!(
1819 "trigger {:?} insert target {table:?} not found",
1820 trigger.name
1821 )));
1822 }
1823 }
1824 TriggerStep::UpdateByPk { table, pk, cells } => {
1825 let pk = eval_trigger_value(pk, event, selected)?;
1826 let cells = eval_trigger_cells(cells, event, selected)?;
1827 if self.external_table(table).is_some() {
1828 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
1829 table: table.clone(),
1830 pk,
1831 cells,
1832 });
1833 } else {
1834 let row_id = self
1835 .table(table)?
1836 .lock()
1837 .lookup_pk(&pk.encode_key())
1838 .ok_or_else(|| {
1839 MongrelError::NotFound(format!(
1840 "trigger {:?} update target not found",
1841 trigger.name
1842 ))
1843 })?;
1844 let handle = self.table(table)?;
1845 let snapshot = Snapshot::at(self.epoch.visible());
1846 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
1847 MongrelError::NotFound(format!(
1848 "trigger {:?} update target not visible",
1849 trigger.name
1850 ))
1851 })?;
1852 let mut merged = old.columns;
1853 for (column_id, value) in cells {
1854 merged.insert(column_id, value);
1855 }
1856 out.added
1857 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
1858 out.added_stacks.push(trigger_stack.to_vec());
1859 out.added.push((
1860 self.table_id(table)?,
1861 crate::txn::Staged::Put(merged.into_iter().collect()),
1862 ));
1863 out.added_stacks.push(trigger_stack.to_vec());
1864 }
1865 }
1866 TriggerStep::DeleteByPk { table, pk } => {
1867 let pk = eval_trigger_value(pk, event, selected)?;
1868 if self.external_table(table).is_some() {
1869 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
1870 table: table.clone(),
1871 pk,
1872 });
1873 } else {
1874 let row_id = self
1875 .table(table)?
1876 .lock()
1877 .lookup_pk(&pk.encode_key())
1878 .ok_or_else(|| {
1879 MongrelError::NotFound(format!(
1880 "trigger {:?} delete target not found",
1881 trigger.name
1882 ))
1883 })?;
1884 out.added
1885 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
1886 out.added_stacks.push(trigger_stack.to_vec());
1887 }
1888 }
1889 TriggerStep::Select {
1890 id,
1891 table,
1892 conditions,
1893 } => {
1894 let schema = self.table(table)?.lock().schema().clone();
1895 let snapshot = Snapshot::at(read_epoch);
1896 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
1897 let mut matched = Vec::new();
1898 for row in rows {
1899 let image = TriggerRowImage::from_row(row);
1900 let passes = conditions
1901 .iter()
1902 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
1903 .collect::<Result<Vec<_>>>()?
1904 .into_iter()
1905 .all(|b| b);
1906 if passes {
1907 matched.push(image);
1908 }
1909 }
1910 if let Some(pk) = schema.primary_key() {
1911 matched.sort_by(|a, b| {
1912 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
1913 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
1914 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
1915 });
1916 }
1917 select_results.insert(id.clone(), matched);
1918 }
1919 TriggerStep::Foreach { id, steps } => {
1920 let rows = select_results.get(id).ok_or_else(|| {
1921 MongrelError::InvalidArgument(format!(
1922 "trigger {:?} foreach references unknown select id {id:?}",
1923 trigger.name
1924 ))
1925 })?;
1926 if rows.len() > config.max_loop_iterations as usize {
1927 return Err(MongrelError::InvalidArgument(format!(
1928 "trigger {:?} foreach exceeded max_loop_iterations ({})",
1929 trigger.name, config.max_loop_iterations
1930 )));
1931 }
1932 for row in rows.clone() {
1933 let result = self.execute_trigger_steps(
1934 trigger,
1935 steps,
1936 event,
1937 staging.as_deref_mut(),
1938 out,
1939 trigger_stack,
1940 config,
1941 select_results,
1942 depth + 1,
1943 Some(&row),
1944 read_epoch,
1945 )?;
1946 if result == TriggerProgramOutcome::Ignore {
1947 return Ok(TriggerProgramOutcome::Ignore);
1948 }
1949 }
1950 }
1951 TriggerStep::DeleteWhere { table, conditions } => {
1952 let schema = self.table(table)?.lock().schema().clone();
1953 let snapshot = Snapshot::at(read_epoch);
1954 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
1955 let table_id = self.table_id(table)?;
1956 let mut to_delete = Vec::new();
1957 for row in rows {
1958 let image = TriggerRowImage::from_row(row.clone());
1959 let passes = conditions
1960 .iter()
1961 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
1962 .collect::<Result<Vec<_>>>()?
1963 .into_iter()
1964 .all(|b| b);
1965 if passes {
1966 to_delete.push((table_id, row.row_id));
1967 }
1968 }
1969 for (table_id, row_id) in to_delete {
1970 out.added
1971 .push((table_id, crate::txn::Staged::Delete(row_id)));
1972 out.added_stacks.push(trigger_stack.to_vec());
1973 }
1974 }
1975 TriggerStep::UpdateWhere {
1976 table,
1977 conditions,
1978 cells,
1979 } => {
1980 let schema = self.table(table)?.lock().schema().clone();
1981 let snapshot = Snapshot::at(read_epoch);
1982 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
1983 let table_id = self.table_id(table)?;
1984 let mut to_update = Vec::new();
1985 for row in rows {
1986 let image = TriggerRowImage::from_row(row.clone());
1987 let passes = conditions
1988 .iter()
1989 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
1990 .collect::<Result<Vec<_>>>()?
1991 .into_iter()
1992 .all(|b| b);
1993 if passes {
1994 let new_cells = cells
1995 .iter()
1996 .map(|cell| {
1997 Ok((
1998 cell.column_id,
1999 eval_trigger_value(&cell.value, event, Some(&image))?,
2000 ))
2001 })
2002 .collect::<Result<Vec<_>>>()?;
2003 let mut merged = row.columns.clone();
2004 for (column_id, value) in new_cells {
2005 merged.insert(column_id, value);
2006 }
2007 to_update.push((table_id, row.row_id, merged));
2008 }
2009 }
2010 for (table_id, row_id, merged) in to_update {
2011 out.added
2012 .push((table_id, crate::txn::Staged::Delete(row_id)));
2013 out.added_stacks.push(trigger_stack.to_vec());
2014 out.added.push((
2015 table_id,
2016 crate::txn::Staged::Put(merged.into_iter().collect()),
2017 ));
2018 out.added_stacks.push(trigger_stack.to_vec());
2019 }
2020 }
2021 TriggerStep::Raise { action, message } => match action {
2022 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
2023 TriggerRaiseAction::Abort
2024 | TriggerRaiseAction::Fail
2025 | TriggerRaiseAction::Rollback => {
2026 let message = eval_trigger_value(message, event, selected)?;
2027 return Err(MongrelError::Conflict(format!(
2028 "trigger {:?} raised: {}; trigger stack: {}",
2029 trigger.name,
2030 trigger_message(message),
2031 Self::format_trigger_stack(trigger_stack)
2032 )));
2033 }
2034 },
2035 }
2036 }
2037 Ok(TriggerProgramOutcome::Continue)
2038 }
2039
2040 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
2041 let Some(stacks) = stacks else {
2042 return Vec::new();
2043 };
2044 let mut out = Vec::new();
2045 for idx in indices {
2046 let Some(stack) = stacks.get(*idx) else {
2047 continue;
2048 };
2049 for name in stack {
2050 if !out.iter().any(|existing| existing == name) {
2051 out.push(name.clone());
2052 }
2053 }
2054 }
2055 out
2056 }
2057
2058 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
2059 let mut out = stack.to_vec();
2060 out.push(trigger_name.to_string());
2061 out
2062 }
2063
2064 fn format_trigger_stack(stack: &[String]) -> String {
2065 if stack.is_empty() {
2066 "<root>".into()
2067 } else {
2068 stack.join(" -> ")
2069 }
2070 }
2071
2072 fn validate_constraints(
2087 &self,
2088 staging: &mut Vec<(u64, crate::txn::Staged)>,
2089 read_epoch: Epoch,
2090 ) -> Result<()> {
2091 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
2092 use crate::memtable::Row;
2093 use crate::txn::Staged;
2094 use std::collections::HashSet;
2095
2096 let snapshot = Snapshot::at(read_epoch);
2097 let cat = self.catalog.read();
2098
2099 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
2101 .tables
2102 .iter()
2103 .filter(|e| matches!(e.state, TableState::Live))
2104 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
2105 .collect();
2106
2107 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
2109 if !any_constraints {
2110 return Ok(());
2111 }
2112
2113 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
2115 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
2116 if let Some(r) = rows_cache.get(&table_id) {
2117 return Ok(r.clone());
2118 }
2119 let handle = self.table_by_id(table_id)?;
2120 let rows = handle.lock().visible_rows(snapshot)?;
2121 rows_cache.insert(table_id, rows.clone());
2122 Ok(rows)
2123 };
2124
2125 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
2130 loop {
2131 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
2132 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
2133 .iter()
2134 .filter_map(|(t, op)| match op {
2135 Staged::Delete(rid) => Some((*t, *rid)),
2136 _ => None,
2137 })
2138 .collect();
2139 for (table_id, rid) in deletes {
2140 if !cascaded.insert((table_id, rid.0)) {
2141 continue;
2142 }
2143 let Some(tname) = live
2144 .iter()
2145 .find(|(t, _, _)| *t == table_id)
2146 .map(|(_, n, _)| *n)
2147 else {
2148 continue;
2149 };
2150 let parent_handle = self.table_by_id(table_id)?;
2151 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
2152 continue;
2153 };
2154 for (child_id, _child_name, child_schema) in &live {
2155 for fk in &child_schema.constraints.foreign_keys {
2156 if fk.ref_table != tname {
2157 continue;
2158 }
2159 let Some(parent_key) =
2160 encode_composite_key(&fk.ref_columns, &parent_row.columns)
2161 else {
2162 continue;
2163 };
2164 match fk.on_delete {
2165 FkAction::Restrict => continue,
2166 FkAction::Cascade => {
2167 let child_rows = load_rows(*child_id)?;
2168 for cr in &child_rows {
2169 if !cascaded.contains(&(*child_id, cr.row_id.0))
2170 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
2171 == Some(parent_key.as_slice())
2172 {
2173 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
2174 }
2175 }
2176 }
2177 FkAction::SetNull => {
2178 let child_rows = load_rows(*child_id)?;
2179 for cr in &child_rows {
2180 if !cascaded.contains(&(*child_id, cr.row_id.0))
2181 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
2182 == Some(parent_key.as_slice())
2183 {
2184 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
2187 .columns
2188 .iter()
2189 .map(|(k, v)| (*k, v.clone()))
2190 .collect();
2191 for cid in &fk.columns {
2192 cells.retain(|(k, _)| k != cid);
2193 cells.push((*cid, crate::memtable::Value::Null));
2194 }
2195 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
2196 new_ops.push((*child_id, Staged::Put(cells)));
2197 }
2198 }
2199 }
2200 }
2201 }
2202 }
2203 }
2204 if new_ops.is_empty() {
2205 break;
2206 }
2207 staging.extend(new_ops);
2208 }
2209
2210 let staged_deletes: HashSet<(u64, u64)> = staging
2214 .iter()
2215 .filter_map(|(t, op)| match op {
2216 Staged::Delete(rid) => Some((*t, rid.0)),
2217 _ => None,
2218 })
2219 .collect();
2220
2221 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
2223
2224 for (table_id, op) in staging.iter() {
2226 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
2227 else {
2228 continue;
2229 };
2230 let cells_map: HashMap<u16, crate::memtable::Value>;
2231 match op {
2232 Staged::Put(cells) => {
2233 cells_map = cells.iter().cloned().collect();
2234
2235 if !schema.constraints.checks.is_empty() {
2237 validate_checks(&schema.constraints.checks, &cells_map)?;
2238 }
2239
2240 for uc in &schema.constraints.uniques {
2242 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
2243 continue; };
2245 let marker = (*table_id, uc.id, key.clone());
2246 if !seen_unique.insert(marker) {
2247 return Err(MongrelError::Conflict(format!(
2248 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
2249 uc.name
2250 )));
2251 }
2252 let rows = load_rows(*table_id)?;
2253 for r in &rows {
2254 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
2257 continue;
2258 }
2259 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
2260 if theirs == key {
2261 return Err(MongrelError::Conflict(format!(
2262 "UNIQUE constraint '{}' on table '{tname}' violated",
2263 uc.name
2264 )));
2265 }
2266 }
2267 }
2268 }
2269
2270 for fk in &schema.constraints.foreign_keys {
2272 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
2273 continue; };
2275 let Some(parent_id) = cat
2276 .tables
2277 .iter()
2278 .find(|t| t.name == fk.ref_table)
2279 .map(|t| t.table_id)
2280 else {
2281 return Err(MongrelError::InvalidArgument(format!(
2282 "FOREIGN KEY '{}' references unknown table '{}'",
2283 fk.name, fk.ref_table
2284 )));
2285 };
2286 let parent_rows = load_rows(parent_id)?;
2287 let mut found = false;
2288 for r in &parent_rows {
2289 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
2290 continue;
2291 }
2292 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
2293 if pkey == child_key {
2294 found = true;
2295 break;
2296 }
2297 }
2298 }
2299 if !found {
2300 return Err(MongrelError::Conflict(format!(
2301 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
2302 fk.name, fk.ref_table
2303 )));
2304 }
2305 }
2306 }
2307 Staged::Delete(rid) => {
2308 let parent_handle = self.table_by_id(*table_id)?;
2312 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
2313 continue;
2314 };
2315 for (child_id, child_name, child_schema) in &live {
2316 for fk in &child_schema.constraints.foreign_keys {
2317 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
2318 continue;
2319 }
2320 let Some(parent_key) =
2321 encode_composite_key(&fk.ref_columns, &parent_row.columns)
2322 else {
2323 continue;
2324 };
2325 let child_rows = load_rows(*child_id)?;
2326 for r in &child_rows {
2327 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
2330 continue;
2331 }
2332 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
2333 if ck == parent_key {
2334 return Err(MongrelError::Conflict(format!(
2335 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
2336 fk.name
2337 )));
2338 }
2339 }
2340 }
2341 }
2342 }
2343 }
2344 Staged::Truncate => {
2345 for (child_id, child_name, child_schema) in &live {
2349 for fk in &child_schema.constraints.foreign_keys {
2350 if fk.ref_table != tname {
2351 continue;
2352 }
2353 let child_rows = load_rows(*child_id)?;
2354 if child_rows
2355 .iter()
2356 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
2357 {
2358 return Err(MongrelError::Conflict(format!(
2359 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
2360 fk.name
2361 )));
2362 }
2363 }
2364 }
2365 }
2366 }
2367 }
2368 Ok(())
2369 }
2370
2371 pub(crate) fn commit_transaction_with_external_states(
2378 &self,
2379 txn_id: u64,
2380 read_epoch: Epoch,
2381 mut staging: Vec<(u64, crate::txn::Staged)>,
2382 external_states: Vec<(String, Vec<u8>)>,
2383 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
2384 ) -> Result<Epoch> {
2385 use crate::memtable::Row;
2386 use crate::txn::{Staged, StagedOp, WriteKey};
2387 use crate::wal::Op;
2388 use std::collections::hash_map::DefaultHasher;
2389 use std::hash::{Hash, Hasher};
2390 use std::sync::atomic::Ordering;
2391
2392 if self.poisoned.load(Ordering::Relaxed) {
2393 return Err(MongrelError::Other(
2394 "database poisoned by fsync error".into(),
2395 ));
2396 }
2397 let mut external_states = dedup_external_states(external_states);
2398 if !external_states.is_empty() {
2399 let cat = self.catalog.read();
2400 for (name, _) in &external_states {
2401 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
2402 return Err(MongrelError::NotFound(format!(
2403 "external table {name:?} not found"
2404 )));
2405 }
2406 }
2407 }
2408
2409 self.fill_auto_increment_for_staging(&mut staging)?;
2412 self.expand_table_triggers(
2413 &mut staging,
2414 read_epoch,
2415 external_trigger_bridge,
2416 &mut external_states,
2417 )?;
2418 self.fill_auto_increment_for_staging(&mut staging)?;
2419 external_states = dedup_external_states(external_states);
2420
2421 self.validate_constraints(&mut staging, read_epoch)?;
2426
2427 let write_keys = {
2428 let cat = self.catalog.read();
2429 let mut keys: Vec<WriteKey> = Vec::new();
2430 for (table_id, staged) in &staging {
2431 match staged {
2432 Staged::Put(cells) => {
2433 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
2434 for col in &entry.schema.columns {
2435 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
2436 if let Some((_, val)) =
2437 cells.iter().find(|(id, _)| *id == col.id)
2438 {
2439 let mut h = DefaultHasher::new();
2440 val.encode_key().hash(&mut h);
2441 keys.push(WriteKey::Unique {
2442 table_id: *table_id,
2443 index_id: 0,
2444 key_hash: h.finish(),
2445 });
2446 }
2447 }
2448 }
2449 for uc in &entry.schema.constraints.uniques {
2456 if let Some(key_bytes) = crate::constraint::encode_composite_key(
2457 &uc.columns,
2458 &cells.iter().cloned().collect(),
2459 ) {
2460 let mut h = DefaultHasher::new();
2461 key_bytes.hash(&mut h);
2462 keys.push(WriteKey::Unique {
2463 table_id: *table_id,
2464 index_id: uc.id | 0x8000,
2465 key_hash: h.finish(),
2466 });
2467 }
2468 }
2469 }
2470 }
2471 Staged::Delete(rid) => keys.push(WriteKey::Row {
2472 table_id: *table_id,
2473 row_id: rid.0,
2474 }),
2475 Staged::Truncate => keys.push(WriteKey::Table {
2476 table_id: *table_id,
2477 }),
2478 }
2479 }
2480 for (name, _) in &external_states {
2481 let mut h = DefaultHasher::new();
2482 name.hash(&mut h);
2483 keys.push(WriteKey::Unique {
2484 table_id: EXTERNAL_TABLE_ID,
2485 index_id: 0,
2486 key_hash: h.finish(),
2487 });
2488 }
2489 keys
2490 };
2491
2492 let min_active = self.active_txns.min_read_epoch();
2494 if min_active < u64::MAX {
2495 self.conflicts.prune_below(Epoch(min_active));
2496 }
2497
2498 if self.conflicts.conflicts(&write_keys, read_epoch) {
2502 return Err(MongrelError::Conflict(
2503 "write-write conflict (pre-validate, first-committer-wins)".into(),
2504 ));
2505 }
2506 let pre_validate_version = self.conflicts.version();
2507
2508 let mut spilled: Vec<SpilledRun> = Vec::new();
2512 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
2513 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
2517 {
2518 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
2519 for (table_id, staged) in &staging {
2520 if let Staged::Put(cells) = staged {
2521 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
2522 }
2523 }
2524 let tables = self.tables.read();
2525 for (&table_id, &bytes) in &table_bytes {
2526 if bytes as u64
2527 <= self
2528 .spill_threshold
2529 .load(std::sync::atomic::Ordering::Relaxed)
2530 {
2531 continue;
2532 }
2533 let Some(handle) = tables.get(&table_id) else {
2534 continue;
2535 };
2536 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
2537 let mut t = handle.lock();
2538 let tdir = t.table_dir().to_path_buf();
2539 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
2540 std::fs::create_dir_all(&txn_dir)?;
2541 let run_id = t.alloc_run_id() as u128;
2542 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
2543
2544 let mut rows: Vec<Row> = Vec::new();
2545 for (tid, staged) in &staging {
2546 if *tid != table_id {
2547 continue;
2548 }
2549 if let Staged::Put(cells) = staged {
2550 t.validate_cells_not_null(cells)?;
2551 let row_id = t.alloc_row_id();
2552 let mut row = Row::new(row_id, Epoch(0));
2553 for (c, v) in cells {
2554 row.columns.insert(*c, v.clone());
2555 }
2556 rows.push(row);
2557 }
2558 }
2559 let schema = t.schema_ref().clone();
2560 let kek = t.kek_ref().cloned();
2561 let specs = t.indexable_column_specs();
2562 drop(t);
2563
2564 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
2565 .uniform_epoch(true);
2566 if let Some(ref kek) = kek {
2567 writer = writer.with_encryption(kek.as_ref(), specs);
2568 }
2569 let header = writer.write(&pending_path, &rows)?;
2570 let row_count = header.row_count;
2571 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
2572 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
2573
2574 spilled.push(SpilledRun {
2575 table_id,
2576 run_id,
2577 pending_path,
2578 rows,
2579 row_count,
2580 min_rid,
2581 max_rid,
2582 });
2583 spilled_tables.insert(table_id);
2584 }
2585 }
2586
2587 if spill_guard.is_some() {
2589 if let Some(hook) = self.spill_hook.lock().as_ref() {
2590 hook();
2591 }
2592 }
2593
2594 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
2605 {
2606 let tables = self.tables.read();
2607 for (table_id, staged) in &staging {
2608 match staged {
2609 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
2610 let handle = tables.get(table_id).ok_or_else(|| {
2611 MongrelError::NotFound(format!("table {table_id} not mounted"))
2612 })?;
2613 let mut t = handle.lock();
2614 t.validate_cells_not_null(cells)?;
2615 let row_id = t.alloc_row_id();
2616 drop(t);
2617 let mut row = Row::new(row_id, Epoch(0));
2618 for (c, v) in cells {
2619 row.columns.insert(*c, v.clone());
2620 }
2621 prebuilt.push(Some(row));
2622 }
2623 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
2624 }
2625 }
2626 }
2627
2628 let mut prepared_external = Vec::with_capacity(external_states.len());
2629 for (name, state) in &external_states {
2630 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
2631 prepared_external.push((name.clone(), state.clone(), pending));
2632 }
2633
2634 let added_runs: Vec<crate::wal::AddedRun> = spilled
2636 .iter()
2637 .map(|s| crate::wal::AddedRun {
2638 table_id: s.table_id,
2639 run_id: s.run_id,
2640 row_count: s.row_count,
2641 level: 0,
2642 min_row_id: s.min_rid,
2643 max_row_id: s.max_rid,
2644 content_hash: [0u8; 32],
2645 })
2646 .collect();
2647 let (new_epoch, applies, commit_seq) = {
2648 let mut wal = self.shared_wal.lock();
2649
2650 if self.conflicts.version() != pre_validate_version
2655 && self.conflicts.conflicts(&write_keys, read_epoch)
2656 {
2657 drop(wal);
2661 for s in &spilled {
2662 if let Some(parent) = s.pending_path.parent() {
2663 let _ = std::fs::remove_dir_all(parent);
2664 }
2665 }
2666 for (_, _, pending) in &prepared_external {
2667 let _ = std::fs::remove_file(pending);
2668 }
2669 return Err(MongrelError::Conflict(
2670 "write-write conflict (sequencer delta re-check)".into(),
2671 ));
2672 }
2673
2674 let new_epoch = self.epoch.bump_assigned();
2675 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
2676
2677 for (idx, (table_id, staged)) in staging.iter().enumerate() {
2678 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
2681 continue;
2682 }
2683 let mut ops = Vec::new();
2684 match staged {
2685 Staged::Put(_) => {
2686 let mut row = prebuilt[idx].take().expect("prebuilt put row");
2688 row.committed_epoch = new_epoch;
2689 let payload = bincode::serialize(&vec![row.clone()])
2690 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
2691 wal.append(
2692 txn_id,
2693 *table_id,
2694 Op::Put {
2695 table_id: *table_id,
2696 rows: payload,
2697 },
2698 )?;
2699 ops.push(StagedOp::Put(row));
2700 }
2701 Staged::Delete(rid) => {
2702 wal.append(
2703 txn_id,
2704 *table_id,
2705 Op::Delete {
2706 table_id: *table_id,
2707 row_ids: vec![*rid],
2708 },
2709 )?;
2710 ops.push(StagedOp::Delete(*rid));
2711 }
2712 Staged::Truncate => {
2713 wal.append(
2714 txn_id,
2715 *table_id,
2716 Op::TruncateTable {
2717 table_id: *table_id,
2718 },
2719 )?;
2720 ops.push(StagedOp::Truncate);
2721 }
2722 }
2723 applies.push((*table_id, ops));
2724 }
2725
2726 for (name, state, _) in &prepared_external {
2727 wal.append(
2728 txn_id,
2729 EXTERNAL_TABLE_ID,
2730 Op::ExternalTableState {
2731 name: name.clone(),
2732 state: state.clone(),
2733 },
2734 )?;
2735 }
2736
2737 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
2738
2739 self.conflicts.record(&write_keys, new_epoch);
2744 (new_epoch, applies, commit_seq)
2745 };
2746
2747 self.group
2749 .await_durable(&self.shared_wal, commit_seq)
2750 .inspect_err(|_| {
2751 self.poisoned.store(true, Ordering::Relaxed);
2752 })?;
2753
2754 {
2756 let tables = self.tables.read();
2757 for s in &spilled {
2759 if let Some(handle) = tables.get(&s.table_id) {
2760 let mut t = handle.lock();
2761 let dest = t.run_path(s.run_id as u64);
2762 std::fs::rename(&s.pending_path, &dest)?;
2763 if let Some(parent) = s.pending_path.parent() {
2765 let _ = std::fs::remove_dir_all(parent);
2766 }
2767 t.link_run(crate::manifest::RunRef {
2768 run_id: s.run_id,
2769 level: 0,
2770 epoch_created: new_epoch.0,
2771 row_count: s.row_count,
2772 });
2773 t.apply_run_metadata(&s.rows)?;
2778 t.invalidate_pending_cache();
2779 t.persist_manifest(new_epoch)?;
2780 }
2781 }
2782 for (table_id, ops) in applies {
2784 if let Some(handle) = tables.get(&table_id) {
2785 let mut t = handle.lock();
2786 for op in ops {
2787 match op {
2788 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
2789 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
2790 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
2791 }
2792 }
2793 t.invalidate_pending_cache();
2794 t.persist_manifest(new_epoch)?;
2795 }
2796 }
2797 }
2798 for (name, _, pending) in &prepared_external {
2799 publish_external_state_file(&self.root, name, pending)?;
2800 }
2801
2802 self.advance_visible(new_epoch);
2803 Ok(new_epoch)
2804 }
2805
2806 fn advance_visible(&self, published: Epoch) {
2812 self.epoch.publish_in_order(published);
2813 }
2814
2815 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
2818 let e = self.epoch.visible();
2819 let g = self.snapshots.register(e);
2820 (Snapshot::at(e), g)
2821 }
2822
2823 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
2826 let e = self.epoch.visible();
2827 let g = self.snapshots.register_owned(e);
2828 (Snapshot::at(e), g)
2829 }
2830
2831 pub fn table_names(&self) -> Vec<String> {
2833 self.catalog
2834 .read()
2835 .tables
2836 .iter()
2837 .filter(|t| matches!(t.state, TableState::Live))
2838 .map(|t| t.name.clone())
2839 .collect()
2840 }
2841
2842 pub fn close(&self) -> Result<()> {
2848 for name in self.table_names() {
2849 if let Ok(handle) = self.table(&name) {
2850 if let Err(e) = handle.lock().close() {
2851 eprintln!("[close] flush failed for {name}: {e}");
2852 }
2853 }
2854 }
2855 Ok(())
2856 }
2857
2858 pub fn compact(&self) -> Result<(usize, usize)> {
2864 let mut compacted = 0;
2865 let mut skipped = 0;
2866 for name in self.table_names() {
2867 let Ok(handle) = self.table(&name) else {
2868 continue;
2869 };
2870 {
2871 let mut t = handle.lock();
2872 let before = t.run_count();
2873 if before < 2 {
2874 skipped += 1;
2875 continue;
2876 }
2877 match t.compact() {
2878 Ok(()) => {
2879 let after = t.run_count();
2880 compacted += 1;
2881 eprintln!("[compact] {name}: {before} -> {after} runs");
2882 }
2883 Err(e) => {
2884 eprintln!("[compact] {name}: compaction failed: {e}");
2885 skipped += 1;
2886 }
2887 }
2888 }
2889 }
2890 Ok((compacted, skipped))
2891 }
2892
2893 pub fn compact_table(&self, name: &str) -> Result<bool> {
2896 let handle = self.table(name)?;
2897 let mut t = handle.lock();
2898 let before = t.run_count();
2899 if before < 2 {
2900 return Ok(false);
2901 }
2902 t.compact()?;
2903 Ok(t.run_count() < before)
2904 }
2905
2906 pub fn table(&self, name: &str) -> Result<TableHandle> {
2908 let cat = self.catalog.read();
2909 let entry = cat
2910 .live(name)
2911 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
2912 let id = entry.table_id;
2913 drop(cat);
2914 self.tables
2915 .read()
2916 .get(&id)
2917 .cloned()
2918 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
2919 }
2920
2921 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
2924 self.tables
2925 .read()
2926 .get(&id)
2927 .cloned()
2928 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
2929 }
2930
2931 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
2937 use crate::wal::DdlOp;
2938 use std::sync::atomic::Ordering;
2939
2940 if self.poisoned.load(Ordering::Relaxed) {
2941 return Err(MongrelError::Other(
2942 "database poisoned by fsync error".into(),
2943 ));
2944 }
2945
2946 let _g = self.ddl_lock.lock();
2947 {
2948 let cat = self.catalog.read();
2949 if cat.live(name).is_some() {
2950 return Err(MongrelError::InvalidArgument(format!(
2951 "table {name:?} already exists"
2952 )));
2953 }
2954 }
2955
2956 let commit_lock = Arc::clone(&self.commit_lock);
2959 let _c = commit_lock.lock();
2960 let table_id = {
2961 let mut cat = self.catalog.write();
2962 let id = cat.next_table_id;
2963 cat.next_table_id += 1;
2964 id
2965 };
2966 let epoch = self.epoch.bump_assigned();
2967 let txn_id = self.alloc_txn_id();
2968
2969 let mut schema = schema;
2973 schema.schema_id = table_id;
2974 schema.validate_auto_increment()?;
2981
2982 let schema_json = DdlOp::encode_schema(&schema)?;
2985 let commit_seq = {
2986 let mut wal = self.shared_wal.lock();
2987 wal.append(
2988 txn_id,
2989 table_id,
2990 crate::wal::Op::Ddl(DdlOp::CreateTable {
2991 table_id,
2992 name: name.to_string(),
2993 schema_json,
2994 }),
2995 )?;
2996 wal.append_commit(txn_id, epoch, &[])?
2997 };
2998 self.group
2999 .await_durable(&self.shared_wal, commit_seq)
3000 .inspect_err(|_| {
3001 self.poisoned.store(true, Ordering::Relaxed);
3002 })?;
3003
3004 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
3006 std::fs::create_dir_all(&tdir)?;
3007 let ctx = SharedCtx {
3008 epoch: Arc::clone(&self.epoch),
3009 page_cache: Arc::clone(&self.page_cache),
3010 decoded_cache: Arc::clone(&self.decoded_cache),
3011 snapshots: Arc::clone(&self.snapshots),
3012 kek: self.kek.clone(),
3013 commit_lock: Arc::clone(&self.commit_lock),
3014 shared: Some(crate::engine::SharedWalCtx {
3015 wal: Arc::clone(&self.shared_wal),
3016 group: Arc::clone(&self.group),
3017 poisoned: Arc::clone(&self.poisoned),
3018 txn_ids: Arc::clone(&self.next_txn_id),
3019 }),
3020 };
3021 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
3022
3023 {
3026 let mut cat = self.catalog.write();
3027 cat.tables.push(CatalogEntry {
3028 table_id,
3029 name: name.to_string(),
3030 schema,
3031 state: TableState::Live,
3032 created_epoch: epoch.0,
3033 });
3034 }
3035 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3036 self.tables
3037 .write()
3038 .insert(table_id, Arc::new(Mutex::new(table)));
3039
3040 self.advance_visible(epoch);
3041 Ok(table_id)
3042 }
3043
3044 pub fn drop_table(&self, name: &str) -> Result<()> {
3046 use crate::wal::DdlOp;
3047 use std::sync::atomic::Ordering;
3048
3049 if self.poisoned.load(Ordering::Relaxed) {
3050 return Err(MongrelError::Other(
3051 "database poisoned by fsync error".into(),
3052 ));
3053 }
3054
3055 let _g = self.ddl_lock.lock();
3056 let table_id = {
3057 let cat = self.catalog.read();
3058 cat.live(name)
3059 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
3060 .table_id
3061 };
3062
3063 let commit_lock = Arc::clone(&self.commit_lock);
3064 let _c = commit_lock.lock();
3065 let epoch = self.epoch.bump_assigned();
3066 let txn_id = self.alloc_txn_id();
3067 let commit_seq = {
3068 let mut wal = self.shared_wal.lock();
3069 wal.append(
3070 txn_id,
3071 table_id,
3072 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
3073 )?;
3074 wal.append_commit(txn_id, epoch, &[])?
3075 };
3076 self.group
3077 .await_durable(&self.shared_wal, commit_seq)
3078 .inspect_err(|_| {
3079 self.poisoned.store(true, Ordering::Relaxed);
3080 })?;
3081
3082 {
3083 let mut cat = self.catalog.write();
3084 let entry = cat
3085 .tables
3086 .iter_mut()
3087 .find(|t| t.table_id == table_id)
3088 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3089 entry.state = TableState::Dropped { at_epoch: epoch.0 };
3090 cat.triggers.retain(|trigger| {
3091 !matches!(
3092 &trigger.trigger.target,
3093 TriggerTarget::Table(target) if target == name
3094 )
3095 });
3096 }
3097 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3098 self.tables.write().remove(&table_id);
3099
3100 self.advance_visible(epoch);
3101 Ok(())
3102 }
3103
3104 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
3113 use crate::wal::DdlOp;
3114 use std::sync::atomic::Ordering;
3115
3116 if self.poisoned.load(Ordering::Relaxed) {
3117 return Err(MongrelError::Other(
3118 "database poisoned by fsync error".into(),
3119 ));
3120 }
3121
3122 if name == new_name {
3125 return Ok(());
3126 }
3127 if new_name.is_empty() {
3128 return Err(MongrelError::InvalidArgument(
3129 "rename_table: new name must not be empty".into(),
3130 ));
3131 }
3132
3133 let _g = self.ddl_lock.lock();
3134 let table_id = {
3135 let cat = self.catalog.read();
3136 let src = cat
3137 .live(name)
3138 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3139 if cat.live(new_name).is_some() {
3143 return Err(MongrelError::InvalidArgument(format!(
3144 "rename_table: a table named {new_name:?} already exists"
3145 )));
3146 }
3147 src.table_id
3148 };
3149
3150 let commit_lock = Arc::clone(&self.commit_lock);
3151 let _c = commit_lock.lock();
3152 let epoch = self.epoch.bump_assigned();
3153 let txn_id = self.alloc_txn_id();
3154 let commit_seq = {
3155 let mut wal = self.shared_wal.lock();
3156 wal.append(
3157 txn_id,
3158 table_id,
3159 crate::wal::Op::Ddl(DdlOp::RenameTable {
3160 table_id,
3161 new_name: new_name.to_string(),
3162 }),
3163 )?;
3164 wal.append_commit(txn_id, epoch, &[])?
3165 };
3166 self.group
3167 .await_durable(&self.shared_wal, commit_seq)
3168 .inspect_err(|_| {
3169 self.poisoned.store(true, Ordering::Relaxed);
3170 })?;
3171
3172 {
3173 let mut cat = self.catalog.write();
3174 let entry = cat
3175 .tables
3176 .iter_mut()
3177 .find(|t| t.table_id == table_id)
3178 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3179 entry.name = new_name.to_string();
3180 for trigger in &mut cat.triggers {
3181 if matches!(
3182 &trigger.trigger.target,
3183 TriggerTarget::Table(target) if target == name
3184 ) {
3185 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
3186 }
3187 }
3188 }
3189 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3190 self.advance_visible(epoch);
3193 Ok(())
3194 }
3195
3196 pub fn alter_column(
3197 &self,
3198 table_name: &str,
3199 column_name: &str,
3200 change: AlterColumn,
3201 ) -> Result<ColumnDef> {
3202 use crate::wal::DdlOp;
3203 use std::sync::atomic::Ordering;
3204
3205 if self.poisoned.load(Ordering::Relaxed) {
3206 return Err(MongrelError::Other(
3207 "database poisoned by fsync error".into(),
3208 ));
3209 }
3210
3211 let _g = self.ddl_lock.lock();
3212 let table_id = {
3213 let cat = self.catalog.read();
3214 cat.live(table_name)
3215 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
3216 .table_id
3217 };
3218 let handle =
3219 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
3220 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
3221 })?;
3222 let mut table = handle.lock();
3223 let column = table.prepare_alter_column(column_name, &change)?;
3224 let renamed_column = (column.name != column_name).then(|| column.name.clone());
3225 if table
3226 .schema()
3227 .columns
3228 .iter()
3229 .find(|c| c.id == column.id)
3230 .is_some_and(|c| c == &column)
3231 {
3232 return Ok(column);
3233 }
3234
3235 let commit_lock = Arc::clone(&self.commit_lock);
3236 let _c = commit_lock.lock();
3237 let epoch = self.epoch.bump_assigned();
3238 let txn_id = self.alloc_txn_id();
3239 let column_json = DdlOp::encode_column(&column)?;
3240 let commit_seq = {
3241 let mut wal = self.shared_wal.lock();
3242 wal.append(
3243 txn_id,
3244 table_id,
3245 crate::wal::Op::Ddl(DdlOp::AlterTable {
3246 table_id,
3247 column_json,
3248 }),
3249 )?;
3250 wal.append_commit(txn_id, epoch, &[])?
3251 };
3252 self.group
3253 .await_durable(&self.shared_wal, commit_seq)
3254 .inspect_err(|_| {
3255 self.poisoned.store(true, Ordering::Relaxed);
3256 })?;
3257
3258 table.apply_altered_column(column.clone())?;
3259 let schema = table.schema().clone();
3260 drop(table);
3261
3262 {
3263 let mut cat = self.catalog.write();
3264 let entry = cat
3265 .tables
3266 .iter_mut()
3267 .find(|t| t.table_id == table_id)
3268 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
3269 entry.schema = schema;
3270 if let Some(new_column_name) = renamed_column {
3271 for trigger in &mut cat.triggers {
3272 if matches!(
3273 &trigger.trigger.target,
3274 TriggerTarget::Table(target) if target == table_name
3275 ) {
3276 trigger.trigger = trigger.trigger.renamed_update_column(
3277 column_name,
3278 new_column_name.clone(),
3279 epoch.0,
3280 )?;
3281 }
3282 }
3283 }
3284 }
3285 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3286
3287 self.advance_visible(epoch);
3288 Ok(column)
3289 }
3290
3291 pub fn gc(&self) -> Result<usize> {
3297 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
3298 let mut reclaimed = 0;
3299
3300 let cat = self.catalog.read();
3302 for entry in &cat.tables {
3303 if let TableState::Dropped { at_epoch } = entry.state {
3304 if at_epoch <= min_active {
3305 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
3306 if tdir.exists() {
3307 std::fs::remove_dir_all(&tdir)?;
3308 reclaimed += 1;
3309 }
3310 }
3311 }
3312 }
3313 drop(cat);
3314
3315 let cat = self.catalog.read();
3320 for entry in &cat.tables {
3321 if !matches!(entry.state, TableState::Live) {
3322 continue;
3323 }
3324 let txn_dir = self
3325 .root
3326 .join(TABLES_DIR)
3327 .join(entry.table_id.to_string())
3328 .join("_txn");
3329 if !txn_dir.exists() {
3330 continue;
3331 }
3332 for sub in std::fs::read_dir(&txn_dir)? {
3333 let sub = sub?;
3334 let name = sub.file_name();
3335 let Some(name) = name.to_str() else { continue };
3336 let is_active = name
3338 .parse::<u64>()
3339 .map(|id| self.active_spills.is_active(id))
3340 .unwrap_or(false);
3341 if is_active {
3342 continue;
3343 }
3344 std::fs::remove_dir_all(sub.path())?;
3345 reclaimed += 1;
3346 }
3347 }
3348 drop(cat);
3349
3350 let external_names = {
3351 let cat = self.catalog.read();
3352 cat.external_tables
3353 .iter()
3354 .map(|entry| entry.name.clone())
3355 .collect::<std::collections::HashSet<_>>()
3356 };
3357 let vtab_dir = self.root.join(VTAB_DIR);
3358 if vtab_dir.exists() {
3359 for entry in std::fs::read_dir(&vtab_dir)? {
3360 let entry = entry?;
3361 let name = entry.file_name();
3362 let Some(name) = name.to_str() else { continue };
3363 if external_names.contains(name) {
3364 continue;
3365 }
3366 let path = entry.path();
3367 if path.is_dir() {
3368 std::fs::remove_dir_all(path)?;
3369 } else {
3370 std::fs::remove_file(path)?;
3371 }
3372 reclaimed += 1;
3373 }
3374 }
3375
3376 let tables = self.tables.read();
3380 for handle in tables.values() {
3381 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
3382 }
3383
3384 let all_durable = self.active_spills.is_idle()
3392 && tables.values().all(|h| {
3393 let g = h.lock();
3394 g.memtable_len() == 0 && g.mutable_run_len() == 0
3395 });
3396 drop(tables);
3397 if all_durable {
3398 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
3399 }
3400
3401 Ok(reclaimed)
3402 }
3403 fn alloc_txn_id(&self) -> u64 {
3404 let mut g = self.next_txn_id.lock();
3405 let id = *g;
3406 *g = g.wrapping_add(1);
3407 id
3408 }
3409
3410 pub fn set_spill_threshold(&self, bytes: u64) {
3414 self.spill_threshold
3415 .store(bytes, std::sync::atomic::Ordering::Relaxed);
3416 }
3417
3418 #[doc(hidden)]
3422 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
3423 *self.spill_hook.lock() = Some(Box::new(f));
3424 }
3425
3426 #[doc(hidden)]
3430 pub fn __wal_group_sync_count(&self) -> u64 {
3431 self.shared_wal.lock().group_sync_count()
3432 }
3433
3434 #[doc(hidden)]
3437 pub fn __poison(&self) {
3438 self.poisoned
3439 .store(true, std::sync::atomic::Ordering::Relaxed);
3440 }
3441
3442 pub fn check(&self) -> Vec<CheckIssue> {
3455 let mut issues = Vec::new();
3456 let cat = self.catalog.read();
3457 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
3458 for entry in &cat.tables {
3459 if !matches!(entry.state, TableState::Live) {
3460 continue;
3461 }
3462 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
3463 let mut err = |sev: &str, desc: String| {
3464 issues.push(CheckIssue {
3465 table_id: entry.table_id,
3466 table_name: entry.name.clone(),
3467 severity: sev.into(),
3468 description: desc,
3469 });
3470 };
3471 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
3472 Ok(m) => m,
3473 Err(e) => {
3474 err("error", format!("manifest read failed: {e}"));
3475 continue;
3476 }
3477 };
3478 if m.flushed_epoch > m.current_epoch {
3479 err(
3480 "error",
3481 format!(
3482 "flushed_epoch {} exceeds current_epoch {} (impossible)",
3483 m.flushed_epoch, m.current_epoch
3484 ),
3485 );
3486 }
3487
3488 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
3489 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
3490 for rr in &m.runs {
3491 referenced.insert(rr.run_id);
3492 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
3493 if !run_path.exists() {
3494 err("error", format!("missing run file: r-{}.sr", rr.run_id));
3495 continue;
3496 }
3497 match crate::sorted_run::RunReader::open(
3498 &run_path,
3499 entry.schema.clone(),
3500 self.kek.clone(),
3501 ) {
3502 Ok(reader) => {
3503 if reader.row_count() as u64 != rr.row_count {
3504 err(
3505 "error",
3506 format!(
3507 "run r-{} row count mismatch: manifest {} vs run {}",
3508 rr.run_id,
3509 rr.row_count,
3510 reader.row_count()
3511 ),
3512 );
3513 }
3514 }
3515 Err(e) => {
3516 err(
3517 "error",
3518 format!("run r-{} integrity check failed: {e}", rr.run_id),
3519 );
3520 }
3521 }
3522 }
3523
3524 for r in &m.retiring {
3528 referenced.insert(r.run_id);
3529 }
3530
3531 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
3533 for ent in rd.flatten() {
3534 let p = ent.path();
3535 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
3536 continue;
3537 }
3538 let run_id = p
3539 .file_stem()
3540 .and_then(|s| s.to_str())
3541 .and_then(|s| s.strip_prefix("r-"))
3542 .and_then(|s| s.parse::<u128>().ok());
3543 if let Some(id) = run_id {
3544 if !referenced.contains(&id) {
3545 err(
3546 "warning",
3547 format!("orphan run file r-{id}.sr not referenced by the manifest"),
3548 );
3549 }
3550 }
3551 }
3552 }
3553 }
3554
3555 let external_names = cat
3556 .external_tables
3557 .iter()
3558 .map(|entry| entry.name.clone())
3559 .collect::<std::collections::HashSet<_>>();
3560 let vtab_dir = self.root.join(VTAB_DIR);
3561 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
3562 for entry in entries.flatten() {
3563 let name = entry.file_name();
3564 let Some(name) = name.to_str() else { continue };
3565 if !external_names.contains(name) {
3566 issues.push(CheckIssue {
3567 table_id: EXTERNAL_TABLE_ID,
3568 table_name: name.to_string(),
3569 severity: "warning".into(),
3570 description: format!(
3571 "orphan external table state entry {:?} not referenced by the catalog",
3572 entry.path()
3573 ),
3574 });
3575 }
3576 }
3577 }
3578
3579 for (seg, msg) in self.shared_wal.lock().verify_segments() {
3586 issues.push(CheckIssue {
3587 table_id: WAL_TABLE_ID,
3588 table_name: "<wal>".into(),
3589 severity: "error".into(),
3590 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
3591 });
3592 }
3593 issues
3594 }
3595
3596 pub fn doctor(&self) -> Result<Vec<u64>> {
3600 let _ddl = self.ddl_lock.lock();
3603 let issues = self.check();
3604 let bad_tables: std::collections::HashSet<u64> = issues
3609 .iter()
3610 .filter(|i| {
3611 i.severity == "error"
3612 && i.table_id != WAL_TABLE_ID
3613 && i.table_id != EXTERNAL_TABLE_ID
3614 })
3615 .map(|i| i.table_id)
3616 .collect();
3617 if bad_tables.is_empty() {
3618 return Ok(Vec::new());
3619 }
3620
3621 let qdir = self.root.join("_quarantine");
3622 std::fs::create_dir_all(&qdir)?;
3623 let mut quarantined = Vec::new();
3624 for &table_id in &bad_tables {
3625 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
3626 if tdir.exists() {
3627 let dest = qdir.join(table_id.to_string());
3628 std::fs::rename(&tdir, &dest)?;
3629 }
3630 {
3631 let mut cat = self.catalog.write();
3632 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
3633 entry.state = TableState::Dropped {
3634 at_epoch: self.epoch.visible().0,
3635 };
3636 }
3637 }
3638 self.tables.write().remove(&table_id);
3640 quarantined.push(table_id);
3641 }
3642 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3643 Ok(quarantined)
3644 }
3645
3646 #[allow(dead_code)]
3648 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
3649 self.kek.as_ref()
3650 }
3651
3652 #[allow(dead_code)]
3654 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
3655 &self.epoch
3656 }
3657
3658 #[allow(dead_code)]
3660 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
3661 &self.snapshots
3662 }
3663}
3664
3665fn external_state_dir(root: &Path, name: &str) -> PathBuf {
3666 root.join(VTAB_DIR).join(name)
3667}
3668
3669fn filter_ignored_staging(
3670 staging: Vec<(u64, crate::txn::Staged)>,
3671 ignored_indices: &std::collections::BTreeSet<usize>,
3672) -> Vec<(u64, crate::txn::Staged)> {
3673 if ignored_indices.is_empty() {
3674 return staging;
3675 }
3676 staging
3677 .into_iter()
3678 .enumerate()
3679 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
3680 .collect()
3681}
3682
3683fn external_state_file(root: &Path, name: &str) -> PathBuf {
3684 external_state_dir(root, name).join("state.json")
3685}
3686
3687fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
3688 let path = external_state_file(root, name);
3689 match std::fs::read(path) {
3690 Ok(bytes) => Ok(bytes),
3691 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
3692 Err(e) => Err(e.into()),
3693 }
3694}
3695
3696fn current_external_state_bytes(
3697 root: &Path,
3698 external_states: &[(String, Vec<u8>)],
3699 name: &str,
3700) -> Result<Vec<u8>> {
3701 for (table, state) in external_states.iter().rev() {
3702 if table == name {
3703 return Ok(state.clone());
3704 }
3705 }
3706 read_external_state_file(root, name)
3707}
3708
3709fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
3710 let mut out = external_states;
3711 dedup_external_states_in_place(&mut out);
3712 out
3713}
3714
3715fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
3716 let mut seen = std::collections::HashSet::new();
3717 let mut out = Vec::with_capacity(external_states.len());
3718 for (name, state) in std::mem::take(external_states).into_iter().rev() {
3719 if seen.insert(name.clone()) {
3720 out.push((name, state));
3721 }
3722 }
3723 out.reverse();
3724 *external_states = out;
3725}
3726
3727fn prepare_external_state_file(
3728 root: &Path,
3729 name: &str,
3730 state: &[u8],
3731 txn_id: u64,
3732) -> Result<PathBuf> {
3733 let dir = external_state_dir(root, name);
3734 std::fs::create_dir_all(&dir)?;
3735 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
3736 {
3737 let mut file = std::fs::File::create(&pending)?;
3738 file.write_all(state)?;
3739 file.sync_all()?;
3740 }
3741 Ok(pending)
3742}
3743
3744fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
3745 let path = external_state_file(root, name);
3746 std::fs::rename(pending, &path)?;
3747 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
3748 let _ = dir.sync_all();
3749 }
3750 Ok(())
3751}
3752
3753fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
3754 let pending = prepare_external_state_file(root, name, state, 0)?;
3755 publish_external_state_file(root, name, &pending)
3756}
3757
3758fn recover_shared_wal(
3767 root: &Path,
3768 tables: &HashMap<u64, TableHandle>,
3769 epoch: &EpochAuthority,
3770 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
3771) -> Result<()> {
3772 use crate::memtable::Row;
3773 use crate::rowid::RowId;
3774 use crate::wal::{Op, SharedWal};
3775
3776 let records = SharedWal::replay_with_dek(root, wal_dek)?;
3777
3778 let mut committed: HashMap<u64, u64> = HashMap::new();
3780 let mut spilled_to_link: Vec<(
3781 u64, u64, Vec<crate::wal::AddedRun>,
3784 )> = Vec::new();
3785 for r in &records {
3786 if let Op::TxnCommit {
3787 epoch: ce,
3788 ref added_runs,
3789 } = r.op
3790 {
3791 committed.insert(r.txn_id, ce);
3792 if !added_runs.is_empty() {
3793 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
3794 }
3795 }
3796 }
3797
3798 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
3800 let mut stage: HashMap<u64, TableStage> = HashMap::new();
3801 let mut max_epoch = epoch.visible().0;
3802 for r in records {
3803 let Some(&ce) = committed.get(&r.txn_id) else {
3804 continue; };
3806 let commit_epoch = Epoch(ce);
3807 max_epoch = max_epoch.max(ce);
3808 match r.op {
3809 Op::Put { table_id, rows } => {
3810 let skip = tables
3812 .get(&table_id)
3813 .map(|h| h.lock().flushed_epoch() >= ce)
3814 .unwrap_or(true);
3815 if skip {
3816 continue;
3817 }
3818 let rows: Vec<Row> = match bincode::deserialize(&rows) {
3819 Ok(v) => v,
3820 Err(_) => continue,
3821 };
3822 let rows: Vec<Row> = rows
3825 .into_iter()
3826 .map(|mut row| {
3827 row.committed_epoch = commit_epoch;
3828 row
3829 })
3830 .collect();
3831 let entry = stage
3832 .entry(table_id)
3833 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
3834 entry.0.extend(rows);
3835 entry.3 = commit_epoch;
3836 }
3837 Op::Delete { table_id, row_ids } => {
3838 let skip = tables
3839 .get(&table_id)
3840 .map(|h| h.lock().flushed_epoch() >= ce)
3841 .unwrap_or(true);
3842 if skip {
3843 continue;
3844 }
3845 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
3846 let entry = stage
3847 .entry(table_id)
3848 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
3849 entry.1.extend(dels);
3850 entry.3 = commit_epoch;
3851 }
3852 Op::TruncateTable { table_id } => {
3853 let skip = tables
3854 .get(&table_id)
3855 .map(|h| h.lock().flushed_epoch() >= ce)
3856 .unwrap_or(true);
3857 if skip {
3858 continue;
3859 }
3860 stage.insert(
3861 table_id,
3862 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
3863 );
3864 }
3865 Op::ExternalTableState { name, state } => {
3866 write_external_state_file(root, &name, &state)?;
3867 }
3868 Op::Flush { .. } | Op::TxnCommit { .. } | Op::TxnAbort | Op::Ddl(_) => {}
3869 }
3870 }
3871 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
3872 let Some(handle) = tables.get(&table_id) else {
3873 continue;
3874 };
3875 let mut t = handle.lock();
3876 if let Some(epoch) = truncate_epoch {
3877 t.apply_truncate(epoch)?;
3878 }
3879 t.recover_apply(rows, deletes)?;
3880 if truncate_epoch.is_some() {
3881 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
3882 t.live_count = rows.len() as u64;
3883 t.persist_manifest(table_epoch)?;
3884 }
3885 }
3886
3887 for (txn_id, ce, added_runs) in &spilled_to_link {
3891 for ar in added_runs {
3892 let Some(handle) = tables.get(&ar.table_id) else {
3893 continue;
3894 };
3895 let mut t = handle.lock();
3896 let dest = t.run_path(ar.run_id as u64);
3897 if !dest.exists() {
3898 let pending = root
3899 .join(TABLES_DIR)
3900 .join(ar.table_id.to_string())
3901 .join("_txn")
3902 .join(txn_id.to_string())
3903 .join(format!("r-{}.sr", ar.run_id));
3904 if pending.exists() {
3905 if let Some(parent) = pending.parent() {
3906 std::fs::rename(&pending, &dest)?;
3907 let _ = std::fs::remove_dir_all(parent);
3908 }
3909 }
3910 }
3911 if t.run_path(ar.run_id as u64).exists() {
3917 t.recover_spilled_run(crate::manifest::RunRef {
3918 run_id: ar.run_id,
3919 level: ar.level,
3920 epoch_created: *ce,
3921 row_count: ar.row_count,
3922 });
3923 }
3924 }
3925 }
3926
3927 epoch.advance_recovered(Epoch(max_epoch));
3928 Ok(())
3929}
3930
3931fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
3932 match condition {
3933 ProcedureCondition::Pk { .. } => {
3934 if schema.primary_key().is_none() {
3935 return Err(MongrelError::InvalidArgument(
3936 "procedure condition Pk references a table without a primary key".into(),
3937 ));
3938 }
3939 }
3940 ProcedureCondition::BitmapEq { column_id, .. }
3941 | ProcedureCondition::BitmapIn { column_id, .. }
3942 | ProcedureCondition::Range { column_id, .. }
3943 | ProcedureCondition::RangeF64 { column_id, .. }
3944 | ProcedureCondition::IsNull { column_id }
3945 | ProcedureCondition::IsNotNull { column_id }
3946 | ProcedureCondition::FmContains { column_id, .. } => {
3947 validate_column_id(*column_id, schema)?;
3948 }
3949 }
3950 Ok(())
3951}
3952
3953fn bind_procedure_args(
3954 procedure: &StoredProcedure,
3955 mut args: HashMap<String, crate::Value>,
3956) -> Result<HashMap<String, crate::Value>> {
3957 let mut out = HashMap::new();
3958 for param in &procedure.params {
3959 let value = match args.remove(¶m.name) {
3960 Some(value) => value,
3961 None => param.default.clone().ok_or_else(|| {
3962 MongrelError::InvalidArgument(format!(
3963 "missing required procedure parameter {:?}",
3964 param.name
3965 ))
3966 })?,
3967 };
3968 if !param.nullable && matches!(value, crate::Value::Null) {
3969 return Err(MongrelError::InvalidArgument(format!(
3970 "procedure parameter {:?} must not be NULL",
3971 param.name
3972 )));
3973 }
3974 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty) {
3975 return Err(MongrelError::InvalidArgument(format!(
3976 "procedure parameter {:?} has wrong type",
3977 param.name
3978 )));
3979 }
3980 out.insert(param.name.clone(), value);
3981 }
3982 if let Some(extra) = args.keys().next() {
3983 return Err(MongrelError::InvalidArgument(format!(
3984 "unknown procedure parameter {extra:?}"
3985 )));
3986 }
3987 Ok(out)
3988}
3989
3990fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
3991 matches!(
3992 (value, ty),
3993 (crate::Value::Bool(_), crate::TypeId::Bool)
3994 | (crate::Value::Int64(_), crate::TypeId::Int8)
3995 | (crate::Value::Int64(_), crate::TypeId::Int16)
3996 | (crate::Value::Int64(_), crate::TypeId::Int32)
3997 | (crate::Value::Int64(_), crate::TypeId::Int64)
3998 | (crate::Value::Int64(_), crate::TypeId::UInt8)
3999 | (crate::Value::Int64(_), crate::TypeId::UInt16)
4000 | (crate::Value::Int64(_), crate::TypeId::UInt32)
4001 | (crate::Value::Int64(_), crate::TypeId::UInt64)
4002 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
4003 | (crate::Value::Int64(_), crate::TypeId::Date32)
4004 | (crate::Value::Float64(_), crate::TypeId::Float32)
4005 | (crate::Value::Float64(_), crate::TypeId::Float64)
4006 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
4007 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
4008 )
4009}
4010
4011fn eval_cells(
4012 cells: &[crate::procedure::ProcedureCell],
4013 args: &HashMap<String, crate::Value>,
4014 outputs: &HashMap<String, ProcedureCallOutput>,
4015) -> Result<Vec<(u16, crate::Value)>> {
4016 cells
4017 .iter()
4018 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
4019 .collect()
4020}
4021
4022fn eval_condition(
4023 condition: &ProcedureCondition,
4024 args: &HashMap<String, crate::Value>,
4025 outputs: &HashMap<String, ProcedureCallOutput>,
4026) -> Result<crate::Condition> {
4027 Ok(match condition {
4028 ProcedureCondition::Pk { value } => {
4029 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
4030 }
4031 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
4032 column_id: *column_id,
4033 value: eval_value(value, args, outputs)?.encode_key(),
4034 },
4035 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
4036 column_id: *column_id,
4037 values: values
4038 .iter()
4039 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
4040 .collect::<Result<Vec<_>>>()?,
4041 },
4042 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
4043 column_id: *column_id,
4044 lo: expect_i64(eval_value(lo, args, outputs)?)?,
4045 hi: expect_i64(eval_value(hi, args, outputs)?)?,
4046 },
4047 ProcedureCondition::RangeF64 {
4048 column_id,
4049 lo,
4050 lo_inclusive,
4051 hi,
4052 hi_inclusive,
4053 } => crate::Condition::RangeF64 {
4054 column_id: *column_id,
4055 lo: expect_f64(eval_value(lo, args, outputs)?)?,
4056 lo_inclusive: *lo_inclusive,
4057 hi: expect_f64(eval_value(hi, args, outputs)?)?,
4058 hi_inclusive: *hi_inclusive,
4059 },
4060 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
4061 column_id: *column_id,
4062 },
4063 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
4064 column_id: *column_id,
4065 },
4066 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
4067 column_id: *column_id,
4068 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
4069 },
4070 })
4071}
4072
4073fn eval_value(
4074 value: &ProcedureValue,
4075 args: &HashMap<String, crate::Value>,
4076 outputs: &HashMap<String, ProcedureCallOutput>,
4077) -> Result<crate::Value> {
4078 match value {
4079 ProcedureValue::Literal(value) => Ok(value.clone()),
4080 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
4081 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
4082 }),
4083 ProcedureValue::StepScalar(id) => match outputs.get(id) {
4084 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
4085 _ => Err(MongrelError::InvalidArgument(format!(
4086 "procedure step {id:?} did not return a scalar"
4087 ))),
4088 },
4089 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
4090 Err(MongrelError::InvalidArgument(
4091 "row-valued procedure reference cannot be used as a scalar".into(),
4092 ))
4093 }
4094 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
4095 "structured procedure value cannot be used as a scalar cell".into(),
4096 )),
4097 }
4098}
4099
4100fn eval_return_output(
4101 value: &ProcedureValue,
4102 args: &HashMap<String, crate::Value>,
4103 outputs: &HashMap<String, ProcedureCallOutput>,
4104) -> Result<ProcedureCallOutput> {
4105 match value {
4106 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
4107 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
4108 args.get(name).cloned().ok_or_else(|| {
4109 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
4110 })?,
4111 )),
4112 ProcedureValue::StepRows(id)
4113 | ProcedureValue::StepRow(id)
4114 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
4115 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
4116 }),
4117 ProcedureValue::Object(fields) => {
4118 let mut out = Vec::with_capacity(fields.len());
4119 for (name, value) in fields {
4120 out.push((name.clone(), eval_return_output(value, args, outputs)?));
4121 }
4122 Ok(ProcedureCallOutput::Object(out))
4123 }
4124 ProcedureValue::Array(values) => {
4125 let mut out = Vec::with_capacity(values.len());
4126 for value in values {
4127 out.push(eval_return_output(value, args, outputs)?);
4128 }
4129 Ok(ProcedureCallOutput::Array(out))
4130 }
4131 }
4132}
4133
4134fn expect_i64(value: crate::Value) -> Result<i64> {
4135 match value {
4136 crate::Value::Int64(value) => Ok(value),
4137 _ => Err(MongrelError::InvalidArgument(
4138 "procedure value must be Int64".into(),
4139 )),
4140 }
4141}
4142
4143fn expect_f64(value: crate::Value) -> Result<f64> {
4144 match value {
4145 crate::Value::Float64(value) => Ok(value),
4146 _ => Err(MongrelError::InvalidArgument(
4147 "procedure value must be Float64".into(),
4148 )),
4149 }
4150}
4151
4152fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
4153 match value {
4154 crate::Value::Bytes(value) => Ok(value),
4155 _ => Err(MongrelError::InvalidArgument(
4156 "procedure value must be Bytes".into(),
4157 )),
4158 }
4159}
4160
4161fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
4162 if schema.columns.iter().any(|c| c.id == column_id) {
4163 Ok(())
4164 } else {
4165 Err(MongrelError::InvalidArgument(format!(
4166 "unknown column id {column_id}"
4167 )))
4168 }
4169}
4170
4171fn trigger_matches_event(
4172 trigger: &StoredTrigger,
4173 event: &WriteEvent,
4174 cat: &Catalog,
4175) -> Result<bool> {
4176 if trigger.event != event.kind {
4177 return Ok(false);
4178 }
4179 let TriggerTarget::Table(target) = &trigger.target else {
4180 return Ok(false);
4181 };
4182 if target != &event.table {
4183 return Ok(false);
4184 }
4185 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
4186 let schema = &cat
4187 .live(target)
4188 .ok_or_else(|| {
4189 MongrelError::InvalidArgument(format!(
4190 "trigger {:?} references unknown table {target:?}",
4191 trigger.name
4192 ))
4193 })?
4194 .schema;
4195 let mut watched = Vec::with_capacity(trigger.update_of.len());
4196 for name in &trigger.update_of {
4197 let col = schema.column(name).ok_or_else(|| {
4198 MongrelError::InvalidArgument(format!(
4199 "trigger {:?} references unknown UPDATE OF column {name:?}",
4200 trigger.name
4201 ))
4202 })?;
4203 watched.push(col.id);
4204 }
4205 if !event
4206 .changed_columns
4207 .iter()
4208 .any(|column_id| watched.contains(column_id))
4209 {
4210 return Ok(false);
4211 }
4212 }
4213 Ok(true)
4214}
4215
4216fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
4217 let mut ids = std::collections::BTreeSet::new();
4218 if let Some(old) = old {
4219 ids.extend(old.columns.keys().copied());
4220 }
4221 if let Some(new) = new {
4222 ids.extend(new.columns.keys().copied());
4223 }
4224 ids.into_iter()
4225 .filter(|id| {
4226 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
4227 })
4228 .collect()
4229}
4230
4231fn eval_trigger_cells(
4232 cells: &[crate::trigger::TriggerCell],
4233 event: &WriteEvent,
4234 selected: Option<&TriggerRowImage>,
4235) -> Result<Vec<(u16, Value)>> {
4236 cells
4237 .iter()
4238 .map(|cell| {
4239 Ok((
4240 cell.column_id,
4241 eval_trigger_value(&cell.value, event, selected)?,
4242 ))
4243 })
4244 .collect()
4245}
4246
4247fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
4248 match expr {
4249 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
4250 Value::Bool(value) => Ok(value),
4251 Value::Null => Ok(false),
4252 other => Err(MongrelError::InvalidArgument(format!(
4253 "trigger WHEN value must be boolean, got {other:?}"
4254 ))),
4255 },
4256 TriggerExpr::Eq { left, right } => Ok(values_equal(
4257 &eval_trigger_value(left, event, None)?,
4258 &eval_trigger_value(right, event, None)?,
4259 )),
4260 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
4261 &eval_trigger_value(left, event, None)?,
4262 &eval_trigger_value(right, event, None)?,
4263 )),
4264 TriggerExpr::Lt { left, right } => match value_order(
4265 &eval_trigger_value(left, event, None)?,
4266 &eval_trigger_value(right, event, None)?,
4267 ) {
4268 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
4269 None => Ok(false),
4270 },
4271 TriggerExpr::Lte { left, right } => match value_order(
4272 &eval_trigger_value(left, event, None)?,
4273 &eval_trigger_value(right, event, None)?,
4274 ) {
4275 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
4276 None => Ok(false),
4277 },
4278 TriggerExpr::Gt { left, right } => match value_order(
4279 &eval_trigger_value(left, event, None)?,
4280 &eval_trigger_value(right, event, None)?,
4281 ) {
4282 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
4283 None => Ok(false),
4284 },
4285 TriggerExpr::Gte { left, right } => match value_order(
4286 &eval_trigger_value(left, event, None)?,
4287 &eval_trigger_value(right, event, None)?,
4288 ) {
4289 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
4290 None => Ok(false),
4291 },
4292 TriggerExpr::IsNull(value) => Ok(matches!(
4293 eval_trigger_value(value, event, None)?,
4294 Value::Null
4295 )),
4296 TriggerExpr::IsNotNull(value) => Ok(!matches!(
4297 eval_trigger_value(value, event, None)?,
4298 Value::Null
4299 )),
4300 TriggerExpr::And { left, right } => {
4301 if !eval_trigger_expr(left, event)? {
4302 Ok(false)
4303 } else {
4304 Ok(eval_trigger_expr(right, event)?)
4305 }
4306 }
4307 TriggerExpr::Or { left, right } => {
4308 if eval_trigger_expr(left, event)? {
4309 Ok(true)
4310 } else {
4311 Ok(eval_trigger_expr(right, event)?)
4312 }
4313 }
4314 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
4315 }
4316}
4317
4318fn eval_trigger_condition(
4319 condition: &TriggerCondition,
4320 event: &WriteEvent,
4321 selected: &TriggerRowImage,
4322 schema: &Schema,
4323) -> Result<bool> {
4324 match condition {
4325 TriggerCondition::Pk { value } => {
4326 let pk = schema.primary_key().ok_or_else(|| {
4327 MongrelError::InvalidArgument(
4328 "trigger condition Pk references a table without a primary key".into(),
4329 )
4330 })?;
4331 let lhs = eval_trigger_value(value, event, Some(selected))?;
4332 Ok(values_equal(
4333 &lhs,
4334 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
4335 ))
4336 }
4337 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
4338 selected.columns.get(column_id).unwrap_or(&Value::Null),
4339 &eval_trigger_value(value, event, Some(selected))?,
4340 )),
4341 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
4342 selected.columns.get(column_id).unwrap_or(&Value::Null),
4343 &eval_trigger_value(value, event, Some(selected))?,
4344 )),
4345 TriggerCondition::Lt { column_id, value } => match value_order(
4346 selected.columns.get(column_id).unwrap_or(&Value::Null),
4347 &eval_trigger_value(value, event, Some(selected))?,
4348 ) {
4349 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
4350 None => Ok(false),
4351 },
4352 TriggerCondition::Lte { column_id, value } => match value_order(
4353 selected.columns.get(column_id).unwrap_or(&Value::Null),
4354 &eval_trigger_value(value, event, Some(selected))?,
4355 ) {
4356 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
4357 None => Ok(false),
4358 },
4359 TriggerCondition::Gt { column_id, value } => match value_order(
4360 selected.columns.get(column_id).unwrap_or(&Value::Null),
4361 &eval_trigger_value(value, event, Some(selected))?,
4362 ) {
4363 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
4364 None => Ok(false),
4365 },
4366 TriggerCondition::Gte { column_id, value } => match value_order(
4367 selected.columns.get(column_id).unwrap_or(&Value::Null),
4368 &eval_trigger_value(value, event, Some(selected))?,
4369 ) {
4370 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
4371 None => Ok(false),
4372 },
4373 TriggerCondition::IsNull { column_id } => Ok(matches!(
4374 selected.columns.get(column_id),
4375 None | Some(Value::Null)
4376 )),
4377 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
4378 selected.columns.get(column_id),
4379 None | Some(Value::Null)
4380 )),
4381 TriggerCondition::And { left, right } => {
4382 if !eval_trigger_condition(left, event, selected, schema)? {
4383 Ok(false)
4384 } else {
4385 Ok(eval_trigger_condition(right, event, selected, schema)?)
4386 }
4387 }
4388 TriggerCondition::Or { left, right } => {
4389 if eval_trigger_condition(left, event, selected, schema)? {
4390 Ok(true)
4391 } else {
4392 Ok(eval_trigger_condition(right, event, selected, schema)?)
4393 }
4394 }
4395 TriggerCondition::Not(condition) => {
4396 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
4397 }
4398 }
4399}
4400
4401fn eval_trigger_value(
4402 value: &TriggerValue,
4403 event: &WriteEvent,
4404 selected: Option<&TriggerRowImage>,
4405) -> Result<Value> {
4406 match value {
4407 TriggerValue::Literal(value) => Ok(value.clone()),
4408 TriggerValue::NewColumn(column_id) => event
4409 .new
4410 .as_ref()
4411 .and_then(|row| row.columns.get(column_id))
4412 .cloned()
4413 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
4414 TriggerValue::OldColumn(column_id) => event
4415 .old
4416 .as_ref()
4417 .and_then(|row| row.columns.get(column_id))
4418 .cloned()
4419 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
4420 TriggerValue::SelectedColumn(column_id) => selected
4421 .and_then(|row| row.columns.get(column_id))
4422 .cloned()
4423 .ok_or_else(|| {
4424 MongrelError::InvalidArgument("SELECTED column is not available".into())
4425 }),
4426 }
4427}
4428
4429fn values_equal(left: &Value, right: &Value) -> bool {
4430 match (left, right) {
4431 (Value::Null, Value::Null) => true,
4432 (Value::Bool(a), Value::Bool(b)) => a == b,
4433 (Value::Int64(a), Value::Int64(b)) => a == b,
4434 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
4435 (Value::Bytes(a), Value::Bytes(b)) => a == b,
4436 (Value::Embedding(a), Value::Embedding(b)) => {
4437 a.len() == b.len()
4438 && a.iter()
4439 .zip(b.iter())
4440 .all(|(a, b)| a.to_bits() == b.to_bits())
4441 }
4442 _ => false,
4443 }
4444}
4445
4446fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
4447 match (left, right) {
4448 (Value::Null, _) | (_, Value::Null) => None,
4449 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
4450 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
4451 (Value::Int64(a), Value::Float64(b)) => {
4454 let af = *a as f64;
4455 Some(af.total_cmp(b))
4456 }
4457 (Value::Float64(a), Value::Int64(b)) => {
4460 let bf = *b as f64;
4461 Some(a.total_cmp(&bf))
4462 }
4463 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
4464 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
4465 (Value::Embedding(_), Value::Embedding(_)) => None,
4466 _ => None,
4467 }
4468}
4469
4470fn trigger_message(value: Value) -> String {
4471 match value {
4472 Value::Null => "NULL".into(),
4473 Value::Bool(value) => value.to_string(),
4474 Value::Int64(value) => value.to_string(),
4475 Value::Float64(value) => value.to_string(),
4476 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
4477 Value::Embedding(value) => format!("{value:?}"),
4478 Value::Decimal(value) => value.to_string(),
4479 Value::Interval {
4480 months,
4481 days,
4482 nanos,
4483 } => format!("{months}m {days}d {nanos}ns"),
4484 }
4485}
4486
4487fn validate_trigger_step<'a>(
4488 step: &TriggerStep,
4489 cat: &'a Catalog,
4490 target_schema: &Schema,
4491 event: TriggerEvent,
4492 select_schemas: &mut HashMap<String, &'a Schema>,
4493) -> Result<()> {
4494 match step {
4495 TriggerStep::SetNew { cells } => {
4496 if event == TriggerEvent::Delete {
4497 return Err(MongrelError::InvalidArgument(
4498 "SetNew trigger step is not valid for DELETE triggers".into(),
4499 ));
4500 }
4501 for cell in cells {
4502 validate_column_id(cell.column_id, target_schema)?;
4503 validate_trigger_value(&cell.value, target_schema, event)?;
4504 }
4505 }
4506 TriggerStep::Insert { table, cells } => {
4507 let schema = trigger_write_schema(cat, table, "insert")?;
4508 for cell in cells {
4509 validate_column_id(cell.column_id, schema)?;
4510 validate_trigger_value(&cell.value, target_schema, event)?;
4511 }
4512 }
4513 TriggerStep::UpdateByPk { table, pk, cells } => {
4514 let schema = trigger_write_schema(cat, table, "update")?;
4515 if schema.primary_key().is_none() {
4516 return Err(MongrelError::InvalidArgument(format!(
4517 "trigger update_by_pk references table {table:?} without a primary key"
4518 )));
4519 }
4520 validate_trigger_value(pk, target_schema, event)?;
4521 for cell in cells {
4522 validate_column_id(cell.column_id, schema)?;
4523 validate_trigger_value(&cell.value, target_schema, event)?;
4524 }
4525 }
4526 TriggerStep::DeleteByPk { table, pk } => {
4527 let schema = trigger_write_schema(cat, table, "delete")?;
4528 if schema.primary_key().is_none() {
4529 return Err(MongrelError::InvalidArgument(format!(
4530 "trigger delete_by_pk references table {table:?} without a primary key"
4531 )));
4532 }
4533 validate_trigger_value(pk, target_schema, event)?;
4534 }
4535 TriggerStep::Select {
4536 id,
4537 table,
4538 conditions,
4539 } => {
4540 let schema = trigger_read_schema(cat, table)?;
4541 for condition in conditions {
4542 validate_trigger_condition(condition, schema, target_schema, event)?;
4543 }
4544 if select_schemas.contains_key(id) {
4545 return Err(MongrelError::InvalidArgument(format!(
4546 "duplicate select id {id:?} in trigger program"
4547 )));
4548 }
4549 select_schemas.insert(id.clone(), schema);
4550 }
4551 TriggerStep::Foreach { id, steps } => {
4552 if !select_schemas.contains_key(id) {
4553 return Err(MongrelError::InvalidArgument(format!(
4554 "foreach references unknown select id {id:?}"
4555 )));
4556 }
4557 let mut inner_select_schemas = select_schemas.clone();
4558 for step in steps {
4559 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
4560 }
4561 }
4562 TriggerStep::DeleteWhere { table, conditions } => {
4563 let schema = trigger_write_schema(cat, table, "delete")?;
4564 for condition in conditions {
4565 validate_trigger_condition(condition, schema, target_schema, event)?;
4566 }
4567 }
4568 TriggerStep::UpdateWhere {
4569 table,
4570 conditions,
4571 cells,
4572 } => {
4573 let schema = trigger_write_schema(cat, table, "update")?;
4574 for condition in conditions {
4575 validate_trigger_condition(condition, schema, target_schema, event)?;
4576 }
4577 for cell in cells {
4578 validate_column_id(cell.column_id, schema)?;
4579 validate_trigger_value(&cell.value, target_schema, event)?;
4580 }
4581 }
4582 TriggerStep::Raise { message, .. } => {
4583 validate_trigger_value(message, target_schema, event)?
4584 }
4585 }
4586 Ok(())
4587}
4588
4589fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
4590 if let Some(entry) = cat.live(table) {
4591 return Ok(&entry.schema);
4592 }
4593 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
4594 let allowed = match op {
4595 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
4596 "update" | "delete" => entry.capabilities.writable,
4597 _ => false,
4598 };
4599 if !allowed {
4600 return Err(MongrelError::InvalidArgument(format!(
4601 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
4602 entry.module
4603 )));
4604 }
4605 if !entry.capabilities.transaction_safe {
4606 return Err(MongrelError::InvalidArgument(format!(
4607 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
4608 entry.module
4609 )));
4610 }
4611 return Ok(&entry.declared_schema);
4612 }
4613 Err(MongrelError::InvalidArgument(format!(
4614 "trigger references unknown table {table:?}"
4615 )))
4616}
4617
4618fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
4619 if let Some(entry) = cat.live(table) {
4620 return Ok(&entry.schema);
4621 }
4622 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
4623 if entry.capabilities.trigger_safe {
4624 return Ok(&entry.declared_schema);
4625 }
4626 return Err(MongrelError::InvalidArgument(format!(
4627 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
4628 entry.module
4629 )));
4630 }
4631 Err(MongrelError::InvalidArgument(format!(
4632 "trigger references unknown table {table:?}"
4633 )))
4634}
4635
4636fn validate_trigger_condition(
4637 condition: &TriggerCondition,
4638 schema: &Schema,
4639 target_schema: &Schema,
4640 event: TriggerEvent,
4641) -> Result<()> {
4642 match condition {
4643 TriggerCondition::Pk { value } => {
4644 if schema.primary_key().is_none() {
4645 return Err(MongrelError::InvalidArgument(
4646 "trigger condition Pk references a table without a primary key".into(),
4647 ));
4648 }
4649 validate_trigger_value(value, target_schema, event)
4650 }
4651 TriggerCondition::Eq { column_id, value }
4652 | TriggerCondition::NotEq { column_id, value }
4653 | TriggerCondition::Lt { column_id, value }
4654 | TriggerCondition::Lte { column_id, value }
4655 | TriggerCondition::Gt { column_id, value }
4656 | TriggerCondition::Gte { column_id, value } => {
4657 validate_column_id(*column_id, schema)?;
4658 validate_trigger_value(value, target_schema, event)
4659 }
4660 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
4661 validate_column_id(*column_id, schema)
4662 }
4663 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
4664 validate_trigger_condition(left, schema, target_schema, event)?;
4665 validate_trigger_condition(right, schema, target_schema, event)
4666 }
4667 TriggerCondition::Not(condition) => {
4668 validate_trigger_condition(condition, schema, target_schema, event)
4669 }
4670 }
4671}
4672
4673fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
4674 match expr {
4675 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
4676 validate_trigger_value(value, schema, event)
4677 }
4678 TriggerExpr::Eq { left, right }
4679 | TriggerExpr::NotEq { left, right }
4680 | TriggerExpr::Lt { left, right }
4681 | TriggerExpr::Lte { left, right }
4682 | TriggerExpr::Gt { left, right }
4683 | TriggerExpr::Gte { left, right } => {
4684 validate_trigger_value(left, schema, event)?;
4685 validate_trigger_value(right, schema, event)
4686 }
4687 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
4688 validate_trigger_expr(left, schema, event)?;
4689 validate_trigger_expr(right, schema, event)
4690 }
4691 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
4692 }
4693}
4694
4695fn validate_trigger_value(
4696 value: &TriggerValue,
4697 schema: &Schema,
4698 event: TriggerEvent,
4699) -> Result<()> {
4700 match value {
4701 TriggerValue::Literal(_) => Ok(()),
4702 TriggerValue::NewColumn(id) => {
4703 if event == TriggerEvent::Delete {
4704 return Err(MongrelError::InvalidArgument(
4705 "DELETE triggers cannot reference NEW".into(),
4706 ));
4707 }
4708 validate_column_id(*id, schema)
4709 }
4710 TriggerValue::OldColumn(id) => {
4711 if event == TriggerEvent::Insert {
4712 return Err(MongrelError::InvalidArgument(
4713 "INSERT triggers cannot reference OLD".into(),
4714 ));
4715 }
4716 validate_column_id(*id, schema)
4717 }
4718 TriggerValue::SelectedColumn(_) => Ok(()),
4722 }
4723}
4724
4725fn recover_ddl_from_wal(
4731 root: &Path,
4732 cat: &mut Catalog,
4733 meta_dek: Option<&[u8; META_DEK_LEN]>,
4734 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
4735) -> Result<()> {
4736 use crate::wal::{DdlOp, Op, SharedWal};
4737
4738 let records = match SharedWal::replay_with_dek(root, wal_dek) {
4739 Ok(r) => r,
4740 Err(_) => return Ok(()),
4741 };
4742
4743 let mut committed: HashMap<u64, u64> = HashMap::new();
4744 for r in &records {
4745 if let Op::TxnCommit { epoch: ce, .. } = r.op {
4746 committed.insert(r.txn_id, ce);
4747 }
4748 }
4749
4750 let mut changed = false;
4751 for r in records {
4752 let Some(&ce) = committed.get(&r.txn_id) else {
4753 continue;
4754 };
4755 match r.op {
4756 Op::Ddl(DdlOp::CreateTable {
4757 table_id,
4758 ref name,
4759 ref schema_json,
4760 }) => {
4761 if cat.tables.iter().any(|t| t.table_id == table_id) {
4762 continue;
4763 }
4764 let schema = DdlOp::decode_schema(schema_json)?;
4765 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
4766 if !tdir.exists() {
4767 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
4768 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
4769 crate::engine::write_schema(&tdir, &schema)?;
4770 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
4776 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
4777 }
4778 cat.tables.push(CatalogEntry {
4779 table_id,
4780 name: name.clone(),
4781 schema,
4782 state: TableState::Live,
4783 created_epoch: ce,
4784 });
4785 cat.next_table_id = cat.next_table_id.max(table_id + 1);
4786 changed = true;
4787 }
4788 Op::Ddl(DdlOp::DropTable { table_id }) => {
4789 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4790 if matches!(entry.state, TableState::Live) {
4791 entry.state = TableState::Dropped { at_epoch: ce };
4792 changed = true;
4793 }
4794 }
4795 }
4796 Op::Ddl(DdlOp::RenameTable {
4797 table_id,
4798 ref new_name,
4799 }) => {
4800 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4801 if entry.name != *new_name {
4802 entry.name = new_name.clone();
4803 changed = true;
4804 }
4805 }
4806 }
4810 Op::Ddl(DdlOp::AlterTable {
4811 table_id,
4812 ref column_json,
4813 }) => {
4814 let column = DdlOp::decode_column(column_json)?;
4815 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4816 if apply_recovered_column_def(&mut entry.schema, column) {
4817 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
4818 if tdir.exists() {
4819 crate::engine::write_schema(&tdir, &entry.schema)?;
4820 }
4821 changed = true;
4822 }
4823 }
4824 }
4825 _ => {}
4826 }
4827 }
4828
4829 if changed {
4830 catalog::write_atomic(root, cat, meta_dek)?;
4831 }
4832 Ok(())
4833}
4834
4835fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
4836 match schema.columns.iter_mut().find(|c| c.id == column.id) {
4837 Some(existing) if *existing == column => false,
4838 Some(existing) => {
4839 *existing = column;
4840 schema.schema_id = schema.schema_id.saturating_add(1);
4841 true
4842 }
4843 None => {
4844 schema.columns.push(column);
4845 schema.schema_id = schema.schema_id.saturating_add(1);
4846 true
4847 }
4848 }
4849}
4850
4851fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
4856 for entry in &cat.tables {
4857 let txn_dir = root
4858 .join(TABLES_DIR)
4859 .join(entry.table_id.to_string())
4860 .join("_txn");
4861 if txn_dir.exists() {
4862 let _ = std::fs::remove_dir_all(&txn_dir);
4863 }
4864 }
4865}
4866
4867#[cfg(test)]
4868mod trigger_engine_tests {
4869 use super::*;
4870
4871 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
4872 WriteEvent {
4873 table: "test".into(),
4874 kind: TriggerEvent::Insert,
4875 new: Some(TriggerRowImage {
4876 columns: new_cells.iter().cloned().collect(),
4877 }),
4878 old: Some(TriggerRowImage {
4879 columns: old_cells.iter().cloned().collect(),
4880 }),
4881 changed_columns: Vec::new(),
4882 op_indices: Vec::new(),
4883 put_idx: None,
4884 trigger_stack: Vec::new(),
4885 }
4886 }
4887
4888 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
4889 WriteEvent {
4890 table: "test".into(),
4891 kind: TriggerEvent::Insert,
4892 new: Some(TriggerRowImage {
4893 columns: new_cells.iter().cloned().collect(),
4894 }),
4895 old: None,
4896 changed_columns: Vec::new(),
4897 op_indices: Vec::new(),
4898 put_idx: None,
4899 trigger_stack: Vec::new(),
4900 }
4901 }
4902
4903 #[test]
4904 fn value_order_int64_vs_float64() {
4905 assert_eq!(
4906 value_order(&Value::Int64(5), &Value::Float64(5.0)),
4907 Some(std::cmp::Ordering::Equal)
4908 );
4909 assert_eq!(
4910 value_order(&Value::Int64(5), &Value::Float64(3.0)),
4911 Some(std::cmp::Ordering::Greater)
4912 );
4913 assert_eq!(
4914 value_order(&Value::Int64(2), &Value::Float64(3.0)),
4915 Some(std::cmp::Ordering::Less)
4916 );
4917 }
4918
4919 #[test]
4920 fn value_order_null_returns_none() {
4921 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
4922 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
4923 assert_eq!(value_order(&Value::Null, &Value::Null), None);
4924 }
4925
4926 #[test]
4927 fn value_order_cross_group_returns_none() {
4928 assert_eq!(
4929 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
4930 None
4931 );
4932 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
4933 assert_eq!(
4934 value_order(
4935 &Value::Embedding(vec![1.0, 2.0]),
4936 &Value::Embedding(vec![1.0, 2.0])
4937 ),
4938 None
4939 );
4940 }
4941
4942 #[test]
4943 fn eval_trigger_expr_ranges_and_booleans() {
4944 let expr = TriggerExpr::And {
4945 left: Box::new(TriggerExpr::Gt {
4946 left: TriggerValue::NewColumn(1),
4947 right: TriggerValue::Literal(Value::Int64(0)),
4948 }),
4949 right: Box::new(TriggerExpr::Lte {
4950 left: TriggerValue::NewColumn(1),
4951 right: TriggerValue::Literal(Value::Int64(100)),
4952 }),
4953 };
4954 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
4955 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
4956 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
4957
4958 let or_expr = TriggerExpr::Or {
4959 left: Box::new(TriggerExpr::Lt {
4960 left: TriggerValue::NewColumn(1),
4961 right: TriggerValue::Literal(Value::Int64(0)),
4962 }),
4963 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
4964 TriggerValue::OldColumn(2),
4965 )))),
4966 };
4967 assert!(eval_trigger_expr(
4968 &or_expr,
4969 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
4970 )
4971 .unwrap());
4972 assert!(!eval_trigger_expr(
4973 &or_expr,
4974 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
4975 )
4976 .unwrap());
4977
4978 assert!(eval_trigger_expr(
4979 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
4980 &event_insert(&[])
4981 )
4982 .unwrap());
4983 assert!(!eval_trigger_expr(
4984 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
4985 &event_insert(&[])
4986 )
4987 .unwrap());
4988 assert!(!eval_trigger_expr(
4989 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
4990 &event_insert(&[])
4991 )
4992 .unwrap());
4993 }
4994}