Skip to main content

noxu_dbi/
trigger.rs

1//! Database / transaction triggers.
2//!
3//! Port of JE `com.sleepycat.je.trigger.Trigger` + `TransactionTrigger`.
4//!
5//! A `Trigger` is a user-supplied callback object registered on a database via
6//! [`crate::DatabaseConfig`].  The engine fires its methods on data changes
7//! (`put` / `delete`) and on transaction resolution (`commit` / `abort`).
8//!
9//! # JE mapping (faithful)
10//!
11//! JE splits the contract across two Java interfaces that a single trigger
12//! object may both implement:
13//!
14//! * `com.sleepycat.je.trigger.Trigger` — `getName`, lifecycle
15//!   (`addTrigger` / `removeTrigger`) and the record operations
16//!   `put(txn, key, oldData, newData)` / `delete(txn, key, oldData)`.
17//! * `com.sleepycat.je.trigger.TransactionTrigger` — `commit(txn)` /
18//!   `abort(txn)`, invoked from `Txn.commit` / `Txn.abort` for every database
19//!   that was modified within the transaction (`TriggerManager.runCommitTriggers`
20//!   / `runAbortTriggers`).
21//!
22//! JE dispatches to `TransactionTrigger` via `instanceof` (a trigger that does
23//! not implement it simply has no commit/abort behaviour).  The Rust idiom is a
24//! single `Trigger` trait whose `commit` / `abort` methods default to no-ops:
25//! a trigger that only cares about record operations leaves them unimplemented,
26//! exactly mirroring "does not implement `TransactionTrigger`".  This avoids a
27//! second trait object and the downcast dance while preserving the JE
28//! semantics.
29//!
30//! # Transaction argument
31//!
32//! JE passes the public `Transaction` handle.  Noxu passes the transaction id
33//! (`Option<u64>`; `None` when the operation is non-transactional /
34//! auto-commit) instead.  The trait lives in `noxu-dbi`, below `noxu-db` in the
35//! dependency graph, so it cannot name `noxu_db::Transaction`; the id is the
36//! faithful, dependency-clean signal of "which transaction this fired under"
37//! and matches JE's `Transaction.getId()`.
38//!
39//! # Firing semantics (faithful to JE)
40//!
41//! * `put` / `delete` fire **within** the transaction, **after** the record
42//!   modification has been applied — JE `Cursor.putNotify` /
43//!   `Cursor.deleteInternal` call `TriggerManager.runPutTriggers` /
44//!   `runDeleteTriggers` after the actual tree mutation.  A trigger therefore
45//!   observes the change and can make accompanying changes under the same
46//!   transaction; on abort those changes are rolled back with the transaction.
47//! * `commit` / `abort` fire on the transaction's resolution, once per
48//!   modified database, in trigger registration order (JE iterates
49//!   `dbImpl.getTriggers()` in list order).
50//! * Multiple triggers fire in **registration order** (JE stores them in a
51//!   `List<Trigger>` and iterates it).
52//!
53//! # Persistence / replication adaptation (diverges from JE — documented)
54//!
55//! JE's `PersistentTrigger` serializes the trigger's *class name* into the
56//! database record and re-instantiates the trigger by name on open.  A Rust
57//! closure / trait object has no portable, reconstructable name, so — exactly
58//! as the DBI-14 comparator API does — Noxu triggers are **runtime-registered
59//! only**: they are *not* persisted and *not* replicated.  Applications must
60//! re-register triggers on every [`crate::DatabaseConfig`] open.  This matches
61//! JE's own current state: the `Trigger.java` Javadoc warns that "Only
62//! transient triggers are currently supported" and that triggers "must be
63//! configured on each node in a rep group separately".
64
65/// A user-supplied database / transaction trigger.
66///
67/// Register one or more triggers on a [`crate::DatabaseConfig`]; the engine
68/// fires the record-operation methods ([`put`](Trigger::put) /
69/// [`delete`](Trigger::delete)) within the transaction after each change, and
70/// the transaction-lifecycle methods ([`commit`](Trigger::commit) /
71/// [`abort`](Trigger::abort)) when the transaction resolves.
72///
73/// JE `com.sleepycat.je.trigger.Trigger` + `TransactionTrigger`.
74pub trait Trigger: Send + Sync {
75    /// The trigger's name.  All triggers on one database must have unique
76    /// names.  JE `Trigger.getName`.
77    fn name(&self) -> &str;
78
79    /// The trigger method invoked after a successful `put`, i.e. one that
80    /// actually modified the database.
81    ///
82    /// For a new insert, `old_data` is `None`; for an update of an existing
83    /// record, `old_data` is `Some(previous)`.  `new_data` is always present.
84    /// Fired within the transaction, after the change is applied.
85    ///
86    /// JE `Trigger.put(Transaction, DatabaseEntry key, DatabaseEntry oldData,
87    /// DatabaseEntry newData)`.
88    ///
89    /// * `txn_id` — the transaction id, or `None` if non-transactional.
90    /// * `key` — the (non-null) primary key.
91    /// * `old_data` — the data before the change, or `None` if the record did
92    ///   not previously exist.
93    /// * `new_data` — the (non-null) data after the change.
94    fn put(
95        &self,
96        txn_id: Option<u64>,
97        key: &[u8],
98        old_data: Option<&[u8]>,
99        new_data: &[u8],
100    );
101
102    /// The trigger method invoked after a successful `delete`, i.e. one that
103    /// actually removed a key/data pair.  Fired within the transaction, after
104    /// the change is applied.
105    ///
106    /// JE `Trigger.delete(Transaction, DatabaseEntry key,
107    /// DatabaseEntry oldData)`.
108    ///
109    /// * `txn_id` — the transaction id, or `None` if non-transactional.
110    /// * `key` — the (non-null) primary key.
111    /// * `old_data` — the (non-null) data that was associated with the deleted
112    ///   key.
113    fn delete(&self, txn_id: Option<u64>, key: &[u8], old_data: &[u8]);
114
115    /// The trigger method invoked after the transaction that modified this
116    /// trigger's database has committed.  Only invoked if the database was
117    /// modified during the transaction.  Default: no-op (JE: trigger does not
118    /// implement `TransactionTrigger`).
119    ///
120    /// JE `TransactionTrigger.commit(Transaction)`.
121    fn commit(&self, _txn_id: u64) {}
122
123    /// The trigger method invoked after the transaction that modified this
124    /// trigger's database has aborted.  Only invoked if the database was
125    /// modified during the transaction.  Default: no-op.
126    ///
127    /// JE `TransactionTrigger.abort(Transaction)`.
128    fn abort(&self, _txn_id: u64) {}
129
130    /// Lifecycle hook invoked when the trigger is added to the database
131    /// (the first trigger method invoked, exactly once).  Default: no-op.
132    ///
133    /// JE `Trigger.addTrigger(Transaction)`.
134    fn add_trigger(&self, _txn_id: Option<u64>) {}
135
136    /// Lifecycle hook invoked when the trigger is removed from the database
137    /// (e.g. on close).  Default: no-op.
138    ///
139    /// JE `Trigger.removeTrigger(Transaction)`.
140    fn remove_trigger(&self, _txn_id: Option<u64>) {}
141}