highlandcows_isam/transaction.rs
1/// `Transaction<'a, K, V>` — a bounded execution context for ISAM operations.
2///
3/// Holds the `MutexGuard` for its entire lifetime (serializable isolation) and
4/// an undo log so that a rollback (explicit or via `Drop`) can restore the
5/// index to the state it had before the transaction began.
6use std::sync::MutexGuard;
7
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10
11use crate::error::IsamResult;
12use crate::storage::IsamStorage;
13use crate::store::RecordRef;
14
15// ── Undo log entry ───────────────────────────────────────────────────────── //
16
17pub(crate) enum UndoEntry<K, V> {
18 /// Undo an insert by deleting the key from the primary index and secondary indices.
19 Insert { key: K, value: V },
20 /// Undo an update by restoring the old RecordRef and reversing secondary index changes.
21 Update { key: K, old_rec: RecordRef, old_value: V, new_value: V },
22 /// Undo a delete by re-inserting the key with its old RecordRef and secondary indices.
23 Delete { key: K, old_rec: RecordRef, value: V },
24}
25
26// ── Transaction ──────────────────────────────────────────────────────────── //
27
28pub struct Transaction<'a, K, V>
29where
30 K: Serialize + DeserializeOwned + Ord + Clone,
31 V: Serialize + DeserializeOwned,
32{
33 guard: MutexGuard<'a, IsamStorage<K, V>>,
34 undo_log: Vec<UndoEntry<K, V>>,
35 committed: bool,
36}
37
38impl<'a, K, V> Transaction<'a, K, V>
39where
40 K: Serialize + DeserializeOwned + Ord + Clone,
41 V: Serialize + DeserializeOwned,
42{
43 pub(crate) fn new(guard: MutexGuard<'a, IsamStorage<K, V>>) -> Self {
44 Self {
45 guard,
46 undo_log: Vec::new(),
47 committed: false,
48 }
49 }
50
51 // ── pub(crate) interface consumed by Isam ──────────────────────────── //
52
53 pub(crate) fn storage_mut(&mut self) -> &mut IsamStorage<K, V> {
54 &mut self.guard
55 }
56
57 pub(crate) fn log_insert(&mut self, key: K, value: V) {
58 self.undo_log.push(UndoEntry::Insert { key, value });
59 }
60
61 pub(crate) fn log_update(&mut self, key: K, old_rec: RecordRef, old_value: V, new_value: V) {
62 self.undo_log.push(UndoEntry::Update { key, old_rec, old_value, new_value });
63 }
64
65 pub(crate) fn log_delete(&mut self, key: K, old_rec: RecordRef, value: V) {
66 self.undo_log.push(UndoEntry::Delete { key, old_rec, value });
67 }
68
69 // ── Public interface consumed by callers ───────────────────────────── //
70
71 /// Flush store and index to disk (`fsync`) and release the database lock.
72 ///
73 /// After `commit` returns the changes are durable and the lock is released,
74 /// allowing other threads to begin their own transactions.
75 ///
76 /// # Example
77 /// ```
78 /// # use tempfile::TempDir;
79 /// # use highlandcows_isam::Isam;
80 /// # let dir = TempDir::new().unwrap();
81 /// # let path = dir.path().join("db");
82 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
83 /// let mut txn = db.begin_transaction().unwrap();
84 /// db.insert(&mut txn, 1u32, &"hello".to_string()).unwrap();
85 /// txn.commit().unwrap();
86 ///
87 /// // Data is now durable — visible after reopen.
88 /// let db2: Isam<u32, String> = Isam::open(&path).unwrap();
89 /// let mut txn2 = db2.begin_transaction().unwrap();
90 /// assert_eq!(db2.get(&mut txn2, &1u32).unwrap(), Some("hello".to_string()));
91 /// txn2.commit().unwrap();
92 /// ```
93 pub fn commit(mut self) -> IsamResult<()> {
94 self.guard.fsync()?;
95 self.committed = true;
96 Ok(())
97 // MutexGuard drops here, releasing the lock
98 }
99
100 /// Roll back all changes made in this transaction and release the lock.
101 ///
102 /// The undo log is applied in reverse order, restoring the index to the
103 /// state it had before the transaction began. Dropping a `Transaction`
104 /// without calling `commit` has the same effect automatically.
105 ///
106 /// # Example
107 /// ```
108 /// # use tempfile::TempDir;
109 /// # use highlandcows_isam::Isam;
110 /// # let dir = TempDir::new().unwrap();
111 /// # let path = dir.path().join("db");
112 /// # let db: Isam<u32, String> = Isam::create(&path).unwrap();
113 /// let mut txn = db.begin_transaction().unwrap();
114 /// db.insert(&mut txn, 1u32, &"oops".to_string()).unwrap();
115 /// txn.rollback().unwrap();
116 ///
117 /// // Insert was rolled back — key is absent.
118 /// let mut txn2 = db.begin_transaction().unwrap();
119 /// assert_eq!(db.get(&mut txn2, &1u32).unwrap(), None);
120 /// txn2.commit().unwrap();
121 /// ```
122 pub fn rollback(mut self) -> IsamResult<()> {
123 self.do_rollback()?;
124 self.committed = true;
125 Ok(())
126 }
127
128 // ── Private helpers ────────────────────────────────────────────────── //
129
130 fn do_rollback(&mut self) -> IsamResult<()> {
131 // Apply in reverse order
132 while let Some(entry) = self.undo_log.pop() {
133 match entry {
134 UndoEntry::Insert { key, value } => {
135 for si in &mut self.guard.secondary_indices {
136 let _ = si.undo_insert(&key, &value);
137 }
138 let _ = self.guard.index.delete(&key);
139 }
140 UndoEntry::Update { key, old_rec, old_value, new_value } => {
141 for si in &mut self.guard.secondary_indices {
142 let _ = si.undo_update(&key, &old_value, &new_value);
143 }
144 let _ = self.guard.index.update(&key, old_rec);
145 }
146 UndoEntry::Delete { key, old_rec, value } => {
147 for si in &mut self.guard.secondary_indices {
148 let _ = si.undo_delete(&key, &value);
149 }
150 let _ = self.guard.index.insert(&key, old_rec);
151 }
152 }
153 }
154 Ok(())
155 }
156}
157
158impl<'a, K, V> Drop for Transaction<'a, K, V>
159where
160 K: Serialize + DeserializeOwned + Ord + Clone,
161 V: Serialize + DeserializeOwned,
162{
163 fn drop(&mut self) {
164 if !self.committed {
165 let _ = self.do_rollback();
166 }
167 }
168}