Skip to main content

walletkit_db/sqlite/
transaction.rs

1//! Safe wrapper around a `SQLite` transaction.
2//!
3//! Automatically rolls back on drop unless explicitly committed.
4
5use super::connection::Connection;
6use super::error::DbResult;
7use super::statement::{Row, Statement};
8use super::value::Value;
9
10/// An open database transaction.
11///
12/// Created via [`Connection::transaction`] or [`Connection::transaction_immediate`].
13/// If the `Transaction` is dropped without calling [`commit`](Self::commit),
14/// the transaction is rolled back automatically.
15pub struct Transaction<'conn> {
16    conn: &'conn Connection,
17    committed: bool,
18}
19
20impl<'conn> Transaction<'conn> {
21    /// Begins a new transaction on `conn`.
22    ///
23    /// When `immediate` is true, the transaction acquires a RESERVED lock
24    /// immediately (`BEGIN IMMEDIATE`) rather than deferring it.
25    pub(super) fn begin(conn: &'conn Connection, immediate: bool) -> DbResult<Self> {
26        let sql = if immediate {
27            "BEGIN IMMEDIATE"
28        } else {
29            "BEGIN DEFERRED"
30        };
31        conn.execute_batch(sql)?;
32        Ok(Self {
33            conn,
34            committed: false,
35        })
36    }
37
38    /// Commits the transaction.
39    ///
40    /// # Errors
41    ///
42    /// Returns `Error` if the COMMIT statement fails.
43    pub fn commit(mut self) -> DbResult<()> {
44        self.conn.execute_batch("COMMIT")?;
45        self.committed = true;
46        Ok(())
47    }
48
49    // -- Delegated Connection methods -----------------------------------------
50
51    /// See [`Connection::execute_batch`].
52    ///
53    /// # Errors
54    ///
55    /// Returns `Error` if any statement fails.
56    #[allow(dead_code)]
57    pub fn execute_batch(&self, sql: &str) -> DbResult<()> {
58        self.conn.execute_batch(sql)
59    }
60
61    /// See [`Connection::execute`].
62    ///
63    /// # Errors
64    ///
65    /// Returns `Error` if preparation or execution fails.
66    pub fn execute(&self, sql: &str, params: &[Value]) -> DbResult<usize> {
67        self.conn.execute(sql, params)
68    }
69
70    /// See [`Connection::query_row`].
71    ///
72    /// # Errors
73    ///
74    /// Returns `Error` if preparation, execution, or the mapper fails.
75    pub fn query_row<T>(
76        &self,
77        sql: &str,
78        params: &[Value],
79        mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
80    ) -> DbResult<T> {
81        self.conn.query_row(sql, params, mapper)
82    }
83
84    /// See [`Connection::prepare`].
85    ///
86    /// # Errors
87    ///
88    /// Returns `Error` if the SQL is invalid.
89    pub fn prepare(&self, sql: &str) -> DbResult<Statement<'_>> {
90        self.conn.prepare(sql)
91    }
92}
93
94impl Drop for Transaction<'_> {
95    fn drop(&mut self) {
96        if !self.committed {
97            let _ = self.conn.execute_batch("ROLLBACK");
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::Connection;
105    use crate::params;
106    use crate::test_utils::init_sqlite;
107
108    #[test]
109    fn test_transaction_commit() {
110        init_sqlite();
111        let conn = Connection::open_in_memory().expect("open in-memory db");
112        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);")
113            .expect("create table");
114        {
115            let tx = conn.transaction().expect("begin tx");
116            tx.execute("INSERT INTO t (id) VALUES (?1)", params![42_i64])
117                .expect("insert");
118            tx.commit().expect("commit");
119        }
120        let result = conn
121            .query_row("SELECT id FROM t WHERE id = 42", &[], |stmt| {
122                Ok(stmt.column_i64(0))
123            })
124            .expect("query");
125        assert_eq!(result, 42);
126    }
127
128    #[test]
129    fn test_transaction_rollback_on_drop() {
130        init_sqlite();
131        let conn = Connection::open_in_memory().expect("open in-memory db");
132        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);")
133            .expect("create table");
134        {
135            let tx = conn.transaction().expect("begin tx");
136            tx.execute("INSERT INTO t (id) VALUES (?1)", params![99_i64])
137                .expect("insert");
138            // Drop without commit -> rollback
139        }
140        let result = conn
141            .query_row_optional("SELECT id FROM t WHERE id = 99", &[], |stmt| {
142                Ok(stmt.column_i64(0))
143            })
144            .expect("query");
145        assert!(result.is_none());
146    }
147}