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}