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}