Skip to main content

orbit_orm/
database.rs

1use std::time::Duration;
2
3use sqlx::Database as SqlxDatabase;
4#[cfg(feature = "mysql")]
5use sqlx::MySql;
6#[cfg(feature = "mysql")]
7use sqlx::MySqlPool;
8#[cfg(feature = "postgres")]
9use sqlx::PgPool;
10#[cfg(feature = "postgres")]
11use sqlx::Postgres;
12#[cfg(feature = "sqlite")]
13use sqlx::Sqlite;
14#[cfg(feature = "sqlite")]
15use sqlx::SqlitePool;
16
17use crate::transaction::Target;
18use crate::{Driver, Error, Transaction};
19
20/// The settings for the connection pool, given to `Database::connect_with`. `Default` gives the
21/// settings that `Database::connect` uses.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct PoolOptions {
24    /// The most connections the pool opens at once. Further requests wait for one to be released.
25    pub max_connections: u32,
26    /// How long a request waits for a connection before it fails.
27    pub acquire_timeout: Duration,
28    /// The connections the pool keeps open while idle.
29    pub min_connections: u32,
30}
31
32impl Default for PoolOptions {
33    fn default() -> Self {
34        Self {
35            max_connections: 5,
36            acquire_timeout: Duration::from_secs(10),
37            min_connections: 0,
38        }
39    }
40}
41
42/// The pool of the database that the URL named. One variant per enabled driver.
43#[derive(Debug)]
44#[doc(hidden)]
45pub enum Pool {
46    #[cfg(feature = "mysql")]
47    MySql(MySqlPool),
48    #[cfg(feature = "postgres")]
49    Postgres(PgPool),
50    #[cfg(feature = "sqlite")]
51    Sqlite(SqlitePool),
52}
53
54impl Pool {
55    pub(crate) fn driver(&self) -> Driver {
56        match self {
57            #[cfg(feature = "mysql")]
58            Self::MySql(_) => Driver::MySql,
59            #[cfg(feature = "postgres")]
60            Self::Postgres(_) => Driver::Postgres,
61            #[cfg(feature = "sqlite")]
62            Self::Sqlite(_) => Driver::Sqlite,
63        }
64    }
65}
66
67/// The connection pool for one database. Clone-free: share it behind an `Arc` or in Rocket's state.
68///
69/// The driver comes from the URL's scheme: `mysql://` (or `mariadb://`), `postgres://` (or
70/// `postgresql://`) and `sqlite:`. A scheme whose feature is not enabled is an error.
71#[derive(Debug)]
72pub struct Database {
73    pub(crate) pool: Pool,
74}
75
76/// The driver that a URL's scheme names, if its feature is enabled.
77fn driver_for(url: &str) -> Result<Driver, Error> {
78    let scheme = url.split_once(':').map_or(url, |(scheme, _)| scheme);
79    #[cfg(feature = "mysql")]
80    if <MySql as SqlxDatabase>::URL_SCHEMES.contains(&scheme) {
81        return Ok(Driver::MySql);
82    }
83    #[cfg(feature = "postgres")]
84    if <Postgres as SqlxDatabase>::URL_SCHEMES.contains(&scheme) {
85        return Ok(Driver::Postgres);
86    }
87    #[cfg(feature = "sqlite")]
88    if <Sqlite as SqlxDatabase>::URL_SCHEMES.contains(&scheme) {
89        return Ok(Driver::Sqlite);
90    }
91    Err(Error::UnsupportedDriver {
92        scheme: scheme.to_owned(),
93    })
94}
95
96impl Database {
97    /// Connects to the database at `url`, such as `mysql://user:password@host/database`,
98    /// `postgres://user:password@host/database` or `sqlite:path/to/file.db`.
99    pub async fn connect(url: &str) -> Result<Self, Error> {
100        Self::connect_with(url, PoolOptions::default()).await
101    }
102
103    /// Connects to the database at `url` with the pool settings in `options`. `connect` uses
104    /// `PoolOptions::default()`.
105    pub async fn connect_with(url: &str, options: PoolOptions) -> Result<Self, Error> {
106        let pool = match driver_for(url)? {
107            #[cfg(feature = "mysql")]
108            Driver::MySql => Pool::MySql(options.apply::<MySql>().connect(url).await?),
109            #[cfg(feature = "postgres")]
110            Driver::Postgres => Pool::Postgres(options.apply::<Postgres>().connect(url).await?),
111            #[cfg(feature = "sqlite")]
112            Driver::Sqlite => Pool::Sqlite(options.apply::<Sqlite>().connect(url).await?),
113        };
114        Ok(Self { pool })
115    }
116
117    /// The database this pool talks to.
118    pub fn driver(&self) -> Driver {
119        self.pool.driver()
120    }
121
122    /// The MySQL pool, for queries that Orbit does not wrap yet.
123    ///
124    /// # Panics
125    ///
126    /// Panics when the database is not MySQL. Check `driver()` first, or use `postgres_pool` or
127    /// `sqlite_pool`.
128    #[cfg(feature = "mysql")]
129    pub fn pool(&self) -> &MySqlPool {
130        match &self.pool {
131            Pool::MySql(pool) => pool,
132            #[cfg(any(feature = "postgres", feature = "sqlite"))]
133            _ => panic!(
134                "`pool` is for MySQL; this database is {}",
135                self.driver().name()
136            ),
137        }
138    }
139
140    /// The PostgreSQL pool, for queries that Orbit does not wrap yet. `None` for other databases.
141    #[cfg(feature = "postgres")]
142    pub fn postgres_pool(&self) -> Option<&PgPool> {
143        match &self.pool {
144            Pool::Postgres(pool) => Some(pool),
145            #[cfg(any(feature = "mysql", feature = "sqlite"))]
146            _ => None,
147        }
148    }
149
150    /// The SQLite pool, for queries that Orbit does not wrap yet. `None` for other databases.
151    #[cfg(feature = "sqlite")]
152    pub fn sqlite_pool(&self) -> Option<&SqlitePool> {
153        match &self.pool {
154            Pool::Sqlite(pool) => Some(pool),
155            #[cfg(any(feature = "mysql", feature = "postgres"))]
156            _ => None,
157        }
158    }
159
160    /// Checks that the database answers.
161    pub async fn ping(&self) -> Result<(), Error> {
162        crate::backend::execute(Target::Pool(&self.pool), "SELECT 1", &[]).await?;
163        Ok(())
164    }
165
166    /// Runs `f` in a transaction and returns what it returns. The transaction commits when `f`
167    /// returns `Ok` and rolls back when it returns `Err`. A panic in `f` rolls it back too, since
168    /// nothing commits before `f` returns. Queries that must run inside the transaction take it
169    /// as `&mut *tx`.
170    ///
171    /// The closure is an async closure: `db.transaction(async |tx| { ... Ok(value) }).await`.
172    pub async fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
173    where
174        E: From<Error>,
175        F: AsyncFnOnce(&mut Transaction) -> Result<T, E>,
176    {
177        let mut tx = Transaction::begin(&self.pool).await?;
178        match f(&mut tx).await {
179            Ok(value) => {
180                tx.commit().await?;
181                Ok(value)
182            }
183            Err(error) => {
184                // The error from `f` is the one to report, so a failed rollback is ignored.
185                let _ = tx.rollback().await;
186                Err(error)
187            }
188        }
189    }
190
191    /// Closes every connection in the pool.
192    pub async fn close(&self) {
193        match &self.pool {
194            #[cfg(feature = "mysql")]
195            Pool::MySql(pool) => pool.close().await,
196            #[cfg(feature = "postgres")]
197            Pool::Postgres(pool) => pool.close().await,
198            #[cfg(feature = "sqlite")]
199            Pool::Sqlite(pool) => pool.close().await,
200        }
201    }
202}
203
204impl PoolOptions {
205    /// The sqlx pool options with these settings, for driver `DB`.
206    fn apply<DB: SqlxDatabase>(self) -> sqlx::pool::PoolOptions<DB> {
207        sqlx::pool::PoolOptions::<DB>::new()
208            .max_connections(self.max_connections)
209            .min_connections(self.min_connections)
210            .acquire_timeout(self.acquire_timeout)
211    }
212}