Skip to main content

sqlx_cloudflare_do/
batch.rs

1use std::cell::RefCell;
2use std::future::Future;
3use std::rc::Rc;
4
5use futures_core::Stream;
6use futures_util::{stream, TryFutureExt};
7use sqlx_cloudflare_core::BatchResult;
8use sqlx_core::error::Error;
9use sqlx_core::executor::Execute;
10use sqlx_core::query::Query;
11use worker::send::SendFuture;
12
13use crate::{js, Do, DoArguments, DoBatchResult, DoConnection, DoQueryResult};
14
15impl DoConnection {
16    /// Runs `queries` in order and atomically: if any statement fails, none
17    /// of them take effect.
18    ///
19    /// This is the Durable Object answer to a transaction, which `sql.exec`
20    /// does not allow (see [`DoTransactionManager`](crate::DoTransactionManager)).
21    /// The statements run inside `Storage::transaction`, and a failing one
22    /// rolls the others back. Returns what each statement did, in the order
23    /// given; for the rows each statement returns as well, see
24    /// [`fetch_batch`](Self::fetch_batch).
25    ///
26    /// ```no_run
27    /// # async fn transfer(
28    /// #     conn: &sqlx_cloudflare_do::DoConnection,
29    /// #     amount: i64,
30    /// #     from: i64,
31    /// #     to: i64,
32    /// # ) -> Result<(), sqlx::Error> {
33    /// let results = conn
34    ///     .execute_batch([
35    ///         sqlx::query("UPDATE accounts SET balance = balance - ?1 WHERE id = ?2")
36    ///             .bind(amount)
37    ///             .bind(from),
38    ///         sqlx::query("UPDATE accounts SET balance = balance + ?1 WHERE id = ?2")
39    ///             .bind(amount)
40    ///             .bind(to),
41    ///     ])
42    ///     .await?;
43    /// # Ok(())
44    /// # }
45    /// ```
46    ///
47    /// # Errors
48    ///
49    /// When binding an argument fails, in which case nothing ran, or when any
50    /// statement fails -- in which case the whole batch was rolled back.
51    pub fn execute_batch<'q, I>(
52        &self,
53        queries: I,
54    ) -> impl Future<Output = Result<Vec<DoQueryResult>, Error>> + Send + '_
55    where
56        I: IntoIterator<Item = Query<'q, Do, DoArguments>>,
57    {
58        self.run_batch(queries).map_ok(|results| {
59            results
60                .into_iter()
61                .map(|result| result.into_parts().1)
62                .collect()
63        })
64    }
65
66    /// Runs `queries` in order and atomically, as
67    /// [`execute_batch`](Self::execute_batch) does, and streams each
68    /// statement's rows along with what it did, in the order given.
69    ///
70    /// The shape sqlc's `:batchexec`, `:batchone` and `:batchmany` queries
71    /// take, each a stream with one item per statement -- but atomic. So
72    /// unlike a loop of queries it is not lazy: the whole batch runs when the
73    /// stream is first polled, and a failure is the one item it yields, with
74    /// nothing applied.
75    ///
76    /// ```no_run
77    /// # async fn users(conn: &sqlx_cloudflare_do::DoConnection) -> Result<(), sqlx::Error> {
78    /// use futures_util::TryStreamExt;
79    /// use sqlx::Row;
80    ///
81    /// let names: Vec<String> = conn
82    ///     .fetch_batch([1_i64, 2, 3].map(|id| {
83    ///         sqlx::query("SELECT name FROM users WHERE id = ?").bind(id)
84    ///     }))
85    ///     .map_ok(|result| result.rows()[0].get("name"))
86    ///     .try_collect()
87    ///     .await?;
88    /// # Ok(())
89    /// # }
90    /// ```
91    ///
92    /// # Errors
93    ///
94    /// The stream's one item is an error when binding an argument fails, in
95    /// which case nothing ran, or when any statement fails -- in which case
96    /// the whole batch was rolled back.
97    pub fn fetch_batch<'q, I>(
98        &self,
99        queries: I,
100    ) -> impl Stream<Item = Result<DoBatchResult, Error>> + Send + '_
101    where
102        I: IntoIterator<Item = Query<'q, Do, DoArguments>>,
103    {
104        self.run_batch(queries)
105            .map_ok(|results| stream::iter(results.into_iter().map(Ok)))
106            .try_flatten_stream()
107    }
108
109    fn run_batch<'q, I>(
110        &self,
111        queries: I,
112    ) -> impl Future<Output = Result<Vec<DoBatchResult>, Error>> + Send + '_
113    where
114        I: IntoIterator<Item = Query<'q, Do, DoArguments>>,
115    {
116        // Taken apart here, before the future, so it holds only owned SQL and
117        // arguments: the queries themselves need not outlive this call, and
118        // the transaction's callback has to own what it runs.
119        let queries = queries
120            .into_iter()
121            .map(|mut query| {
122                let arguments = query.take_arguments().map_err(Error::Encode)?;
123                Ok((query.sql(), arguments.unwrap_or_default()))
124            })
125            .collect::<Result<Vec<_>, Error>>();
126
127        // `Storage::transaction` and its callback's future hold JavaScript
128        // handles, so neither is `Send`; `SendFuture` asserts it, soundly,
129        // because a Durable Object runs on one thread.
130        SendFuture::new(async move {
131            let queries = queries?;
132
133            if queries.is_empty() {
134                return Ok(Vec::new());
135            }
136
137            // The callback reports through this rather than its return value,
138            // which `worker` flattens to a string: the statement's own error,
139            // with its `ErrorKind`, is what the caller should see.
140            let outcome = Rc::new(RefCell::new(None));
141            let slot = Rc::clone(&outcome);
142            let sql = self.sql.clone();
143
144            let committed = self
145                .storage
146                .transaction(move |_| async move {
147                    let results = queries
148                        .iter()
149                        .map(|(query, arguments)| {
150                            js::execute(&sql, query.as_str(), arguments.values(), None)
151                                .map(|(rows, result)| BatchResult::new(rows, result))
152                        })
153                        .collect::<Result<Vec<_>, Error>>();
154                    let failed = results.is_err();
155                    *slot.borrow_mut() = Some(results);
156
157                    if failed {
158                        // Any error will do: rejecting is what rolls back.
159                        Err(worker::Error::RustError("a statement failed".into()))
160                    } else {
161                        Ok(())
162                    }
163                })
164                .await;
165
166            let outcome = outcome.borrow_mut().take();
167            match (committed, outcome) {
168                (_, Some(Err(error))) => Err(error),
169                (Ok(()), Some(Ok(results))) => Ok(results),
170                (Err(error), _) => Err(Error::Protocol(format!(
171                    "the Durable Object storage transaction failed: {error}"
172                ))),
173                (Ok(()), None) => Err(Error::Protocol(
174                    "the Durable Object storage transaction never ran its callback".into(),
175                )),
176            }
177        })
178    }
179}