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