use std::marker::PhantomData;
use crate::error::QueryResult;
use crate::filter::{Filter, FilterValue};
use crate::traits::{Model, QueryEngine};
use crate::types::Select;
pub struct DeleteOperation<E: QueryEngine, M: Model> {
engine: E,
filter: Filter,
select: Select,
_model: PhantomData<M>,
}
impl<E: QueryEngine, M: Model> DeleteOperation<E, M> {
pub fn new(engine: E) -> Self {
Self {
engine,
filter: Filter::None,
select: Select::All,
_model: PhantomData,
}
}
pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
let new_filter = filter.into();
self.filter = self.filter.and_then(new_filter);
self
}
pub fn select(mut self, select: impl Into<Select>) -> Self {
self.select = select.into();
self
}
pub fn build_sql(&self) -> (String, Vec<FilterValue>) {
let (where_sql, params) = self.filter.to_sql(0);
let mut sql = String::new();
sql.push_str("DELETE FROM ");
sql.push_str(M::TABLE_NAME);
if !self.filter.is_none() {
sql.push_str(" WHERE ");
sql.push_str(&where_sql);
}
sql.push_str(" RETURNING ");
sql.push_str(&self.select.to_sql());
(sql, params)
}
fn build_sql_count(&self) -> (String, Vec<FilterValue>) {
let (where_sql, params) = self.filter.to_sql(0);
let mut sql = String::new();
sql.push_str("DELETE FROM ");
sql.push_str(M::TABLE_NAME);
if !self.filter.is_none() {
sql.push_str(" WHERE ");
sql.push_str(&where_sql);
}
(sql, params)
}
pub async fn exec(self) -> QueryResult<Vec<M>>
where
M: Send + 'static,
{
let (sql, params) = self.build_sql();
self.engine.execute_update::<M>(&sql, params).await
}
pub async fn exec_count(self) -> QueryResult<u64> {
let (sql, params) = self.build_sql_count();
self.engine.execute_delete(&sql, params).await
}
}
pub struct DeleteManyOperation<E: QueryEngine, M: Model> {
engine: E,
filter: Filter,
_model: PhantomData<M>,
}
impl<E: QueryEngine, M: Model> DeleteManyOperation<E, M> {
pub fn new(engine: E) -> Self {
Self {
engine,
filter: Filter::None,
_model: PhantomData,
}
}
pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
let new_filter = filter.into();
self.filter = self.filter.and_then(new_filter);
self
}
pub fn build_sql(&self) -> (String, Vec<FilterValue>) {
let (where_sql, params) = self.filter.to_sql(0);
let mut sql = String::new();
sql.push_str("DELETE FROM ");
sql.push_str(M::TABLE_NAME);
if !self.filter.is_none() {
sql.push_str(" WHERE ");
sql.push_str(&where_sql);
}
(sql, params)
}
pub async fn exec(self) -> QueryResult<u64> {
let (sql, params) = self.build_sql();
self.engine.execute_delete(&sql, params).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::QueryError;
struct TestModel;
impl Model for TestModel {
const MODEL_NAME: &'static str = "TestModel";
const TABLE_NAME: &'static str = "test_models";
const PRIMARY_KEY: &'static [&'static str] = &["id"];
const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
}
#[derive(Clone)]
struct MockEngine {
delete_count: u64,
}
impl MockEngine {
fn new() -> Self {
Self { delete_count: 0 }
}
fn with_count(count: u64) -> Self {
Self {
delete_count: count,
}
}
}
impl QueryEngine for MockEngine {
fn query_many<T: Model + Send + 'static>(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
Box::pin(async { Ok(Vec::new()) })
}
fn query_one<T: Model + Send + 'static>(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
Box::pin(async { Err(QueryError::not_found("test")) })
}
fn query_optional<T: Model + Send + 'static>(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
Box::pin(async { Ok(None) })
}
fn execute_insert<T: Model + Send + 'static>(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
Box::pin(async { Err(QueryError::not_found("test")) })
}
fn execute_update<T: Model + Send + 'static>(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
Box::pin(async { Ok(Vec::new()) })
}
fn execute_delete(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
let count = self.delete_count;
Box::pin(async move { Ok(count) })
}
fn execute_raw(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
Box::pin(async { Ok(0) })
}
fn count(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
Box::pin(async { Ok(0) })
}
}
#[test]
fn test_delete_new() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
let (sql, params) = op.build_sql();
assert!(sql.contains("DELETE FROM test_models"));
assert!(sql.contains("RETURNING *"));
assert!(params.is_empty());
}
#[test]
fn test_delete_with_filter() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
let (sql, params) = op.build_sql();
assert!(sql.contains("DELETE FROM test_models"));
assert!(sql.contains("WHERE"));
assert!(sql.contains("id = $1"));
assert!(sql.contains("RETURNING *"));
assert_eq!(params.len(), 1);
}
#[test]
fn test_delete_with_select() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
.select(Select::fields(["id", "name"]));
let (sql, _) = op.build_sql();
assert!(sql.contains("RETURNING id, name"));
assert!(!sql.contains("RETURNING *"));
}
#[test]
fn test_delete_with_compound_filter() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::Equals(
"status".into(),
FilterValue::String("deleted".to_string()),
))
.r#where(Filter::Lt(
"updated_at".into(),
FilterValue::String("2024-01-01".to_string()),
));
let (sql, params) = op.build_sql();
assert!(sql.contains("WHERE"));
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_delete_without_filter() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new());
let (sql, _) = op.build_sql();
assert!(!sql.contains("WHERE"));
assert!(sql.contains("DELETE FROM test_models"));
}
#[test]
fn test_delete_build_sql_count() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
let (sql, params) = op.build_sql_count();
assert!(sql.contains("DELETE FROM test_models"));
assert!(sql.contains("WHERE"));
assert!(!sql.contains("RETURNING")); assert_eq!(params.len(), 1);
}
#[test]
fn test_delete_with_or_filter() {
let op =
DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::or([
Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
Filter::Equals("status".into(), FilterValue::String("archived".to_string())),
]));
let (sql, params) = op.build_sql();
assert!(sql.contains("OR"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_delete_with_in_filter() {
let op =
DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(Filter::In(
"id".into(),
vec![
FilterValue::Int(1),
FilterValue::Int(2),
FilterValue::Int(3),
],
));
let (sql, params) = op.build_sql();
assert!(sql.contains("IN"));
assert_eq!(params.len(), 3);
}
#[tokio::test]
async fn test_delete_exec() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
let result = op.exec().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_delete_exec_count() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::with_count(5))
.r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
let result = op.exec_count().await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 5);
}
#[test]
fn test_delete_many_new() {
let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
let (sql, params) = op.build_sql();
assert!(sql.contains("DELETE FROM test_models"));
assert!(!sql.contains("RETURNING"));
assert!(params.is_empty());
}
#[test]
fn test_delete_many() {
let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]),
);
let (sql, params) = op.build_sql();
assert!(sql.contains("DELETE FROM test_models"));
assert!(sql.contains("IN"));
assert!(!sql.contains("RETURNING"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_delete_many_with_compound_filter() {
let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::Equals("tenant_id".into(), FilterValue::Int(1)))
.r#where(Filter::Equals("deleted".into(), FilterValue::Bool(true)));
let (sql, params) = op.build_sql();
assert!(sql.contains("WHERE"));
assert!(sql.contains("AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn test_delete_many_without_filter() {
let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
let (sql, _) = op.build_sql();
assert!(!sql.contains("WHERE"));
}
#[test]
fn test_delete_many_with_not_in_filter() {
let op = DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::new()).r#where(
Filter::NotIn(
"status".into(),
vec![
FilterValue::String("active".to_string()),
FilterValue::String("pending".to_string()),
],
),
);
let (sql, params) = op.build_sql();
assert!(sql.contains("NOT IN"));
assert_eq!(params.len(), 2);
}
#[tokio::test]
async fn test_delete_many_exec() {
let op =
DeleteManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(10)).r#where(
Filter::Equals("status".into(), FilterValue::String("deleted".to_string())),
);
let result = op.exec().await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 10);
}
#[test]
fn test_delete_sql_structure() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
.select(Select::fields(["id"]));
let (sql, _) = op.build_sql();
let delete_pos = sql.find("DELETE FROM").unwrap();
let where_pos = sql.find("WHERE").unwrap();
let returning_pos = sql.find("RETURNING").unwrap();
assert!(delete_pos < where_pos);
assert!(where_pos < returning_pos);
}
#[test]
fn test_delete_with_null_check() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::IsNull("deleted_at".into()));
let (sql, params) = op.build_sql();
assert!(sql.contains("IS NULL"));
assert!(params.is_empty()); }
#[test]
fn test_delete_with_not_null_check() {
let op = DeleteOperation::<MockEngine, TestModel>::new(MockEngine::new())
.r#where(Filter::IsNotNull("email".into()));
let (sql, params) = op.build_sql();
assert!(sql.contains("IS NOT NULL"));
assert!(params.is_empty());
}
}