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