Skip to main content

duckdb/
transaction.rs

1use crate::{Connection, Result};
2use std::ops::Deref;
3
4/// Options for how a Transaction should behave when it is dropped.
5#[derive(Copy, Clone, Debug, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum DropBehavior {
8    /// Roll back the changes. This is the default.
9    Rollback,
10
11    /// Commit the changes.
12    Commit,
13
14    /// Do not commit or roll back changes - this will leave the transaction open, so should be used with care.
15    Ignore,
16
17    /// Panic. Used to enforce intentional behavior during development.
18    Panic,
19}
20
21/// Represents a transaction on a database connection.
22///
23/// ## Note
24///
25/// Transactions will roll back by default. Use `commit` method to explicitly
26/// commit the transaction, or use `set_drop_behavior` to change what happens
27/// when the transaction is dropped.
28///
29/// ## Example
30///
31/// ```rust,no_run
32/// # use duckdb::{Connection, Result};
33/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
34/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
35/// fn perform_queries(conn: &mut Connection) -> Result<()> {
36///     let tx = conn.transaction()?;
37///
38///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
39///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
40///
41///     tx.commit()
42/// }
43/// ```
44#[derive(Debug)]
45pub struct Transaction<'conn> {
46    conn: &'conn Connection,
47    drop_behavior: DropBehavior,
48}
49
50impl Transaction<'_> {
51    /// Begin a new transaction. Cannot be nested.
52    ///
53    /// Even though we don't mutate the connection, we take a `&mut Connection`
54    /// so as to prevent nested transactions on the same connection. For cases
55    /// where this is unacceptable, [`Transaction::new_unchecked`] is available.
56    #[inline]
57    pub fn new(conn: &mut Connection) -> Result<Transaction<'_>> {
58        Self::new_unchecked(conn)
59    }
60
61    /// Begin a new transaction, failing if a transaction is open.
62    ///
63    /// If a transaction is already open, this will return an error. Where
64    /// possible, [`Transaction::new`] should be preferred, as it provides a
65    /// compile-time guarantee that transactions are not nested.
66    #[inline]
67    pub fn new_unchecked(conn: &Connection) -> Result<Transaction<'_>> {
68        let query = "BEGIN TRANSACTION";
69        conn.execute_batch(query).map(move |_| Transaction {
70            conn,
71            drop_behavior: DropBehavior::Rollback,
72        })
73    }
74
75    /// Get the current setting for what happens to the transaction when it is
76    /// dropped.
77    #[inline]
78    pub fn drop_behavior(&self) -> DropBehavior {
79        self.drop_behavior
80    }
81
82    /// Configure the transaction to perform the specified action when it is
83    /// dropped.
84    #[inline]
85    pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
86        self.drop_behavior = drop_behavior
87    }
88
89    /// A convenience method which consumes and commits a transaction.
90    #[inline]
91    pub fn commit(mut self) -> Result<()> {
92        self.commit_()
93    }
94
95    #[inline]
96    fn commit_(&mut self) -> Result<()> {
97        self.conn.execute_batch("COMMIT")
98    }
99
100    /// A convenience method which consumes and rolls back a transaction.
101    #[inline]
102    pub fn rollback(mut self) -> Result<()> {
103        self.rollback_()
104    }
105
106    #[inline]
107    fn rollback_(&mut self) -> Result<()> {
108        self.conn.execute_batch("ROLLBACK")
109    }
110
111    /// Consumes the transaction, committing or rolling back according to the
112    /// current setting (see `drop_behavior`).
113    ///
114    /// Functionally equivalent to the `Drop` implementation, but allows
115    /// callers to see any errors that occur.
116    #[inline]
117    pub fn finish(mut self) -> Result<()> {
118        self.finish_()
119    }
120
121    #[inline]
122    fn finish_(&mut self) -> Result<()> {
123        // if self.conn.is_autocommit() {
124        //     println!("is autocommit");
125        //     return Ok(());
126        // }
127        match self.drop_behavior() {
128            DropBehavior::Commit => self.commit_().or_else(|_| self.rollback_()),
129            DropBehavior::Rollback => self.rollback_(),
130            DropBehavior::Ignore => Ok(()),
131            DropBehavior::Panic => panic!("Transaction dropped unexpectedly."),
132        }
133    }
134}
135
136impl Deref for Transaction<'_> {
137    type Target = Connection;
138
139    #[inline]
140    fn deref(&self) -> &Connection {
141        self.conn
142    }
143}
144
145impl Drop for Transaction<'_> {
146    #[inline]
147    fn drop(&mut self) {
148        let _ = self.finish_();
149    }
150}
151
152impl Connection {
153    /// Begin a new transaction with the default behavior (DEFERRED).
154    ///
155    /// The transaction defaults to rolling back when it is dropped. If you
156    /// want the transaction to commit, you must call
157    /// [`commit`](Transaction::commit) or [`set_drop_behavior(DropBehavior:
158    /// :Commit)`](Transaction::set_drop_behavior).
159    ///
160    /// ## Example
161    ///
162    /// ```rust,no_run
163    /// # use duckdb::{Connection, Result};
164    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
165    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
166    /// fn perform_queries(conn: &mut Connection) -> Result<()> {
167    ///     let tx = conn.transaction()?;
168    ///
169    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
170    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
171    ///
172    ///     tx.commit()
173    /// }
174    /// ```
175    ///
176    /// # Failure
177    ///
178    /// Will return `Err` if the underlying DuckDB call fails.
179    #[inline]
180    pub fn transaction(&mut self) -> Result<Transaction<'_>> {
181        Transaction::new(self)
182    }
183
184    /// Begin a new transaction with the default behavior (DEFERRED).
185    ///
186    /// Attempt to open a nested transaction will result in a DuckDB error.
187    /// `Connection::transaction` prevents this at compile time by taking `&mut
188    /// self`, but `Connection::unchecked_transaction()` may be used to defer
189    /// the checking until runtime.
190    ///
191    /// See [`Connection::transaction`] and [`Transaction::new_unchecked`]
192    /// (which can be used if the default transaction behavior is undesirable).
193    ///
194    /// ## Example
195    ///
196    /// ```rust,no_run
197    /// # use duckdb::{Connection, Result};
198    /// # use std::rc::Rc;
199    /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
200    /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
201    /// fn perform_queries(conn: Rc<Connection>) -> Result<()> {
202    ///     let tx = conn.unchecked_transaction()?;
203    ///
204    ///     do_queries_part_1(&tx)?; // tx causes rollback if this fails
205    ///     do_queries_part_2(&tx)?; // tx causes rollback if this fails
206    ///
207    ///     tx.commit()
208    /// }
209    /// ```
210    ///
211    /// # Failure
212    ///
213    /// Will return `Err` if the underlying DuckDB call fails. The specific
214    /// error returned if transactions are nested is currently unspecified.
215    pub fn unchecked_transaction(&self) -> Result<Transaction<'_>> {
216        Transaction::new_unchecked(self)
217    }
218}
219
220#[cfg(test)]
221mod test {
222    use super::DropBehavior;
223    use crate::{Connection, Result};
224
225    fn checked_no_autocommit_memory_handle() -> Result<Connection> {
226        let db = Connection::open_in_memory()?;
227        db.execute_batch(
228            r"
229            CREATE TABLE foo (x INTEGER);
230            -- SET AUTOCOMMIT TO OFF;
231        ",
232        )?;
233        Ok(db)
234    }
235
236    #[test]
237    fn test_drop() -> Result<()> {
238        let mut db = checked_no_autocommit_memory_handle()?;
239        {
240            let tx = db.transaction()?;
241            assert!(tx.execute_batch("INSERT INTO foo VALUES(1)").is_ok());
242            // default: rollback
243        }
244        {
245            let mut tx = db.transaction()?;
246            tx.execute_batch("INSERT INTO foo VALUES(2)")?;
247            tx.set_drop_behavior(DropBehavior::Commit);
248        }
249        {
250            let tx = db.transaction()?;
251            assert_eq!(
252                2i32,
253                tx.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", [], |r| r.get(0))?
254            );
255        }
256        Ok(())
257    }
258
259    #[test]
260    fn test_unchecked_nesting() -> Result<()> {
261        let db = checked_no_autocommit_memory_handle()?;
262
263        {
264            db.unchecked_transaction()?;
265            // default: rollback
266        }
267        {
268            let tx = db.unchecked_transaction()?;
269            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
270            // Ensure this doesn't interfere with ongoing transaction
271            // let e = tx.unchecked_transaction().unwrap_err();
272            // assert_nested_tx_error(e);
273            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
274            tx.commit()?;
275        }
276
277        assert_eq!(
278            2i32,
279            db.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", [], |r| r.get(0))?
280        );
281        Ok(())
282    }
283
284    #[test]
285    fn test_explicit_rollback_commit() -> Result<()> {
286        let mut db = checked_no_autocommit_memory_handle()?;
287        {
288            let tx = db.transaction()?;
289            tx.execute_batch("INSERT INTO foo VALUES(1)")?;
290            tx.rollback()?;
291        }
292        {
293            let tx = db.transaction()?;
294            tx.execute_batch("INSERT INTO foo VALUES(4)")?;
295            tx.commit()?;
296        }
297        {
298            let tx = db.transaction()?;
299            assert_eq!(
300                4i32,
301                tx.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", [], |r| r.get(0))?
302            );
303        }
304        Ok(())
305    }
306
307    #[test]
308    fn test_rc() -> Result<()> {
309        use std::rc::Rc;
310        let mut conn = Connection::open_in_memory()?;
311        let rc_txn = Rc::new(conn.transaction()?);
312
313        // This will compile only if Transaction is Debug
314        Rc::try_unwrap(rc_txn).unwrap();
315        Ok(())
316    }
317}