Skip to main content

sqlx_cloudflare_do/
connection.rs

1use std::fmt::{self, Debug, Formatter};
2use std::future::{self, Future};
3use std::str::FromStr;
4use std::time::Duration;
5
6use log::LevelFilter;
7use sqlx_core::connection::{ConnectOptions, Connection};
8use sqlx_core::error::Error;
9use sqlx_core::transaction::Transaction;
10use sqlx_core::Url;
11
12use crate::Do;
13
14/// A connection to a Durable Object's SQL storage.
15///
16/// There is no socket behind it and nothing to pool: the database lives in
17/// the Durable Object itself. Build one from the object's storage with
18/// [`DoConnection::new`], in its constructor or per request.
19///
20/// Both `&mut DoConnection` and `&DoConnection` are sqlx executors, so a
21/// connection kept in the object's struct serves every request through
22/// `&self`.
23pub struct DoConnection {
24    /// For [`execute_batch`](Self::execute_batch) and
25    /// [`fetch_batch`](Self::fetch_batch), which run through
26    /// `Storage::transaction`.
27    pub(crate) storage: worker::Storage,
28    /// `storage.sql()`, kept rather than fetched for every query.
29    pub(crate) sql: worker::SqlStorage,
30}
31
32// `worker::Storage` is neither, only because it holds a JavaScript handle --
33// and `worker` itself makes `SqlStorage` both on the grounds that follow. A
34// Durable Object runs on one thread, so the handle never meets another one.
35// The executor's futures rely on this: they hold `&DoConnection`, and sqlx
36// requires them to be `Send`.
37//
38// SAFETY: a Durable Object is single-threaded; see above.
39unsafe impl Send for DoConnection {}
40// SAFETY: as for `Send`.
41unsafe impl Sync for DoConnection {}
42
43impl DoConnection {
44    /// Wraps a Durable Object's storage: `state.storage()`.
45    #[must_use]
46    pub fn new(storage: worker::Storage) -> Self {
47        let sql = storage.sql();
48        Self { storage, sql }
49    }
50
51    /// The storage this connection wraps, for anything this crate does not
52    /// cover -- the key-value API, alarms -- while keeping the connection.
53    #[must_use]
54    pub fn storage(&self) -> &worker::Storage {
55        &self.storage
56    }
57
58    /// The SQL API this connection runs queries through.
59    #[must_use]
60    pub fn sql(&self) -> &worker::SqlStorage {
61        &self.sql
62    }
63
64    /// The storage this connection wraps, giving up the connection. See
65    /// [`storage`](Self::storage) to borrow it instead.
66    #[must_use]
67    pub fn into_storage(self) -> worker::Storage {
68        self.storage
69    }
70}
71
72impl From<worker::Storage> for DoConnection {
73    fn from(storage: worker::Storage) -> Self {
74        Self::new(storage)
75    }
76}
77
78impl Debug for DoConnection {
79    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
80        f.debug_struct("DoConnection").finish_non_exhaustive()
81    }
82}
83
84impl Connection for DoConnection {
85    type Database = Do;
86
87    type Options = DoConnectOptions;
88
89    // Nothing to close: the storage belongs to the Durable Object, not to us.
90    fn close(self) -> impl Future<Output = Result<(), Error>> + Send + 'static {
91        future::ready(Ok(()))
92    }
93
94    fn close_hard(self) -> impl Future<Output = Result<(), Error>> + Send + 'static {
95        future::ready(Ok(()))
96    }
97
98    // The database is in-process: there is nothing to go stale.
99    fn ping(&mut self) -> impl Future<Output = Result<(), Error>> + Send + '_ {
100        future::ready(Ok(()))
101    }
102
103    /// Always fails -- see [`DoTransactionManager`](crate::DoTransactionManager).
104    fn begin(&mut self) -> impl Future<Output = Result<Transaction<'_, Do>, Error>> + Send + '_ {
105        Transaction::begin(self, None)
106    }
107
108    fn shrink_buffers(&mut self) {}
109
110    fn flush(&mut self) -> impl Future<Output = Result<(), Error>> + Send + '_ {
111        future::ready(Ok(()))
112    }
113
114    fn should_flush(&self) -> bool {
115        false
116    }
117}
118
119/// The options type sqlx requires of every connection.
120///
121/// It parses `do://` so the type is constructible, but
122/// [`connect`](ConnectOptions::connect) always fails: a connection comes from
123/// a Durable Object's storage, which only the object itself holds. Use
124/// [`DoConnection::new`].
125///
126/// Non-exhaustive, so parsing is the only way to get one -- as sqlx itself
127/// does -- and a field can be added without breaking anyone.
128#[derive(Debug, Clone, PartialEq, Eq)]
129#[non_exhaustive]
130pub struct DoConnectOptions;
131
132impl FromStr for DoConnectOptions {
133    type Err = Error;
134
135    fn from_str(s: &str) -> Result<Self, Error> {
136        let url = Url::parse(s).map_err(|error| Error::Configuration(error.into()))?;
137        Self::from_url(&url)
138    }
139}
140
141impl ConnectOptions for DoConnectOptions {
142    type Connection = DoConnection;
143
144    fn from_url(url: &Url) -> Result<Self, Error> {
145        if url.scheme() == "do" {
146            Ok(Self)
147        } else {
148            Err(Error::Configuration(
149                format!("expected a `do://` URL, got scheme `{}`", url.scheme()).into(),
150            ))
151        }
152    }
153
154    fn to_url_lossy(&self) -> Url {
155        Url::parse("do://").expect("a constant URL")
156    }
157
158    fn connect(&self) -> impl Future<Output = Result<DoConnection, Error>> + Send + '_ {
159        future::ready(Err(Error::Configuration(
160            "a Durable Object connection cannot be opened from a URL; build it from the object's \
161             storage with `DoConnection::new(state.storage())`"
162                .into(),
163        )))
164    }
165
166    // No statement logging yet, so there is nothing for these to configure.
167    fn log_statements(self, _level: LevelFilter) -> Self {
168        self
169    }
170
171    fn log_slow_statements(self, _level: LevelFilter, _duration: Duration) -> Self {
172        self
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use sqlx_core::connection::ConnectOptions;
179
180    use super::DoConnectOptions;
181
182    #[test]
183    fn parses_a_do_url() {
184        let options: DoConnectOptions = "do://".parse().unwrap();
185        assert_eq!(options.to_url_lossy().as_str(), "do://");
186    }
187
188    #[test]
189    fn rejects_other_schemes() {
190        assert!("sqlite://db".parse::<DoConnectOptions>().is_err());
191        assert!("d1://DB".parse::<DoConnectOptions>().is_err());
192    }
193}