1use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::{Filter, FilterValue};
7use crate::traits::{Model, QueryEngine};
8use crate::types::Select;
9
10pub 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 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 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 pub fn select(mut self, select: impl Into<Select>) -> Self {
49 self.select = select.into();
50 self
51 }
52
53 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 sql.push_str("DELETE FROM ");
61 sql.push_str(M::TABLE_NAME);
62
63 if !self.filter.is_none() {
65 sql.push_str(" WHERE ");
66 sql.push_str(&where_sql);
67 }
68
69 sql.push_str(" RETURNING ");
71 sql.push_str(&self.select.to_sql());
72
73 (sql, params)
74 }
75
76 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 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 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
109pub 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 pub fn new(engine: E) -> Self {
119 Self {
120 engine,
121 filter: Filter::None,
122 _model: PhantomData,
123 }
124 }
125
126 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 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 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 #[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")); 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 #[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 #[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()); }
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