sqlx_utils/traits/repository/
delete.rs

1//! Trait for adding delete capabilities to a repository
2
3use crate::prelude::{Database, SqlFilter};
4use crate::traits::{Model, Repository};
5use crate::types::Query;
6use crate::utils::{BatchOperator, DEFAULT_BATCH_SIZE};
7use sqlx::{Executor, QueryBuilder};
8
9/// Trait for repositories that can delete records from the database.
10///
11/// The `DeleteRepository` trait extends the base [`Repository`] trait with methods
12/// for deleting records. It provides standardized ways to delete both individual
13/// records and batches of records, optimizing database interactions while maintaining
14/// data integrity.
15///
16/// # Type Parameters
17///
18/// * `M` - The model type that this repository deletes. Must implement the [`Model`] trait.
19///
20/// # Examples
21///
22/// Basic implementation:
23/// ```rust
24/// # use sqlx_utils::prelude::{QueryBuilder, SqlFilter};
25/// use sqlx_utils::traits::{Model, Repository, DeleteRepository};
26/// # use sqlx_utils::types::{Pool, Query};
27/// # struct User { id: i32, name: String }
28/// # impl Model for User {
29/// #     type Id = i32;
30/// #     fn get_id(&self) -> Option<Self::Id> { Some(self.id) }
31/// # }
32/// # struct UserRepository { pool: Pool }
33/// # impl Repository<User> for UserRepository {
34/// #     fn pool(&self) -> &Pool { &self.pool }
35/// # }
36///
37/// impl DeleteRepository<User> for UserRepository {
38///     fn delete_by_id_query(id: &i32) -> Query<'_> {
39///         sqlx::query("DELETE FROM users WHERE id = $1")
40///             .bind(id)
41///     }
42///
43///     fn delete_by_filter_query<'args>(filter: impl SqlFilter<'args>) -> QueryBuilder<'args> {
44///         let mut builder = QueryBuilder::new("DELETE FROM users");
45///
46///         if filter.should_apply_filter() {
47///             builder.push("WHERE ");
48///             filter.apply_filter(&mut builder);
49///         }
50///
51///         builder
52///     }
53/// }
54///
55/// // Usage
56/// # async fn example(repo: &UserRepository) -> sqlx_utils::Result<()> {
57/// // Delete a single user
58/// repo.delete_by_id(1).await?;
59///
60/// // Delete multiple users
61/// let ids = vec![1, 2, 3];
62/// repo.delete_many_by_id(ids).await?;
63/// # Ok(())
64/// # }
65/// ```
66///
67/// Using the macro for simpler implementation:
68/// ```rust
69/// # use sqlx_utils::prelude::*;
70/// # struct User { id: i32, name: String }
71/// # impl Model for User {
72/// #     type Id = i32;
73/// #     fn get_id(&self) -> Option<Self::Id> { Some(self.id) }
74/// # }
75///
76/// repository! {
77///     UserRepository<User>;
78///
79///     // if you need to override any method other than `Repository::pool` they will go here
80/// }
81///
82/// repository_delete! {
83///     UserRepository<User>;
84///
85///     delete_by_id_query(id) {
86///         sqlx::query("DELETE FROM users WHERE id = $1")
87///             .bind(id)
88///     }
89///
90///     delete_by_filter_query(filter) {
91///         let mut builder = QueryBuilder::new("DELETE FROM users");
92///
93///         if filter.should_apply_filter() {
94///             builder.push("WHERE ");
95///             filter.apply_filter(&mut builder);
96///         }
97///
98///         builder
99///     }
100/// }
101/// ```
102///
103/// # Implementation Notes
104///
105/// 1. Required method: [`delete_by_id_query`](DeleteRepository::delete_by_id_query) - Defines how to create a deletion query for a model ID
106/// 2. Provided methods:
107///    - [`delete_by_id`](DeleteRepository::delete_by_id) - Deletes a single record by ID
108///    - [`delete_many_by_id`](DeleteRepository::delete_many_by_id) - Deletes multiple records by id using the default batch size
109///    - [`delete_batch_by_id`](DeleteRepository::delete_batch_by_id) - Deletes multiple records by id with a custom batch size
110/// 3. All batch operations use transactions to ensure data consistency
111/// 4. Performance is optimized through batching and connection pooling
112/// 5. Consider implementing soft deletes if required by your application
113#[diagnostic::on_unimplemented(
114    note = "Type `{Self}` does not implement the `DeleteRepository<{M}>` trait",
115    label = "this type does not implement `DeleteRepository` for model type `{M}`",
116    message = "`{Self}` must implement `DeleteRepository<{M}>` to delete `{M}` records"
117)]
118#[async_trait::async_trait]
119pub trait DeleteRepository<M: Model>: Repository<M> {
120    /// Creates a SQL query to delete a record by its ID.
121    ///
122    /// This method generates a DELETE statement that will remove exactly one record
123    /// from the database based on its primary key. It's designed to be both safe
124    /// and efficient by using parameterized queries.
125    ///
126    /// # Parameters
127    ///
128    /// * `id` - A reference to the ID of the record to delete
129    ///
130    /// # Returns
131    ///
132    /// * [`Query`] - A prepared SQL DELETE query
133    ///
134    /// # Implementation Notes
135    ///
136    /// Consider:
137    /// 1. Handling soft deletes if required
138    /// 2. Checking foreign key constraints
139    /// 3. Implementing cascading deletes if needed
140    fn delete_by_id_query(id: &M::Id) -> Query<'_>;
141
142    /// Creates a SQL query to delete a record by a given [`SqlFilter`].
143    ///
144    /// This method generates a DELETE statement that will remove all records that match a given [`SqlFilter`]. It's designed to be both safe
145    /// and efficient by using parameterized queries. The query can do a soft delete or a complete remove of it,
146    /// that detail is up to the implementor, the rest of the Trait expects the query to not return anything however and the query should reflect that.
147    ///
148    /// # Parameters
149    ///
150    /// * `filter` - The filter used when generating the query.
151    ///
152    /// # Returns
153    ///
154    /// * [`Query`] - A prepared SQL query to DELETE records or soft delete them
155    ///
156    /// # Implementation Notes
157    ///
158    /// Consider:
159    /// 1. Handling soft deletes if required
160    /// 2. Checking foreign key constraints
161    /// 3. Implementing cascading deletes if needed
162    /// 4. If called via the default implementation of [`delete_by_filter_with_executor`](Self::delete_by_filter_with_executor)
163    ///    the filter will be guaranteed to be applied.
164    fn delete_by_filter_query<'args>(
165        filter: impl SqlFilter<'args>,
166    ) -> QueryBuilder<'args, Database>;
167
168    /// Removes a single record from the database by its identifier.
169    ///
170    /// This method executes the deletion query generated by [`delete_by_id_query`](Self::delete_by_id_query) and uses the [`Executor`] `tx` for doing it. It provides
171    /// a simple interface for removing individual records while handling all necessary database
172    /// interactions and error management.
173    ///
174    /// # Parameters
175    ///
176    /// * `tx` - The executor to use for the query
177    /// * `id` - Any value that can be converted into the model's ID type
178    ///
179    /// # Returns
180    ///
181    /// * [`crate::Result<()>`](crate::Result) - Success if the deletion was executed, or an error if the operation failed
182    ///
183    /// # Example
184    ///
185    /// ```ignore
186    /// async fn remove_user(repo: &UserRepository, user_id: i32) -> crate::Result<()> {
187    ///     repo.delete_by_id_with_executor(repo.pool(), user_id).await
188    /// }
189    /// ```
190    #[inline(always)]
191    #[cfg_attr(feature = "log_err", tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "delete_by_id", err))]
192    #[cfg_attr(not(feature = "log_err"), tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "delete_by_id"))]
193    async fn delete_by_id_with_executor<'c, E>(
194        &self,
195        tx: E,
196        id: impl Into<M::Id> + Send,
197    ) -> crate::Result<()>
198    where
199        'c: 'async_trait,
200        E: Executor<'c, Database = Database> + Send,
201    {
202        Self::delete_by_id_query(&id.into()).execute(tx).await?;
203        Ok(())
204    }
205
206    /// Removes a single record from the database by its identifier.
207    ///
208    /// This method executes the deletion query generated by [`delete_by_id_query`](Self::delete_by_id_query). It provides
209    /// a simple interface for removing individual records while handling all necessary database
210    /// interactions and error management.
211    ///
212    /// # Parameters
213    ///
214    /// * `id` - Any value that can be converted into the model's ID type
215    ///
216    /// # Returns
217    ///
218    /// * [`crate::Result<()>`](crate::Result) - Success if the deletion was executed, or an error if the operation failed
219    ///
220    /// # Example
221    ///
222    /// ```no_compile
223    /// async fn remove_user(repo: &UserRepository, user_id: i32) -> crate::Result<()> {
224    ///     repo.delete_by_id(user_id).await
225    /// }
226    /// ```
227    #[inline(always)]
228    async fn delete_by_id(&self, id: impl Into<M::Id> + Send) -> crate::Result<()> {
229        self.delete_by_id_with_executor(self.pool(), id).await
230    }
231
232    /// Deletes multiple records using the default batch size.
233    ///
234    /// This is a convenience wrapper around [`delete_batch`](Self::delete_batch_by_id) that uses [`DEFAULT_BATCH_SIZE`].
235    /// It provides a simpler interface for bulk deletions when the default batch size
236    /// is appropriate.
237    ///
238    /// # Parameters
239    ///
240    /// * `ids` - An iterator yielding IDs of records to delete
241    ///
242    /// # Returns
243    ///
244    /// * [`crate::Result<()>`](crate::Result) - Success if all deletions were executed, or an error if any operation failed
245    #[inline(always)]
246    async fn delete_many_by_id<I>(&self, ids: I) -> crate::Result<()>
247    where
248        I: IntoIterator<Item = M::Id> + Send,
249        I::IntoIter: Send,
250    {
251        <Self as DeleteRepository<M>>::delete_batch_by_id::<DEFAULT_BATCH_SIZE, I>(self, ids).await
252    }
253
254    /// Performs a batched deletion operation with a specified batch size.
255    ///
256    /// Similar to other batch operations, this method uses [`BatchOperator`] to efficiently
257    /// process large numbers of deletions in chunks, maintaining optimal performance
258    /// and preventing resource exhaustion.
259    ///
260    /// # Type Parameters
261    ///
262    /// * `N` - The size of each batch to process
263    ///
264    /// # Parameters
265    ///
266    /// * `ids` - An iterator yielding IDs of records to delete
267    ///
268    /// # Returns
269    ///
270    /// * [`crate::Result<()>`](crate::Result) - Success if all batches were processed, or an error if any operation failed
271    ///
272    /// # Implementation Details
273    ///
274    /// The method:
275    /// 1. Chunks the input IDs into batches of size N
276    /// 2. Processes each batch in a transactions using [`delete_query_by_id`](Self::delete_by_id_query)
277    /// 3. Maintains ACID properties within each batch
278    ///
279    /// # Performance Considerations
280    ///
281    /// Consider batch size carefully:
282    /// - Too small: More overhead from multiple transactions
283    /// - Too large: Higher memory usage and longer transactions times
284    #[inline(always)]
285    #[cfg_attr(feature = "log_err", tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "delete_batch_by_id", err))]
286    #[cfg_attr(not(feature = "log_err"), tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "delete_batch_by_id"))]
287    async fn delete_batch_by_id<const N: usize, I>(&self, ids: I) -> crate::Result<()>
288    where
289        I: IntoIterator<Item = M::Id> + Send,
290        I::IntoIter: Send,
291    {
292        let span = tracing::Span::current();
293        span.record("BATCH_SIZE", N);
294
295        BatchOperator::<M::Id, N>::execute_query(ids, self.pool(), Self::delete_by_id_query).await
296    }
297
298    /// Removes records from the database by a filter.
299    ///
300    /// This method executes the deletion query generated by [`delete_by_filter_query`](Self::delete_by_filter_query)
301    /// and uses the [`Executor`] `tx` for doing it. It provides a simple interface for removing records with a filter
302    /// while handling all necessary database interactions and error management.
303    ///
304    /// # Parameters
305    ///
306    /// * `tx` - The executor to use for the query
307    /// * `filter` - A [`SqlFilter`] to define what records should be deleted
308    ///
309    /// # Returns
310    ///
311    /// * [`crate::Result<()>`](crate::Result) - Success if the deletion was executed, or an error if the operation failed
312    ///
313    /// # Example
314    ///
315    /// ```ignore
316    /// async fn remove_user(repo: &UserRepository, filter: impl SqlFilter<'_>) -> crate::Result<()> {
317    ///     repo.delete_by_filter_with_executor(repo.pool(), filter).await
318    /// }
319    /// ```
320    #[inline(always)]
321    #[cfg_attr(feature = "log_err", tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "delete_by_filter", err))]
322    #[cfg_attr(not(feature = "log_err"), tracing::instrument(skip_all, level = "debug", parent = &(Self::repository_span()), name = "delete_by_filter"))]
323    async fn delete_by_filter_with_executor<'c, E>(
324        &self,
325        tx: E,
326        filter: impl SqlFilter<'_> + Send,
327    ) -> crate::Result<()>
328    where
329        E: Executor<'c, Database = Database> + Send,
330    {
331        if !filter.should_apply_filter() {
332            return Err(crate::Error::Repository {
333                message: "Can not Delete from table with a empty filter.".into(),
334            });
335        }
336
337        Self::delete_by_filter_query(filter)
338            .build()
339            .execute(tx)
340            .await?;
341        Ok(())
342    }
343
344    /// Removes records from the database by a filter.
345    ///
346    /// This method executes the deletion query generated by [`delete_by_filter_query`](Self::delete_by_filter_query)
347    /// and uses the [`Repository::pool`] as a [`Executor`]. It provides a simple interface for removing records with a filter
348    /// while handling all necessary database interactions and error management.
349    ///
350    /// # Parameters
351    ///
352    /// * `filter` - A [`SqlFilter`] to define what records should be deleted
353    ///
354    /// # Returns
355    ///
356    /// * [`crate::Result<()>`](crate::Result) - Success if the deletion was executed, or an error if the operation failed
357    ///
358    /// # Example
359    ///
360    /// ```no_compile
361    /// async fn remove_user(repo: &UserRepository, filter: impl SqlFilter<'_>) -> crate::Result<()> {
362    ///     repo.delete_by_filter(filter).await
363    /// }
364    /// ```
365    #[inline(always)]
366    async fn delete_by_filter(&self, filter: impl SqlFilter<'_> + Send) -> crate::Result<()> {
367        self.delete_by_filter_with_executor(self.pool(), filter)
368            .await
369    }
370}