Skip to main content

walletkit_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::query_row_optional`].
85    ///
86    /// # Errors
87    ///
88    /// Returns `Error` if preparation, execution, or the mapper fails.
89    pub fn query_row_optional<T>(
90        &self,
91        sql: &str,
92        params: &[Value],
93        mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
94    ) -> DbResult<Option<T>> {
95        self.conn.query_row_optional(sql, params, mapper)
96    }
97
98    /// See [`Connection::prepare`].
99    ///
100    /// # Errors
101    ///
102    /// Returns `Error` if the SQL is invalid.
103    pub fn prepare(&self, sql: &str) -> DbResult<Statement<'_>> {
104        self.conn.prepare(sql)
105    }
106}
107
108impl Drop for Transaction<'_> {
109    fn drop(&mut self) {
110        if !self.committed {
111            let _ = self.conn.execute_batch("ROLLBACK");
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::Connection;
119    use crate::params;
120    use crate::test_utils::init_sqlite;
121
122    #[test]
123    fn test_transaction_commit() {
124        init_sqlite();
125        let conn = Connection::open_in_memory().expect("open in-memory db");
126        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);")
127            .expect("create table");
128        {
129            let tx = conn.transaction().expect("begin tx");
130            tx.execute("INSERT INTO t (id) VALUES (?1)", params![42_i64])
131                .expect("insert");
132            tx.commit().expect("commit");
133        }
134        let result = conn
135            .query_row("SELECT id FROM t WHERE id = 42", &[], |stmt| {
136                Ok(stmt.column_i64(0))
137            })
138            .expect("query");
139        assert_eq!(result, 42);
140    }
141
142    #[test]
143    fn test_transaction_rollback_on_drop() {
144        init_sqlite();
145        let conn = Connection::open_in_memory().expect("open in-memory db");
146        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);")
147            .expect("create table");
148        {
149            let tx = conn.transaction().expect("begin tx");
150            tx.execute("INSERT INTO t (id) VALUES (?1)", params![99_i64])
151                .expect("insert");
152            // Drop without commit -> rollback
153        }
154        let result = conn
155            .query_row_optional("SELECT id FROM t WHERE id = 99", &[], |stmt| {
156                Ok(stmt.column_i64(0))
157            })
158            .expect("query");
159        assert!(result.is_none());
160    }
161}