Skip to main content

sqlx_cloudflare_d1/
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::D1;
13
14/// A connection to a D1 database: a Workers D1 binding.
15///
16/// There is no socket behind it and nothing to pool. Build one per request
17/// from the binding, with [`D1Connection::from_env`] or [`D1Connection::new`].
18///
19/// Both `&mut D1Connection` and `&D1Connection` are sqlx executors. The
20/// binding is stateless, so a shared reference is enough to run a query, and
21/// one connection can serve concurrent queries within a request.
22pub struct D1Connection {
23    // `worker::D1Database` is already `Send + Sync` (`worker` implements both,
24    // on the grounds that Workers are single-threaded), so it needs no wrapper
25    // of ours -- only the JavaScript futures it produces do.
26    pub(crate) db: worker::D1Database,
27}
28
29impl D1Connection {
30    /// Wraps a D1 binding.
31    #[must_use]
32    pub fn new(db: worker::D1Database) -> Self {
33        Self { db }
34    }
35
36    /// Looks up the D1 binding called `binding` in the Worker's environment.
37    ///
38    /// # Errors
39    ///
40    /// When there is no binding by that name, or it is not a D1 database.
41    pub fn from_env(env: &worker::Env, binding: &str) -> worker::Result<Self> {
42        env.d1(binding).map(Self::new)
43    }
44
45    /// The binding this connection wraps, for anything this crate does not
46    /// cover -- `dump`, `exec`, sessions -- while keeping the connection.
47    #[must_use]
48    pub fn database(&self) -> &worker::D1Database {
49        &self.db
50    }
51
52    /// The binding this connection wraps, giving up the connection. See
53    /// [`database`](Self::database) to borrow it instead.
54    #[must_use]
55    pub fn into_database(self) -> worker::D1Database {
56        self.db
57    }
58}
59
60impl Debug for D1Connection {
61    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
62        f.debug_struct("D1Connection").finish_non_exhaustive()
63    }
64}
65
66impl Connection for D1Connection {
67    type Database = D1;
68
69    type Options = D1ConnectOptions;
70
71    // Nothing to close: the binding belongs to the Worker, not to us.
72    fn close(self) -> impl Future<Output = Result<(), Error>> + Send + 'static {
73        future::ready(Ok(()))
74    }
75
76    fn close_hard(self) -> impl Future<Output = Result<(), Error>> + Send + 'static {
77        future::ready(Ok(()))
78    }
79
80    // A binding cannot go stale the way a socket can, so there is nothing a
81    // round trip would tell us that the next query won't.
82    fn ping(&mut self) -> impl Future<Output = Result<(), Error>> + Send + '_ {
83        future::ready(Ok(()))
84    }
85
86    /// Always fails -- see [`D1TransactionManager`](crate::D1TransactionManager).
87    fn begin(&mut self) -> impl Future<Output = Result<Transaction<'_, D1>, Error>> + Send + '_ {
88        Transaction::begin(self, None)
89    }
90
91    fn shrink_buffers(&mut self) {}
92
93    fn flush(&mut self) -> impl Future<Output = Result<(), Error>> + Send + '_ {
94        future::ready(Ok(()))
95    }
96
97    fn should_flush(&self) -> bool {
98        false
99    }
100}
101
102/// The options type sqlx requires of every connection.
103///
104/// It parses `d1://<binding>` so the type is constructible, but
105/// [`connect`](ConnectOptions::connect) always fails: a D1 connection comes
106/// from a Workers binding, which only the Worker's `Env` can hand out. Use
107/// [`D1Connection::from_env`].
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct D1ConnectOptions {
110    binding: String,
111}
112
113impl D1ConnectOptions {
114    /// The binding name from the URL.
115    #[must_use]
116    pub fn binding(&self) -> &str {
117        &self.binding
118    }
119}
120
121impl FromStr for D1ConnectOptions {
122    type Err = Error;
123
124    fn from_str(s: &str) -> Result<Self, Error> {
125        let url = Url::parse(s).map_err(|error| Error::Configuration(error.into()))?;
126        Self::from_url(&url)
127    }
128}
129
130impl ConnectOptions for D1ConnectOptions {
131    type Connection = D1Connection;
132
133    fn from_url(url: &Url) -> Result<Self, Error> {
134        if url.scheme() != "d1" {
135            return Err(Error::Configuration(
136                format!(
137                    "expected a `d1://<binding>` URL, got scheme `{}`",
138                    url.scheme()
139                )
140                .into(),
141            ));
142        }
143
144        match url.host_str() {
145            Some(binding) if !binding.is_empty() => Ok(Self {
146                binding: binding.to_owned(),
147            }),
148            _ => Err(Error::Configuration(
149                "expected a `d1://<binding>` URL with a binding name".into(),
150            )),
151        }
152    }
153
154    fn to_url_lossy(&self) -> Url {
155        Url::parse(&format!("d1://{}", self.binding)).expect("a binding name parsed from a URL")
156    }
157
158    fn connect(&self) -> impl Future<Output = Result<D1Connection, Error>> + Send + '_ {
159        future::ready(Err(Error::Configuration(
160            format!(
161                "a D1 connection cannot be opened from a URL; get the `{}` binding from the \
162                 Worker's environment with `D1Connection::from_env`",
163                self.binding
164            )
165            .into(),
166        )))
167    }
168
169    // No statement logging yet, so there is nothing for these to configure.
170    fn log_statements(self, _level: LevelFilter) -> Self {
171        self
172    }
173
174    fn log_slow_statements(self, _level: LevelFilter, _duration: Duration) -> Self {
175        self
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use sqlx_core::connection::ConnectOptions;
182
183    use super::D1ConnectOptions;
184
185    #[test]
186    fn parses_a_binding_url() {
187        let options: D1ConnectOptions = "d1://DB".parse().unwrap();
188
189        // `Url` lowercases a special scheme's host but leaves an unknown
190        // scheme's alone, so a binding keeps its case.
191        assert_eq!(options.binding(), "DB");
192        assert_eq!(options.to_url_lossy().as_str(), "d1://DB");
193    }
194
195    #[test]
196    fn rejects_other_schemes_and_a_missing_binding() {
197        assert!("sqlite://DB".parse::<D1ConnectOptions>().is_err());
198        assert!("d1://".parse::<D1ConnectOptions>().is_err());
199    }
200}