sqlx_crud/
traits.rs

1use std::pin::Pin;
2
3use futures::stream::Stream;
4use futures::stream::TryCollect;
5use futures::Future;
6use futures::{future, TryFutureExt, TryStreamExt};
7use sqlx::database::HasArguments;
8use sqlx::{Database, Encode, Executor, FromRow, IntoArguments, Type};
9
10/// Type alias for methods returning a single element. The future resolves to and
11/// `Result<T, sqlx::Error>`.
12pub type CrudFut<'e, T> = Pin<Box<dyn Future<Output = Result<T, sqlx::Error>> + Send + 'e>>;
13
14/// Type alias for a [`Stream`] returning items of type `Result<T, sqlxError>`.
15pub type CrudStream<'e, T> =
16    Pin<Box<dyn Stream<Item = Result<T, sqlx::Error>> + std::marker::Send + 'e>>;
17
18/// Type alias for a [`TryCollect`] future that resolves to `Result<Vec<T>, sqlx::Error>`.
19pub type TryCollectFut<'e, T> = TryCollect<CrudStream<'e, T>, Vec<T>>;
20
21/// Database schema information about a struct implementing sqlx [FromRow].
22/// [Schema] defines methods for accessing the derived database schema
23/// and query information.
24///
25/// This trait is implemented by the [SqlxCrud] derive macro.
26///
27/// # Example
28///
29/// ```rust
30/// use sqlx::FromRow;
31/// use sqlx_crud::SqlxCrud;
32///
33/// #[derive(FromRow, SqlxCrud)]
34/// pub struct User {
35///     user_id: i32,
36///     name: String,
37/// }
38/// ```
39///
40/// [FromRow]: https://docs.rs/sqlx/latest/sqlx/trait.FromRow.html
41pub trait Schema {
42    /// Type of the table primary key column.
43    type Id: Copy + Send + Sync;
44
45    /// Database name of the table. Used by the query generation code and
46    /// available for introspection. This is generated by taking the plural
47    /// _snake_case_ of the struct's name. See: [Inflector to_table_case].
48    ///
49    /// ```rust
50    /// use sqlx::FromRow;
51    /// use sqlx_crud::{Schema, SqlxCrud};
52    ///
53    /// #[derive(FromRow, SqlxCrud)]
54    /// struct GoogleIdToken {
55    ///     id: i32,
56    ///     audience: String,
57    /// }
58    ///
59    /// assert_eq!("google_id_tokens", GoogleIdToken::table_name());
60    /// ```
61    ///
62    /// [Inflector to_table_case]: https://docs.rs/Inflector/latest/inflector/cases/tablecase/fn.to_table_case.html
63    fn table_name() -> &'static str;
64
65    /// Returns the id of the current instance.
66    fn id(&self) -> Self::Id;
67
68    /// Returns the column name of the primary key.
69    fn id_column() -> &'static str;
70
71    /// Returns an array of column names.
72    fn columns() -> &'static [&'static str];
73
74    /// Returns the SQL string for a SELECT query against the table.
75    ///
76    /// # Example
77    ///
78    /// ```rust
79    /// # sqlx_crud::doctest_setup! { |pool| {
80    /// use sqlx_crud::Schema;
81    ///
82    /// assert_eq!(r#"SELECT "users"."user_id", "users"."name" FROM "users""#, User::select_sql());
83    /// # }}
84    /// ```
85    fn select_sql() -> &'static str;
86
87    /// Returns the SQL string for a SELECT query against the table with a
88    /// WHERE clause for the primary key.
89    ///
90    /// # Example
91    ///
92    /// ```rust
93    /// # sqlx_crud::doctest_setup! { |pool| {
94    /// use sqlx_crud::Schema;
95    ///
96    /// assert_eq!(
97    ///     r#"SELECT "users"."user_id", "users"."name" FROM "users" WHERE "users"."user_id" = ? LIMIT 1"#,
98    ///     User::select_by_id_sql()
99    /// );
100    /// # }}
101    /// ```
102    fn select_by_id_sql() -> &'static str;
103
104    /// Returns the SQL for inserting a new record in to the database. The
105    /// `#[external_id]` attribute may be used to specify IDs are assigned
106    /// outside of the database.
107    ///
108    ///
109    /// # Example
110    ///
111    /// ```rust
112    /// # sqlx_crud::doctest_setup! { |pool| {
113    /// use sqlx::FromRow;
114    /// use sqlx_crud::{Schema, SqlxCrud};
115    ///
116    /// #[derive(Debug, FromRow, SqlxCrud)]
117    /// #[external_id]
118    /// pub struct UserExternalId {
119    ///     pub user_id: i32,
120    ///     pub name: String,
121    /// }
122    ///
123    /// assert_eq!(r#"INSERT INTO "users" ("name") VALUES (?) RETURNING "users"."user_id", "users"."name""#, User::insert_sql());
124    /// 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());
125    /// # }}
126    /// ```
127    fn insert_sql() -> &'static str;
128
129    /// Returns the SQL for updating an existing record in the database.
130    ///
131    /// # Example
132    ///
133    /// ```rust
134    /// # sqlx_crud::doctest_setup! { |pool| {
135    /// use sqlx_crud::Schema;
136    ///
137    /// assert_eq!(r#"UPDATE "users" SET "name" = ? WHERE "users"."user_id" = ? RETURNING "users"."user_id", "users"."name""#, User::update_by_id_sql());
138    /// # }}
139    /// ```
140    fn update_by_id_sql() -> &'static str;
141
142    /// Returns the SQL for deleting an existing record by ID from the database.
143    ///
144    /// # Example
145    ///
146    /// ```rust
147    /// # sqlx_crud::doctest_setup! { |pool| {
148    /// use sqlx_crud::Schema;
149    ///
150    /// assert_eq!(r#"DELETE FROM "users" WHERE "users"."user_id" = ?"#, User::delete_by_id_sql());
151    /// # }}
152    /// ```
153    fn delete_by_id_sql() -> &'static str;
154}
155
156/// Common Create, Read, Update, and Delete behaviors. This trait requires that
157/// [Schema] and [FromRow] are implemented for Self.
158///
159/// This trait is implemented by the [SqlxCrud] derive macro. Implementors
160/// define how to assign query insert and update bindings.
161///
162/// [FromRow]: https://docs.rs/sqlx/latest/sqlx/trait.FromRow.html
163/// [Schema]: trait.Schema.html
164/// [SqlxCrud]: ../derive.SqlxCrud.html
165pub trait Crud<'e, E>
166where
167    Self: 'e + Sized + Send + Unpin + for<'r> FromRow<'r, <E::Database as Database>::Row> + Schema,
168    <Self as Schema>::Id:
169        Encode<'e, <E as Executor<'e>>::Database> + Type<<E as Executor<'e>>::Database>,
170    E: Executor<'e> + 'e,
171    <E::Database as HasArguments<'e>>::Arguments: IntoArguments<'e, <E as Executor<'e>>::Database>,
172{
173    /// Returns an owned instance of [sqlx::Arguments]. self is consumed.
174    /// Values in the fields are moved in to the `Arguments` instance.
175    ///
176    fn insert_args(self) -> <E::Database as HasArguments<'e>>::Arguments;
177
178    /// Returns an owned instance of [sqlx::Arguments]. self is consumed.
179    /// Values in the fields are moved in to the `Arguments` instance.
180    ///
181    fn update_args(self) -> <E::Database as HasArguments<'e>>::Arguments;
182
183    /// Returns a future that resolves to an insert or `sqlx::Error` of the
184    /// current instance.
185    ///
186    /// # Example
187    ///
188    /// ```rust
189    /// # sqlx_crud::doctest_setup! { |pool| {
190    /// use sqlx_crud::{Crud, Schema};
191    ///
192    /// let user = User { user_id: 1, name: "test".to_string() };
193    /// let user = user.create(&pool).await?;
194    /// assert_eq!("test", user.name);
195    /// # }}
196    /// ```
197    fn create(self, pool: E) -> CrudFut<'e, Self> {
198        Box::pin({
199            let args = self.insert_args();
200            ::sqlx::query_with::<E::Database, _>(Self::insert_sql(), args)
201                .try_map(|r| Self::from_row(&r))
202                .fetch_one(pool)
203        })
204    }
205
206    /// Queries all records from the table and returns a future that returns
207    /// to a [try_collect] stream, which resolves to a `Vec<Self>` or a
208    /// `sqlx::Error` on error.
209    ///
210    /// # Example
211    ///
212    /// ```rust
213    /// # sqlx_crud::doctest_setup! { |pool| {
214    /// use sqlx_crud::Crud;
215    ///
216    /// let all_users: Vec<User> = User::all(&pool).await?;
217    /// # }}
218    /// ```
219    ///
220    /// [try_collect]: https://docs.rs/futures/latest/futures/stream/trait.TryStreamExt.html#method.try_collect
221    fn all(pool: E) -> TryCollectFut<'e, Self> {
222        let stream =
223            sqlx::query_as::<E::Database, Self>(<Self as Schema>::select_sql()).fetch(pool);
224        stream.try_collect()
225    }
226
227    #[doc(hidden)]
228    fn paged(_pool: E) -> TryCollectFut<'e, Self> {
229        unimplemented!()
230    }
231
232    /// Looks up a row by ID and returns a future that resolves an
233    /// `Option<Self>`. Returns `None` if and a record with the corresponding ID
234    /// cannot be found and `Some` if it exists.
235    ///
236    /// # Example
237    ///
238    /// ```rust
239    /// # sqlx_crud::doctest_setup! { |pool| {
240    /// use sqlx_crud::Crud;
241    ///
242    /// let user: Option<User> = User::by_id(&pool, 1).await?;
243    /// assert!(user.is_some());
244    /// # }}
245    /// ```
246    fn by_id(pool: E, id: <Self as Schema>::Id) -> CrudFut<'e, Option<Self>> {
247        Box::pin({
248            use ::sqlx::Arguments as _;
249            let arg0 = id;
250            let mut args = <E::Database as HasArguments<'e>>::Arguments::default();
251            args.reserve(
252                1usize,
253                ::sqlx::encode::Encode::<E::Database>::size_hint(&arg0),
254            );
255            args.add(arg0);
256            ::sqlx::query_with::<E::Database, _>(Self::select_by_id_sql(), args)
257                .try_map(|r| Self::from_row(&r))
258                .fetch_optional(pool)
259        })
260    }
261
262    /// Updates the database with the current instance state and returns a
263    /// future that resolves to the new `Self` returned from the database.
264    ///
265    /// # Example
266    ///
267    /// ```rust
268    /// # sqlx_crud::doctest_setup! { |pool| {
269    /// use sqlx_crud::Crud;
270    ///
271    /// if let Some(mut user) = User::by_id(&pool, 1).await? {
272    ///     assert_eq!("test", user.name);
273    ///
274    ///     // Update the record
275    ///     user.name = "Harry".to_string();
276    ///     let user = user.update(&pool).await?;
277    ///
278    ///     // Confirm the name changed
279    ///     assert_eq!("Harry", user.name);
280    /// }
281    /// # }}
282    /// ```
283    fn update(self, pool: E) -> CrudFut<'e, Self> {
284        Box::pin({
285            let args = self.update_args();
286            ::sqlx::query_with::<E::Database, _>(Self::update_by_id_sql(), args)
287                .try_map(|r| Self::from_row(&r))
288                .fetch_one(pool)
289        })
290    }
291
292    /// Deletes a record from the database by ID and returns a future that
293    /// resolves to `()` on success or `sqlx::Error` on failure.
294    ///
295    /// # Example
296    ///
297    /// ```rust
298    /// # sqlx_crud::doctest_setup! { |pool| {
299    /// use sqlx_crud::Crud;
300    ///
301    /// if let Some(user) = User::by_id(&pool, 1).await? {
302    ///     user.delete(&pool).await?;
303    /// }
304    /// assert!(User::by_id(&pool, 1).await?.is_none());
305    /// # }}
306    /// ```
307    fn delete(self, pool: E) -> CrudFut<'e, ()> {
308        let query = sqlx::query(<Self as Schema>::delete_by_id_sql()).bind(self.id());
309        Box::pin(query.execute(pool).and_then(|_| future::ok(())))
310    }
311}