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}