Skip to main content

prax_query/operations/
delete.rs

1//! Delete operation for removing records.
2
3use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::{Filter, FilterValue};
7use crate::traits::{Model, QueryEngine};
8use crate::types::Select;
9
10/// A delete operation for removing records.
11///
12/// # Example
13///
14/// ```rust,ignore
15/// let deleted = client
16///     .user()
17///     .delete()
18///     .r#where(user::id::equals(1))
19///     .exec()
20///     .await?;
21/// ```
22pub struct DeleteOperation<E: QueryEngine, M: Model> {
23    engine: E,
24    filter: Filter,
25    select: Select,
26    _model: PhantomData<M>,
27}
28
29impl<E: QueryEngine, M: Model + crate::row::FromRow> DeleteOperation<E, M> {
30    /// Create a new Delete operation.
31    pub fn new(engine: E) -> Self {
32        Self {
33            engine,
34            filter: Filter::None,
35            select: Select::All,
36            _model: PhantomData,
37        }
38    }
39
40    /// Add a filter condition.
41    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
42        let new_filter = filter.into();
43        self.filter = self.filter.and_then(new_filter);
44        self
45    }
46
47    /// Select specific fields to return from deleted records.
48    pub fn select(mut self, select: impl Into<Select>) -> Self {
49        self.select = select.into();
50        self
51    }
52
53    /// Build the SQL query.
54    pub fn build_sql(
55        &self,
56        dialect: &dyn crate::dialect::SqlDialect,
57    ) -> (String, Vec<FilterValue>) {
58        let (where_sql, params) = self.filter.to_sql(0, dialect);
59
60        let mut sql = String::new();
61
62        // DELETE FROM clause
63        sql.push_str("DELETE FROM ");
64        sql.push_str(M::TABLE_NAME);
65
66        // WHERE clause
67        if !self.filter.is_none() {
68            sql.push_str(" WHERE ");
69            sql.push_str(&where_sql);
70        }
71
72        // RETURNING clause
73        sql.push_str(&dialect.returning_clause(&self.select.to_sql()));
74
75        (sql, params)
76    }
77
78    /// Build SQL without RETURNING (for count).
79    fn build_sql_count(
80        &self,
81        dialect: &dyn crate::dialect::SqlDialect,
82    ) -> (String, Vec<FilterValue>) {
83        let (where_sql, params) = self.filter.to_sql(0, dialect);
84
85        let mut sql = String::new();
86
87        sql.push_str("DELETE FROM ");
88        sql.push_str(M::TABLE_NAME);
89
90        if !self.filter.is_none() {
91            sql.push_str(" WHERE ");
92            sql.push_str(&where_sql);
93        }
94
95        (sql, params)
96    }
97
98    /// Execute the delete and return deleted records.
99    pub async fn exec(self) -> QueryResult<Vec<M>>
100    where
101        M: Send + 'static,
102    {
103        let dialect = self.engine.dialect();
104        let (sql, params) = self.build_sql(dialect);
105        self.engine.execute_update::<M>(&sql, params).await
106    }
107
108    /// Execute the delete and return the count of deleted records.
109    pub async fn exec_count(self) -> QueryResult<u64> {
110        let dialect = self.engine.dialect();
111        let (sql, params) = self.build_sql_count(dialect);
112        self.engine.execute_delete(&sql, params).await
113    }
114}
115
116/// Delete many records at once.
117pub struct DeleteManyOperation<E: QueryEngine, M: Model> {
118    engine: E,
119    filter: Filter,
120    _model: PhantomData<M>,
121}
122
123impl<E: QueryEngine, M: Model> DeleteManyOperation<E, M> {
124    /// Create a new DeleteMany operation.
125    pub fn new(engine: E) -> Self {
126        Self {
127            engine,
128            filter: Filter::None,
129            _model: PhantomData,
130        }
131    }
132
133    /// Add a filter condition.
134    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
135        let new_filter = filter.into();
136        self.filter = self.filter.and_then(new_filter);
137        self
138    }
139
140    /// Build the SQL query.
141    pub fn build_sql(
142        &self,
143        dialect: &dyn crate::dialect::SqlDialect,
144    ) -> (String, Vec<FilterValue>) {
145        let (where_sql, params) = self.filter.to_sql(0, dialect);
146
147        let mut sql = String::new();
148
149        sql.push_str("DELETE FROM ");
150        sql.push_str(M::TABLE_NAME);
151
152        if !self.filter.is_none() {
153            sql.push_str(" WHERE ");
154            sql.push_str(&where_sql);
155        }
156
157        (sql, params)
158    }
159
160    /// Execute the delete and return the count of deleted records.
161    pub async fn exec(self) -> QueryResult<u64> {
162        let dialect = self.engine.dialect();
163        let (sql, params) = self.build_sql(dialect);
164        self.engine.execute_delete(&sql, params).await
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::error::QueryError;
172
173    struct TestModel;
174
175    impl Model for TestModel {
176        const MODEL_NAME: &'static str = "TestModel";
177        const TABLE_NAME: &'static str = "test_models";
178        const PRIMARY_KEY: &'static [&'static str] = &["id"];
179        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
180    }
181
182    impl crate::row::FromRow for TestModel {
183        fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
184            Ok(TestModel)
185        }
186    }
187
188    #[derive(Clone)]
189    struct MockEngine {
190        delete_count: u64,
191    }
192
193    impl MockEngine {
194        fn new() -> Self {
195            Self { delete_count: 0 }
196        }
197
198        fn with_count(count: u64) -> Self {
199            Self {
200                delete_count: count,
201            }
202        }
203    }
204
205    impl QueryEngine for MockEngine {
206        fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
207            &crate::dialect::Postgres
208        }
209
210        fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
211            &self,
212            _sql: &str,
213            _params: Vec<FilterValue>,
214        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
215            Box::pin(async { Ok(Vec::new()) })
216        }
217
218        fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
219            &self,
220            _sql: &str,
221            _params: Vec<FilterValue>,
222        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
223            Box::pin(async { Err(QueryError::not_found("test")) })
224        }
225
226        fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
227            &self,
228            _sql: &str,
229            _params: Vec<FilterValue>,
230        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
231            Box::pin(async { Ok(None) })
232        }
233
234        fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
235            &self,
236            _sql: &str,
237            _params: Vec<FilterValue>,
238        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
239            Box::pin(async { Err(QueryError::not_found("test")) })
240        }
241
242        fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
243            &self,
244            _sql: &str,
245            _params: Vec<FilterValue>,
246        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
247            Box::pin(async { Ok(Vec::new()) })
248        }
249
250        fn execute_delete(
251            &self,
252            _sql: &str,
253            _params: Vec<FilterValue>,
254        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
255            let count = self.delete_count;
256            Box::pin(async move { Ok(count) })
257        }
258
259        fn execute_raw(
260            &self,
261            _sql: &str,
262            _params: Vec<FilterValue>,
263        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
264            Box::pin(async { Ok(0) })
265        }
266
267        fn count(
268            &self,
269            _sql: &str,
270            _params: Vec<FilterValue>,
271        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
272            Box::pin(async { Ok(0) })
273        }
274    }
275
276    // ========== DeleteOperation Tests ==========
277
278    #[test]
279    fn test_delete_new() {
280        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
281        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
282
283        assert!(sql.contains("DELETE FROM test_models"));
284        assert!(sql.contains("RETURNING *"));
285        assert!(params.is_empty());
286    }
287
288    #[test]
289    fn test_delete_with_filter() {
290        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
291            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
292
293        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
294
295        assert!(sql.contains("DELETE FROM test_models"));
296        assert!(sql.contains("WHERE"));
297        assert!(sql.contains(r#""id" = $1"#));
298        assert!(sql.contains("RETURNING *"));
299        assert_eq!(params.len(), 1);
300    }
301
302    #[test]
303    fn test_delete_with_select() {
304        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
305            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
306            .select(Select::fields(["id", "name"]));
307
308        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
309
310        assert!(sql.contains("RETURNING id, name"));
311        assert!(!sql.contains("RETURNING *"));
312    }
313
314    #[test]
315    fn test_delete_with_compound_filter() {
316        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
317            .r#where(Filter::Equals(
318                "status".into(),
319                FilterValue::String("deleted".to_string()),
320            ))
321            .r#where(Filter::Lt(
322                "updated_at".into(),
323                FilterValue::String("2024-01-01".to_string()),
324            ));
325
326        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
327
328        assert!(sql.contains("WHERE"));
329        assert!(sql.contains("AND"));
330        assert_eq!(params.len(), 2);
331    }
332
333    #[test]
334    fn test_delete_without_filter() {
335        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
336        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
337
338        assert!(!sql.contains("WHERE"));
339        assert!(sql.contains("DELETE FROM test_models"));
340    }
341
342    #[test]
343    fn test_delete_build_sql_count() {
344        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
345            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
346
347        let (sql, params) = op.build_sql_count(&crate::dialect::Postgres);
348
349        assert!(sql.contains("DELETE FROM test_models"));
350        assert!(sql.contains("WHERE"));
351        assert!(!sql.contains("RETURNING")); // No RETURNING for count
352        assert_eq!(params.len(), 1);
353    }
354
355    #[test]
356    fn test_delete_with_or_filter() {
357        let op =
358            DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::or([
359                Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
360                Filter::Equals("status".into(), FilterValue::String("archived".to_string())),
361            ]));
362
363        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
364
365        assert!(sql.contains("OR"));
366        assert_eq!(params.len(), 2);
367    }
368
369    #[test]
370    fn test_delete_with_in_filter() {
371        let op =
372            DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::In(
373                "id".into(),
374                vec![
375                    FilterValue::Int(1),
376                    FilterValue::Int(2),
377                    FilterValue::Int(3),
378                ],
379            ));
380
381        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
382
383        assert!(sql.contains("IN"));
384        assert_eq!(params.len(), 3);
385    }
386
387    #[tokio::test]
388    async fn test_delete_exec() {
389        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
390            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
391
392        let result = op.exec().await;
393        assert!(result.is_ok());
394    }
395
396    #[tokio::test]
397    async fn test_delete_exec_count() {
398        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::with_count(5))
399            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
400
401        let result = op.exec_count().await;
402        assert!(result.is_ok());
403        assert_eq!(result.unwrap(), 5);
404    }
405
406    // ========== DeleteManyOperation Tests ==========
407
408    #[test]
409    fn test_delete_many_new() {
410        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
411        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
412
413        assert!(sql.contains("DELETE FROM test_models"));
414        assert!(!sql.contains("RETURNING"));
415        assert!(params.is_empty());
416    }
417
418    #[test]
419    fn test_delete_many() {
420        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
421            Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]),
422        );
423
424        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
425
426        assert!(sql.contains("DELETE FROM test_models"));
427        assert!(sql.contains("IN"));
428        assert!(!sql.contains("RETURNING"));
429        assert_eq!(params.len(), 2);
430    }
431
432    #[test]
433    fn test_delete_many_with_compound_filter() {
434        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
435            .r#where(Filter::Equals("tenant_id".into(), FilterValue::Int(1)))
436            .r#where(Filter::Equals("deleted".into(), FilterValue::Bool(true)));
437
438        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
439
440        assert!(sql.contains("WHERE"));
441        assert!(sql.contains("AND"));
442        assert_eq!(params.len(), 2);
443    }
444
445    #[test]
446    fn test_delete_many_without_filter() {
447        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
448        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
449
450        assert!(!sql.contains("WHERE"));
451    }
452
453    #[test]
454    fn test_delete_many_with_not_in_filter() {
455        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
456            Filter::NotIn(
457                "status".into(),
458                vec![
459                    FilterValue::String("active".to_string()),
460                    FilterValue::String("pending".to_string()),
461                ],
462            ),
463        );
464
465        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
466
467        assert!(sql.contains("NOT IN"));
468        assert_eq!(params.len(), 2);
469    }
470
471    #[tokio::test]
472    async fn test_delete_many_exec() {
473        let op =
474            DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(10)).r#where(
475                Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
476            );
477
478        let result = op.exec().await;
479        assert!(result.is_ok());
480        assert_eq!(result.unwrap(), 10);
481    }
482
483    // ========== SQL Structure Tests ==========
484
485    #[test]
486    fn test_delete_sql_structure() {
487        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
488            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
489            .select(Select::fields(["id"]));
490
491        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
492
493        let delete_pos = sql.find("DELETE FROM").unwrap();
494        let where_pos = sql.find("WHERE").unwrap();
495        let returning_pos = sql.find("RETURNING").unwrap();
496
497        assert!(delete_pos < where_pos);
498        assert!(where_pos < returning_pos);
499    }
500
501    #[test]
502    fn test_delete_with_null_check() {
503        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
504            .r#where(Filter::IsNull("deleted_at".into()));
505
506        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
507
508        assert!(sql.contains("IS NULL"));
509        assert!(params.is_empty()); // IS NULL doesn't have params
510    }
511
512    #[test]
513    fn test_delete_with_not_null_check() {
514        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
515            .r#where(Filter::IsNotNull("email".into()));
516
517        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
518
519        assert!(sql.contains("IS NOT NULL"));
520        assert!(params.is_empty());
521    }
522}