Skip to main content

clt_database/turso/src/
transaction.rs

1use std::{ops::Deref, sync::atomic::Ordering};
2
3use crate::turso::{Connection, Result, Statement};
4
5/// Options for transaction behavior. See [BEGIN
6/// TRANSACTION](http://www.sqlite.org/lang_transaction.html) for details.
7#[derive(Copy, Clone)]
8#[non_exhaustive]
9pub enum TransactionBehavior {
10    /// DEFERRED means that the transaction does not actually start until the
11    /// database is first accessed.
12    Deferred,
13    /// IMMEDIATE cause the database connection to start a new write
14    /// immediately, without waiting for a writes statement.
15    Immediate,
16    /// EXCLUSIVE prevents other database connections from reading the database
17    /// while the transaction is underway.
18    Exclusive,
19}
20
21/// Options for how a Transaction should behave when it is dropped.
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum DropBehavior {
25    /// Roll back the changes. This is the default.
26    Rollback,
27
28    /// Commit the changes.
29    Commit,
30
31    /// Do not commit or roll back changes - this will leave the transaction or
32    /// savepoint open, so should be used with care.
33    Ignore,
34
35    /// Panic. Used to enforce intentional behavior during development.
36    Panic,
37}
38
39impl From<DropBehavior> for u8 {
40    fn from(behavior: DropBehavior) -> Self {
41        match behavior {
42            DropBehavior::Rollback => 0,
43            DropBehavior::Commit => 1,
44            DropBehavior::Ignore => 2,
45            DropBehavior::Panic => 3,
46        }
47    }
48}
49
50impl From<u8> for DropBehavior {
51    fn from(value: u8) -> Self {
52        match value {
53            0 => DropBehavior::Rollback,
54            1 => DropBehavior::Commit,
55            2 => DropBehavior::Ignore,
56            3 => DropBehavior::Panic,
57            _ => panic!("Invalid drop behavior: {value}"),
58        }
59    }
60}
61
62/// Represents a transaction on a database connection.
63///
64/// ## Note
65///
66/// Transactions will roll back by default. Use `commit` method to explicitly
67/// commit the transaction, or use `set_drop_behavior` to change what happens
68/// on the next access to the connection after the transaction is dropped.
69///
70/// ## Example
71///
72/// ```rust,no_run
73/// # use turso::{Connection, Result};
74/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
75/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
76/// async fn perform_queries(conn: &mut Connection) -> Result<()> {
77///     let tx = conn.transaction().await?;
78///
79///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
80///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
81///
82///     tx.commit().await
83/// }
84/// ```
85#[derive(Debug)]
86pub struct Transaction<'conn> {
87    conn: &'conn Connection,
88    drop_behavior: DropBehavior,
89    in_progress: bool,
90}
91
92impl Transaction<'_> {
93    /// Begin a new transaction. Cannot be nested;
94    ///
95    /// Even though we don't mutate the connection, we take a `&mut Connection`
96    /// to prevent nested transactions on the same connection. For cases
97    /// where this is unacceptable, [`Transaction::new_unchecked`] is available.
98    #[inline]
99    pub async fn new(
100        conn: &mut Connection,
101        behavior: TransactionBehavior,
102    ) -> Result<Transaction<'_>> {
103        Self::new_unchecked(conn, behavior).await
104    }
105
106    /// Begin a new transaction, failing if a transaction is open.
107    ///
108    /// If a transaction is already open, this will return an error. Where
109    /// possible, [`Transaction::new`] should be preferred, as it provides a
110    /// compile-time guarantee that transactions are not nested.
111    #[inline]
112    pub async fn new_unchecked(
113        conn: &Connection,
114        behavior: TransactionBehavior,
115    ) -> Result<Transaction<'_>> {
116        let query = match behavior {
117            TransactionBehavior::Deferred => "BEGIN DEFERRED",
118            TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
119            TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
120        };
121        // TODO: Use execute_batch instead
122        conn.execute(query, ()).await.map(move |_| Transaction {
123            conn,
124            drop_behavior: DropBehavior::Rollback,
125            in_progress: true,
126        })
127    }
128
129    // Use the Connection to Prepare a statement.
130    // This allows a database update function to be passed a transaction,
131    // prepare a statement, and use it without needing direct access to the
132    // Connection
133    pub async fn prepare(&self, sql: &str) -> Result<Statement> {
134        self.conn.prepare(sql).await
135    }
136
137    /// Get the current setting for what happens to the transaction when it is
138    /// dropped.
139    #[inline]
140    #[must_use]
141    pub fn drop_behavior(&self) -> DropBehavior {
142        self.drop_behavior
143    }
144
145    /// Configure the transaction to perform the specified action when it is
146    /// dropped.
147    #[inline]
148    pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
149        self.drop_behavior = drop_behavior;
150    }
151
152    /// A convenience method which consumes and commits a transaction.
153    #[inline]
154    pub async fn commit(mut self) -> Result<()> {
155        self._commit().await
156    }
157
158    #[inline]
159    async fn _commit(&mut self) -> Result<()> {
160        self.conn.execute("COMMIT", ()).await?;
161        self.in_progress = false;
162        Ok(())
163    }
164
165    /// A convenience method which consumes and rolls back a transaction.
166    #[inline]
167    pub async fn rollback(mut self) -> Result<()> {
168        self._rollback().await
169    }
170
171    #[inline]
172    async fn _rollback(&mut self) -> Result<()> {
173        self.conn.execute("ROLLBACK", ()).await?;
174        self.in_progress = false;
175        Ok(())
176    }
177
178    /// Consumes the transaction, committing or rolling back according to the
179    /// current setting (see `drop_behavior`).
180    ///
181    /// Functionally equivalent to the `Drop` implementation, but allows
182    /// callers to see any errors that occur.
183    #[inline]
184    pub async fn finish(mut self) -> Result<()> {
185        self._finish().await
186    }
187
188    #[inline]
189    async fn _finish(&mut self) -> Result<()> {
190        if self.conn.is_autocommit()? {
191            return Ok(());
192        }
193        match self.drop_behavior() {
194            DropBehavior::Commit => {
195                if (self._commit().await).is_err() {
196                    self._rollback().await
197                } else {
198                    Ok(())
199                }
200            }
201            DropBehavior::Rollback => self._rollback().await,
202            DropBehavior::Ignore => Ok(()),
203            DropBehavior::Panic => panic!("Transaction dropped unexpectedly."),
204        }
205    }
206}
207
208impl Deref for Transaction<'_> {
209    type Target = Connection;
210
211    #[inline]
212    fn deref(&self) -> &Connection {
213        self.conn
214    }
215}
216
217impl Drop for Transaction<'_> {
218    #[inline]
219    fn drop(&mut self) {
220        if self.in_progress {
221            self.conn
222                .dangling_tx
223                .store(self.drop_behavior(), Ordering::SeqCst);
224        } else {
225            self.conn
226                .dangling_tx
227                .store(DropBehavior::Ignore, Ordering::SeqCst);
228        }
229    }
230}
231
232impl Connection {
233    /// Begin a new transaction with the default behavior (DEFERRED).
234    ///
235    /// The transaction defaults to rolling back on the next access to the connection
236    /// if it is not finished when the transaction is dropped. If you
237    /// want the transaction to commit, you must call
238    /// [`commit`](Transaction::commit) or
239    /// [`set_drop_behavior(DropBehavior::Commit)`](Transaction::set_drop_behavior).
240    ///
241    /// ## Example
242    ///
243    /// ```rust,no_run
244    /// # use turso::{Connection, Result};
245    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
246    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
247    /// async fn perform_queries(conn: &mut Connection) -> Result<()> {
248    ///     let tx = conn.transaction().await?;
249    ///
250    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
251    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
252    ///
253    ///     tx.commit().await
254    /// }
255    /// ```
256    ///
257    /// # Failure
258    ///
259    /// Will return `Err` if the call fails.
260    #[inline]
261    pub async fn transaction(&mut self) -> Result<Transaction<'_>> {
262        self.transaction_with_behavior(self.transaction_behavior)
263            .await
264    }
265
266    /// Begin a new transaction with a specified behavior.
267    ///
268    /// See [`transaction`](Connection::transaction).
269    ///
270    /// # Failure
271    ///
272    /// Will return `Err` if the call fails.
273    #[inline]
274    pub async fn transaction_with_behavior(
275        &mut self,
276        behavior: TransactionBehavior,
277    ) -> Result<Transaction<'_>> {
278        self.maybe_handle_dangling_tx().await?;
279        Transaction::new(self, behavior).await
280    }
281
282    /// Begin a new transaction with the default behavior (DEFERRED).
283    ///
284    /// Attempt to open a nested transaction will result in a SQLite error.
285    /// `Connection::transaction` prevents this at compile time by taking `&mut
286    /// self`, but `Connection::unchecked_transaction()` may be used to defer
287    /// the checking until runtime.
288    ///
289    /// See [`Connection::transaction`] and [`Transaction::new_unchecked`]
290    /// (which can be used if the default transaction behavior is undesirable).
291    ///
292    /// ## Example
293    ///
294    /// ```rust,no_run
295    /// # use turso::{Connection, Result};
296    /// # use std::rc::Rc;
297    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
298    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
299    /// async fn perform_queries(conn: Rc<Connection>) -> Result<()> {
300    ///     let tx = conn.unchecked_transaction().await?;
301    ///
302    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
303    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
304    ///
305    ///     tx.commit().await
306    /// }
307    /// ```
308    ///
309    /// # Failure
310    ///
311    /// Will return `Err` if the underlying SQLite call fails. The specific
312    /// error returned if transactions are nested is currently unspecified.
313    pub async fn unchecked_transaction(&self) -> Result<Transaction<'_>> {
314        Transaction::new_unchecked(self, self.transaction_behavior).await
315    }
316
317    /// Set the default transaction behavior for the connection.
318    ///
319    /// ## Note
320    ///
321    /// This will only apply to transactions initiated by [`transaction`](Connection::transaction)
322    /// or [`unchecked_transaction`](Connection::unchecked_transaction).
323    ///
324    /// ## Example
325    ///
326    /// ```rust,no_run
327    /// # use turso::{Connection, Result};
328    /// # use turso::transaction::TransactionBehavior;
329    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
330    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
331    /// async fn perform_queries(conn: &mut Connection) -> Result<()> {
332    ///     conn.set_transaction_behavior(TransactionBehavior::Immediate);
333    ///
334    ///     let tx = conn.transaction().await?;
335    ///
336    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
337    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
338    ///
339    ///     tx.commit().await
340    /// }
341    /// ```
342    pub fn set_transaction_behavior(&mut self, behavior: TransactionBehavior) {
343        self.transaction_behavior = behavior;
344    }
345}
346
347#[cfg(clt_turso_tests)]
348mod test {
349    use crate::turso::{Builder, Connection, Error, Result};
350
351    use super::DropBehavior;
352
353    async fn checked_memory_handle() -> Result<Connection> {
354        let db = Builder::new_local(":memory:").build().await?;
355        let conn = db.connect()?;
356        conn.execute("CREATE TABLE foo (x INTEGER)", ()).await?;
357        Ok(conn)
358    }
359
360    #[tokio::test]
361    async fn test_drop_rollback_on_new_transaction() {
362        let mut conn = checked_memory_handle().await.unwrap();
363        {
364            let tx = conn.transaction().await.unwrap();
365            tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap();
366            // Drop without finish - should be rolled back when next transaction starts
367        }
368
369        // Start a new transaction - this should rollback the dangling one
370        let tx = conn.transaction().await.unwrap();
371        tx.execute("INSERT INTO foo VALUES(?)", &[2]).await.unwrap();
372        let result = tx
373            .prepare("SELECT SUM(x) FROM foo")
374            .await
375            .unwrap()
376            .query_row(())
377            .await
378            .unwrap();
379
380        // The insert from the dropped transaction should have been rolled back
381        assert_eq!(2, result.get::<i32>(0).unwrap());
382        tx.finish().await.unwrap();
383    }
384
385    #[tokio::test]
386    async fn test_drop_rollback_on_query() {
387        let mut conn = checked_memory_handle().await.unwrap();
388        {
389            let tx = conn.transaction().await.unwrap();
390            tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap();
391            // Drop without finish - should be rolled back when conn.query is called
392        }
393
394        // Using conn.query should rollback the dangling transaction
395        let mut rows = conn.query("SELECT count(*) FROM foo", ()).await.unwrap();
396        let result = rows.next().await.unwrap().unwrap();
397
398        // The insert from the dropped transaction should have been rolled back
399        assert_eq!(0, result.get::<i32>(0).unwrap());
400    }
401
402    #[tokio::test]
403    async fn test_drop_rollback_on_execute() {
404        let mut conn = checked_memory_handle().await.unwrap();
405        {
406            let tx = conn.transaction().await.unwrap();
407            tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap();
408            // Drop without finish - should be rolled back when conn.execute is called
409        }
410
411        // Using conn.execute should rollback the dangling transaction
412        conn.execute("INSERT INTO foo VALUES(?)", &[2])
413            .await
414            .unwrap();
415
416        let mut rows = conn.query("SELECT count(*) FROM foo", ()).await.unwrap();
417        let result = rows.next().await.unwrap().unwrap();
418
419        // The insert from the dropped transaction should have been rolled back
420        assert_eq!(1, result.get::<i32>(0).unwrap());
421    }
422
423    #[tokio::test]
424    async fn test_drop() -> Result<()> {
425        let _ = tracing_subscriber::fmt::try_init();
426        let mut conn = checked_memory_handle().await?;
427        {
428            let tx = conn.transaction().await?;
429            tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
430            // default: rollback
431        }
432        {
433            let mut tx = conn.transaction().await?;
434            tx.execute("INSERT INTO foo VALUES(?)", &[2]).await?;
435            tx.set_drop_behavior(DropBehavior::Commit);
436        }
437        {
438            let tx = conn.transaction().await?;
439            let result = tx
440                .prepare("SELECT SUM(x) FROM foo")
441                .await?
442                .query_row(())
443                .await?;
444
445            assert_eq!(2, result.get::<i32>(0)?);
446        }
447        Ok(())
448    }
449
450    fn assert_nested_tx_error(e: Error) {
451        if let Error::Error(e) = &e {
452            assert!(e.contains("transaction"));
453        } else {
454            panic!("Unexpected error type: {e:?}");
455        }
456    }
457
458    #[tokio::test]
459    async fn test_unchecked_nesting() -> Result<()> {
460        let conn = checked_memory_handle().await?;
461
462        {
463            let tx = conn.unchecked_transaction().await?;
464            let e = tx.unchecked_transaction().await.unwrap_err();
465            assert_nested_tx_error(e);
466            tx.finish().await?;
467            // default: rollback
468        }
469        {
470            let tx = conn.unchecked_transaction().await?;
471            tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
472            // Ensure this doesn't interfere with ongoing transaction
473            let e = tx.unchecked_transaction().await.unwrap_err();
474            assert_nested_tx_error(e);
475
476            tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
477            tx.commit().await?;
478        }
479
480        let result = conn
481            .prepare("SELECT SUM(x) FROM foo")
482            .await?
483            .query_row(())
484            .await?;
485        assert_eq!(2, result.get::<i32>(0)?);
486        Ok(())
487    }
488
489    #[tokio::test]
490    async fn test_explicit_rollback_commit() -> Result<()> {
491        let mut conn = checked_memory_handle().await?;
492        {
493            let tx = conn.transaction().await?;
494            tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
495            tx.rollback().await?;
496
497            // This is a current Turso's limitation.
498            // Since we don't have support for savepoints yet,
499            // a rollback ends with a transaction so we need to immediately open a new one.
500            let tx = conn.transaction().await?;
501            tx.execute("INSERT INTO foo VALUES(?)", &[2]).await?;
502            tx.commit().await?;
503        }
504        {
505            let tx = conn.transaction().await?;
506            tx.execute("INSERT INTO foo VALUES(?)", &[4]).await?;
507            tx.commit().await?;
508        }
509        {
510            let result = conn
511                .prepare("SELECT SUM(x) FROM foo")
512                .await?
513                .query_row(())
514                .await?;
515            assert_eq!(6, result.get::<i32>(0)?);
516        }
517        Ok(())
518    }
519}