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