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}