Skip to main content

sqlx_cloudflare_do/
transaction.rs

1use std::cell::RefCell;
2use std::fmt::{self, Debug, Formatter};
3use std::future::{self, Future};
4use std::rc::Rc;
5
6use sqlx_core::error::Error;
7use sqlx_core::sql_str::SqlStr;
8use sqlx_core::transaction::TransactionManager;
9use worker::send::SendFuture;
10
11use crate::error::unsupported;
12use crate::{Do, DoConnection};
13
14const NO_TRANSACTIONS: &str = "Durable Object storage cannot begin a transaction here; \
15     run the statements inside `DoConnection::transaction`, which takes them as a callback";
16
17impl DoConnection {
18    /// Runs `callback` in a transaction: it commits if the callback returns
19    /// `Ok`, and rolls back every write the callback made if it returns
20    /// `Err`.
21    ///
22    /// A real interactive transaction, unlike a batch: the callback reads its
23    /// own writes, and one statement can use what the one before it returned.
24    /// It runs inside `Storage::transaction`, and the callback's value -- or
25    /// its error, as it returned it -- is what this resolves to.
26    ///
27    /// ```no_run
28    /// # async fn create(conn: &sqlx_cloudflare_do::DoConnection, email: String) -> Result<i64, sqlx::Error> {
29    /// let id = conn
30    ///     .transaction(move |tx| async move {
31    ///         let id: i64 = sqlx::query_scalar("INSERT INTO users (email) VALUES (?) RETURNING id")
32    ///             .bind(email)
33    ///             .fetch_one(&tx)
34    ///             .await?;
35    ///         sqlx::query("INSERT INTO posts (user_id, title) VALUES (?, 'hello')")
36    ///             .bind(id)
37    ///             .execute(&tx)
38    ///             .await?;
39    ///         Ok::<_, sqlx::Error>(id)
40    ///     })
41    ///     .await?;
42    /// # Ok(id)
43    /// # }
44    /// ```
45    ///
46    /// # The callback owns what it uses
47    ///
48    /// `Storage::transaction` takes a `'static` callback, so it cannot borrow
49    /// the connection or anything else from the caller: move what it needs
50    /// in, and run its queries through the [`DoTransaction`] it is handed.
51    /// That is also why this is a callback and not sqlx's `begin()`, which
52    /// borrows the connection -- `begin()` still fails.
53    ///
54    /// # It holds the whole object
55    ///
56    /// The Durable Object delivers no other request until the transaction
57    /// ends -- even while the callback awaits something that is not storage.
58    /// That is what keeps other requests' writes out of it, and why it should
59    /// be short: an awaited `fetch()` inside it stalls every caller of the
60    /// object, and a request to the object itself would never be answered.
61    ///
62    /// # Errors
63    ///
64    /// The callback's own error, after rolling back; or, converted into `E`,
65    /// an error from storage when the transaction could not commit.
66    pub fn transaction<F, Fut, T, E>(
67        &self,
68        callback: F,
69    ) -> impl Future<Output = Result<T, E>> + Send + '_
70    where
71        F: FnOnce(DoTransaction) -> Fut + 'static,
72        Fut: Future<Output = Result<T, E>> + 'static,
73        T: 'static,
74        E: From<Error> + 'static,
75    {
76        let tx = DoTransaction {
77            sql: self.sql.clone(),
78        };
79
80        // `Storage::transaction` and the callback's future hold JavaScript
81        // handles, so neither is `Send`; `SendFuture` asserts it, soundly,
82        // because a Durable Object runs on one thread.
83        SendFuture::new(async move {
84            // The callback reports through this rather than its return
85            // value, which `worker` flattens to a string: the caller gets its
86            // value, or its error as it returned it.
87            let outcome = Rc::new(RefCell::new(None));
88            let slot = Rc::clone(&outcome);
89
90            let committed = self
91                .storage
92                .transaction(move |_| async move {
93                    let result = callback(tx).await;
94                    let failed = result.is_err();
95                    *slot.borrow_mut() = Some(result);
96
97                    if failed {
98                        // Any error will do: rejecting is what rolls back.
99                        Err(worker::Error::RustError("the callback failed".into()))
100                    } else {
101                        Ok(())
102                    }
103                })
104                .await;
105
106            let outcome = outcome.borrow_mut().take();
107            match (committed, outcome) {
108                (_, Some(Err(error))) => Err(error),
109                (Ok(()), Some(Ok(value))) => Ok(value),
110                (Err(error), _) => Err(E::from(Error::Protocol(format!(
111                    "the Durable Object storage transaction failed: {error}"
112                )))),
113                (Ok(()), None) => Err(E::from(Error::Protocol(
114                    "the Durable Object storage transaction never ran its callback".into(),
115                ))),
116            }
117        })
118    }
119}
120
121/// An open transaction: the executor [`DoConnection::transaction`] hands its
122/// callback.
123///
124/// `&DoTransaction` and `&mut DoTransaction` are sqlx executors, like a
125/// connection's references, and every query run through one is part of the
126/// transaction.
127pub struct DoTransaction {
128    pub(crate) sql: worker::SqlStorage,
129}
130
131impl Debug for DoTransaction {
132    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
133        f.debug_struct("DoTransaction").finish_non_exhaustive()
134    }
135}
136
137/// Refuses `begin()`.
138///
139/// `sql.exec` rejects `BEGIN` and `SAVEPOINT` outright: the only transactions
140/// Durable Object storage offers wrap a callback, and sqlx's `begin` and
141/// `commit` are two separate calls with the caller's code in between.
142/// Buffering writes until `commit` would look like a transaction and not be
143/// one -- reads inside it would see the database as it was before -- which is
144/// worse than an error. So `conn.begin()` compiles and fails at runtime, and
145/// transactions go through [`DoConnection::transaction`].
146#[derive(Debug)]
147pub struct DoTransactionManager;
148
149impl TransactionManager for DoTransactionManager {
150    type Database = Do;
151
152    fn begin(
153        _conn: &mut DoConnection,
154        _statement: Option<SqlStr>,
155    ) -> impl Future<Output = Result<(), Error>> + Send + '_ {
156        future::ready(Err(unsupported(NO_TRANSACTIONS)))
157    }
158
159    // Unreachable in practice: `begin` never succeeds, so there is never a
160    // transaction to end. They still answer rather than panic.
161    fn commit(_conn: &mut DoConnection) -> impl Future<Output = Result<(), Error>> + Send + '_ {
162        future::ready(Err(unsupported(NO_TRANSACTIONS)))
163    }
164
165    fn rollback(_conn: &mut DoConnection) -> impl Future<Output = Result<(), Error>> + Send + '_ {
166        future::ready(Err(unsupported(NO_TRANSACTIONS)))
167    }
168
169    // `Transaction` calls this from `Drop` when `begin` failed -- it cannot
170    // tell a failed begin from an abandoned transaction -- so it must be a
171    // quiet no-op, not an error.
172    fn start_rollback(_conn: &mut DoConnection) {}
173
174    fn get_transaction_depth(_conn: &DoConnection) -> usize {
175        0
176    }
177}