Skip to main content

core_crypto_keystore/transaction/
savepoint.rs

1use crate::{CryptoKeystoreError, Transaction};
2
3impl Transaction {
4    /// Do an operation within the context of a savepoint.
5    ///
6    /// <https://sqlite.org/lang_savepoint.html>
7    ///
8    /// A savepoint is like a transaction, but has a name and can be nested.
9    /// This allows for more fine-grained control over what makes it into the history,
10    /// or not.
11    ///
12    /// The operation produces an arbitrary result which is forwarded unmodified to the
13    /// output of this method. If that result is ok, the savepoint is released (i.e. committed).
14    /// If that result is an error, the savepoint is rolled back.
15    ///
16    /// Error-mapping is the most complicated part of this. When an error is produced in the course
17    /// of this method, we run the `map_err` function with a static context string describing what was
18    /// happening. That function must return a second function which transforms a [`CryptoKeystoreError`]
19    /// into an instance of `E`.
20    pub async fn with_savepoint<T, E>(
21        &self,
22        savepoint_name: &str,
23        operation: impl AsyncFnOnce() -> Result<T, E>,
24        map_err: impl Fn(&'static str) -> Box<dyn FnOnce(CryptoKeystoreError) -> E>,
25    ) -> Result<T, E> {
26        // note that in this function we don't bother caching the sql statements; they're kind of
27        // ad-hoc given the variable savepoint name, so we'd expect mostly cache misses anyway
28
29        // We'll need to release this guard and its locks so the interior operation can acquire them
30        {
31            let conn = self
32                .conn()
33                .map_err(map_err("getting sql connection to create savepoint"))?;
34            let mut stmt = conn
35                .prepare(&format!("SAVEPOINT {savepoint_name}"))
36                .map_err(Into::into)
37                .map_err(map_err("preparing statement to create savepoint"))?;
38            stmt.execute([])
39                .map_err(Into::into)
40                .map_err(map_err("creating savepoint"))?;
41        }
42
43        let outcome = operation().await;
44
45        // time to finalize the savepoint one way or the other
46        let conn = self
47            .conn()
48            .map_err(map_err("getting sql connection to finalize savepoint"))?;
49
50        if outcome.is_ok() {
51            let mut stmt = conn
52                .prepare(&format!("RELEASE SAVEPOINT {savepoint_name}"))
53                .map_err(Into::into)
54                .map_err(map_err("creating savepoint release stmt"))?;
55            stmt.execute([])
56                .map_err(Into::into)
57                .map_err(map_err("releasing savepoint"))?;
58        } else {
59            let mut stmt = conn
60                .prepare(&format!("ROLLBACK TO SAVEPOINT {savepoint_name}"))
61                .map_err(Into::into)
62                .map_err(map_err("creating savepoint rollback stmt"))?;
63            stmt.execute([])
64                .map_err(Into::into)
65                .map_err(map_err("rolling back savepoint"))?;
66        }
67
68        outcome
69    }
70}