rusqlite/
transaction.rs

1use crate::{Connection, Result};
2use std::ops::Deref;
3
4/// Options for transaction behavior. See [BEGIN
5/// TRANSACTION](http://www.sqlite.org/lang_transaction.html) for details.
6#[derive(Copy, Clone)]
7#[non_exhaustive]
8pub enum TransactionBehavior {
9    /// DEFERRED means that the transaction does not actually start until the
10    /// database is first accessed.
11    Deferred,
12    /// IMMEDIATE cause the database connection to start a new write
13    /// immediately, without waiting for a writes statement.
14    Immediate,
15    /// EXCLUSIVE prevents other database connections from reading the database
16    /// while the transaction is underway.
17    Exclusive,
18}
19
20/// Options for how a Transaction or Savepoint should behave when it is dropped.
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum DropBehavior {
24    /// Roll back the changes. This is the default.
25    Rollback,
26
27    /// Commit the changes.
28    Commit,
29
30    /// Do not commit or roll back changes - this will leave the transaction or
31    /// savepoint open, so should be used with care.
32    Ignore,
33
34    /// Panic. Used to enforce intentional behavior during development.
35    Panic,
36}
37
38/// Represents a transaction on a database connection.
39///
40/// ## Note
41///
42/// Transactions will roll back by default. Use `commit` method to explicitly
43/// commit the transaction, or use `set_drop_behavior` to change what happens
44/// when the transaction is dropped.
45///
46/// ## Example
47///
48/// ```rust,no_run
49/// # use rusqlite::{Connection, Result};
50/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
51/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
52/// fn perform_queries(conn: &mut Connection) -> Result<()> {
53///     let tx = conn.transaction()?;
54///
55///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
56///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
57///
58///     tx.commit()
59/// }
60/// ```
61#[derive(Debug)]
62pub struct Transaction<'conn> {
63    conn: &'conn Connection,
64    drop_behavior: DropBehavior,
65}
66
67/// Represents a savepoint on a database connection.
68///
69/// ## Note
70///
71/// Savepoints will roll back by default. Use `commit` method to explicitly
72/// commit the savepoint, or use `set_drop_behavior` to change what happens
73/// when the savepoint is dropped.
74///
75/// ## Example
76///
77/// ```rust,no_run
78/// # use rusqlite::{Connection, Result};
79/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
80/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
81/// fn perform_queries(conn: &mut Connection) -> Result<()> {
82///     let sp = conn.savepoint()?;
83///
84///     do_queries_part_1(&sp)?; // sp causes rollback if this fails
85///     do_queries_part_2(&sp)?; // sp causes rollback if this fails
86///
87///     sp.commit()
88/// }
89/// ```
90pub struct Savepoint<'conn> {
91    conn: &'conn Connection,
92    name: String,
93    depth: u32,
94    drop_behavior: DropBehavior,
95    committed: bool,
96}
97
98impl Transaction<'_> {
99    /// Begin a new transaction. Cannot be nested; see `savepoint` for nested
100    /// transactions.
101    ///
102    /// Even though we don't mutate the connection, we take a `&mut Connection`
103    /// so as to prevent nested transactions on the same connection. For cases
104    /// where this is unacceptable, [`Transaction::new_unchecked`] is available.
105    #[inline]
106    pub fn new(conn: &mut Connection, behavior: TransactionBehavior) -> Result<Transaction<'_>> {
107        Self::new_unchecked(conn, behavior)
108    }
109
110    /// Begin a new transaction, failing if a transaction is open.
111    ///
112    /// If a transaction is already open, this will return an error. Where
113    /// possible, [`Transaction::new`] should be preferred, as it provides a
114    /// compile-time guarantee that transactions are not nested.
115    #[inline]
116    pub fn new_unchecked(
117        conn: &Connection,
118        behavior: TransactionBehavior,
119    ) -> Result<Transaction<'_>> {
120        let query = match behavior {
121            TransactionBehavior::Deferred => "BEGIN DEFERRED",
122            TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
123            TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
124        };
125        conn.execute_batch(query).map(move |_| Transaction {
126            conn,
127            drop_behavior: DropBehavior::Rollback,
128        })
129    }
130
131    /// Starts a new [savepoint](http://www.sqlite.org/lang_savepoint.html), allowing nested
132    /// transactions.
133    ///
134    /// ## Note
135    ///
136    /// Just like outer level transactions, savepoint transactions rollback by
137    /// default.
138    ///
139    /// ## Example
140    ///
141    /// ```rust,no_run
142    /// # use rusqlite::{Connection, Result};
143    /// # fn perform_queries_part_1_succeeds(_conn: &Connection) -> bool { true }
144    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
145    ///     let mut tx = conn.transaction()?;
146    ///
147    ///     {
148    ///         let sp = tx.savepoint()?;
149    ///         if perform_queries_part_1_succeeds(&sp) {
150    ///             sp.commit()?;
151    ///         }
152    ///         // otherwise, sp will rollback
153    ///     }
154    ///
155    ///     tx.commit()
156    /// }
157    /// ```
158    #[inline]
159    pub fn savepoint(&mut self) -> Result<Savepoint<'_>> {
160        Savepoint::with_depth(self.conn, 1)
161    }
162
163    /// Create a new savepoint with a custom savepoint name. See `savepoint()`.
164    #[inline]
165    pub fn savepoint_with_name<T: Into<String>>(&mut self, name: T) -> Result<Savepoint<'_>> {
166        Savepoint::with_depth_and_name(self.conn, 1, name)
167    }
168
169    /// Get the current setting for what happens to the transaction when it is
170    /// dropped.
171    #[inline]
172    pub fn drop_behavior(&self) -> DropBehavior {
173        self.drop_behavior
174    }
175
176    /// Configure the transaction to perform the specified action when it is
177    /// dropped.
178    #[inline]
179    pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
180        self.drop_behavior = drop_behavior
181    }
182
183    /// A convenience method which consumes and commits a transaction.
184    #[inline]
185    pub fn commit(mut self) -> Result<()> {
186        self.commit_()
187    }
188
189    #[inline]
190    fn commit_(&mut self) -> Result<()> {
191        self.conn.execute_batch("COMMIT")?;
192        Ok(())
193    }
194
195    /// A convenience method which consumes and rolls back a transaction.
196    #[inline]
197    pub fn rollback(mut self) -> Result<()> {
198        self.rollback_()
199    }
200
201    #[inline]
202    fn rollback_(&mut self) -> Result<()> {
203        self.conn.execute_batch("ROLLBACK")?;
204        Ok(())
205    }
206
207    /// Consumes the transaction, committing or rolling back according to the
208    /// current setting (see `drop_behavior`).
209    ///
210    /// Functionally equivalent to the `Drop` implementation, but allows
211    /// callers to see any errors that occur.
212    #[inline]
213    pub fn finish(mut self) -> Result<()> {
214        self.finish_()
215    }
216
217    #[inline]
218    fn finish_(&mut self) -> Result<()> {
219        if self.conn.is_autocommit() {
220            return Ok(());
221        }
222        match self.drop_behavior() {
223            DropBehavior::Commit => self.commit_().or_else(|_| self.rollback_()),
224            DropBehavior::Rollback => self.rollback_(),
225            DropBehavior::Ignore => Ok(()),
226            DropBehavior::Panic => panic!("Transaction dropped unexpectedly."),
227        }
228    }
229}
230
231impl Deref for Transaction<'_> {
232    type Target = Connection;
233
234    #[inline]
235    fn deref(&self) -> &Connection {
236        self.conn
237    }
238}
239
240#[allow(unused_must_use)]
241impl Drop for Transaction<'_> {
242    #[inline]
243    fn drop(&mut self) {
244        self.finish_();
245    }
246}
247
248impl Savepoint<'_> {
249    #[inline]
250    fn with_depth_and_name<T: Into<String>>(
251        conn: &Connection,
252        depth: u32,
253        name: T,
254    ) -> Result<Savepoint<'_>> {
255        let name = name.into();
256        conn.execute_batch(&format!("SAVEPOINT {}", name))
257            .map(|_| Savepoint {
258                conn,
259                name,
260                depth,
261                drop_behavior: DropBehavior::Rollback,
262                committed: false,
263            })
264    }
265
266    #[inline]
267    fn with_depth(conn: &Connection, depth: u32) -> Result<Savepoint<'_>> {
268        let name = format!("_rusqlite_sp_{}", depth);
269        Savepoint::with_depth_and_name(conn, depth, name)
270    }
271
272    /// Begin a new savepoint. Can be nested.
273    #[inline]
274    pub fn new(conn: &mut Connection) -> Result<Savepoint<'_>> {
275        Savepoint::with_depth(conn, 0)
276    }
277
278    /// Begin a new savepoint with a user-provided savepoint name.
279    #[inline]
280    pub fn with_name<T: Into<String>>(conn: &mut Connection, name: T) -> Result<Savepoint<'_>> {
281        Savepoint::with_depth_and_name(conn, 0, name)
282    }
283
284    /// Begin a nested savepoint.
285    #[inline]
286    pub fn savepoint(&mut self) -> Result<Savepoint<'_>> {
287        Savepoint::with_depth(self.conn, self.depth + 1)
288    }
289
290    /// Begin a nested savepoint with a user-provided savepoint name.
291    #[inline]
292    pub fn savepoint_with_name<T: Into<String>>(&mut self, name: T) -> Result<Savepoint<'_>> {
293        Savepoint::with_depth_and_name(self.conn, self.depth + 1, name)
294    }
295
296    /// Get the current setting for what happens to the savepoint when it is
297    /// dropped.
298    #[inline]
299    pub fn drop_behavior(&self) -> DropBehavior {
300        self.drop_behavior
301    }
302
303    /// Configure the savepoint to perform the specified action when it is
304    /// dropped.
305    #[inline]
306    pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
307        self.drop_behavior = drop_behavior
308    }
309
310    /// A convenience method which consumes and commits a savepoint.
311    #[inline]
312    pub fn commit(mut self) -> Result<()> {
313        self.commit_()
314    }
315
316    #[inline]
317    fn commit_(&mut self) -> Result<()> {
318        self.conn.execute_batch(&format!("RELEASE {}", self.name))?;
319        self.committed = true;
320        Ok(())
321    }
322
323    /// A convenience method which rolls back a savepoint.
324    ///
325    /// ## Note
326    ///
327    /// Unlike `Transaction`s, savepoints remain active after they have been
328    /// rolled back, and can be rolled back again or committed.
329    #[inline]
330    pub fn rollback(&mut self) -> Result<()> {
331        self.conn
332            .execute_batch(&format!("ROLLBACK TO {}", self.name))
333    }
334
335    /// Consumes the savepoint, committing or rolling back according to the
336    /// current setting (see `drop_behavior`).
337    ///
338    /// Functionally equivalent to the `Drop` implementation, but allows
339    /// callers to see any errors that occur.
340    #[inline]
341    pub fn finish(mut self) -> Result<()> {
342        self.finish_()
343    }
344
345    #[inline]
346    fn finish_(&mut self) -> Result<()> {
347        if self.committed {
348            return Ok(());
349        }
350        match self.drop_behavior() {
351            DropBehavior::Commit => self.commit_().or_else(|_| self.rollback()),
352            DropBehavior::Rollback => self.rollback(),
353            DropBehavior::Ignore => Ok(()),
354            DropBehavior::Panic => panic!("Savepoint dropped unexpectedly."),
355        }
356    }
357}
358
359impl Deref for Savepoint<'_> {
360    type Target = Connection;
361
362    #[inline]
363    fn deref(&self) -> &Connection {
364        self.conn
365    }
366}
367
368#[allow(unused_must_use)]
369impl Drop for Savepoint<'_> {
370    #[inline]
371    fn drop(&mut self) {
372        self.finish_();
373    }
374}
375
376impl Connection {
377    /// Begin a new transaction with the default behavior (DEFERRED).
378    ///
379    /// The transaction defaults to rolling back when it is dropped. If you
380    /// want the transaction to commit, you must call [`commit`](Transaction::commit) or
381    /// [`set_drop_behavior(DropBehavior::Commit)`](Transaction::set_drop_behavior).
382    ///
383    /// ## Example
384    ///
385    /// ```rust,no_run
386    /// # use rusqlite::{Connection, Result};
387    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
388    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
389    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
390    ///     let tx = conn.transaction()?;
391    ///
392    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
393    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
394    ///
395    ///     tx.commit()
396    /// }
397    /// ```
398    ///
399    /// # Failure
400    ///
401    /// Will return `Err` if the underlying SQLite call fails.
402    #[inline]
403    pub fn transaction(&mut self) -> Result<Transaction<'_>> {
404        Transaction::new(self, TransactionBehavior::Deferred)
405    }
406
407    /// Begin a new transaction with a specified behavior.
408    ///
409    /// See [`transaction`](Connection::transaction).
410    ///
411    /// # Failure
412    ///
413    /// Will return `Err` if the underlying SQLite call fails.
414    #[inline]
415    pub fn transaction_with_behavior(
416        &mut self,
417        behavior: TransactionBehavior,
418    ) -> Result<Transaction<'_>> {
419        Transaction::new(self, behavior)
420    }
421
422    /// Begin a new transaction with the default behavior (DEFERRED).
423    ///
424    /// Attempt to open a nested transaction will result in a SQLite error.
425    /// `Connection::transaction` prevents this at compile time by taking `&mut
426    /// self`, but `Connection::unchecked_transaction()` may be used to defer
427    /// the checking until runtime.
428    ///
429    /// See [`Connection::transaction`] and [`Transaction::new_unchecked`]
430    /// (which can be used if the default transaction behavior is undesirable).
431    ///
432    /// ## Example
433    ///
434    /// ```rust,no_run
435    /// # use rusqlite::{Connection, Result};
436    /// # use std::rc::Rc;
437    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
438    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
439    /// fn perform_queries(conn: Rc<Connection>) -> Result<()> {
440    ///     let tx = conn.unchecked_transaction()?;
441    ///
442    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
443    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
444    ///
445    ///     tx.commit()
446    /// }
447    /// ```
448    ///
449    /// # Failure
450    ///
451    /// Will return `Err` if the underlying SQLite call fails. The specific
452    /// error returned if transactions are nested is currently unspecified.
453    pub fn unchecked_transaction(&self) -> Result<Transaction<'_>> {
454        Transaction::new_unchecked(self, TransactionBehavior::Deferred)
455    }
456
457    /// Begin a new savepoint with the default behavior (DEFERRED).
458    ///
459    /// The savepoint defaults to rolling back when it is dropped. If you want
460    /// the savepoint to commit, you must call [`commit`](Savepoint::commit) or
461    /// [`set_drop_behavior(DropBehavior::Commit)`](Savepoint::set_drop_behavior).
462    ///
463    /// ## Example
464    ///
465    /// ```rust,no_run
466    /// # use rusqlite::{Connection, Result};
467    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
468    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
469    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
470    ///     let sp = conn.savepoint()?;
471    ///
472    ///     do_queries_part_1(&sp)?; // sp causes rollback if this fails
473    ///     do_queries_part_2(&sp)?; // sp causes rollback if this fails
474    ///
475    ///     sp.commit()
476    /// }
477    /// ```
478    ///
479    /// # Failure
480    ///
481    /// Will return `Err` if the underlying SQLite call fails.
482    #[inline]
483    pub fn savepoint(&mut self) -> Result<Savepoint<'_>> {
484        Savepoint::new(self)
485    }
486
487    /// Begin a new savepoint with a specified name.
488    ///
489    /// See [`savepoint`](Connection::savepoint).
490    ///
491    /// # Failure
492    ///
493    /// Will return `Err` if the underlying SQLite call fails.
494    #[inline]
495    pub fn savepoint_with_name<T: Into<String>>(&mut self, name: T) -> Result<Savepoint<'_>> {
496        Savepoint::with_name(self, name)
497    }
498}
499
500#[cfg(test)]
501mod test {
502    use super::DropBehavior;
503    use crate::{Connection, Error, Result};
504
505    fn checked_memory_handle() -> Result<Connection> {
506        let db = Connection::open_in_memory()?;
507        db.execute_batch("CREATE TABLE foo (x INTEGER)")?;
508        Ok(db)
509    }
510
511    #[test]
512    fn test_drop() -> Result<()> {
513        let mut db = checked_memory_handle()?;
514        {
515            let tx = db.transaction()?;
516            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
517            // default: rollback
518        }
519        {
520            let mut tx = db.transaction()?;
521            tx.execute_batch("INSERT INTO foo VALUES(2)")?;
522            tx.set_drop_behavior(DropBehavior::Commit)
523        }
524        {
525            let tx = db.transaction()?;
526            assert_eq!(
527                2i32,
528                tx.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", [], |r| r.get(0))?
529            );
530        }
531        Ok(())
532    }
533    fn assert_nested_tx_error(e: crate::Error) {
534        if let Error::SqliteFailure(e, Some(m)) = &e {
535            assert_eq!(e.extended_code, crate::ffi::SQLITE_ERROR);
536            // FIXME: Not ideal...
537            assert_eq!(e.code, crate::ErrorCode::Unknown);
538            assert!(m.contains("transaction"));
539        } else {
540            panic!("Unexpected error type: {:?}", e);
541        }
542    }
543
544    #[test]
545    fn test_unchecked_nesting() -> Result<()> {
546        let db = checked_memory_handle()?;
547
548        {
549            let tx = db.unchecked_transaction()?;
550            let e = tx.unchecked_transaction().unwrap_err();
551            assert_nested_tx_error(e);
552            // default: rollback
553        }
554        {
555            let tx = db.unchecked_transaction()?;
556            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
557            // Ensure this doesn't interfere with ongoing transaction
558            let e = tx.unchecked_transaction().unwrap_err();
559            assert_nested_tx_error(e);
560
561            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
562            tx.commit()?;
563        }
564
565        assert_eq!(
566            2i32,
567            db.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", [], |r| r.get(0))?
568        );
569        Ok(())
570    }
571
572    #[test]
573    fn test_explicit_rollback_commit() -> Result<()> {
574        let mut db = checked_memory_handle()?;
575        {
576            let mut tx = db.transaction()?;
577            {
578                let mut sp = tx.savepoint()?;
579                sp.execute_batch("INSERT INTO foo VALUES(1)")?;
580                sp.rollback()?;
581                sp.execute_batch("INSERT INTO foo VALUES(2)")?;
582                sp.commit()?;
583            }
584            tx.commit()?;
585        }
586        {
587            let tx = db.transaction()?;
588            tx.execute_batch("INSERT INTO foo VALUES(4)")?;
589            tx.commit()?;
590        }
591        {
592            let tx = db.transaction()?;
593            assert_eq!(
594                6i32,
595                tx.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", [], |r| r.get(0))?
596            );
597        }
598        Ok(())
599    }
600
601    #[test]
602    fn test_savepoint() -> Result<()> {
603        let mut db = checked_memory_handle()?;
604        {
605            let mut tx = db.transaction()?;
606            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
607            assert_current_sum(1, &tx)?;
608            tx.set_drop_behavior(DropBehavior::Commit);
609            {
610                let mut sp1 = tx.savepoint()?;
611                sp1.execute_batch("INSERT INTO foo VALUES(2)")?;
612                assert_current_sum(3, &sp1)?;
613                // will rollback sp1
614                {
615                    let mut sp2 = sp1.savepoint()?;
616                    sp2.execute_batch("INSERT INTO foo VALUES(4)")?;
617                    assert_current_sum(7, &sp2)?;
618                    // will rollback sp2
619                    {
620                        let sp3 = sp2.savepoint()?;
621                        sp3.execute_batch("INSERT INTO foo VALUES(8)")?;
622                        assert_current_sum(15, &sp3)?;
623                        sp3.commit()?;
624                        // committed sp3, but will be erased by sp2 rollback
625                    }
626                    assert_current_sum(15, &sp2)?;
627                }
628                assert_current_sum(3, &sp1)?;
629            }
630            assert_current_sum(1, &tx)?;
631        }
632        assert_current_sum(1, &db)?;
633        Ok(())
634    }
635
636    #[test]
637    fn test_ignore_drop_behavior() -> Result<()> {
638        let mut db = checked_memory_handle()?;
639
640        let mut tx = db.transaction()?;
641        {
642            let mut sp1 = tx.savepoint()?;
643            insert(1, &sp1)?;
644            sp1.rollback()?;
645            insert(2, &sp1)?;
646            {
647                let mut sp2 = sp1.savepoint()?;
648                sp2.set_drop_behavior(DropBehavior::Ignore);
649                insert(4, &sp2)?;
650            }
651            assert_current_sum(6, &sp1)?;
652            sp1.commit()?;
653        }
654        assert_current_sum(6, &tx)?;
655        Ok(())
656    }
657
658    #[test]
659    fn test_savepoint_names() -> Result<()> {
660        let mut db = checked_memory_handle()?;
661
662        {
663            let mut sp1 = db.savepoint_with_name("my_sp")?;
664            insert(1, &sp1)?;
665            assert_current_sum(1, &sp1)?;
666            {
667                let mut sp2 = sp1.savepoint_with_name("my_sp")?;
668                sp2.set_drop_behavior(DropBehavior::Commit);
669                insert(2, &sp2)?;
670                assert_current_sum(3, &sp2)?;
671                sp2.rollback()?;
672                assert_current_sum(1, &sp2)?;
673                insert(4, &sp2)?;
674            }
675            assert_current_sum(5, &sp1)?;
676            sp1.rollback()?;
677            {
678                let mut sp2 = sp1.savepoint_with_name("my_sp")?;
679                sp2.set_drop_behavior(DropBehavior::Ignore);
680                insert(8, &sp2)?;
681            }
682            assert_current_sum(8, &sp1)?;
683            sp1.commit()?;
684        }
685        assert_current_sum(8, &db)?;
686        Ok(())
687    }
688
689    #[test]
690    fn test_rc() -> Result<()> {
691        use std::rc::Rc;
692        let mut conn = Connection::open_in_memory()?;
693        let rc_txn = Rc::new(conn.transaction()?);
694
695        // This will compile only if Transaction is Debug
696        Rc::try_unwrap(rc_txn).unwrap();
697        Ok(())
698    }
699
700    fn insert(x: i32, conn: &Connection) -> Result<usize> {
701        conn.execute("INSERT INTO foo VALUES(?)", [x])
702    }
703
704    fn assert_current_sum(x: i32, conn: &Connection) -> Result<()> {
705        let i = conn.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", [], |r| r.get(0))?;
706        assert_eq!(x, i);
707        Ok(())
708    }
709}