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}