sqlx-crud 0.4.0

Derive macro for SQLx to implement Create, Read, Update, and Delete (CRUD) methods for you.
Documentation
use std::pin::Pin;

use futures::stream::Stream;
use futures::stream::TryCollect;
use futures::Future;
use futures::{future, TryFutureExt, TryStreamExt};
use sqlx::database::HasArguments;
use sqlx::{Database, Encode, Executor, FromRow, IntoArguments, Type};

/// Type alias for methods returning a single element. The future resolves to and
/// `Result<T, sqlx::Error>`.
pub type CrudFut<'e, T> = Pin<Box<dyn Future<Output = Result<T, sqlx::Error>> + Send + 'e>>;

/// Type alias for a [`Stream`] returning items of type `Result<T, sqlxError>`.
pub type CrudStream<'e, T> =
    Pin<Box<dyn Stream<Item = Result<T, sqlx::Error>> + std::marker::Send + 'e>>;

/// Type alias for a [`TryCollect`] future that resolves to `Result<Vec<T>, sqlx::Error>`.
pub type TryCollectFut<'e, T> = TryCollect<CrudStream<'e, T>, Vec<T>>;

/// Database schema information about a struct implementing sqlx [FromRow].
/// [Schema] defines methods for accessing the derived database schema
/// and query information.
///
/// This trait is implemented by the [SqlxCrud] derive macro.
///
/// # Example
///
/// ```rust
/// use sqlx::FromRow;
/// use sqlx_crud::SqlxCrud;
///
/// #[derive(FromRow, SqlxCrud)]
/// pub struct User {
///     user_id: i32,
///     name: String,
/// }
/// ```
///
/// [FromRow]: https://docs.rs/sqlx/latest/sqlx/trait.FromRow.html
pub trait Schema {
    /// Type of the table primary key column.
    type Id: Copy + Send + Sync;

    /// Database name of the table. Used by the query generation code and
    /// available for introspection. This is generated by taking the plural
    /// _snake_case_ of the struct's name. See: [Inflector to_table_case].
    ///
    /// ```rust
    /// use sqlx::FromRow;
    /// use sqlx_crud::{Schema, SqlxCrud};
    ///
    /// #[derive(FromRow, SqlxCrud)]
    /// struct GoogleIdToken {
    ///     id: i32,
    ///     audience: String,
    /// }
    ///
    /// assert_eq!("google_id_tokens", GoogleIdToken::table_name());
    /// ```
    ///
    /// [Inflector to_table_case]: https://docs.rs/Inflector/latest/inflector/cases/tablecase/fn.to_table_case.html
    fn table_name() -> &'static str;

    /// Returns the id of the current instance.
    fn id(&self) -> Self::Id;

    /// Returns the column name of the primary key.
    fn id_column() -> &'static str;

    /// Returns an array of column names.
    fn columns() -> &'static [&'static str];

    /// Returns the SQL string for a SELECT query against the table.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Schema;
    ///
    /// assert_eq!(r#"SELECT "users"."user_id", "users"."name" FROM "users""#, User::select_sql());
    /// # }}
    /// ```
    fn select_sql() -> &'static str;

    /// Returns the SQL string for a SELECT query against the table with a
    /// WHERE clause for the primary key.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Schema;
    ///
    /// assert_eq!(
    ///     r#"SELECT "users"."user_id", "users"."name" FROM "users" WHERE "users"."user_id" = ? LIMIT 1"#,
    ///     User::select_by_id_sql()
    /// );
    /// # }}
    /// ```
    fn select_by_id_sql() -> &'static str;

    /// Returns the SQL for inserting a new record in to the database. The
    /// `#[external_id]` attribute may be used to specify IDs are assigned
    /// outside of the database.
    ///
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx::FromRow;
    /// use sqlx_crud::{Schema, SqlxCrud};
    ///
    /// #[derive(Debug, FromRow, SqlxCrud)]
    /// #[external_id]
    /// pub struct UserExternalId {
    ///     pub user_id: i32,
    ///     pub name: String,
    /// }
    ///
    /// assert_eq!(r#"INSERT INTO "users" ("name") VALUES (?) RETURNING "users"."user_id", "users"."name""#, User::insert_sql());
    /// assert_eq!(r#"INSERT INTO "user_external_ids" ("user_id", "name") VALUES (?, ?) RETURNING "user_external_ids"."user_id", "user_external_ids"."name""#, UserExternalId::insert_sql());
    /// # }}
    /// ```
    fn insert_sql() -> &'static str;

    /// Returns the SQL for updating an existing record in the database.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Schema;
    ///
    /// assert_eq!(r#"UPDATE "users" SET "name" = ? WHERE "users"."user_id" = ? RETURNING "users"."user_id", "users"."name""#, User::update_by_id_sql());
    /// # }}
    /// ```
    fn update_by_id_sql() -> &'static str;

    /// Returns the SQL for deleting an existing record by ID from the database.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Schema;
    ///
    /// assert_eq!(r#"DELETE FROM "users" WHERE "users"."user_id" = ?"#, User::delete_by_id_sql());
    /// # }}
    /// ```
    fn delete_by_id_sql() -> &'static str;
}

/// Common Create, Read, Update, and Delete behaviors. This trait requires that
/// [Schema] and [FromRow] are implemented for Self.
///
/// This trait is implemented by the [SqlxCrud] derive macro. Implementors
/// define how to assign query insert and update bindings.
///
/// [FromRow]: https://docs.rs/sqlx/latest/sqlx/trait.FromRow.html
/// [Schema]: trait.Schema.html
/// [SqlxCrud]: ../derive.SqlxCrud.html
pub trait Crud<'e, E>
where
    Self: 'e + Sized + Send + Unpin + for<'r> FromRow<'r, <E::Database as Database>::Row> + Schema,
    <Self as Schema>::Id:
        Encode<'e, <E as Executor<'e>>::Database> + Type<<E as Executor<'e>>::Database>,
    E: Executor<'e> + 'e,
    <E::Database as HasArguments<'e>>::Arguments: IntoArguments<'e, <E as Executor<'e>>::Database>,
{
    /// Returns an owned instance of [sqlx::Arguments]. self is consumed.
    /// Values in the fields are moved in to the `Arguments` instance.
    ///
    fn insert_args(self) -> <E::Database as HasArguments<'e>>::Arguments;

    /// Returns an owned instance of [sqlx::Arguments]. self is consumed.
    /// Values in the fields are moved in to the `Arguments` instance.
    ///
    fn update_args(self) -> <E::Database as HasArguments<'e>>::Arguments;

    /// Returns a future that resolves to an insert or `sqlx::Error` of the
    /// current instance.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::{Crud, Schema};
    ///
    /// let user = User { user_id: 1, name: "test".to_string() };
    /// let user = user.create(&pool).await?;
    /// assert_eq!("test", user.name);
    /// # }}
    /// ```
    fn create(self, pool: E) -> CrudFut<'e, Self> {
        Box::pin({
            let args = self.insert_args();
            ::sqlx::query_with::<E::Database, _>(Self::insert_sql(), args)
                .try_map(|r| Self::from_row(&r))
                .fetch_one(pool)
        })
    }

    /// Queries all records from the table and returns a future that returns
    /// to a [try_collect] stream, which resolves to a `Vec<Self>` or a
    /// `sqlx::Error` on error.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Crud;
    ///
    /// let all_users: Vec<User> = User::all(&pool).await?;
    /// # }}
    /// ```
    ///
    /// [try_collect]: https://docs.rs/futures/latest/futures/stream/trait.TryStreamExt.html#method.try_collect
    fn all(pool: E) -> TryCollectFut<'e, Self> {
        let stream =
            sqlx::query_as::<E::Database, Self>(<Self as Schema>::select_sql()).fetch(pool);
        stream.try_collect()
    }

    #[doc(hidden)]
    fn paged(_pool: E) -> TryCollectFut<'e, Self> {
        unimplemented!()
    }

    /// Looks up a row by ID and returns a future that resolves an
    /// `Option<Self>`. Returns `None` if and a record with the corresponding ID
    /// cannot be found and `Some` if it exists.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Crud;
    ///
    /// let user: Option<User> = User::by_id(&pool, 1).await?;
    /// assert!(user.is_some());
    /// # }}
    /// ```
    fn by_id(pool: E, id: <Self as Schema>::Id) -> CrudFut<'e, Option<Self>> {
        Box::pin({
            use ::sqlx::Arguments as _;
            let arg0 = id;
            let mut args = <E::Database as HasArguments<'e>>::Arguments::default();
            args.reserve(
                1usize,
                ::sqlx::encode::Encode::<E::Database>::size_hint(&arg0),
            );
            args.add(arg0);
            ::sqlx::query_with::<E::Database, _>(Self::select_by_id_sql(), args)
                .try_map(|r| Self::from_row(&r))
                .fetch_optional(pool)
        })
    }

    /// Updates the database with the current instance state and returns a
    /// future that resolves to the new `Self` returned from the database.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Crud;
    ///
    /// if let Some(mut user) = User::by_id(&pool, 1).await? {
    ///     assert_eq!("test", user.name);
    ///
    ///     // Update the record
    ///     user.name = "Harry".to_string();
    ///     let user = user.update(&pool).await?;
    ///
    ///     // Confirm the name changed
    ///     assert_eq!("Harry", user.name);
    /// }
    /// # }}
    /// ```
    fn update(self, pool: E) -> CrudFut<'e, Self> {
        Box::pin({
            let args = self.update_args();
            ::sqlx::query_with::<E::Database, _>(Self::update_by_id_sql(), args)
                .try_map(|r| Self::from_row(&r))
                .fetch_one(pool)
        })
    }

    /// Deletes a record from the database by ID and returns a future that
    /// resolves to `()` on success or `sqlx::Error` on failure.
    ///
    /// # Example
    ///
    /// ```rust
    /// # sqlx_crud::doctest_setup! { |pool| {
    /// use sqlx_crud::Crud;
    ///
    /// if let Some(user) = User::by_id(&pool, 1).await? {
    ///     user.delete(&pool).await?;
    /// }
    /// assert!(User::by_id(&pool, 1).await?.is_none());
    /// # }}
    /// ```
    fn delete(self, pool: E) -> CrudFut<'e, ()> {
        let query = sqlx::query(<Self as Schema>::delete_by_id_sql()).bind(self.id());
        Box::pin(query.execute(pool).and_then(|_| future::ok(())))
    }
}