Merge pull request #693 from thomcc/unchecked-transaction

Allow opting out of compile-time transaction checking
This commit is contained in:
Thom Chiovoloni 2020-04-10 10:06:37 -07:00 committed by GitHub
commit e04426176f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -92,10 +92,23 @@ pub struct Savepoint<'conn> {
impl Transaction<'_> {
/// Begin a new transaction. Cannot be nested; see `savepoint` for nested
/// transactions.
///
/// Even though we don't mutate the connection, we take a `&mut Connection`
/// so as to prevent nested or concurrent transactions on the same
/// connection.
/// so as to prevent nested transactions on the same connection. For cases
/// where this is unacceptable, [`Transaction::new_unchecked`] is available.
pub fn new(conn: &mut Connection, behavior: TransactionBehavior) -> Result<Transaction<'_>> {
Self::new_unchecked(conn, behavior)
}
/// Begin a new transaction, failing if a transaction is open.
///
/// If a transaction is already open, this will return an error. Where
/// possible, [`Transaction::new`] should be preferred, as it provides a
/// compile-time guarantee that transactions are not nested.
pub fn new_unchecked(
conn: &Connection,
behavior: TransactionBehavior,
) -> Result<Transaction<'_>> {
let query = match behavior {
TransactionBehavior::Deferred => "BEGIN DEFERRED",
TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
@ -369,6 +382,41 @@ impl Connection {
Transaction::new(self, behavior)
}
/// Begin a new transaction with the default behavior (DEFERRED).
///
/// Attempt to open a nested transaction will result in a SQLite error.
/// `Connection::transaction` prevents this at compile time by taking `&mut
/// self`, but `Connection::unchecked_transaction()` may be used to defer
/// the checking until runtime.
///
/// See [`Connection::transaction`] and [`Transaction::new_unchecked`]
/// (which can be used if the default transaction behavior is undesirable).
///
/// ## Example
///
/// ```rust,no_run
/// # use rusqlite::{Connection, Result};
/// # use std::rc::Rc;
/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
/// fn perform_queries(conn: Rc<Connection>) -> Result<()> {
/// let tx = conn.unchecked_transaction()?;
///
/// do_queries_part_1(&tx)?; // tx causes rollback if this fails
/// do_queries_part_2(&tx)?; // tx causes rollback if this fails
///
/// tx.commit()
/// }
/// ```
///
/// # Failure
///
/// Will return `Err` if the underlying SQLite call fails. The specific
/// error returned if transactions are nested is currently unspecified.
pub fn unchecked_transaction(&self) -> Result<Transaction<'_>> {
Transaction::new_unchecked(self, TransactionBehavior::Deferred)
}
/// Begin a new savepoint with the default behavior (DEFERRED).
///
/// The savepoint defaults to rolling back when it is dropped. If you want
@ -413,7 +461,7 @@ impl Connection {
#[cfg(test)]
mod test {
use super::DropBehavior;
use crate::{Connection, NO_PARAMS};
use crate::{Connection, Error, NO_PARAMS};
fn checked_memory_handle() -> Connection {
let db = Connection::open_in_memory().unwrap();
@ -443,6 +491,44 @@ mod test {
);
}
}
fn assert_nested_tx_error(e: crate::Error) {
if let Error::SqliteFailure(e, Some(m)) = &e {
assert_eq!(e.extended_code, crate::ffi::SQLITE_ERROR);
// FIXME: Not ideal...
assert_eq!(e.code, crate::ErrorCode::Unknown);
assert!(m.contains("transaction"));
} else {
panic!("Unexpected error type: {:?}", e);
}
}
#[test]
fn test_unchecked_nesting() {
let db = checked_memory_handle();
{
let tx = db.unchecked_transaction().unwrap();
let e = tx.unchecked_transaction().unwrap_err();
assert_nested_tx_error(e);
// default: rollback
}
{
let tx = db.unchecked_transaction().unwrap();
tx.execute_batch("INSERT INTO foo VALUES(1)").unwrap();
// Ensure this doesn't interfere with ongoing transaction
let e = tx.unchecked_transaction().unwrap_err();
assert_nested_tx_error(e);
tx.execute_batch("INSERT INTO foo VALUES(1)").unwrap();
tx.commit().unwrap();
}
assert_eq!(
2i32,
db.query_row::<i32, _, _>("SELECT SUM(x) FROM foo", NO_PARAMS, |r| r.get(0))
.unwrap()
);
}
#[test]
fn test_explicit_rollback_commit() {