Skip to main content

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}