Skip to main content

rusqlite/
transaction.rs

1use crate::pragma::Sql;
2use crate::{Connection, Result};
3use std::ops::Deref;
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 or Savepoint 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
39/// Represents a transaction on a database connection.
40///
41/// ## Note
42///
43/// Transactions will roll back by default. Use `commit` method to explicitly
44/// commit the transaction, or use `set_drop_behavior` to change what happens
45/// when the transaction is dropped.
46///
47/// ## Example
48///
49/// ```rust,no_run
50/// # use rusqlite::{Connection, Result};
51/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
52/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
53/// fn perform_queries(conn: &mut Connection) -> Result<()> {
54///     let tx = conn.transaction()?;
55///
56///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
57///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
58///
59///     tx.commit()
60/// }
61/// ```
62#[derive(Debug)]
63pub struct Transaction<'conn> {
64    conn: &'conn Connection,
65    drop_behavior: DropBehavior,
66}
67
68/// Represents a savepoint on a database connection.
69///
70/// ## Note
71///
72/// Savepoints will roll back by default. Use `commit` method to explicitly
73/// commit the savepoint, or use `set_drop_behavior` to change what happens
74/// when the savepoint is dropped.
75///
76/// ## Example
77///
78/// ```rust,no_run
79/// # use rusqlite::{Connection, Result};
80/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
81/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
82/// fn perform_queries(conn: &mut Connection) -> Result<()> {
83///     let sp = conn.savepoint()?;
84///
85///     do_queries_part_1(&sp)?; // sp causes rollback if this fails
86///     do_queries_part_2(&sp)?; // sp causes rollback if this fails
87///
88///     sp.commit()
89/// }
90/// ```
91#[derive(Debug)]
92pub struct Savepoint<'conn> {
93    conn: &'conn Connection,
94    name: String,
95    drop_behavior: DropBehavior,
96    committed: bool,
97}
98
99impl Transaction<'_> {
100    /// Begin a new transaction. Cannot be nested; see `savepoint` for nested
101    /// transactions.
102    ///
103    /// Even though we don't mutate the connection, we take a `&mut Connection`
104    /// to prevent nested transactions on the same connection. For cases
105    /// where this is unacceptable, [`Transaction::new_unchecked`] is available.
106    #[inline]
107    pub fn new(conn: &mut Connection, behavior: TransactionBehavior) -> Result<Transaction<'_>> {
108        Self::new_unchecked(conn, behavior)
109    }
110
111    /// Begin a new transaction, failing if a transaction is open.
112    ///
113    /// If a transaction is already open, this will return an error. Where
114    /// possible, [`Transaction::new`] should be preferred, as it provides a
115    /// compile-time guarantee that transactions are not nested.
116    #[inline]
117    pub fn new_unchecked(
118        conn: &Connection,
119        behavior: TransactionBehavior,
120    ) -> Result<Transaction<'_>> {
121        let query = match behavior {
122            TransactionBehavior::Deferred => "BEGIN DEFERRED",
123            TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
124            TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
125        };
126        conn.execute_batch(query).map(move |()| Transaction {
127            conn,
128            drop_behavior: DropBehavior::Rollback,
129        })
130    }
131
132    /// Starts a new [savepoint](http://www.sqlite.org/lang_savepoint.html), allowing nested
133    /// transactions.
134    ///
135    /// ## Note
136    ///
137    /// Just like outer level transactions, savepoint transactions rollback by
138    /// default.
139    ///
140    /// ## Example
141    ///
142    /// ```rust,no_run
143    /// # use rusqlite::{Connection, Result};
144    /// # fn perform_queries_part_1_succeeds(_conn: &Connection) -> bool { true }
145    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
146    ///     let mut tx = conn.transaction()?;
147    ///
148    ///     {
149    ///         let sp = tx.savepoint()?;
150    ///         if perform_queries_part_1_succeeds(&sp) {
151    ///             sp.commit()?;
152    ///         }
153    ///         // otherwise, sp will rollback
154    ///     }
155    ///
156    ///     tx.commit()
157    /// }
158    /// ```
159    #[inline]
160    pub fn savepoint(&mut self) -> Result<Savepoint<'_>> {
161        Savepoint::new_(self.conn)
162    }
163
164    /// Create a new savepoint with a custom savepoint name. See `savepoint()`.
165    #[inline]
166    pub fn savepoint_with_name<T: Into<String>>(&mut self, name: T) -> Result<Savepoint<'_>> {
167        Savepoint::with_name_(self.conn, name)
168    }
169
170    /// Get the current setting for what happens to the transaction when it is
171    /// dropped.
172    #[inline]
173    #[must_use]
174    pub fn drop_behavior(&self) -> DropBehavior {
175        self.drop_behavior
176    }
177
178    /// Configure the transaction to perform the specified action when it is
179    /// dropped.
180    #[inline]
181    pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
182        self.drop_behavior = drop_behavior;
183    }
184
185    /// A convenience method which consumes and commits a transaction.
186    #[inline]
187    pub fn commit(mut self) -> Result<()> {
188        self.commit_()
189    }
190
191    #[inline]
192    fn commit_(&mut self) -> Result<()> {
193        self.conn.execute_batch("COMMIT")?;
194        Ok(())
195    }
196
197    /// A convenience method which consumes and rolls back a transaction.
198    #[inline]
199    pub fn rollback(mut self) -> Result<()> {
200        self.rollback_()
201    }
202
203    #[inline]
204    fn rollback_(&mut self) -> Result<()> {
205        self.conn.execute_batch("ROLLBACK")?;
206        Ok(())
207    }
208
209    /// Consumes the transaction, committing or rolling back according to the
210    /// current setting (see `drop_behavior`).
211    ///
212    /// Functionally equivalent to the `Drop` implementation, but allows
213    /// callers to see any errors that occur.
214    #[inline]
215    pub fn finish(mut self) -> Result<()> {
216        self.finish_()
217    }
218
219    #[inline]
220    fn finish_(&mut self) -> Result<()> {
221        if self.conn.is_autocommit() {
222            return Ok(());
223        }
224        match self.drop_behavior() {
225            DropBehavior::Commit => self.commit_().or_else(|_| self.rollback_()),
226            DropBehavior::Rollback => self.rollback_(),
227            DropBehavior::Ignore => Ok(()),
228            DropBehavior::Panic => panic!("Transaction dropped unexpectedly."),
229        }
230    }
231}
232
233impl Deref for Transaction<'_> {
234    type Target = Connection;
235
236    #[inline]
237    fn deref(&self) -> &Connection {
238        self.conn
239    }
240}
241
242#[expect(unused_must_use)]
243impl Drop for Transaction<'_> {
244    #[inline]
245    fn drop(&mut self) {
246        self.finish_();
247    }
248}
249
250impl Savepoint<'_> {
251    #[inline]
252    fn with_name_<T: Into<String>>(conn: &Connection, name: T) -> Result<Savepoint<'_>> {
253        let name = name.into();
254        let sql = cmd("SAVEPOINT", false, name.as_str())?;
255        conn.execute_batch(sql.as_str()).map(|()| Savepoint {
256            conn,
257            name,
258            drop_behavior: DropBehavior::Rollback,
259            committed: false,
260        })
261    }
262
263    #[inline]
264    fn new_(conn: &Connection) -> Result<Savepoint<'_>> {
265        Savepoint::with_name_(conn, "_rusqlite_sp")
266    }
267
268    /// Begin a new savepoint. Can be nested.
269    #[inline]
270    pub fn new(conn: &mut Connection) -> Result<Savepoint<'_>> {
271        Savepoint::new_(conn)
272    }
273
274    /// Begin a new savepoint with a user-provided savepoint name.
275    #[inline]
276    pub fn with_name<T: Into<String>>(conn: &mut Connection, name: T) -> Result<Savepoint<'_>> {
277        Savepoint::with_name_(conn, name)
278    }
279
280    /// Begin a nested savepoint.
281    #[inline]
282    pub fn savepoint(&mut self) -> Result<Savepoint<'_>> {
283        Savepoint::new_(self.conn)
284    }
285
286    /// Begin a nested savepoint with a user-provided savepoint name.
287    #[inline]
288    pub fn savepoint_with_name<T: Into<String>>(&mut self, name: T) -> Result<Savepoint<'_>> {
289        Savepoint::with_name_(self.conn, name)
290    }
291
292    /// Get the current setting for what happens to the savepoint when it is
293    /// dropped.
294    #[inline]
295    #[must_use]
296    pub fn drop_behavior(&self) -> DropBehavior {
297        self.drop_behavior
298    }
299
300    /// Configure the savepoint to perform the specified action when it is
301    /// dropped.
302    #[inline]
303    pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
304        self.drop_behavior = drop_behavior;
305    }
306
307    /// A convenience method which consumes and commits a savepoint.
308    #[inline]
309    pub fn commit(mut self) -> Result<()> {
310        self.commit_()
311    }
312
313    #[inline]
314    fn commit_(&mut self) -> Result<()> {
315        let sql = cmd("RELEASE", false, self.name.as_str())?;
316        self.conn.execute_batch(sql.as_str())?;
317        self.committed = true;
318        Ok(())
319    }
320
321    /// A convenience method which rolls back a savepoint.
322    ///
323    /// ## Note
324    ///
325    /// Unlike `Transaction`s, savepoints remain active after they have been
326    /// rolled back, and can be rolled back again or committed.
327    #[inline]
328    pub fn rollback(&mut self) -> Result<()> {
329        let sql = cmd("ROLLBACK", true, self.name.as_str())?;
330        self.conn.execute_batch(sql.as_str())
331    }
332
333    /// Consumes the savepoint, committing or rolling back according to the
334    /// current setting (see `drop_behavior`).
335    ///
336    /// Functionally equivalent to the `Drop` implementation, but allows
337    /// callers to see any errors that occur.
338    #[inline]
339    pub fn finish(mut self) -> Result<()> {
340        self.finish_()
341    }
342
343    #[inline]
344    fn finish_(&mut self) -> Result<()> {
345        if self.committed {
346            return Ok(());
347        }
348        match self.drop_behavior() {
349            DropBehavior::Commit => self
350                .commit_()
351                .or_else(|_| self.rollback().and_then(|()| self.commit_())),
352            DropBehavior::Rollback => self.rollback().and_then(|()| self.commit_()),
353            DropBehavior::Ignore => Ok(()),
354            DropBehavior::Panic => panic!("Savepoint dropped unexpectedly."),
355        }
356    }
357}
358
359fn cmd(cmd: &'static str, to: bool, name: &str) -> Result<Sql> {
360    let mut sql = Sql::new();
361    sql.push_keyword(cmd)?;
362    sql.push_space();
363    if to {
364        sql.push_keyword("TO")?;
365        sql.push_space();
366    }
367    sql.push_identifier(name);
368    Ok(sql)
369}
370
371impl Deref for Savepoint<'_> {
372    type Target = Connection;
373
374    #[inline]
375    fn deref(&self) -> &Connection {
376        self.conn
377    }
378}
379
380#[expect(unused_must_use)]
381impl Drop for Savepoint<'_> {
382    #[inline]
383    fn drop(&mut self) {
384        self.finish_();
385    }
386}
387
388/// Transaction state of a database
389#[derive(Clone, Copy, Debug, PartialEq, Eq)]
390#[non_exhaustive]
391#[cfg(feature = "modern_sqlite")] // 3.37.0
392pub enum TransactionState {
393    /// Equivalent to `SQLITE_TXN_NONE`
394    None,
395    /// Equivalent to `SQLITE_TXN_READ`
396    Read,
397    /// Equivalent to `SQLITE_TXN_WRITE`
398    Write,
399}
400
401impl Connection {
402    /// Begin a new transaction with the default behavior (DEFERRED).
403    ///
404    /// The transaction defaults to rolling back when it is dropped. If you
405    /// want the transaction to commit, you must call
406    /// [`commit`](Transaction::commit) or
407    /// [`set_drop_behavior(DropBehavior::Commit)`](Transaction::set_drop_behavior).
408    ///
409    /// ## Example
410    ///
411    /// ```rust,no_run
412    /// # use rusqlite::{Connection, Result};
413    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
414    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
415    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
416    ///     let tx = conn.transaction()?;
417    ///
418    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
419    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
420    ///
421    ///     tx.commit()
422    /// }
423    /// ```
424    ///
425    /// # Failure
426    ///
427    /// Will return `Err` if the underlying SQLite call fails.
428    #[inline]
429    pub fn transaction(&mut self) -> Result<Transaction<'_>> {
430        Transaction::new(self, self.transaction_behavior)
431    }
432
433    /// Begin a new transaction with a specified behavior.
434    ///
435    /// See [`transaction`](Connection::transaction).
436    ///
437    /// # Failure
438    ///
439    /// Will return `Err` if the underlying SQLite call fails.
440    #[inline]
441    pub fn transaction_with_behavior(
442        &mut self,
443        behavior: TransactionBehavior,
444    ) -> Result<Transaction<'_>> {
445        Transaction::new(self, behavior)
446    }
447
448    /// Begin a new transaction with the default behavior (DEFERRED).
449    ///
450    /// Attempt to open a nested transaction will result in a SQLite error.
451    /// `Connection::transaction` prevents this at compile time by taking `&mut
452    /// self`, but `Connection::unchecked_transaction()` may be used to defer
453    /// the checking until runtime.
454    ///
455    /// See [`Connection::transaction`] and [`Transaction::new_unchecked`]
456    /// (which can be used if the default transaction behavior is undesirable).
457    ///
458    /// ## Example
459    ///
460    /// ```rust,no_run
461    /// # use rusqlite::{Connection, Result};
462    /// # use std::rc::Rc;
463    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
464    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
465    /// fn perform_queries(conn: Rc<Connection>) -> Result<()> {
466    ///     let tx = conn.unchecked_transaction()?;
467    ///
468    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
469    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
470    ///
471    ///     tx.commit()
472    /// }
473    /// ```
474    ///
475    /// # Failure
476    ///
477    /// Will return `Err` if the underlying SQLite call fails. The specific
478    /// error returned if transactions are nested is currently unspecified.
479    pub fn unchecked_transaction(&self) -> Result<Transaction<'_>> {
480        Transaction::new_unchecked(self, self.transaction_behavior)
481    }
482
483    /// Begin a new savepoint with the default behavior (DEFERRED).
484    ///
485    /// The savepoint defaults to rolling back when it is dropped. If you want
486    /// the savepoint to commit, you must call [`commit`](Savepoint::commit) or
487    /// [`set_drop_behavior(DropBehavior::Commit)`](Savepoint::set_drop_behavior).
488    ///
489    /// ## Example
490    ///
491    /// ```rust,no_run
492    /// # use rusqlite::{Connection, Result};
493    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
494    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
495    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
496    ///     let sp = conn.savepoint()?;
497    ///
498    ///     do_queries_part_1(&sp)?; // sp causes rollback if this fails
499    ///     do_queries_part_2(&sp)?; // sp causes rollback if this fails
500    ///
501    ///     sp.commit()
502    /// }
503    /// ```
504    ///
505    /// # Failure
506    ///
507    /// Will return `Err` if the underlying SQLite call fails.
508    #[inline]
509    pub fn savepoint(&mut self) -> Result<Savepoint<'_>> {
510        Savepoint::new(self)
511    }
512
513    /// Begin a new savepoint with a specified name.
514    ///
515    /// See [`savepoint`](Connection::savepoint).
516    ///
517    /// # Failure
518    ///
519    /// Will return `Err` if the underlying SQLite call fails.
520    #[inline]
521    pub fn savepoint_with_name<T: Into<String>>(&mut self, name: T) -> Result<Savepoint<'_>> {
522        Savepoint::with_name(self, name)
523    }
524
525    /// Determine the transaction state of a database
526    #[cfg(feature = "modern_sqlite")] // 3.37.0
527    pub fn transaction_state<N: crate::Name>(
528        &self,
529        db_name: Option<N>,
530    ) -> Result<TransactionState> {
531        self.db.borrow().txn_state(db_name)
532    }
533
534    /// Set the default transaction behavior for the connection.
535    ///
536    /// ## Note
537    ///
538    /// This will only apply to transactions initiated by [`transaction`](Connection::transaction)
539    /// or [`unchecked_transaction`](Connection::unchecked_transaction).
540    ///
541    /// ## Example
542    ///
543    /// ```rust,no_run
544    /// # use rusqlite::{Connection, Result, TransactionBehavior};
545    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
546    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
547    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
548    ///     conn.set_transaction_behavior(TransactionBehavior::Immediate);
549    ///
550    ///     let tx = conn.transaction()?;
551    ///
552    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
553    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
554    ///
555    ///     tx.commit()
556    /// }
557    /// ```
558    pub fn set_transaction_behavior(&mut self, behavior: TransactionBehavior) {
559        self.transaction_behavior = behavior;
560    }
561}
562
563#[cfg(all(test, not(miri)))]
564mod test {
565    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
566    use wasm_bindgen_test::wasm_bindgen_test as test;
567
568    use super::DropBehavior;
569    use crate::{Connection, Error, Result};
570
571    fn checked_memory_handle() -> Result<Connection> {
572        let db = Connection::open_in_memory()?;
573        db.execute_batch("CREATE TABLE foo (x INTEGER)")?;
574        Ok(db)
575    }
576
577    #[test]
578    fn test_drop() -> Result<()> {
579        let mut db = checked_memory_handle()?;
580        {
581            let tx = db.transaction()?;
582            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
583            // default: rollback
584        }
585        {
586            let mut tx = db.transaction()?;
587            tx.execute_batch("INSERT INTO foo VALUES(2)")?;
588            tx.set_drop_behavior(DropBehavior::Commit);
589        }
590        {
591            let tx = db.transaction()?;
592            assert_eq!(2, tx.one_column::<i32, _>("SELECT SUM(x) FROM foo", [])?);
593        }
594        Ok(())
595    }
596    fn assert_nested_tx_error(e: Error) {
597        if let Error::SqliteFailure(e, Some(m)) = &e {
598            assert_eq!(e.extended_code, crate::ffi::SQLITE_ERROR);
599            // FIXME: Not ideal...
600            assert_eq!(e.code, crate::ErrorCode::Unknown);
601            assert!(m.contains("transaction"));
602        } else {
603            panic!("Unexpected error type: {e:?}");
604        }
605    }
606
607    #[test]
608    fn test_unchecked_nesting() -> Result<()> {
609        let db = checked_memory_handle()?;
610
611        {
612            let tx = db.unchecked_transaction()?;
613            let e = tx.unchecked_transaction().unwrap_err();
614            assert_nested_tx_error(e);
615            // default: rollback
616        }
617        {
618            let tx = db.unchecked_transaction()?;
619            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
620            // Ensure this doesn't interfere with ongoing transaction
621            let e = tx.unchecked_transaction().unwrap_err();
622            assert_nested_tx_error(e);
623
624            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
625            tx.commit()?;
626        }
627
628        assert_eq!(2, db.one_column::<i32, _>("SELECT SUM(x) FROM foo", [])?);
629        Ok(())
630    }
631
632    #[test]
633    fn test_explicit_rollback_commit() -> Result<()> {
634        let mut db = checked_memory_handle()?;
635        {
636            let mut tx = db.transaction()?;
637            {
638                let mut sp = tx.savepoint()?;
639                sp.execute_batch("INSERT INTO foo VALUES(1)")?;
640                sp.rollback()?;
641                sp.execute_batch("INSERT INTO foo VALUES(2)")?;
642                sp.commit()?;
643            }
644            tx.commit()?;
645        }
646        {
647            let tx = db.transaction()?;
648            tx.execute_batch("INSERT INTO foo VALUES(4)")?;
649            tx.commit()?;
650        }
651        {
652            let tx = db.transaction()?;
653            assert_eq!(6, tx.one_column::<i32, _>("SELECT SUM(x) FROM foo", [])?);
654        }
655        Ok(())
656    }
657
658    #[test]
659    fn test_savepoint() -> Result<()> {
660        let mut db = checked_memory_handle()?;
661        {
662            let mut tx = db.transaction()?;
663            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
664            assert_current_sum(1, &tx)?;
665            tx.set_drop_behavior(DropBehavior::Commit);
666            {
667                let mut sp1 = tx.savepoint()?;
668                sp1.execute_batch("INSERT INTO foo VALUES(2)")?;
669                assert_current_sum(3, &sp1)?;
670                // will roll back sp1
671                {
672                    let mut sp2 = sp1.savepoint()?;
673                    sp2.execute_batch("INSERT INTO foo VALUES(4)")?;
674                    assert_current_sum(7, &sp2)?;
675                    // will roll back sp2
676                    {
677                        let sp3 = sp2.savepoint()?;
678                        sp3.execute_batch("INSERT INTO foo VALUES(8)")?;
679                        assert_current_sum(15, &sp3)?;
680                        sp3.commit()?;
681                        // committed sp3, but will be erased by sp2 rollback
682                    }
683                    assert_current_sum(15, &sp2)?;
684                }
685                assert_current_sum(3, &sp1)?;
686            }
687            assert_current_sum(1, &tx)?;
688        }
689        assert_current_sum(1, &db)?;
690        Ok(())
691    }
692
693    #[test]
694    fn test_ignore_drop_behavior() -> Result<()> {
695        let mut db = checked_memory_handle()?;
696
697        let mut tx = db.transaction()?;
698        {
699            let mut sp1 = tx.savepoint()?;
700            insert(1, &sp1)?;
701            sp1.rollback()?;
702            insert(2, &sp1)?;
703            {
704                let mut sp2 = sp1.savepoint()?;
705                sp2.set_drop_behavior(DropBehavior::Ignore);
706                insert(4, &sp2)?;
707            }
708            assert_current_sum(6, &sp1)?;
709            sp1.commit()?;
710        }
711        assert_current_sum(6, &tx)?;
712        Ok(())
713    }
714
715    #[test]
716    fn test_savepoint_drop_behavior_releases() -> Result<()> {
717        let mut db = checked_memory_handle()?;
718
719        {
720            let mut sp = db.savepoint()?;
721            sp.set_drop_behavior(DropBehavior::Commit);
722        }
723        assert!(db.is_autocommit());
724        {
725            let mut sp = db.savepoint()?;
726            sp.set_drop_behavior(DropBehavior::Rollback);
727        }
728        assert!(db.is_autocommit());
729
730        Ok(())
731    }
732
733    #[test]
734    fn test_savepoint_release_error() -> Result<()> {
735        let mut db = checked_memory_handle()?;
736
737        db.pragma_update(None, "foreign_keys", true)?;
738        db.execute_batch("CREATE TABLE r(n INTEGER PRIMARY KEY NOT NULL); CREATE TABLE f(n REFERENCES r(n) DEFERRABLE INITIALLY DEFERRED);")?;
739        {
740            let mut sp = db.savepoint()?;
741            sp.execute("INSERT INTO f VALUES (0)", [])?;
742            sp.set_drop_behavior(DropBehavior::Commit);
743        }
744        assert!(db.is_autocommit());
745
746        Ok(())
747    }
748
749    #[test]
750    fn test_savepoint_names() -> Result<()> {
751        let mut db = checked_memory_handle()?;
752
753        {
754            let mut sp1 = db.savepoint_with_name("my_sp")?;
755            insert(1, &sp1)?;
756            assert_current_sum(1, &sp1)?;
757            {
758                let mut sp2 = sp1.savepoint_with_name("my_sp")?;
759                sp2.set_drop_behavior(DropBehavior::Commit);
760                insert(2, &sp2)?;
761                assert_current_sum(3, &sp2)?;
762                sp2.rollback()?;
763                assert_current_sum(1, &sp2)?;
764                insert(4, &sp2)?;
765            }
766            assert_current_sum(5, &sp1)?;
767            sp1.rollback()?;
768            {
769                let mut sp2 = sp1.savepoint_with_name("my_sp")?;
770                sp2.set_drop_behavior(DropBehavior::Ignore);
771                insert(8, &sp2)?;
772            }
773            assert_current_sum(8, &sp1)?;
774            sp1.commit()?;
775        }
776        assert_current_sum(8, &db)?;
777        Ok(())
778    }
779
780    #[test]
781    fn test_rc() -> Result<()> {
782        use std::rc::Rc;
783        let mut conn = Connection::open_in_memory()?;
784        let rc_txn = Rc::new(conn.transaction()?);
785
786        // This will compile only if Transaction is Debug
787        Rc::try_unwrap(rc_txn).unwrap();
788        Ok(())
789    }
790
791    fn insert(x: i32, conn: &Connection) -> Result<usize> {
792        conn.execute("INSERT INTO foo VALUES(?1)", [x])
793    }
794
795    fn assert_current_sum(x: i32, conn: &Connection) -> Result<()> {
796        assert_eq!(x, conn.one_column::<i32, _>("SELECT SUM(x) FROM foo", [])?);
797        Ok(())
798    }
799
800    #[test]
801    #[cfg(feature = "modern_sqlite")]
802    fn txn_state() -> Result<()> {
803        use super::TransactionState;
804        use crate::MAIN_DB;
805        let db = Connection::open_in_memory()?;
806        assert_eq!(TransactionState::None, db.transaction_state(Some(MAIN_DB))?);
807        assert_eq!(TransactionState::None, db.transaction_state::<&str>(None)?);
808        db.execute_batch("BEGIN")?;
809        assert_eq!(TransactionState::None, db.transaction_state::<&str>(None)?);
810        let _: i32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
811        assert_eq!(TransactionState::Read, db.transaction_state::<&str>(None)?);
812        db.pragma_update(None, "user_version", 1)?;
813        assert_eq!(TransactionState::Write, db.transaction_state::<&str>(None)?);
814        db.execute_batch("ROLLBACK")?;
815        Ok(())
816    }
817
818    #[test]
819    #[cfg(feature = "modern_sqlite")]
820    fn auto_commit() -> Result<()> {
821        use super::TransactionState;
822        let db = Connection::open_in_memory()?;
823        db.execute_batch("CREATE TABLE t(i UNIQUE);")?;
824        assert!(db.is_autocommit());
825        let mut stmt = db.prepare("SELECT name FROM sqlite_master")?;
826        assert_eq!(TransactionState::None, db.transaction_state::<&str>(None)?);
827        {
828            let mut rows = stmt.query([])?;
829            assert!(rows.next()?.is_some()); // start reading
830            assert_eq!(TransactionState::Read, db.transaction_state::<&str>(None)?);
831            db.execute("INSERT INTO t VALUES (1)", [])?; // auto-commit
832            assert_eq!(TransactionState::Read, db.transaction_state::<&str>(None)?);
833            assert!(rows.next()?.is_some()); // still reading
834            assert_eq!(TransactionState::Read, db.transaction_state::<&str>(None)?);
835            assert!(rows.next()?.is_none()); // end
836            assert_eq!(TransactionState::None, db.transaction_state::<&str>(None)?);
837        }
838        Ok(())
839    }
840}