diff --git a/contrib/db_pools/README.md b/contrib/db_pools/README.md index 755974a9..b35e4161 100644 --- a/contrib/db_pools/README.md +++ b/contrib/db_pools/README.md @@ -3,58 +3,65 @@ [crates.io]: https://img.shields.io/crates/v/rocket_db_pools.svg [crate]: https://crates.io/crates/rocket_db_pools [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/master/rocket_db_pools +[crate docs]: https://api.rocket.rs/v0.5-rc/rocket_db_pools [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions -This crate provides traits, utilities, and a procedural macro for configuring -and accessing database connection pools in Rocket. +Asynchronous database driver integration for Rocket. See the [crate docs] for +full usage details. ## Usage -First, enable the feature corresponding to your database type: +1. Add `rocket_db_pools` as a dependency with one or more [database driver + features] enabled: -```toml -[dependencies.rocket_db_pools] -version = "0.1.0-dev" -features = ["sqlx_sqlite"] -``` + ```toml + [dependencies.rocket_db_pools] + version = "0.1.0-rc" + features = ["sqlx_sqlite"] + ``` -A full list of supported databases and their associated feature names is -available in the [crate docs]. In whichever configuration source you choose, -configure a `databases` dictionary with a key for each database, here -`sqlite_logs` in a TOML source: +2. Choose a name for your database, here `sqlite_logs`. [Configure] _at least_ a + URL for the database: -```toml -[default.databases] -sqlite_logs = { url = "/path/to/database.sqlite" } -``` + ```toml + [default.databases.sqlite_logs] + url = "/path/to/database.sqlite" + ``` -In your application's source code: +3. [Derive `Database`] for a unit type (`Logs` here) which + wraps the selected driver's [`Pool`] type and is decorated with + `#[database("name")]`. Attach `Type::init()` to your application's `Rocket` + to initialize the database pool: -```rust -#[macro_use] extern crate rocket; -use rocket::serde::json::Json; + ```rust + use rocket_db_pools::{Database, Connection}; -use rocket_db_pools::{Database, sqlx}; + #[derive(Database)] + #[database("sqlite_logs")] + struct Logs(sqlx::SqlitePool); -#[derive(Database)] -#[database("sqlite_logs")] -struct LogsDb(sqlx::SqlitePool); + #[launch] + fn rocket() -> _ { + rocket::build().attach(Logs::init()) + } + ``` -type LogsDbConn = ::Connection; +4. Use [`Connection`] as a request guard to retrieve an + active database connection: -#[get("/logs/")] -async fn get_logs(mut db: LogsDbConn, id: usize) -> Result>> { - let logs = sqlx::query!("SELECT text FROM logs;").execute(&mut *db).await?; + ```rust + #[get("/")] + async fn read(mut db: Connection, id: i64) -> Result { + sqlx::query!("SELECT content FROM logs WHERE id = ?", id) + .fetch_one(&mut *db) + .map_ok(|r| Log(r.content)) + .await + } + ``` - Ok(Json(logs)) -} - -#[launch] -fn rocket() -> _ { - rocket::build().attach(LogsDb::fairing()) -} -``` - -See the [crate docs] for full details. +[database driver features]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#supported-drivers +[`Pool`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#supported-drivers +[Configure]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#configuration +[Derive `Database`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/derive.Database.html +[`Connection`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/struct.Connection.html diff --git a/contrib/db_pools/codegen/Cargo.toml b/contrib/db_pools/codegen/Cargo.toml index be90e892..a9aa7315 100644 --- a/contrib/db_pools/codegen/Cargo.toml +++ b/contrib/db_pools/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_db_pools_codegen" -version = "0.1.0-dev" +version = "0.1.0-rc" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Procedural macros for rocket_db_pools." repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" @@ -15,3 +15,7 @@ proc-macro = true [dependencies] devise = "0.3" quote = "1" + +[dev-dependencies] +rocket = { path = "../../../core/lib", default-features = false } +rocket_db_pools = { path = "../lib", features = ["deadpool_postgres"] } diff --git a/contrib/db_pools/codegen/src/database.rs b/contrib/db_pools/codegen/src/database.rs index f3d168a8..52e5dbf6 100644 --- a/contrib/db_pools/codegen/src/database.rs +++ b/contrib/db_pools/codegen/src/database.rs @@ -2,7 +2,10 @@ use proc_macro::TokenStream; use devise::{DeriveGenerator, FromMeta, MapperBuild, Support, ValidatorBuild}; use devise::proc_macro2_diagnostics::SpanDiagnosticExt; -use devise::syn::{Fields, spanned::Spanned}; +use devise::syn::{self, spanned::Spanned}; + +const ONE_DATABASE_ATTR: &str = "missing `#[database(\"name\")]` attribute"; +const ONE_UNNAMED_FIELD: &str = "struct must have exactly one unnamed field"; #[derive(Debug, FromMeta)] struct DatabaseAttribute { @@ -10,61 +13,92 @@ struct DatabaseAttribute { name: String, } -const ONE_DATABASE_ATTR: &str = "`Database` derive requires exactly one \ - `#[database(\"\")] attribute`"; -const ONE_UNNAMED_FIELD: &str = "`Database` derive can only be applied to \ - structs with exactly one unnamed field"; - pub fn derive_database(input: TokenStream) -> TokenStream { DeriveGenerator::build_for(input, quote!(impl rocket_db_pools::Database)) .support(Support::TupleStruct) .validator(ValidatorBuild::new() - .struct_validate(|_, struct_| { - if struct_.fields.len() == 1 { + .struct_validate(|_, s| { + if s.fields.len() == 1 { Ok(()) } else { - return Err(struct_.fields.span().error(ONE_UNNAMED_FIELD)) + Err(s.fields.span().error(ONE_UNNAMED_FIELD)) } }) ) - .inner_mapper(MapperBuild::new() - .try_struct_map(|_, struct_| { - let krate = quote_spanned!(struct_.span() => ::rocket_db_pools); - let db_name = match DatabaseAttribute::one_from_attrs("database", &struct_.attrs)? { - Some(attr) => attr.name, - None => return Err(struct_.span().error(ONE_DATABASE_ATTR)), - }; - let fairing_name = format!("'{}' Database Pool", db_name); - - let pool_type = match &struct_.fields { - Fields::Unnamed(f) => &f.unnamed[0].ty, - _ => unreachable!("Support::TupleStruct"), - }; - - Ok(quote_spanned! { struct_.span() => - const NAME: &'static str = #db_name; - type Pool = #pool_type; - fn fairing() -> #krate::Fairing { - #krate::Fairing::new(#fairing_name) - } - fn pool(&self) -> &Self::Pool { &self.0 } - }) - }) - ) .outer_mapper(MapperBuild::new() - .try_struct_map(|_, struct_| { - let decorated_type = &struct_.ident; - let pool_type = match &struct_.fields { - Fields::Unnamed(f) => &f.unnamed[0].ty, + .struct_map(|_, s| { + let decorated_type = &s.ident; + let pool_type = match &s.fields { + syn::Fields::Unnamed(f) => &f.unnamed[0].ty, _ => unreachable!("Support::TupleStruct"), }; - Ok(quote_spanned! { struct_.span() => + quote_spanned! { s.span() => impl From<#pool_type> for #decorated_type { fn from(pool: #pool_type) -> Self { Self(pool) } } + + impl std::ops::Deref for #decorated_type { + type Target = #pool_type; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl std::ops::DerefMut for #decorated_type { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + #[rocket::async_trait] + impl<'r> rocket::request::FromRequest<'r> for &'r #decorated_type { + type Error = (); + + async fn from_request( + req: &'r rocket::request::Request<'_> + ) -> rocket::request::Outcome { + match #decorated_type::fetch(req.rocket()) { + Some(db) => rocket::outcome::Outcome::Success(db), + None => rocket::outcome::Outcome::Failure(( + rocket::http::Status::InternalServerError, ())) + } + } + } + + impl rocket::Sentinel for &#decorated_type { + fn abort(rocket: &rocket::Rocket) -> bool { + #decorated_type::fetch(rocket).is_none() + } + } + } + }) + ) + .outer_mapper(quote!(#[rocket::async_trait])) + .inner_mapper(MapperBuild::new() + .try_struct_map(|_, s| { + let db_name = DatabaseAttribute::one_from_attrs("database", &s.attrs)? + .map(|attr| attr.name) + .ok_or_else(|| s.span().error(ONE_DATABASE_ATTR))?; + + let fairing_name = format!("'{}' Database Pool", db_name); + + let pool_type = match &s.fields { + syn::Fields::Unnamed(f) => &f.unnamed[0].ty, + _ => unreachable!("Support::TupleStruct"), + }; + + Ok(quote_spanned! { s.span() => + type Pool = #pool_type; + + const NAME: &'static str = #db_name; + + fn init() -> rocket_db_pools::Initializer { + rocket_db_pools::Initializer::with_name(#fairing_name) + } }) }) ) diff --git a/contrib/db_pools/codegen/src/lib.rs b/contrib/db_pools/codegen/src/lib.rs index 9eb765f8..00c423ef 100644 --- a/contrib/db_pools/codegen/src/lib.rs +++ b/contrib/db_pools/codegen/src/lib.rs @@ -1,41 +1,61 @@ #![recursion_limit="256"] - #![warn(rust_2018_idioms)] -//! # `rocket_databases` - Code Generation +//! # `rocket_db_pool` - Code Generation //! -//! This crate implements the code generation portion of the `rocket_databases` -//! crate. +//! Implements the code generation portion of the `rocket_db_pool` crate. This +//! is an implementation detail. This create should never be depended on +//! directly. #[macro_use] extern crate quote; mod database; -use proc_macro::TokenStream; - -/// Defines a database type and implements [`Database`] on it. +/// Automatic derive for the [`Database`] trait. +/// +/// ```rust +/// use rocket_db_pools::Database; +/// # type PoolType = rocket_db_pools::deadpool_postgres::Pool; /// -/// ```ignore /// #[derive(Database)] /// #[database("database_name")] /// struct Db(PoolType); /// ``` /// -/// `PoolType` must implement [`Pool`]. +/// The derive generates an implementation of [`Database`] as follows: /// -/// This macro generates the following code, implementing the [`Database`] trait -/// on the struct. Custom implementations of `Database` should usually also -/// start with roughly this code: +/// * [`Database::NAME`] is set to the value in the `#[database("name")]` +/// attribute. /// -/// ```ignore -/// impl Database for Db { -/// const NAME: &'static str = "config_name"; -/// type Pool = PoolType; -/// fn fairing() -> Fairing { Fairing::new(|p| Self(p)) } -/// fn pool(&self) -> &Self::Pool { &self.0 } -/// } -/// ``` +/// This names the database, providing an anchor to configure the database via +/// `Rocket.toml` or any other configuration source. Specifically, the +/// configuration in `databases.name` is used to configure the driver. +/// +/// * [`Database::Pool`] is set to the wrapped type: `PoolType` above. The type +/// must implement [`Pool`]. +/// +/// To meet the required [`Database`] supertrait bounds, this derive also +/// generates implementations for: +/// +/// * `From` +/// +/// * `Deref` +/// +/// * `DerefMut` +/// +/// * `FromRequest<'_> for &Db` +/// +/// * `Sentinel for &Db` +/// +/// The `Deref` impls enable accessing the database pool directly from +/// references `&Db` or `&mut Db`. To force a dereference to the underlying +/// type, use `&db.0` or `&**db` or their `&mut` variants. +/// +/// [`Database`]: ../rocket_db_pools/trait.Database.html +/// [`Database::NAME`]: ../rocket_db_pools/trait.Database.html#associatedconstant.NAME +/// [`Database::Pool`]: ../rocket_db_pools/trait.Database.html#associatedtype.Pool +/// [`Pool`]: ../rocket_db_pools/trait.Pool.html #[proc_macro_derive(Database, attributes(database))] -pub fn derive_database(input: TokenStream) -> TokenStream { +pub fn derive_database(input: proc_macro::TokenStream) -> proc_macro::TokenStream { crate::database::derive_database(input) } diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 4b6cd23b..80c5e40e 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_db_pools" -version = "0.1.0-dev" +version = "0.1.0-rc" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Rocket async database pooling support" repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" @@ -9,32 +9,64 @@ keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" edition = "2018" +[package.metadata.docs.rs] +all-features = true + [features] -deadpool_postgres = ["deadpool-postgres"] -deadpool_redis = ["deadpool-redis"] +# deadpool features +deadpool_postgres = ["deadpool-postgres", "deadpool"] +deadpool_redis = ["deadpool-redis", "deadpool"] +# sqlx features sqlx_mysql = ["sqlx", "sqlx/mysql"] sqlx_postgres = ["sqlx", "sqlx/postgres"] sqlx_sqlite = ["sqlx", "sqlx/sqlite"] - -[dependencies] -rocket_db_pools_codegen = { path = "../codegen" } - -# integration-specific -deadpool-postgres = { version = "0.9", default-features = false, optional = true } -deadpool-redis = { version = "0.8", default-features = false, optional = true } -mongodb = { version = "1", default-features = false, features = ["tokio-runtime"], optional = true } -mysql_async = { version = "0.27", default-features = false, optional = true } -redis = { version = "0.20", default-features = false, features = ["aio", "tokio-comp"] } -sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls"], optional = true } +sqlx_mssql = ["sqlx", "sqlx/mssql"] +sqlx_macros = ["sqlx/macros"] +# implicit features: mongodb [dependencies.rocket] path = "../../../core/lib" +version = "0.5.0-rc.1" default-features = false -[package.metadata.docs.rs] -all-features = true +[dependencies.rocket_db_pools_codegen] +path = "../codegen" +version = "0.1.0-rc" + +[dependencies.deadpool] +version = "0.8" +default-features = false +features = ["rt_tokio_1", "managed"] +optional = true + +[dependencies.deadpool-postgres] +version = "0.9" +default-features = false +features = ["rt_tokio_1"] +optional = true + +[dependencies.deadpool-redis] +version = "0.8.1" +default-features = false +features = ["rt_tokio_1"] +optional = true + +[dependencies.mongodb] +version = "1" +default-features = false +features = ["tokio-runtime"] +optional = true + +[dependencies.sqlx] +version = "0.5" +default-features = false +features = ["runtime-tokio-rustls"] +optional = true [dev-dependencies.rocket] path = "../../../core/lib" default-features = false features = ["json"] + +[build-dependencies] +version_check = "0.9" diff --git a/contrib/db_pools/lib/src/config.rs b/contrib/db_pools/lib/src/config.rs index c4909cb1..fcbbc345 100644 --- a/contrib/db_pools/lib/src/config.rs +++ b/contrib/db_pools/lib/src/config.rs @@ -1,64 +1,83 @@ -use rocket::figment::{self, Figment, providers::Serialized}; use rocket::serde::{Deserialize, Serialize}; -use rocket::{Build, Rocket}; -/// A base `Config` for any `Pool` type. +/// Base configuration for all database drivers. /// -/// For the following configuration: +/// A dictionary matching this structure is extracted from the active +/// [`Figment`](crate::figment::Figment), scoped to `databases.name`, where +/// `name` is the name of the database, by the +/// [`Initializer`](crate::Initializer) fairing on ignition and used to +/// configure the relevant database and database pool. +/// +/// With the default provider, these parameters are typically configured in a +/// `Rocket.toml` file: /// /// ```toml -/// [global.databases.my_database] -/// url = "postgres://root:root@localhost/my_database" -/// pool_size = 10 +/// [default.databases.db_name] +/// url = "/path/to/db.sqlite" +/// +/// # only `url` is required. `Initializer` provides defaults for the rest. +/// min_connections = 64 +/// max_connections = 1024 +/// connect_timeout = 5 +/// idle_timeout = 120 /// ``` /// -/// ...the following struct would be passed to [`Pool::initialize()`]: +/// Alternatively, a custom provider can be used. For example, a custom `Figment` +/// with a global `databases.name` configuration: /// /// ```rust -/// # use rocket_db_pools::Config; -/// Config { -/// url: "postgres://root:root@localhost/my_database".into(), -/// pool_size: 10, -/// timeout: 5, -/// }; +/// # use rocket::launch; +/// #[launch] +/// fn rocket() -> _ { +/// let figment = rocket::Config::figment() +/// .merge(("databases.name", rocket_db_pools::Config { +/// url: "db:specific@config&url".into(), +/// min_connections: None, +/// max_connections: 1024, +/// connect_timeout: 3, +/// idle_timeout: None, +/// })); +/// +/// rocket::custom(figment) +/// } /// ``` /// -/// If you want to implement your own custom database adapter and need some more -/// configuration options, you may need to define a custom `Config` struct. -/// -/// [`Pool::initialize()`]: crate::Pool::initialize +/// For general information on configuration in Rocket, see [`rocket::config`]. +/// For higher-level details on configuring a database, see the [crate-level +/// docs](crate#configuration). #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(crate = "rocket::serde")] pub struct Config { - /// Connection URL specified in the Rocket configuration. + /// Database-specific connection and configuration URL. + /// + /// The format of the URL is database specific; consult your database's + /// documentation. pub url: String, - /// Initial pool size. Defaults to the number of Rocket workers * 4. - pub pool_size: u32, - /// How long to wait, in seconds, for a new connection before timing out. - /// Defaults to `5`. - // FIXME: Use `time`. - pub timeout: u8, -} - -impl Config { - pub fn from(db_name: &str, rocket: &Rocket) -> Result { - Self::figment(db_name, rocket).extract::() - } - - pub fn figment(db_name: &str, rocket: &Rocket) -> Figment { - let db_key = format!("databases.{}", db_name); - let default_pool_size = rocket.figment() - .extract_inner::(rocket::Config::WORKERS) - .map(|workers| workers * 4) - .ok(); - - let figment = Figment::from(rocket.figment()) - .focus(&db_key) - .join(Serialized::default("timeout", 5)); - - match default_pool_size { - Some(pool_size) => figment.join(Serialized::default("pool_size", pool_size)), - None => figment - } - } + /// Minimum number of connections to maintain in the pool. + /// + /// **Note:** `deadpool` drivers do not support and thus ignore this value. + /// + /// _Default:_ `None`. + pub min_connections: Option, + /// Maximum number of connections to maintain in the pool. + /// + /// _Default:_ `workers * 4`. + pub max_connections: usize, + /// Number of seconds to wait for a connection before timing out. + /// + /// If the timeout elapses before a connection can be made or retrieved from + /// a pool, an error is returned. + /// + /// _Default:_ `5`. + pub connect_timeout: u64, + /// Maximum number of seconds to keep a connection alive for. + /// + /// After a connection is established, it is maintained in a pool for + /// efficient connection retrieval. When an `idle_timeout` is set, that + /// connection will be closed after the timeout elapses. If an + /// `idle_timeout` is not specified, the behavior is driver specific but + /// typically defaults to keeping a connection active indefinitely. + /// + /// _Default:_ `None`. + pub idle_timeout: Option, } diff --git a/contrib/db_pools/lib/src/database.rs b/contrib/db_pools/lib/src/database.rs index 2aa461b1..e6a21c76 100644 --- a/contrib/db_pools/lib/src/database.rs +++ b/contrib/db_pools/lib/src/database.rs @@ -1,119 +1,279 @@ -use rocket::fairing::{Info, Kind}; -use rocket::futures::future::BoxFuture; -use rocket::http::Status; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; + +use rocket::{error, info_, Build, Ignite, Phase, Rocket, Sentinel}; +use rocket::fairing::{self, Fairing, Info, Kind}; use rocket::request::{FromRequest, Outcome, Request}; +use rocket::http::Status; + use rocket::yansi::Paint; -use rocket::{Build, Ignite, Rocket, Sentinel}; +use rocket::figment::providers::Serialized; -use crate::{Error, Pool}; +use crate::Pool; -/// Trait implemented to define a database connection pool. -pub trait Database: Sized + Send + Sync + 'static { - /// The name of this connection pool in the configuration. - const NAME: &'static str; - - /// The underlying connection type returned by this pool. - /// Must implement [`Pool`]. +/// Derivable trait which ties a database [`Pool`] with a configuration name. +/// +/// This trait should rarely, if ever, be implemented manually. Instead, it +/// should be derived: +/// +/// ```rust +/// # #[cfg(feature = "deadpool_redis")] mod _inner { +/// # use rocket::launch; +/// use rocket_db_pools::{deadpool_redis, Database}; +/// +/// #[derive(Database)] +/// #[database("memdb")] +/// struct Db(deadpool_redis::Pool); +/// +/// #[launch] +/// fn rocket() -> _ { +/// rocket::build().attach(Db::init()) +/// } +/// # } +/// ``` +/// +/// See the [`Database` derive](derive@crate::Database) for details. +pub trait Database: From + DerefMut + Send + Sync + 'static { + /// The [`Pool`] type of connections to this database. + /// + /// When `Database` is derived, this takes the value of the `Inner` type in + /// `struct Db(Inner)`. type Pool: Pool; - /// Returns a fairing that attaches this connection pool to the server. - fn fairing() -> Fairing; + /// The configuration name for this database. + /// + /// When `Database` is derived, this takes the value `"name"` in the + /// `#[database("name")]` attribute. + const NAME: &'static str; - /// Direct shared access to the underlying database pool - fn pool(&self) -> &Self::Pool; + /// Returns a fairing that initializes the database and its connection pool. + /// + /// # Example + /// + /// ```rust + /// # #[cfg(feature = "deadpool_postgres")] mod _inner { + /// # use rocket::launch; + /// use rocket_db_pools::{deadpool_postgres, Database}; + /// + /// #[derive(Database)] + /// #[database("pg_db")] + /// struct Db(deadpool_postgres::Pool); + /// + /// #[launch] + /// fn rocket() -> _ { + /// rocket::build().attach(Db::init()) + /// } + /// # } + /// ``` + fn init() -> Initializer { + Initializer::new() + } - /// get().await returns a connection from the pool (or an error) - fn get(&self) -> BoxFuture<'_, Result, ::GetError>> { - Box::pin(async move { self.pool().get().await.map(Connection)} ) + /// Returns a reference to the initialized database in `rocket`. The + /// initializer fairing returned by `init()` must have already executed for + /// `Option` to be `Some`. This is guaranteed to be the case if the fairing + /// is attached and either: + /// + /// * Rocket is in the [`Orbit`](rocket::Orbit) phase. That is, the + /// application is running. This is always the case in request guards + /// and liftoff fairings, + /// * _or_ Rocket is in the [`Build`](rocket::Build) or + /// [`Ignite`](rocket::Ignite) phase and the `Initializer` fairing has + /// already been run. This is the case in all fairing callbacks + /// corresponding to fairings attached _after_ the `Initializer` + /// fairing. + /// + /// # Example + /// + /// Run database migrations in an ignite fairing. It is imperative that the + /// migration fairing be registered _after_ the `init()` fairing. + /// + /// ```rust + /// # #[cfg(feature = "sqlx_sqlite")] mod _inner { + /// # use rocket::launch; + /// use rocket::{Rocket, Build}; + /// use rocket::fairing::{self, AdHoc}; + /// + /// use rocket_db_pools::{sqlx, Database}; + /// + /// #[derive(Database)] + /// #[database("sqlite_db")] + /// struct Db(sqlx::SqlitePool); + /// + /// async fn run_migrations(rocket: Rocket) -> fairing::Result { + /// if let Some(db) = Db::fetch(&rocket) { + /// // run migrations using `db`. get the inner type with &db.0. + /// Ok(rocket) + /// } else { + /// Err(rocket) + /// } + /// } + /// + /// #[launch] + /// fn rocket() -> _ { + /// rocket::build() + /// .attach(Db::init()) + /// .attach(AdHoc::try_on_ignite("DB Migrations", run_migrations)) + /// } + /// # } + /// ``` + fn fetch(rocket: &Rocket

) -> Option<&Self> { + if let Some(db) = rocket.state() { + return Some(db); + } + + let dbtype = std::any::type_name::(); + let fairing = Paint::default(format!("{}::init()", dbtype)).bold(); + error!("Attempted to fetch unattached database `{}`.", Paint::default(dbtype).bold()); + info_!("`{}` fairing must be attached prior to using this database.", fairing); + None } } -/// A connection. The underlying connection type is determined by `D`, which -/// must implement [`Database`]. +/// A [`Fairing`] which initializes a [`Database`] and its connection pool. +/// +/// A value of this type can be created for any type `D` that implements +/// [`Database`] via the [`Database::init()`] method on the type. Normally, a +/// value of this type _never_ needs to be constructed directly. This +/// documentation exists purely as a reference. +/// +/// This fairing initializes a database pool. Specifically, it: +/// +/// 1. Reads the configuration at `database.db_name`, where `db_name` is +/// [`Database::NAME`]. +/// +/// 2. Sets [`Config`](crate::Config) defaults on the configuration figment. +/// +/// 3. Calls [`Pool::init()`]. +/// +/// 4. Stores the database instance in managed storage, retrievable via +/// [`Database::fetch()`]. +/// +/// The name of the fairing itself is `Initializer`, with `D` replaced with +/// the type name `D` unless a name is explicitly provided via +/// [`Self::with_name()`]. +pub struct Initializer(Option<&'static str>, PhantomData D>); + +/// A request guard which retrieves a single connection to a [`Database`]. +/// +/// For a database type of `Db`, a request guard of `Connection` retrieves a +/// single connection to `Db`. +/// +/// The request guard succeeds if the database was initialized by the +/// [`Initializer`] fairing and a connection is available within +/// [`connect_timeout`](crate::Config::connect_timeout) seconds. +/// * If the `Initializer` fairing was _not_ attached, the guard _fails_ with +/// status `InternalServerError`. A [`Sentinel`] guards this condition, and so +/// this type of failure is unlikely to occur. A `None` error is returned. +/// * If a connection is not available within `connect_timeout` seconds or +/// another error occurs, the gaurd _fails_ with status `ServiceUnavailable` +/// and the error is returned in `Some`. +/// +/// ## Deref +/// +/// A type of `Connection` dereferences, mutably and immutably, to the +/// native database connection type. The [driver table](crate#supported-drivers) +/// lists the concrete native `Deref` types. +/// +/// # Example +/// +/// ```rust +/// # #[cfg(feature = "sqlx_sqlite")] mod _inner { +/// # use rocket::get; +/// # type Pool = rocket_db_pools::sqlx::SqlitePool; +/// use rocket_db_pools::{Database, Connection}; +/// +/// #[derive(Database)] +/// #[database("db")] +/// struct Db(Pool); +/// +/// #[get("/")] +/// async fn db_op(db: Connection) { +/// // use `&*db` to get an immutable borrow to the native connection type +/// // use `&mut *db` to get a mutable borrow to the native connection type +/// } +/// # } +/// ``` pub struct Connection(::Connection); -impl std::ops::Deref for Connection { - type Target = ::Connection; - fn deref(&self) -> &Self::Target { - &self.0 +impl Initializer { + /// Returns a database initializer fairing for `D`. + /// + /// This method should never need to be called manually. See the [crate + /// docs](crate) for usage information. + pub fn new() -> Self { + Self(None, std::marker::PhantomData) + } + + /// Returns a database initializer fairing for `D` with name `name`. + /// + /// This method should never need to be called manually. See the [crate + /// docs](crate) for usage information. + pub fn with_name(name: &'static str) -> Self { + Self(Some(name), std::marker::PhantomData) } } -impl std::ops::DerefMut for Connection { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 +#[rocket::async_trait] +impl Fairing for Initializer { + fn info(&self) -> Info { + Info { + name: self.0.unwrap_or(std::any::type_name::()), + kind: Kind::Ignite, + } + } + + async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { + let workers: usize = rocket.figment() + .extract_inner(rocket::Config::WORKERS) + .unwrap_or_else(|_| rocket::Config::default().workers); + + let figment = rocket.figment() + .focus(&format!("databases.{}", D::NAME)) + .merge(Serialized::default("max_connections", workers * 4)) + .merge(Serialized::default("connect_timeout", 5)); + + match ::init(&figment).await { + Ok(pool) => Ok(rocket.manage(D::from(pool))), + Err(e) => { + error!("failed to initialize database: {}", e); + Err(rocket) + } + } } } #[rocket::async_trait] impl<'r, D: Database> FromRequest<'r> for Connection { - type Error = Error<::GetError>; + type Error = Option<::Error>; async fn from_request(req: &'r Request<'_>) -> Outcome { - let db: &D = match req.rocket().state() { - Some(p) => p, - _ => { - let dbtype = Paint::default(std::any::type_name::()).bold(); - let fairing = Paint::default(format!("{}::fairing()", dbtype)).wrap().bold(); - error!("requesting `{}` DB connection without attaching `{}`.", dbtype, fairing); - info_!("Attach `{}` to use database connection pooling.", fairing); - return Outcome::Failure((Status::InternalServerError, Error::UnattachedFairing)); - } - }; - - match db.pool().get().await { - Ok(conn) => Outcome::Success(Connection(conn)), - Err(e) => Outcome::Failure((Status::ServiceUnavailable, Error::Db(e))), + match D::fetch(req.rocket()) { + Some(db) => match db.get().await { + Ok(conn) => Outcome::Success(Connection(conn)), + Err(e) => Outcome::Failure((Status::ServiceUnavailable, Some(e))), + }, + None => Outcome::Failure((Status::InternalServerError, None)), } } } impl Sentinel for Connection { fn abort(rocket: &Rocket) -> bool { - if rocket.state::().is_none() { - let dbtype = Paint::default(std::any::type_name::()).bold(); - let fairing = Paint::default(format!("{}::fairing()", dbtype)).wrap().bold(); - error!("requesting `{}` DB connection without attaching `{}`.", dbtype, fairing); - info_!("Attach `{}` to use database connection pooling.", fairing); - return true; - } - - false + D::fetch(rocket).is_none() } } -/// The database fairing for pool types created with the `pool!` macro. -pub struct Fairing(&'static str, std::marker::PhantomData); +impl Deref for Connection { + type Target = ::Connection; -impl> Fairing { - /// Create a new database fairing with the given constructor. This - /// constructor will be called to create an instance of `D` after the pool - /// is initialized and before it is placed into managed state. - pub fn new(fairing_name: &'static str) -> Self { - Self(fairing_name, std::marker::PhantomData) + fn deref(&self) -> &Self::Target { + &self.0 } } -#[rocket::async_trait] -impl> rocket::fairing::Fairing for Fairing { - fn info(&self) -> Info { - Info { - name: self.0, - kind: Kind::Ignite, - } - } - - async fn on_ignite(&self, rocket: Rocket) -> Result, Rocket> { - let pool = match ::initialize(D::NAME, &rocket).await { - Ok(p) => p, - Err(e) => { - error!("error initializing database connection pool: {}", e); - return Err(rocket); - } - }; - - let db: D = pool.into(); - - Ok(rocket.manage(db)) +impl DerefMut for Connection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } diff --git a/contrib/db_pools/lib/src/error.rs b/contrib/db_pools/lib/src/error.rs index b8b3a77b..69bae106 100644 --- a/contrib/db_pools/lib/src/error.rs +++ b/contrib/db_pools/lib/src/error.rs @@ -1,41 +1,35 @@ use std::fmt; -use rocket::figment; - -/// A general error type designed for the `Poolable` trait. -/// -/// [`Pool::initialize`] can return an error for any of several reasons: -/// -/// * Missing or incorrect configuration, including some syntax errors -/// * An error connecting to the database. -/// -/// [`Pool::initialize`]: crate::Pool::initialize +/// A general error type for use by [`Pool`](crate::Pool#implementing) +/// implementors and returned by the [`Connection`](crate::Connection) request +/// guard. #[derive(Debug)] -pub enum Error { - /// A database-specific error occurred - Db(E), +pub enum Error { + /// An error that occured during database/pool initialization. + Init(A), - /// An error occurred in the configuration - Figment(figment::Error), + /// An error that ocurred while retrieving a connection from the pool. + Get(B), - /// Required fairing was not attached - UnattachedFairing, + /// A [`Figment`](crate::figment::Figment) configuration error. + Config(crate::figment::Error), } -impl fmt::Display for Error { +impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Error::Db(e) => e.fmt(f), - Error::Figment(e) => write!(f, "bad configuration: {}", e), - Error::UnattachedFairing => write!(f, "required database fairing was not attached"), + Error::Init(e) => write!(f, "failed to initialize database: {}", e), + Error::Get(e) => write!(f, "failed to get db connection: {}", e), + Error::Config(e) => write!(f, "bad configuration: {}", e), } } } -impl std::error::Error for Error {} +impl std::error::Error for Error + where A: fmt::Debug + fmt::Display, B: fmt::Debug + fmt::Display {} -impl From for Error { - fn from(e: figment::Error) -> Self { - Self::Figment(e) +impl From for Error { + fn from(e: crate::figment::Error) -> Self { + Self::Config(e) } } diff --git a/contrib/db_pools/lib/src/lib.rs b/contrib/db_pools/lib/src/lib.rs index d8f8d67a..f10b6476 100644 --- a/contrib/db_pools/lib/src/lib.rs +++ b/contrib/db_pools/lib/src/lib.rs @@ -1,382 +1,238 @@ -//! Traits, utilities, and a macro for easy database connection pooling. +//! Asynchronous database driver connection pooling integration for Rocket. //! -//! # Overview +//! # Quickstart //! -//! This crate provides traits, utilities, and a procedural macro for -//! configuring and accessing database connection pools in Rocket. A _database -//! connection pool_ is a data structure that maintains active database -//! connections for later use in the application. +//! 1. Add `rocket_db_pools` as a dependency with one or more [database driver +//! features](#supported-drivers) enabled: //! -//! Databases are individually configured through Rocket's regular configuration -//! mechanisms. Connecting a Rocket application to a database using this library -//! occurs in three simple steps: +//! ```toml +//! [dependencies.rocket_db_pools] +//! version = "0.1.0-rc" +//! features = ["sqlx_sqlite"] +//! ``` //! -//! 1. Configure your databases in `Rocket.toml`. -//! (see [Configuration](#configuration)) -//! 2. Associate a Database type and fairing with each database. -//! (see [Guard Types](#guard-types)) -//! 3. Use the request guard to retrieve a connection in a handler. -//! (see [Handlers](#handlers)) +//! 2. Choose a name for your database, here `sqlite_logs`. +//! [Configure](#configuration) _at least_ a URL for the database: //! -//! For a list of supported databases, see [Provided Databases](#provided). This -//! support can be easily extended by implementing the [`Pool`] trait. See -//! [Extending](#extending) for more. +//! ```toml +//! [default.databases.sqlite_logs] +//! url = "/path/to/database.sqlite" +//! ``` //! -//! ## Example +//! 3. [Derive](derive@Database) [`Database`] for a unit type (`Logs` here) +//! which wraps the selected driver's [`Pool`] type (see [the driver +//! table](#supported-drivers)) and is decorated with `#[database("name")]`. +//! Attach `Type::init()` to your application's `Rocket` to initialize the +//! database pool: //! -//! Before using this library, the feature corresponding to your database type -//! in `rocket_db_pools` must be enabled: +//! ```rust +//! # #[cfg(feature = "sqlx_sqlite")] mod _inner { +//! # use rocket::launch; +//! use rocket_db_pools::{sqlx, Database}; +//! +//! #[derive(Database)] +//! #[database("sqlite_logs")] +//! struct Logs(sqlx::SqlitePool); +//! +//! #[launch] +//! fn rocket() -> _ { +//! rocket::build().attach(Logs::init()) +//! } +//! # } +//! ``` +//! +//! 4. Use [`Connection`](Connection) as a request guard to retrieve an +//! active database connection, which dereferences to the native type in the +//! [`Connection` deref](#supported-drivers) column. +//! +//! ```rust +//! # #[cfg(feature = "sqlx_sqlite")] mod _inner { +//! # use rocket::{get, response::Responder}; +//! # use rocket_db_pools::{sqlx, Database}; +//! # #[derive(Database)] +//! # #[database("sqlite_logs")] +//! # struct Logs(sqlx::SqlitePool); +//! # +//! # #[derive(Responder)] +//! # struct Log(String); +//! # +//! use rocket_db_pools::Connection; +//! use rocket_db_pools::sqlx::Row; +//! +//! #[get("/")] +//! async fn read(mut db: Connection, id: i64) -> Option { +//! sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id) +//! .fetch_one(&mut *db).await +//! .and_then(|r| Ok(Log(r.try_get(0)?))) +//! .ok() +//! } +//! # } +//! ``` +//! +//! Alternatively, use a reference to the database type as a request guard to +//! retrieve the entire pool, but note that unlike retrieving a `Connection`, +//! doing so does _not_ guarantee that a connection is available: +//! +//! ```rust +//! # #[cfg(feature = "sqlx_sqlite")] mod _inner { +//! # use rocket::{get, response::Responder}; +//! # use rocket_db_pools::{sqlx, Database}; +//! # #[derive(Database)] +//! # #[database("sqlite_logs")] +//! # struct Logs(sqlx::SqlitePool); +//! # +//! # #[derive(Responder)] +//! # struct Log(String); +//! # +//! use rocket_db_pools::sqlx::Row; +//! +//! #[get("/")] +//! async fn read(db: &Logs, id: i64) -> Option { +//! sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id) +//! .fetch_one(&db.0).await +//! .and_then(|r| Ok(Log(r.try_get(0)?))) +//! .ok() +//! } +//! # } +//! ``` +//! +//! # Supported Drivers +//! +//! At present, this crate supports _three_ drivers: [`deadpool`], [`sqlx`], +//! and [`mongodb`]. Each driver may support multiple databases. +//! +//! ## `deadpool` (v0.8) +//! +//! | Database | Feature | [`Pool`] Type | [`Connection`] Deref | +//! |----------|---------------------|-----------------------------|---------------------------------------| +//! | Postgres | `deadpool_postgres` | [`deadpool_postgres::Pool`] | [`deadpool_postgres::ClientWrapper`] | +//! | Redis | `deadpool_redis` | [`deadpool_redis::Pool`] | [`deadpool_redis::ConnectionWrapper`] | +//! +//! ## `sqlx` (v0.5) +//! +//! | Database | Feature | [`Pool`] Type | [`Connection`] Deref | +//! |----------|-----------------|----------------------|------------------------------------| +//! | Postgres | `sqlx_postgres` | [`sqlx::PgPool`] | [`sqlx::PoolConnection`] | +//! | MySQL | `sqlx_mysql` | [`sqlx::MySqlPool`] | [`sqlx::PoolConnection`] | +//! | SQLite | `sqlx_sqlite` | [`sqlx::SqlitePool`] | [`sqlx::PoolConnection`] | +//! | MSSQL | `sqlx_mssql` | [`sqlx::MssqlPool`] | [`sqlx::PoolConnection`] | +//! +//! [`sqlx::PgPool`]: https://docs.rs/sqlx/0.5/sqlx/type.PgPool.html +//! [`sqlx::MySqlPool`]: https://docs.rs/sqlx/0.5/sqlx/type.MySqlPool.html +//! [`sqlx::SqlitePool`]: https://docs.rs/sqlx/0.5/sqlx/type.SqlitePool.html +//! [`sqlx::MssqlPool`]: https://docs.rs/sqlx/0.5/sqlx/type.MssqlPool.html +//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.5/sqlx/pool/struct.PoolConnection.html +//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.5/sqlx/pool/struct.PoolConnection.html +//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.5/sqlx/pool/struct.PoolConnection.html +//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.5/sqlx/pool/struct.PoolConnection.html +//! +//! ## `mongodb` (v1) +//! +//! | Database | Feature | [`Pool`] Type and [`Connection`] Deref | +//! |----------|-----------|----------------------------------------| +//! | MongoDB | `mongodb` | [`mongodb::Client`] | +//! +//! ## Enabling Additional Driver Features +//! +//! Only the minimal features for each driver crate are enabled by +//! `rocket_db_pools`. To use additional driver functionality exposed via its +//! crate's features, you'll need to depend on the crate directly with those +//! features enabled in `Cargo.toml`: //! //! ```toml +//! [dependencies.sqlx] +//! version = "0.5" +//! default-features = false +//! features = ["macros", "offline", "migrate"] +//! //! [dependencies.rocket_db_pools] -//! version = "0.1.0-dev" +//! version = "0.1.0-rc" //! features = ["sqlx_sqlite"] //! ``` //! -//! See [Provided](#provided) for a list of supported database and their -//! associated feature name. +//! # Configuration //! -//! In whichever configuration source you choose, configure a `databases` -//! dictionary with an internal dictionary for each database, here `sqlite_logs` -//! in a TOML source: +//! Configuration for a database named `db_name` is deserialized from a +//! `databases.db_name` configuration parameter into a [`Config`] structure via +//! Rocket's [configuration facilities](rocket::config). By default, +//! configuration can be provided in `Rocket.toml`: //! //! ```toml -//! [default.databases] -//! sqlite_logs = { url = "/path/to/database.sqlite" } +//! [default.databases.db_name] +//! url = "db.sqlite" +//! +//! # only `url` is required. the rest have defaults and are thus optional +//! min_connections = 64 +//! max_connections = 1024 +//! connect_timeout = 5 +//! idle_timeout = 120 //! ``` //! -//! In your application's source code, one-time: +//! Or via environment variables: //! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[cfg(feature = "sqlx_sqlite")] -//! # mod test { -//! use rocket_db_pools::{Database, Connection, sqlx}; -//! -//! #[derive(Database)] -//! #[database("sqlite_logs")] -//! struct LogsDb(sqlx::SqlitePool); -//! -//! type LogsDbConn = Connection; -//! -//! #[launch] -//! fn rocket() -> _ { -//! rocket::build().attach(LogsDb::fairing()) -//! } -//! # } fn main() {} +//! ```sh +//! ROCKET_DATABASES='{db_name={url="db.sqlite",idle_timeout=120}}' //! ``` //! -//! These steps can be repeated as many times as necessary to configure -//! multiple databases. +//! See [`Config`] for details on configuration parameters. //! -//! Whenever a connection to the database is needed: +//! **Note:** `deadpool` drivers do not support and thus ignore the +//! `min_connections` value. //! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_db_pools; -//! # -//! # #[cfg(feature = "sqlx_sqlite")] -//! # mod test { -//! # use rocket::serde::json::Json; -//! # use rocket_db_pools::{Database, Connection, sqlx}; -//! # -//! # #[derive(Database)] -//! # #[database("sqlite_logs")] -//! # struct LogsDb(sqlx::SqlitePool); -//! # type LogsDbConn = Connection; -//! # -//! # type Result = std::result::Result; -//! # -//! #[get("/logs/")] -//! async fn get_logs(conn: LogsDbConn, id: usize) -> Result>> { -//! # /* -//! let logs = sqlx::query!().await?; -//! Ok(Json(logs)) -//! # */ -//! # Ok(Json(vec![])) -//! } -//! # } fn main() {} -//! ``` +//! ## Driver Defaults //! -//! # Usage +//! Some drivers provide configuration defaults different from the underyling +//! database's defaults. A best-effort attempt is made to document those +//! differences below: //! -//! ## Configuration +//! * `sqlx_sqlite` //! -//! Databases can be configured as any other values. Using the default -//! configuration provider, either via `Rocket.toml` or environment variables. -//! You can also use a custom provider. +//! - foreign keys : `enabled` +//! - journal mode : `WAL` +//! - create-missing : `enabled` +//! - synchronous : `full` (even when `WAL`) +//! - busy timeout : `connection_timeout` //! -//! ### `Rocket.toml` +//! * `sqlx_postgres` //! -//! To configure a database via `Rocket.toml`, add a table for each database -//! to the `databases` table where the key is a name of your choice. The table -//! should have a `url` key and, optionally, a `pool_size` key. This looks as -//! follows: +//! - sslmode : `prefer` +//! - statement-cache-capacity : `100` +//! - user : result of `whoami` //! -//! ```toml -//! # Option 1: -//! [global.databases] -//! sqlite_db = { url = "db.sqlite" } +//! * `sqlx_mysql` //! -//! # Option 2: -//! [global.databases.my_db] -//! url = "postgres://root:root@localhost/my_db" +//! - sslmode : `PREFERRED` +//! - statement-cache-capacity : `100` //! -//! # With a `pool_size` key: -//! [global.databases] -//! sqlite_db = { url = "db.sqlite", pool_size = 20 } -//! ``` +//! # Extending //! -//! Most databases use the default [`Config`] type, for which one key is required: -//! -//! * `url` - the URl to the database -//! -//! And one optional key is accepted: -//! -//! * `pool_size` - the size of the pool, i.e., the number of connections to -//! pool (defaults to the configured number of workers * 4) -//! TODO: currently ignored by most `Pool` implementations. -//! -//! Different options may be required or supported by other adapters, according -//! to the type specified by [`Pool::Config`]. -//! -//! ### Procedurally -//! -//! Databases can also be configured procedurally via `rocket::custom()`. -//! The example below does just this: -//! -//! ```rust -//! # #[cfg(feature = "sqlx_sqlite")] { -//! # use rocket::launch; -//! use rocket::figment::{value::{Map, Value}, util::map}; -//! -//! #[launch] -//! fn rocket() -> _ { -//! let db: Map<_, Value> = map! { -//! "url" => "db.sqlite".into(), -//! "pool_size" => 10.into() -//! }; -//! -//! let figment = rocket::Config::figment() -//! .merge(("databases", map!["my_db" => db])); -//! -//! rocket::custom(figment) -//! } -//! # rocket(); -//! # } -//! ``` -//! -//! ### Environment Variables -//! -//! Lastly, databases can be configured via environment variables by specifying -//! the `databases` table as detailed in the [Environment Variables -//! configuration -//! guide](https://rocket.rs/master/guide/configuration/#environment-variables): -//! -//! ```bash -//! ROCKET_DATABASES='{my_db={url="db.sqlite"}}' -//! ``` -//! -//! Multiple databases can be specified in the `ROCKET_DATABASES` environment variable -//! as well by comma separating them: -//! -//! ```bash -//! ROCKET_DATABASES='{my_db={url="db.sqlite"},my_pg_db={url="postgres://root:root@localhost/my_pg_db"}}' -//! ``` -//! -//! ## Database Types -//! -//! Once a database has been configured, the `#[derive(Database)]` macro can be -//! used to tie a type in your application to a configured database. The derive -//! accepts a single attribute, `#[database("name")]` that indicates the -//! name of the database. This corresponds to the database name set as the -//! database's configuration key. -//! -//! The [`Database`] trait provides a method, `fairing()`, which places an -//! instance of the decorated type in managed state; thus, the database pool can -//! be accessed with a `&State` request guard. -//! -//! The [`Connection`] type also implements [`FromRequest`], allowing it to be -//! used as a request guard. This implementation retrieves a connection from the -//! database pool or fails with a `Status::ServiceUnavailable` if connecting to -//! the database fails or times out. -//! -//! The derive can only be applied to unit-like structs with one type. The -//! internal type of the structure must implement [`Pool`]. -//! -//! ```rust -//! # #[macro_use] extern crate rocket_db_pools; -//! # #[cfg(feature = "sqlx_sqlite")] -//! # mod test { -//! use rocket_db_pools::{Database, sqlx}; -//! -//! #[derive(Database)] -//! #[database("my_db")] -//! struct MyDatabase(sqlx::SqlitePool); -//! # } -//! ``` -//! -//! Other databases can be used by specifying their respective [`Pool`] type: -//! -//! ```rust -//! # #[macro_use] extern crate rocket_db_pools; -//! # #[cfg(feature = "deadpool_postgres")] -//! # mod test { -//! use rocket_db_pools::{Database, deadpool_postgres}; -//! -//! #[derive(Database)] -//! #[database("my_pg_db")] -//! struct MyPgDatabase(deadpool_postgres::Pool); -//! # } -//! ``` -//! -//! The fairing returned from the `fairing()` method _must_ be attached for the -//! request guards to succeed. Putting the pieces together, a use of -//! `#[derive(Database)]` looks as follows: -//! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_db_pools; -//! # -//! # #[cfg(feature = "sqlx_sqlite")] { -//! # use rocket::figment::{value::{Map, Value}, util::map}; -//! use rocket_db_pools::{Database, sqlx}; -//! -//! #[derive(Database)] -//! #[database("my_db")] -//! struct MyDatabase(sqlx::SqlitePool); -//! -//! #[launch] -//! fn rocket() -> _ { -//! # let db: Map<_, Value> = map![ -//! # "url" => "db.sqlite".into(), "pool_size" => 10.into() -//! # ]; -//! # let figment = rocket::Config::figment().merge(("databases", map!["my_db" => db])); -//! rocket::custom(figment).attach(MyDatabase::fairing()) -//! } -//! # } -//! ``` -//! -//! ## Handlers -//! -//! Finally, access your type via `State` in a handler to access -//! the database connection pool: -//! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_db_pools; -//! # -//! # #[cfg(feature = "sqlx_sqlite")] -//! # mod test { -//! # use rocket_db_pools::{Database, Connection, sqlx}; -//! use rocket::State; -//! -//! #[derive(Database)] -//! #[database("my_db")] -//! struct MyDatabase(sqlx::SqlitePool); -//! -//! #[get("/")] -//! fn my_handler(conn: &State) { -//! // ... -//! } -//! # } -//! ``` -//! -//! Alternatively, access a single connection directly via the `Connection` -//! request guard: -//! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_db_pools; -//! # -//! # #[cfg(feature = "sqlx_sqlite")] -//! # mod test { -//! # use rocket_db_pools::{Database, Connection, sqlx}; -//! # type Data = (); -//! #[derive(Database)] -//! #[database("my_db")] -//! struct MyDatabase(sqlx::SqlitePool); -//! -//! type MyConnection = Connection; -//! -//! async fn load_from_db(conn: &mut sqlx::SqliteConnection) -> Data { -//! // Do something with connection, return some data. -//! # () -//! } -//! -//! #[get("/")] -//! async fn my_handler(mut conn: MyConnection) -> Data { -//! load_from_db(&mut conn).await -//! } -//! # } -//! ``` -//! -//! # Database Support -//! -//! Built-in support is provided for many popular databases and drivers. Support -//! can be easily extended by [`Pool`] implementations. -//! -//! ## Provided -//! -//! The list below includes all presently supported database adapters and their -//! corresponding [`Pool`] type. -//! -// Note: Keep this table in sync with site/guite/6-state.md -//! | Kind | Driver | Version | `Pool` Type | Feature | -//! |----------|-----------------------|-----------|--------------------------------|------------------------| -//! | MySQL | [sqlx] | `0.5` | [`sqlx::MySqlPool`] | `sqlx_mysql` | -//! | Postgres | [sqlx] | `0.5` | [`sqlx::PgPool`] | `sqlx_postgres` | -//! | Sqlite | [sqlx] | `0.5` | [`sqlx::SqlitePool`] | `sqlx_sqlite` | -//! | Mongodb | [mongodb] | `2.0.0-beta` | [`mongodb::Client`] | `mongodb` | -//! | MySQL | [mysql_async] | `0.27` | [`mysql_async::Pool`] | `mysql_async` | -//! | Postgres | [deadpool-postgres] | `0.8` | [`deadpool_postgres::Pool`] | `deadpool_postgres` | -//! | Redis | [deadpool-redis] | `0.8` | [`deadpool_redis::Pool`] | `deadpool_redis` | -//! -//! [sqlx]: https://docs.rs/sqlx/0.5/sqlx/ -//! [deadpool-postgres]: https://docs.rs/deadpool-postgres/0.8/deadpool_postgres/ -//! [deadpool-redis]: https://docs.rs/deadpool-redis/0.8/deadpool_redis/ -//! [mongodb]: https://docs.rs/mongodb/2.0.0-beta/mongodb/index.html -//! [mysql_async]: https://docs.rs/mysql_async/0.27/mysql_async/ -//! -//! The above table lists all the supported database adapters in this library. -//! In order to use particular `Pool` type that's included in this library, -//! you must first enable the feature listed in the "Feature" column. The -//! interior type of your decorated database type should match the type in the -//! "`Pool` Type" column. -//! -//! ## Extending -//! -//! Extending Rocket's support to your own custom database adapter is as easy as -//! implementing the [`Pool`] trait. See the documentation for [`Pool`] -//! for more details on how to implement it. -//! -//! [`FromRequest`]: rocket::request::FromRequest -//! [request guards]: rocket::request::FromRequest -//! [`Database`]: crate::Database -//! [`Pool`]: crate::Pool +//! Any database driver can implement support for this libary by implementing +//! the [`Pool`] trait. #![doc(html_root_url = "https://api.rocket.rs/master/rocket_db_pools")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] -#[doc(hidden)] -#[macro_use] -pub extern crate rocket; +#![deny(missing_docs)] +/// Re-export of the `figment` crate. +#[doc(inline)] +pub use rocket::figment; + +pub use rocket; #[cfg(feature = "deadpool_postgres")] pub use deadpool_postgres; #[cfg(feature = "deadpool_redis")] pub use deadpool_redis; -#[cfg(feature = "mysql_async")] pub use mysql_async; #[cfg(feature = "mongodb")] pub use mongodb; #[cfg(feature = "sqlx")] pub use sqlx; -mod config; mod database; mod error; mod pool; +mod config; -pub use self::config::Config; -pub use self::database::{Connection, Database, Fairing}; +pub use self::database::{Connection, Database, Initializer}; pub use self::error::Error; pub use self::pool::Pool; +pub use self::config::Config; pub use rocket_db_pools_codegen::*; diff --git a/contrib/db_pools/lib/src/pool.rs b/contrib/db_pools/lib/src/pool.rs index 9c932533..6776b642 100644 --- a/contrib/db_pools/lib/src/pool.rs +++ b/contrib/db_pools/lib/src/pool.rs @@ -1,61 +1,115 @@ -use rocket::async_trait; -use rocket::{Build, Rocket}; +use rocket::figment::Figment; -use crate::{Config, Error}; +#[allow(unused_imports)] +use {std::time::Duration, crate::{Error, Config}}; -/// This trait is implemented on connection pool types that can be used with the -/// [`Database`] derive macro. +/// Generic [`Database`](crate::Database) driver connection pool trait. /// -/// `Pool` determines how the connection pool is initialized from configuration, -/// such as a connection string and optional pool size, along with the returned -/// `Connection` type. +/// This trait provides a generic interface to various database pooling +/// implementations in the Rust ecosystem. It can be implemented by anyone, but +/// this crate provides implementations for common drivers. /// -/// Implementations of this trait should use `async_trait`. +/// **Implementations of this trait outside of this crate should be rare. You +/// _do not_ need to implement this trait or understand its specifics to use +/// this crate.** /// -/// ## Example +/// ## Async Trait /// -/// ``` -/// use rocket::{Build, Rocket}; +/// [`Pool`] is an _async_ trait. Implementations of `Pool` must be decorated +/// with an attribute of `#[async_trait]`: /// -/// #[derive(Debug)] -/// struct Error { /* ... */ } -/// # impl std::fmt::Display for Error { -/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -/// # unimplemented!("example") -/// # } -/// # } -/// # impl std::error::Error for Error { } -/// -/// struct Pool { /* ... */ } -/// struct Connection { /* .. */ } +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::figment::Figment; +/// use rocket_db_pools::Pool; /// +/// # struct MyPool; +/// # type Connection = (); +/// # type Error = std::convert::Infallible; /// #[rocket::async_trait] -/// impl rocket_db_pools::Pool for Pool { +/// impl Pool for MyPool { /// type Connection = Connection; -/// type InitError = Error; -/// type GetError = Error; /// -/// async fn initialize(db_name: &str, rocket: &Rocket) -/// -> Result> -/// { -/// unimplemented!("example") +/// type Error = Error; +/// +/// async fn init(figment: &Figment) -> Result { +/// todo!("initialize and return an instance of the pool"); /// } /// -/// async fn get(&self) -> Result { -/// unimplemented!("example") +/// async fn get(&self) -> Result { +/// todo!("fetch one connection from the pool"); /// } /// } /// ``` -#[async_trait] +/// +/// ## Implementing +/// +/// Implementations of `Pool` typically trace the following outline: +/// +/// 1. The `Error` associated type is set to [`Error`]. +/// +/// 2. A [`Config`] is [extracted](Figment::extract()) from the `figment` +/// passed to init. +/// +/// 3. The pool is initialized and returned in `init()`, wrapping +/// initialization errors in [`Error::Init`]. +/// +/// 4. A connection is retrieved in `get()`, wrapping errors in +/// [`Error::Get`]. +/// +/// Concretely, this looks like: +/// +/// ```rust +/// use rocket::figment::Figment; +/// use rocket_db_pools::{Pool, Config, Error}; +/// # +/// # type InitError = std::convert::Infallible; +/// # type GetError = std::convert::Infallible; +/// # type Connection = (); +/// # +/// # struct MyPool(Config); +/// # impl MyPool { +/// # fn new(c: Config) -> Result { +/// # Ok(Self(c)) +/// # } +/// # +/// # fn acquire(&self) -> Result { +/// # Ok(()) +/// # } +/// # } +/// +/// #[rocket::async_trait] +/// impl Pool for MyPool { +/// type Connection = Connection; +/// +/// type Error = Error; +/// +/// async fn init(figment: &Figment) -> Result { +/// // Extract the config from `figment`. +/// let config: Config = figment.extract()?; +/// +/// // Read config values, initialize `MyPool`. Map errors of type +/// // `InitError` to `Error` with `Error::Init`. +/// let pool = MyPool::new(config).map_err(Error::Init)?; +/// +/// // Return the fully intialized pool. +/// Ok(pool) +/// } +/// +/// async fn get(&self) -> Result { +/// // Get one connection from the pool, here via an `acquire()` method. +/// // Map errors of type `GetError` to `Error<_, GetError>`. +/// self.acquire().map_err(Error::Get) +/// } +/// } +/// ``` +#[rocket::async_trait] pub trait Pool: Sized + Send + Sync + 'static { - /// The type returned by get(). + /// The connection type managed by this pool, returned by [`Self::get()`]. type Connection; - /// The error type returned by `initialize`. - type InitError: std::error::Error; - - /// The error type returned by `get`. - type GetError: std::error::Error; + /// The error type returned by [`Self::init()`] and [`Self::get()`]. + type Error: std::error::Error; /// Constructs a pool from a [Value](rocket::figment::value::Value). /// @@ -69,219 +123,136 @@ pub trait Pool: Sized + Send + Sync + 'static { /// This method returns an error if the configuration is not compatible, or /// if creating a pool failed due to an unavailable database server, /// insufficient resources, or another database-specific error. - async fn initialize(db_name: &str, rocket: &Rocket) - -> Result>; + async fn init(figment: &Figment) -> Result; - /// Asynchronously gets a connection from the factory or pool. + /// Asynchronously retrieves a connection from the factory or pool. /// /// ## Errors /// /// This method returns an error if a connection could not be retrieved, /// such as a preconfigured timeout elapsing or when the database server is /// unavailable. - async fn get(&self) -> Result; + async fn get(&self) -> Result; } -#[cfg(feature = "deadpool_postgres")] -#[async_trait] -impl Pool for deadpool_postgres::Pool { - type Connection = deadpool_postgres::Client; - type InitError = deadpool_postgres::tokio_postgres::Error; - type GetError = deadpool_postgres::PoolError; +#[cfg(feature = "deadpool")] +mod deadpool_postgres { + use deadpool::managed::{Manager, Pool, PoolConfig, PoolError, Object}; + use super::{Duration, Error, Config, Figment}; - async fn initialize(db_name: &str, rocket: &Rocket) - -> std::result::Result> - { - let config = Config::from(db_name, rocket)?; - let manager = deadpool_postgres::Manager::new( - config.url.parse().map_err(Error::Db)?, - // TODO: add TLS support in config - deadpool_postgres::tokio_postgres::NoTls, - ); - let mut pool_config = deadpool_postgres::PoolConfig::new(config.pool_size as usize); - pool_config.timeouts.wait = Some(std::time::Duration::from_secs(config.timeout.into())); - - Ok(deadpool_postgres::Pool::from_config(manager, pool_config)) + pub trait DeadManager: Manager + Sized + Send + Sync + 'static { + fn new(config: &Config) -> Result; } - async fn get(&self) -> Result { - self.get().await + #[cfg(feature = "deadpool_postgres")] + impl DeadManager for deadpool_postgres::Manager { + fn new(config: &Config) -> Result { + Ok(Self::new(config.url.parse()?, deadpool_postgres::tokio_postgres::NoTls)) + } + } + + #[cfg(feature = "deadpool_redis")] + impl DeadManager for deadpool_redis::Manager { + fn new(config: &Config) -> Result { + Self::new(config.url.as_str()) + } + } + + #[rocket::async_trait] + impl>> crate::Pool for Pool + where M::Type: Send, C: Send + Sync + 'static, M::Error: std::error::Error + { + type Error = Error>; + + type Connection = C; + + async fn init(figment: &Figment) -> Result { + let config: Config = figment.extract()?; + let manager = M::new(&config).map_err(Error::Init)?; + + let mut pool = PoolConfig::new(config.max_connections); + pool.timeouts.create = Some(Duration::from_secs(config.connect_timeout)); + pool.timeouts.wait = Some(Duration::from_secs(config.connect_timeout)); + pool.timeouts.recycle = config.idle_timeout.map(Duration::from_secs); + pool.runtime = deadpool::Runtime::Tokio1; + Ok(Pool::from_config(manager, pool)) + } + + async fn get(&self) -> Result { + self.get().await.map_err(Error::Get) + } } } -#[cfg(feature = "deadpool_redis")] -#[async_trait] -impl Pool for deadpool_redis::Pool { - type Connection = deadpool_redis::ConnectionWrapper; - type InitError = deadpool_redis::redis::RedisError; - type GetError = deadpool_redis::PoolError; +#[cfg(feature = "sqlx")] +mod sqlx { + use sqlx::ConnectOptions; + use super::{Duration, Error, Config, Figment}; - async fn initialize(db_name: &str, rocket: &Rocket) - -> std::result::Result> - { - let config = Config::from(db_name, rocket)?; - let manager = deadpool_redis::Manager::new(config.url).map_err(Error::Db)?; + type Options = <::Connection as sqlx::Connection>::Options; - let mut pool_config = deadpool_redis::PoolConfig::new(config.pool_size as usize); - pool_config.timeouts.wait = Some(std::time::Duration::from_secs(config.timeout.into())); - - Ok(deadpool_redis::Pool::from_config(manager, pool_config)) + // Provide specialized configuration for particular databases. + fn specialize(__options: &mut dyn std::any::Any, __config: &Config) { + #[cfg(feature = "sqlx_sqlite")] + if let Some(o) = __options.downcast_mut::() { + *o = std::mem::take(o) + .busy_timeout(Duration::from_secs(__config.connect_timeout)) + .create_if_missing(true); + } } - async fn get(&self) -> Result { - self.get().await + #[rocket::async_trait] + impl crate::Pool for sqlx::Pool { + type Error = Error; + + type Connection = sqlx::pool::PoolConnection; + + async fn init(figment: &Figment) -> Result { + let config = figment.extract::()?; + let mut opts = config.url.parse::>().map_err(Error::Init)?; + opts.disable_statement_logging(); + specialize(&mut opts, &config); + + sqlx::pool::PoolOptions::new() + .max_connections(config.max_connections as u32) + .connect_timeout(Duration::from_secs(config.connect_timeout)) + .idle_timeout(config.idle_timeout.map(Duration::from_secs)) + .min_connections(config.min_connections.unwrap_or_default()) + .connect_with(opts) + .await + .map_err(Error::Init) + } + + async fn get(&self) -> Result { + self.acquire().await.map_err(Error::Get) + } } } #[cfg(feature = "mongodb")] -#[async_trait] -impl Pool for mongodb::Client { - type Connection = mongodb::Client; - type InitError = mongodb::error::Error; - type GetError = std::convert::Infallible; +mod mongodb { + use mongodb::{Client, options::ClientOptions}; + use super::{Duration, Error, Config, Figment}; - async fn initialize(db_name: &str, rocket: &Rocket) - -> std::result::Result> - { - let config = Config::from(db_name, rocket)?; - let mut options = mongodb::options::ClientOptions::parse(&config.url) - .await - .map_err(Error::Db)?; - options.max_pool_size = Some(config.pool_size); - options.wait_queue_timeout = Some(std::time::Duration::from_secs(config.timeout.into())); + #[rocket::async_trait] + impl crate::Pool for Client { + type Error = Error; - mongodb::Client::with_options(options).map_err(Error::Db) - } + type Connection = Client; - async fn get(&self) -> Result { - Ok(self.clone()) - } -} - -#[cfg(feature = "mysql_async")] -#[async_trait] -impl Pool for mysql_async::Pool { - type Connection = mysql_async::Conn; - type InitError = mysql_async::Error; - type GetError = mysql_async::Error; - - async fn initialize(db_name: &str, rocket: &Rocket) - -> std::result::Result> - { - use rocket::figment::{self, error::{Actual, Kind}}; - - let config = Config::from(db_name, rocket)?; - let original_opts = mysql_async::Opts::from_url(&config.url) - .map_err(|_| figment::Error::from(Kind::InvalidValue( - Actual::Str(config.url.to_string()), - "mysql connection string".to_string() - )))?; - - let new_pool_opts = original_opts.pool_opts() - .clone() - .with_constraints( - mysql_async::PoolConstraints::new(0, config.pool_size as usize) - .expect("usize can't be < 0") - ); - - // TODO: timeout - - let opts = mysql_async::OptsBuilder::from_opts(original_opts) - .pool_opts(new_pool_opts); - - Ok(mysql_async::Pool::new(opts)) - } - - async fn get(&self) -> std::result::Result { - self.get_conn().await - } -} - -#[cfg(feature = "sqlx_mysql")] -#[async_trait] -impl Pool for sqlx::MySqlPool { - type Connection = sqlx::pool::PoolConnection; - type InitError = sqlx::Error; - type GetError = sqlx::Error; - - async fn initialize(db_name: &str, rocket: &Rocket) - -> std::result::Result> - { - use sqlx::ConnectOptions; - - let config = Config::from(db_name, rocket)?; - let mut opts = config.url.parse::() - .map_err(Error::Db)?; - opts.disable_statement_logging(); - sqlx::pool::PoolOptions::new() - .max_connections(config.pool_size) - .connect_timeout(std::time::Duration::from_secs(config.timeout.into())) - .connect_with(opts) - .await - .map_err(Error::Db) - } - - async fn get(&self) -> std::result::Result { - self.acquire().await - } -} - -#[cfg(feature = "sqlx_postgres")] -#[async_trait] -impl Pool for sqlx::PgPool { - type Connection = sqlx::pool::PoolConnection; - type InitError = sqlx::Error; - type GetError = sqlx::Error; - - async fn initialize(db_name: &str, rocket: &Rocket) - -> std::result::Result> - { - use sqlx::ConnectOptions; - - let config = Config::from(db_name, rocket)?; - let mut opts = config.url.parse::() - .map_err(Error::Db)?; - opts.disable_statement_logging(); - sqlx::pool::PoolOptions::new() - .max_connections(config.pool_size) - .connect_timeout(std::time::Duration::from_secs(config.timeout.into())) - .connect_with(opts) - .await - .map_err(Error::Db) - } - - async fn get(&self) -> std::result::Result { - self.acquire().await - } -} - -#[cfg(feature = "sqlx_sqlite")] -#[async_trait] -impl Pool for sqlx::SqlitePool { - type Connection = sqlx::pool::PoolConnection; - type InitError = sqlx::Error; - type GetError = sqlx::Error; - - async fn initialize(db_name: &str, rocket: &Rocket) - -> std::result::Result> - { - use sqlx::ConnectOptions; - - let config = Config::from(db_name, rocket)?; - let mut opts = config.url.parse::() - .map_err(Error::Db)? - .create_if_missing(true); - opts.disable_statement_logging(); - - dbg!(sqlx::pool::PoolOptions::new() - .max_connections(config.pool_size) - .connect_timeout(std::time::Duration::from_secs(config.timeout.into()))) - .connect_with(opts) - .await - .map_err(Error::Db) - } - - async fn get(&self) -> std::result::Result { - self.acquire().await + async fn init(figment: &Figment) -> Result { + let config = figment.extract::()?; + let mut opts = ClientOptions::parse(&config.url).await.map_err(Error::Init)?; + opts.min_pool_size = config.min_connections; + opts.max_pool_size = Some(config.max_connections as u32); + opts.max_idle_time = config.idle_timeout.map(Duration::from_secs); + opts.wait_queue_timeout = Some(Duration::from_secs(config.connect_timeout)); + opts.connect_timeout = Some(Duration::from_secs(config.connect_timeout)); + Client::with_options(opts).map_err(Error::Init) + } + + async fn get(&self) -> Result { + Ok(self.clone()) + } } } diff --git a/examples/databases/src/sqlx.rs b/examples/databases/src/sqlx.rs index 4f7bb09c..ffad0c73 100644 --- a/examples/databases/src/sqlx.rs +++ b/examples/databases/src/sqlx.rs @@ -3,17 +3,14 @@ use rocket::fairing::{self, AdHoc}; use rocket::response::status::Created; use rocket::serde::{Serialize, Deserialize, json::Json}; -use rocket_db_pools::{sqlx, Database}; +use rocket_db_pools::{sqlx, Database, Connection}; -use futures::stream::TryStreamExt; -use futures::future::TryFutureExt; +use futures::{stream::TryStreamExt, future::TryFutureExt}; #[derive(Database)] #[database("sqlx")] struct Db(sqlx::SqlitePool); -type Connection = rocket_db_pools::Connection; - type Result> = std::result::Result; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -26,7 +23,7 @@ struct Post { } #[post("/", data = "")] -async fn create(mut db: Connection, post: Json) -> Result>> { +async fn create(mut db: Connection, post: Json) -> Result>> { // There is no support for `RETURNING`. sqlx::query!("INSERT INTO posts (title, text) VALUES (?, ?)", post.title, post.text) .execute(&mut *db) @@ -36,7 +33,7 @@ async fn create(mut db: Connection, post: Json) -> Result Result>> { +async fn list(mut db: Connection) -> Result>> { let ids = sqlx::query!("SELECT id FROM posts") .fetch(&mut *db) .map_ok(|record| record.id) @@ -47,7 +44,7 @@ async fn list(mut db: Connection) -> Result>> { } #[get("/")] -async fn read(mut db: Connection, id: i64) -> Option> { +async fn read(mut db: Connection, id: i64) -> Option> { sqlx::query!("SELECT id, title, text FROM posts WHERE id = ?", id) .fetch_one(&mut *db) .map_ok(|r| Json(Post { id: Some(r.id), title: r.title, text: r.text })) @@ -56,7 +53,7 @@ async fn read(mut db: Connection, id: i64) -> Option> { } #[delete("/")] -async fn delete(mut db: Connection, id: i64) -> Result> { +async fn delete(mut db: Connection, id: i64) -> Result> { let result = sqlx::query!("DELETE FROM posts WHERE id = ?", id) .execute(&mut *db) .await?; @@ -65,20 +62,20 @@ async fn delete(mut db: Connection, id: i64) -> Result> { } #[delete("/")] -async fn destroy(mut db: Connection) -> Result<()> { +async fn destroy(mut db: Connection) -> Result<()> { sqlx::query!("DELETE FROM posts").execute(&mut *db).await?; Ok(()) } -async fn init_db(rocket: Rocket) -> fairing::Result { - match rocket.state::() { - Some(db) => { - if let Err(e) = sqlx::migrate!("db/sqlx/migrations").run(db.pool()).await { +async fn run_migrations(rocket: Rocket) -> fairing::Result { + match Db::fetch(&rocket) { + Some(db) => match sqlx::migrate!("db/sqlx/migrations").run(&**db).await { + Ok(_) => Ok(rocket), + Err(e) => { error!("Failed to initialize SQLx database: {}", e); - return Err(rocket); + Err(rocket) } - Ok(rocket) } None => Err(rocket), } @@ -86,9 +83,8 @@ async fn init_db(rocket: Rocket) -> fairing::Result { pub fn stage() -> AdHoc { AdHoc::on_ignite("SQLx Stage", |rocket| async { - rocket - .attach(Db::fairing()) - .attach(AdHoc::try_on_ignite("SQLx Database", init_db)) + rocket.attach(Db::init()) + .attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations)) .mount("/sqlx", routes![list, create, read, delete, destroy]) }) } diff --git a/scripts/mk-docs.sh b/scripts/mk-docs.sh index e91550ad..bb0c7375 100755 --- a/scripts/mk-docs.sh +++ b/scripts/mk-docs.sh @@ -21,7 +21,8 @@ echo ":::: Generating the docs..." pushd "${PROJECT_ROOT}" > /dev/null 2>&1 # Set the crate version and fill in missing doc URLs with docs.rs links. RUSTDOCFLAGS="-Zunstable-options --crate-version ${DOC_VERSION}" \ - cargo doc -p rocket -p rocket_sync_db_pools -p rocket_dyn_templates \ + cargo doc -p rocket \ + -p rocket_sync_db_pools -p rocket_dyn_templates -p rocket_db_pools \ -Zrustdoc-map --no-deps --all-features popd > /dev/null 2>&1 diff --git a/scripts/test.sh b/scripts/test.sh index 8f92fdb0..16956841 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -74,13 +74,13 @@ function indir() { function test_contrib() { DB_POOLS_FEATURES=( - deadpool-postgres - deadpool-redis - mongodb - mysql_async + deadpool_postgres + deadpool_redis sqlx_mysql sqlx_postgres sqlx_sqlite + sqlx_mssql + mongodb ) SYNC_DB_POOLS_FEATURES=(