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}