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 {
183                delete_count: count,
184            }
185        }
186    }
187
188    impl QueryEngine for MockEngine {
189        fn query_many<T: Model + Send + 'static>(
190            &self,
191            _sql: &str,
192            _params: Vec<FilterValue>,
193        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
194            Box::pin(async { Ok(Vec::new()) })
195        }
196
197        fn query_one<T: Model + Send + 'static>(
198            &self,
199            _sql: &str,
200            _params: Vec<FilterValue>,
201        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
202            Box::pin(async { Err(QueryError::not_found("test")) })
203        }
204
205        fn query_optional<T: Model + Send + 'static>(
206            &self,
207            _sql: &str,
208            _params: Vec<FilterValue>,
209        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
210            Box::pin(async { Ok(None) })
211        }
212
213        fn execute_insert<T: Model + Send + 'static>(
214            &self,
215            _sql: &str,
216            _params: Vec<FilterValue>,
217        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
218            Box::pin(async { Err(QueryError::not_found("test")) })
219        }
220
221        fn execute_update<T: Model + Send + 'static>(
222            &self,
223            _sql: &str,
224            _params: Vec<FilterValue>,
225        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
226            Box::pin(async { Ok(Vec::new()) })
227        }
228
229        fn execute_delete(
230            &self,
231            _sql: &str,
232            _params: Vec<FilterValue>,
233        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
234            let count = self.delete_count;
235            Box::pin(async move { Ok(count) })
236        }
237
238        fn execute_raw(
239            &self,
240            _sql: &str,
241            _params: Vec<FilterValue>,
242        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
243            Box::pin(async { Ok(0) })
244        }
245
246        fn count(
247            &self,
248            _sql: &str,
249            _params: Vec<FilterValue>,
250        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
251            Box::pin(async { Ok(0) })
252        }
253    }
254
255    // ========== DeleteOperation Tests ==========
256
257    #[test]
258    fn test_delete_new() {
259        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
260        let (sql, params) = op.build_sql();
261
262        assert!(sql.contains("DELETE FROM test_models"));
263        assert!(sql.contains("RETURNING *"));
264        assert!(params.is_empty());
265    }
266
267    #[test]
268    fn test_delete_with_filter() {
269        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
270            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
271
272        let (sql, params) = op.build_sql();
273
274        assert!(sql.contains("DELETE FROM test_models"));
275        assert!(sql.contains("WHERE"));
276        assert!(sql.contains("id = $1"));
277        assert!(sql.contains("RETURNING *"));
278        assert_eq!(params.len(), 1);
279    }
280
281    #[test]
282    fn test_delete_with_select() {
283        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
284            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
285            .select(Select::fields(["id", "name"]));
286
287        let (sql, _) = op.build_sql();
288
289        assert!(sql.contains("RETURNING id, name"));
290        assert!(!sql.contains("RETURNING *"));
291    }
292
293    #[test]
294    fn test_delete_with_compound_filter() {
295        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
296            .r#where(Filter::Equals(
297                "status".into(),
298                FilterValue::String("deleted".to_string()),
299            ))
300            .r#where(Filter::Lt(
301                "updated_at".into(),
302                FilterValue::String("2024-01-01".to_string()),
303            ));
304
305        let (sql, params) = op.build_sql();
306
307        assert!(sql.contains("WHERE"));
308        assert!(sql.contains("AND"));
309        assert_eq!(params.len(), 2);
310    }
311
312    #[test]
313    fn test_delete_without_filter() {
314        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
315        let (sql, _) = op.build_sql();
316
317        assert!(!sql.contains("WHERE"));
318        assert!(sql.contains("DELETE FROM test_models"));
319    }
320
321    #[test]
322    fn test_delete_build_sql_count() {
323        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
324            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
325
326        let (sql, params) = op.build_sql_count();
327
328        assert!(sql.contains("DELETE FROM test_models"));
329        assert!(sql.contains("WHERE"));
330        assert!(!sql.contains("RETURNING")); // No RETURNING for count
331        assert_eq!(params.len(), 1);
332    }
333
334    #[test]
335    fn test_delete_with_or_filter() {
336        let op =
337            DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::or([
338                Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
339                Filter::Equals("status".into(), FilterValue::String("archived".to_string())),
340            ]));
341
342        let (sql, params) = op.build_sql();
343
344        assert!(sql.contains("OR"));
345        assert_eq!(params.len(), 2);
346    }
347
348    #[test]
349    fn test_delete_with_in_filter() {
350        let op =
351            DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::In(
352                "id".into(),
353                vec![
354                    FilterValue::Int(1),
355                    FilterValue::Int(2),
356                    FilterValue::Int(3),
357                ],
358            ));
359
360        let (sql, params) = op.build_sql();
361
362        assert!(sql.contains("IN"));
363        assert_eq!(params.len(), 3);
364    }
365
366    #[tokio::test]
367    async fn test_delete_exec() {
368        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
369            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
370
371        let result = op.exec().await;
372        assert!(result.is_ok());
373    }
374
375    #[tokio::test]
376    async fn test_delete_exec_count() {
377        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::with_count(5))
378            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
379
380        let result = op.exec_count().await;
381        assert!(result.is_ok());
382        assert_eq!(result.unwrap(), 5);
383    }
384
385    // ========== DeleteManyOperation Tests ==========
386
387    #[test]
388    fn test_delete_many_new() {
389        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
390        let (sql, params) = op.build_sql();
391
392        assert!(sql.contains("DELETE FROM test_models"));
393        assert!(!sql.contains("RETURNING"));
394        assert!(params.is_empty());
395    }
396
397    #[test]
398    fn test_delete_many() {
399        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
400            Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]),
401        );
402
403        let (sql, params) = op.build_sql();
404
405        assert!(sql.contains("DELETE FROM test_models"));
406        assert!(sql.contains("IN"));
407        assert!(!sql.contains("RETURNING"));
408        assert_eq!(params.len(), 2);
409    }
410
411    #[test]
412    fn test_delete_many_with_compound_filter() {
413        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
414            .r#where(Filter::Equals("tenant_id".into(), FilterValue::Int(1)))
415            .r#where(Filter::Equals("deleted".into(), FilterValue::Bool(true)));
416
417        let (sql, params) = op.build_sql();
418
419        assert!(sql.contains("WHERE"));
420        assert!(sql.contains("AND"));
421        assert_eq!(params.len(), 2);
422    }
423
424    #[test]
425    fn test_delete_many_without_filter() {
426        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
427        let (sql, _) = op.build_sql();
428
429        assert!(!sql.contains("WHERE"));
430    }
431
432    #[test]
433    fn test_delete_many_with_not_in_filter() {
434        let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
435            Filter::NotIn(
436                "status".into(),
437                vec![
438                    FilterValue::String("active".to_string()),
439                    FilterValue::String("pending".to_string()),
440                ],
441            ),
442        );
443
444        let (sql, params) = op.build_sql();
445
446        assert!(sql.contains("NOT IN"));
447        assert_eq!(params.len(), 2);
448    }
449
450    #[tokio::test]
451    async fn test_delete_many_exec() {
452        let op =
453            DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(10)).r#where(
454                Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
455            );
456
457        let result = op.exec().await;
458        assert!(result.is_ok());
459        assert_eq!(result.unwrap(), 10);
460    }
461
462    // ========== SQL Structure Tests ==========
463
464    #[test]
465    fn test_delete_sql_structure() {
466        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
467            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
468            .select(Select::fields(["id"]));
469
470        let (sql, _) = op.build_sql();
471
472        let delete_pos = sql.find("DELETE FROM").unwrap();
473        let where_pos = sql.find("WHERE").unwrap();
474        let returning_pos = sql.find("RETURNING").unwrap();
475
476        assert!(delete_pos < where_pos);
477        assert!(where_pos < returning_pos);
478    }
479
480    #[test]
481    fn test_delete_with_null_check() {
482        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
483            .r#where(Filter::IsNull("deleted_at".into()));
484
485        let (sql, params) = op.build_sql();
486
487        assert!(sql.contains("IS NULL"));
488        assert!(params.is_empty()); // IS NULL doesn't have params
489    }
490
491    #[test]
492    fn test_delete_with_not_null_check() {
493        let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
494            .r#where(Filter::IsNotNull("email".into()));
495
496        let (sql, params) = op.build_sql();
497
498        assert!(sql.contains("IS NOT NULL"));
499        assert!(params.is_empty());
500    }
501}