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 {
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 #[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")); 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 #[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 #[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()); }
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}