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 + crate::row::FromRow> 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(
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 sql.push_str("DELETE FROM ");
64 sql.push_str(M::TABLE_NAME);
65
66 if !self.filter.is_none() {
68 sql.push_str(" WHERE ");
69 sql.push_str(&where_sql);
70 }
71
72 sql.push_str(&dialect.returning_clause(&self.select.to_sql()));
74
75 (sql, params)
76 }
77
78 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 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 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 pub fn with_where_input<W: crate::inputs::WhereUniqueInput<Model = M>>(mut self, w: W) -> Self {
118 self.filter = w.into_ir();
119 self
120 }
121
122 pub fn with_select_input<S: crate::inputs::SelectInput<Model = M>>(mut self, s: S) -> Self {
124 self.select = s.into_ir();
125 self
126 }
127
128 #[doc(hidden)]
130 pub fn filter_for_test(&self) -> &Filter {
131 &self.filter
132 }
133}
134
135pub struct DeleteManyOperation<E: QueryEngine, M: Model> {
137 engine: E,
138 filter: Filter,
139 _model: PhantomData<M>,
140}
141
142impl<E: QueryEngine, M: Model> DeleteManyOperation<E, M> {
143 pub fn new(engine: E) -> Self {
145 Self {
146 engine,
147 filter: Filter::None,
148 _model: PhantomData,
149 }
150 }
151
152 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
154 let new_filter = filter.into();
155 self.filter = self.filter.and_then(new_filter);
156 self
157 }
158
159 pub fn build_sql(
161 &self,
162 dialect: &dyn crate::dialect::SqlDialect,
163 ) -> (String, Vec<FilterValue>) {
164 let (where_sql, params) = self.filter.to_sql(0, dialect);
165
166 let mut sql = String::new();
167
168 sql.push_str("DELETE FROM ");
169 sql.push_str(M::TABLE_NAME);
170
171 if !self.filter.is_none() {
172 sql.push_str(" WHERE ");
173 sql.push_str(&where_sql);
174 }
175
176 (sql, params)
177 }
178
179 pub async fn exec(self) -> QueryResult<u64> {
181 let dialect = self.engine.dialect();
182 let (sql, params) = self.build_sql(dialect);
183 self.engine.execute_delete(&sql, params).await
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::error::QueryError;
191
192 struct TestModel;
193
194 impl Model for TestModel {
195 const MODEL_NAME: &'static str = "TestModel";
196 const TABLE_NAME: &'static str = "test_models";
197 const PRIMARY_KEY: &'static [&'static str] = &["id"];
198 const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
199 }
200
201 impl crate::row::FromRow for TestModel {
202 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
203 Ok(TestModel)
204 }
205 }
206
207 #[derive(Clone)]
208 struct MockEngine {
209 delete_count: u64,
210 }
211
212 impl MockEngine {
213 fn new() -> Self {
214 Self { delete_count: 0 }
215 }
216
217 fn with_count(count: u64) -> Self {
218 Self {
219 delete_count: count,
220 }
221 }
222 }
223
224 impl QueryEngine for MockEngine {
225 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
226 &crate::dialect::Postgres
227 }
228
229 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
230 &self,
231 _sql: &str,
232 _params: Vec<FilterValue>,
233 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
234 Box::pin(async { Ok(Vec::new()) })
235 }
236
237 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
238 &self,
239 _sql: &str,
240 _params: Vec<FilterValue>,
241 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
242 Box::pin(async { Err(QueryError::not_found("test")) })
243 }
244
245 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
246 &self,
247 _sql: &str,
248 _params: Vec<FilterValue>,
249 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
250 Box::pin(async { Ok(None) })
251 }
252
253 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
254 &self,
255 _sql: &str,
256 _params: Vec<FilterValue>,
257 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
258 Box::pin(async { Err(QueryError::not_found("test")) })
259 }
260
261 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
262 &self,
263 _sql: &str,
264 _params: Vec<FilterValue>,
265 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
266 Box::pin(async { Ok(Vec::new()) })
267 }
268
269 fn execute_delete(
270 &self,
271 _sql: &str,
272 _params: Vec<FilterValue>,
273 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
274 let count = self.delete_count;
275 Box::pin(async move { Ok(count) })
276 }
277
278 fn execute_raw(
279 &self,
280 _sql: &str,
281 _params: Vec<FilterValue>,
282 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
283 Box::pin(async { Ok(0) })
284 }
285
286 fn count(
287 &self,
288 _sql: &str,
289 _params: Vec<FilterValue>,
290 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
291 Box::pin(async { Ok(0) })
292 }
293 }
294
295 #[test]
298 fn test_delete_new() {
299 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
300 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
301
302 assert!(sql.contains("DELETE FROM test_models"));
303 assert!(sql.contains("RETURNING *"));
304 assert!(params.is_empty());
305 }
306
307 #[test]
308 fn test_delete_with_filter() {
309 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
310 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
311
312 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
313
314 assert!(sql.contains("DELETE FROM test_models"));
315 assert!(sql.contains("WHERE"));
316 assert!(sql.contains(r#""id" = $1"#));
317 assert!(sql.contains("RETURNING *"));
318 assert_eq!(params.len(), 1);
319 }
320
321 #[test]
322 fn test_delete_with_select() {
323 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
324 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
325 .select(Select::fields(["id", "name"]));
326
327 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
328
329 assert!(sql.contains("RETURNING id, name"));
330 assert!(!sql.contains("RETURNING *"));
331 }
332
333 #[test]
334 fn test_delete_with_compound_filter() {
335 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
336 .r#where(Filter::Equals(
337 "status".into(),
338 FilterValue::String("deleted".to_string()),
339 ))
340 .r#where(Filter::Lt(
341 "updated_at".into(),
342 FilterValue::String("2024-01-01".to_string()),
343 ));
344
345 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
346
347 assert!(sql.contains("WHERE"));
348 assert!(sql.contains("AND"));
349 assert_eq!(params.len(), 2);
350 }
351
352 #[test]
353 fn test_delete_without_filter() {
354 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
355 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
356
357 assert!(!sql.contains("WHERE"));
358 assert!(sql.contains("DELETE FROM test_models"));
359 }
360
361 #[test]
362 fn test_delete_build_sql_count() {
363 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
364 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
365
366 let (sql, params) = op.build_sql_count(&crate::dialect::Postgres);
367
368 assert!(sql.contains("DELETE FROM test_models"));
369 assert!(sql.contains("WHERE"));
370 assert!(!sql.contains("RETURNING")); assert_eq!(params.len(), 1);
372 }
373
374 #[test]
375 fn test_delete_with_or_filter() {
376 let op =
377 DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::or([
378 Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
379 Filter::Equals("status".into(), FilterValue::String("archived".to_string())),
380 ]));
381
382 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
383
384 assert!(sql.contains("OR"));
385 assert_eq!(params.len(), 2);
386 }
387
388 #[test]
389 fn test_delete_with_in_filter() {
390 let op =
391 DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::In(
392 "id".into(),
393 vec![
394 FilterValue::Int(1),
395 FilterValue::Int(2),
396 FilterValue::Int(3),
397 ],
398 ));
399
400 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
401
402 assert!(sql.contains("IN"));
403 assert_eq!(params.len(), 3);
404 }
405
406 #[tokio::test]
407 async fn test_delete_exec() {
408 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
409 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
410
411 let result = op.exec().await;
412 assert!(result.is_ok());
413 }
414
415 #[tokio::test]
416 async fn test_delete_exec_count() {
417 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::with_count(5))
418 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
419
420 let result = op.exec_count().await;
421 assert!(result.is_ok());
422 assert_eq!(result.unwrap(), 5);
423 }
424
425 #[test]
428 fn test_delete_many_new() {
429 let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
430 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
431
432 assert!(sql.contains("DELETE FROM test_models"));
433 assert!(!sql.contains("RETURNING"));
434 assert!(params.is_empty());
435 }
436
437 #[test]
438 fn test_delete_many() {
439 let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
440 Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]),
441 );
442
443 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
444
445 assert!(sql.contains("DELETE FROM test_models"));
446 assert!(sql.contains("IN"));
447 assert!(!sql.contains("RETURNING"));
448 assert_eq!(params.len(), 2);
449 }
450
451 #[test]
452 fn test_delete_many_with_compound_filter() {
453 let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
454 .r#where(Filter::Equals("tenant_id".into(), FilterValue::Int(1)))
455 .r#where(Filter::Equals("deleted".into(), FilterValue::Bool(true)));
456
457 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
458
459 assert!(sql.contains("WHERE"));
460 assert!(sql.contains("AND"));
461 assert_eq!(params.len(), 2);
462 }
463
464 #[test]
465 fn test_delete_many_without_filter() {
466 let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
467 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
468
469 assert!(!sql.contains("WHERE"));
470 }
471
472 #[test]
473 fn test_delete_many_with_not_in_filter() {
474 let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
475 Filter::NotIn(
476 "status".into(),
477 vec![
478 FilterValue::String("active".to_string()),
479 FilterValue::String("pending".to_string()),
480 ],
481 ),
482 );
483
484 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
485
486 assert!(sql.contains("NOT IN"));
487 assert_eq!(params.len(), 2);
488 }
489
490 #[tokio::test]
491 async fn test_delete_many_exec() {
492 let op =
493 DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(10)).r#where(
494 Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
495 );
496
497 let result = op.exec().await;
498 assert!(result.is_ok());
499 assert_eq!(result.unwrap(), 10);
500 }
501
502 #[test]
505 fn test_delete_sql_structure() {
506 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
507 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
508 .select(Select::fields(["id"]));
509
510 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
511
512 let delete_pos = sql.find("DELETE FROM").unwrap();
513 let where_pos = sql.find("WHERE").unwrap();
514 let returning_pos = sql.find("RETURNING").unwrap();
515
516 assert!(delete_pos < where_pos);
517 assert!(where_pos < returning_pos);
518 }
519
520 #[test]
521 fn test_delete_with_null_check() {
522 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
523 .r#where(Filter::IsNull("deleted_at".into()));
524
525 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
526
527 assert!(sql.contains("IS NULL"));
528 assert!(params.is_empty()); }
530
531 #[test]
532 fn test_delete_with_not_null_check() {
533 let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
534 .r#where(Filter::IsNotNull("email".into()));
535
536 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
537
538 assert!(sql.contains("IS NOT NULL"));
539 assert!(params.is_empty());
540 }
541}