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}