Skip to main content

sqlx_cloudflare_do/
batch.rs

1use std::future::Future;
2
3use futures_core::Stream;
4use futures_util::{stream, TryFutureExt};
5use sqlx_cloudflare_core::BatchResult;
6use sqlx_core::error::Error;
7use sqlx_core::executor::Execute;
8use sqlx_core::query::Query;
9
10use crate::{js, Do, DoArguments, DoBatchResult, DoConnection, DoQueryResult};
11
12impl DoConnection {
13    /// Runs `queries` in order and atomically: if any statement fails, none
14    /// of them take effect.
15    ///
16    /// This is the Durable Object answer to a transaction, which `sql.exec`
17    /// does not allow (see [`DoTransactionManager`](crate::DoTransactionManager)).
18    /// The statements run inside [`transaction`](Self::transaction), and a
19    /// failing one rolls the others back. Returns what each statement did, in the order
20    /// given; for the rows each statement returns as well, see
21    /// [`fetch_batch`](Self::fetch_batch).
22    ///
23    /// ```no_run
24    /// # async fn transfer(
25    /// #     conn: &sqlx_cloudflare_do::DoConnection,
26    /// #     amount: i64,
27    /// #     from: i64,
28    /// #     to: i64,
29    /// # ) -> Result<(), sqlx::Error> {
30    /// let results = conn
31    ///     .execute_batch([
32    ///         sqlx::query("UPDATE accounts SET balance = balance - ?1 WHERE id = ?2")
33    ///             .bind(amount)
34    ///             .bind(from),
35    ///         sqlx::query("UPDATE accounts SET balance = balance + ?1 WHERE id = ?2")
36    ///             .bind(amount)
37    ///             .bind(to),
38    ///     ])
39    ///     .await?;
40    /// # Ok(())
41    /// # }
42    /// ```
43    ///
44    /// # Errors
45    ///
46    /// When binding an argument fails, in which case nothing ran, or when any
47    /// statement fails -- in which case the whole batch was rolled back.
48    pub fn execute_batch<'q, I>(
49        &self,
50        queries: I,
51    ) -> impl Future<Output = Result<Vec<DoQueryResult>, Error>> + Send + '_
52    where
53        I: IntoIterator<Item = Query<'q, Do, DoArguments>>,
54    {
55        self.run_batch(queries).map_ok(|results| {
56            results
57                .into_iter()
58                .map(|result| result.into_parts().1)
59                .collect()
60        })
61    }
62
63    /// Runs `queries` in order and atomically, as
64    /// [`execute_batch`](Self::execute_batch) does, and streams each
65    /// statement's rows along with what it did, in the order given.
66    ///
67    /// The shape sqlc's `:batchexec`, `:batchone` and `:batchmany` queries
68    /// take, each a stream with one item per statement -- but atomic. So
69    /// unlike a loop of queries it is not lazy: the whole batch runs when the
70    /// stream is first polled, and a failure is the one item it yields, with
71    /// nothing applied.
72    ///
73    /// ```no_run
74    /// # async fn users(conn: &sqlx_cloudflare_do::DoConnection) -> Result<(), sqlx::Error> {
75    /// use futures_util::TryStreamExt;
76    /// use sqlx::Row;
77    ///
78    /// let names: Vec<String> = conn
79    ///     .fetch_batch([1_i64, 2, 3].map(|id| {
80    ///         sqlx::query("SELECT name FROM users WHERE id = ?").bind(id)
81    ///     }))
82    ///     .map_ok(|result| result.rows()[0].get("name"))
83    ///     .try_collect()
84    ///     .await?;
85    /// # Ok(())
86    /// # }
87    /// ```
88    ///
89    /// # Errors
90    ///
91    /// The stream's one item is an error when binding an argument fails, in
92    /// which case nothing ran, or when any statement fails -- in which case
93    /// the whole batch was rolled back.
94    pub fn fetch_batch<'q, I>(
95        &self,
96        queries: I,
97    ) -> impl Stream<Item = Result<DoBatchResult, Error>> + Send + '_
98    where
99        I: IntoIterator<Item = Query<'q, Do, DoArguments>>,
100    {
101        self.run_batch(queries)
102            .map_ok(|results| stream::iter(results.into_iter().map(Ok)))
103            .try_flatten_stream()
104    }
105
106    fn run_batch<'q, I>(
107        &self,
108        queries: I,
109    ) -> impl Future<Output = Result<Vec<DoBatchResult>, Error>> + Send + '_
110    where
111        I: IntoIterator<Item = Query<'q, Do, DoArguments>>,
112    {
113        // Taken apart here, before the future, so it holds only owned SQL and
114        // arguments: the queries themselves need not outlive this call, and
115        // the transaction's callback has to own what it runs.
116        let queries = queries
117            .into_iter()
118            .map(|mut query| {
119                let arguments = query.take_arguments().map_err(Error::Encode)?;
120                Ok((query.sql(), arguments.unwrap_or_default()))
121            })
122            .collect::<Result<Vec<_>, Error>>();
123
124        async move {
125            let queries = queries?;
126
127            if queries.is_empty() {
128                return Ok(Vec::new());
129            }
130
131            // Inside a transaction, statements run as they would outside
132            // one; the transaction is what makes a failure undo the rest.
133            self.transaction(move |tx| async move {
134                queries
135                    .iter()
136                    .map(|(query, arguments)| {
137                        js::execute(&tx.sql, query.as_str(), arguments.values(), None)
138                            .map(|(rows, result)| BatchResult::new(rows, result))
139                    })
140                    .collect::<Result<Vec<_>, Error>>()
141            })
142            .await
143        }
144    }
145}