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#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct DoConnectOptions {
127    _private: (),
128}
129
130impl FromStr for DoConnectOptions {
131    type Err = Error;
132
133    fn from_str(s: &str) -> Result<Self, Error> {
134        let url = Url::parse(s).map_err(|error| Error::Configuration(error.into()))?;
135        Self::from_url(&url)
136    }
137}
138
139impl ConnectOptions for DoConnectOptions {
140    type Connection = DoConnection;
141
142    fn from_url(url: &Url) -> Result<Self, Error> {
143        if url.scheme() == "do" {
144            Ok(Self { _private: () })
145        } else {
146            Err(Error::Configuration(
147                format!("expected a `do://` URL, got scheme `{}`", url.scheme()).into(),
148            ))
149        }
150    }
151
152    fn to_url_lossy(&self) -> Url {
153        Url::parse("do://").expect("a constant URL")
154    }
155
156    fn connect(&self) -> impl Future<Output = Result<DoConnection, Error>> + Send + '_ {
157        future::ready(Err(Error::Configuration(
158            "a Durable Object connection cannot be opened from a URL; build it from the object's \
159             storage with `DoConnection::new(state.storage())`"
160                .into(),
161        )))
162    }
163
164    // No statement logging yet, so there is nothing for these to configure.
165    fn log_statements(self, _level: LevelFilter) -> Self {
166        self
167    }
168
169    fn log_slow_statements(self, _level: LevelFilter, _duration: Duration) -> Self {
170        self
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use sqlx_core::connection::ConnectOptions;
177
178    use super::DoConnectOptions;
179
180    #[test]
181    fn parses_a_do_url() {
182        let options: DoConnectOptions = "do://".parse().unwrap();
183        assert_eq!(options.to_url_lossy().as_str(), "do://");
184    }
185
186    #[test]
187    fn rejects_other_schemes() {
188        assert!("sqlite://db".parse::<DoConnectOptions>().is_err());
189        assert!("d1://DB".parse::<DoConnectOptions>().is_err());
190    }
191}