graphlite_sdk/transaction.rs
1//! Transaction support with ACID guarantees
2//!
3//! This module provides transaction support following the rusqlite pattern:
4//! - Transactions automatically roll back when dropped (unless committed)
5//! - RAII ensures no forgotten rollbacks
6//! - Explicit commit() required to persist changes
7
8use crate::connection::Session;
9use crate::error::{Error, Result};
10use graphlite::QueryResult;
11
12/// Represents an active database transaction
13///
14/// Transactions provide ACID guarantees for multi-statement operations.
15/// Following the rusqlite pattern:
16/// - Transactions automatically **roll back** when dropped
17/// - Must explicitly call `commit()` to persist changes
18/// - This prevents accidentally forgetting to commit or rollback
19///
20/// # Examples
21///
22/// ```no_run
23/// # use graphlite_sdk::GraphLite;
24/// # let db = GraphLite::open("./mydb")?;
25/// # let session = db.session("admin")?;
26/// // Transaction with explicit commit
27/// let mut tx = session.transaction()?;
28/// tx.execute("CREATE (p:Person {name: 'Alice'})")?;
29/// tx.execute("CREATE (p:Person {name: 'Bob'})")?;
30/// tx.commit()?; // Changes are persisted
31///
32/// // Transaction that rolls back (dropped without commit)
33/// {
34/// let mut tx = session.transaction()?;
35/// tx.execute("CREATE (p:Person {name: 'Charlie'})")?;
36/// // tx is dropped here, changes are automatically rolled back
37/// }
38/// # Ok::<(), graphlite_sdk::Error>(())
39/// ```
40pub struct Transaction<'conn> {
41 session: &'conn Session,
42 committed: bool,
43 drop_behavior: DropBehavior,
44}
45
46/// Behavior when a transaction is dropped
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DropBehavior {
49 /// Rollback the transaction when dropped (default)
50 Rollback,
51 /// Commit the transaction when dropped
52 Commit,
53 /// Panic if the transaction is dropped without explicit commit/rollback
54 Panic,
55 /// Do nothing when dropped (dangerous - for special cases only)
56 Ignore,
57}
58
59impl<'conn> Transaction<'conn> {
60 /// Begin a new transaction
61 ///
62 /// This is called internally by `Session::transaction()`.
63 /// The transaction will automatically roll back when dropped unless committed.
64 pub(crate) fn begin(session: &'conn Session) -> Result<Self> {
65 // Execute BEGIN TRANSACTION
66 session
67 .coordinator()
68 .process_query("BEGIN TRANSACTION", session.id())
69 .map_err(|e| Error::Transaction(format!("Failed to begin transaction: {}", e)))?;
70
71 Ok(Transaction {
72 session,
73 committed: false,
74 drop_behavior: DropBehavior::Rollback,
75 })
76 }
77
78 /// Execute a GQL statement within this transaction
79 ///
80 /// # Arguments
81 ///
82 /// * `statement` - GQL statement to execute
83 ///
84 /// # Examples
85 ///
86 /// ```no_run
87 /// # use graphlite_sdk::GraphLite;
88 /// # let db = GraphLite::open("./mydb")?;
89 /// # let session = db.session("admin")?;
90 /// let mut tx = session.transaction()?;
91 /// tx.execute("CREATE (p:Person {name: 'Alice'})")?;
92 /// tx.execute("CREATE (p:Person {name: 'Bob'})")?;
93 /// tx.commit()?;
94 /// # Ok::<(), graphlite_sdk::Error>(())
95 /// ```
96 pub fn execute(&mut self, statement: &str) -> Result<()> {
97 if self.committed {
98 return Err(Error::Transaction(
99 "Transaction already committed".to_string(),
100 ));
101 }
102
103 self.session
104 .coordinator()
105 .process_query(statement, self.session.id())
106 .map_err(|e| Error::Transaction(format!("Execute failed: {}", e)))?;
107
108 Ok(())
109 }
110
111 /// Execute a query within this transaction and return results
112 ///
113 /// # Arguments
114 ///
115 /// * `query` - GQL query to execute
116 ///
117 /// # Examples
118 ///
119 /// ```no_run
120 /// # use graphlite_sdk::GraphLite;
121 /// # let db = GraphLite::open("./mydb")?;
122 /// # let session = db.session("admin")?;
123 /// let mut tx = session.transaction()?;
124 /// tx.execute("CREATE (p:Person {name: 'Alice', age: 30})")?;
125 /// let result = tx.query("MATCH (p:Person) RETURN p")?;
126 /// tx.commit()?;
127 /// # Ok::<(), graphlite_sdk::Error>(())
128 /// ```
129 pub fn query(&mut self, query: &str) -> Result<QueryResult> {
130 if self.committed {
131 return Err(Error::Transaction(
132 "Transaction already committed".to_string(),
133 ));
134 }
135
136 self.session
137 .coordinator()
138 .process_query(query, self.session.id())
139 .map_err(|e| Error::Transaction(format!("Query failed: {}", e)))
140 }
141
142 /// Commit the transaction
143 ///
144 /// Persists all changes made within this transaction. After calling commit(),
145 /// the transaction is consumed and cannot be used further.
146 ///
147 /// # Examples
148 ///
149 /// ```no_run
150 /// # use graphlite_sdk::GraphLite;
151 /// # let db = GraphLite::open("./mydb")?;
152 /// # let session = db.session("admin")?;
153 /// let mut tx = session.transaction()?;
154 /// tx.execute("CREATE (p:Person {name: 'Alice'})")?;
155 /// tx.commit()?; // Changes are now persistent
156 /// # Ok::<(), graphlite_sdk::Error>(())
157 /// ```
158 pub fn commit(mut self) -> Result<()> {
159 self.commit_internal()
160 }
161
162 /// Rollback the transaction
163 ///
164 /// Discards all changes made within this transaction. This is called
165 /// automatically when the transaction is dropped, so explicit rollback
166 /// is rarely needed.
167 ///
168 /// # Examples
169 ///
170 /// ```no_run
171 /// # use graphlite_sdk::GraphLite;
172 /// # let db = GraphLite::open("./mydb")?;
173 /// # let session = db.session("admin")?;
174 /// let mut tx = session.transaction()?;
175 /// tx.execute("CREATE (p:Person {name: 'Alice'})")?;
176 /// tx.rollback()?; // Explicit rollback (optional, automatic on drop)
177 /// # Ok::<(), graphlite_sdk::Error>(())
178 /// ```
179 pub fn rollback(mut self) -> Result<()> {
180 self.rollback_internal()
181 }
182
183 /// Set the behavior when this transaction is dropped
184 ///
185 /// By default, transactions roll back when dropped. This can be changed to:
186 /// - `DropBehavior::Commit` - Auto-commit on drop
187 /// - `DropBehavior::Panic` - Panic if dropped without explicit commit/rollback
188 /// - `DropBehavior::Ignore` - Do nothing (dangerous)
189 ///
190 /// # Examples
191 ///
192 /// ```no_run
193 /// # use graphlite_sdk::{GraphLite, transaction::DropBehavior};
194 /// # let db = GraphLite::open("./mydb")?;
195 /// # let session = db.session("admin")?;
196 /// let mut tx = session.transaction()?;
197 /// tx.set_drop_behavior(DropBehavior::Panic);
198 /// tx.execute("CREATE (p:Person {name: 'Alice'})")?;
199 /// // Must explicitly commit or rollback, or will panic on drop
200 /// tx.commit()?;
201 /// # Ok::<(), graphlite_sdk::Error>(())
202 /// ```
203 pub fn set_drop_behavior(&mut self, behavior: DropBehavior) {
204 self.drop_behavior = behavior;
205 }
206
207 /// Internal commit implementation
208 fn commit_internal(&mut self) -> Result<()> {
209 if self.committed {
210 return Err(Error::Transaction(
211 "Transaction already committed".to_string(),
212 ));
213 }
214
215 self.session
216 .coordinator()
217 .process_query("COMMIT", self.session.id())
218 .map_err(|e| Error::Transaction(format!("Failed to commit: {}", e)))?;
219
220 self.committed = true;
221 Ok(())
222 }
223
224 /// Internal rollback implementation
225 fn rollback_internal(&mut self) -> Result<()> {
226 if self.committed {
227 return Ok(()); // Already committed, nothing to rollback
228 }
229
230 self.session
231 .coordinator()
232 .process_query("ROLLBACK", self.session.id())
233 .map_err(|e| Error::Transaction(format!("Failed to rollback: {}", e)))?;
234
235 self.committed = true; // Mark as finished
236 Ok(())
237 }
238}
239
240impl<'conn> Drop for Transaction<'conn> {
241 fn drop(&mut self) {
242 if self.committed {
243 return; // Already committed or rolled back
244 }
245
246 match self.drop_behavior {
247 DropBehavior::Rollback => {
248 // Attempt to rollback, log error if it fails
249 if let Err(e) = self.rollback_internal() {
250 eprintln!("Warning: Failed to rollback transaction on drop: {}", e);
251 }
252 }
253 DropBehavior::Commit => {
254 // Attempt to commit, log error if it fails
255 if let Err(e) = self.commit_internal() {
256 eprintln!("Warning: Failed to commit transaction on drop: {}", e);
257 }
258 }
259 DropBehavior::Panic => {
260 if !std::thread::panicking() {
261 panic!("Transaction dropped without explicit commit or rollback");
262 }
263 }
264 DropBehavior::Ignore => {
265 // Do nothing
266 }
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_drop_behavior() {
277 assert_eq!(DropBehavior::Rollback, DropBehavior::Rollback);
278 assert_ne!(DropBehavior::Rollback, DropBehavior::Commit);
279 }
280}