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