use crate::parser::{ParsedModel, SoftDeleteConfig};
use proc_macro2::TokenStream;
use quote::quote;
#[cfg_attr(test, mutants::skip)]
pub fn build_soft_delete_set_clause(cfg: &SoftDeleteConfig) -> String {
format!("{} = {{VALUE}}", cfg.column)
}
#[cfg_attr(test, mutants::skip)]
pub fn generate_delete_all_logic(parsed: &ParsedModel) -> TokenStream {
let table_name = &parsed.table_name;
if !parsed.has_soft_deletes {
return quote! {
let mut estimated_capacity = 20 + #table_name.len() + self.wheres.iter().map(|(o, c)| o.len() + c.len() + 4).sum::<usize>();
let mut query_str = String::with_capacity(estimated_capacity);
query_str.push_str("DELETE FROM ");
query_str.push_str(#table_name);
};
}
let Some(cfg) = parsed.soft_delete.as_ref() else {
return syn::Error::new(
parsed.name.span(),
"internal ORM macro error: soft-delete configuration is missing",
)
.to_compile_error();
};
let set_fragment = build_soft_delete_set_clause(cfg);
let delval_token: TokenStream = if cfg.delval.trim().is_empty() {
quote! {
let delval = "CURRENT_TIMESTAMP";
}
} else {
let delval_lit = cfg.delval.clone();
quote! {
let delval = #delval_lit;
}
};
let set_template = set_fragment;
let table_lit = table_name.clone();
quote! {
#delval_token
let mut estimated_capacity = 50 + #table_lit.len() + delval.len() + self.wheres.iter().map(|(o, c)| o.len() + c.len() + 4).sum::<usize>();
let mut query_str = String::with_capacity(estimated_capacity);
query_str.push_str("UPDATE ");
query_str.push_str(#table_lit);
query_str.push_str(" SET ");
query_str.push_str(#set_template.replace("{VALUE}", delval).as_str());
}
}
#[cfg_attr(test, mutants::skip)]
pub fn generate_delete_all_methods(parsed: &ParsedModel) -> TokenStream {
let name = &parsed.name;
let table_name = &parsed.table_name;
let delete_all_logic = generate_delete_all_logic(parsed);
let has_policy = !parsed.policy.is_empty();
let trashed_guard = if parsed.has_soft_deletes {
quote! {
let trashed_clause = if self.only_trashed {
Some("only_trashed()")
} else if self.with_trashed {
Some("with_trashed()")
} else {
None
};
if let Some(clause) = trashed_clause {
return Err(rullst_orm::Error::Validation(format!(
"delete_all() does not support {} on a soft-delete model; it would re-stamp the deletion time of rows that are already trashed. Bulk-delete live rows without it, or force_delete() each trashed row",
clause
)));
}
}
} else {
quote! {}
};
quote! {
#[rullst_orm::_tracing::instrument(
name = "rullst.orm.query",
target = "rullst_orm",
skip(self),
fields(orm.model = stringify!(#name), orm.table = #table_name, orm.operation = "delete_all")
)]
pub async fn delete_all(&self) -> Result<u64, rullst_orm::Error> {
rullst_orm::dispatch_executor!(pool, |pool| self.delete_all_with_tx_internal(pool).await)
}
#[rullst_orm::_tracing::instrument(
name = "rullst.orm.query",
target = "rullst_orm",
skip(self, tx),
fields(orm.model = stringify!(#name), orm.table = #table_name, orm.operation = "delete_all_with_tx")
)]
pub async fn delete_all_with_tx(&self, tx: &mut rullst_orm::db::Transaction<'_>) -> Result<u64, rullst_orm::Error> {
self.delete_all_with_tx_internal(&mut **tx).await
}
async fn delete_all_with_tx_internal<'e, E>(&self, executor: E) -> Result<u64, rullst_orm::Error>
where E: rullst_orm::_sqlx::Executor<'e, Database = rullst_orm::RullstDatabase>
{
if !self.errors.is_empty() {
return Err(self.errors[0].clone());
}
if let Some(clause) = self.__rullst_unsupported_delete_clause() {
return Err(rullst_orm::Error::Validation(format!(
"delete_all() does not support {}; it would delete every matching row. Select the IDs with get() or pluck_i32() and delete them with where_in(\"id\", ...)",
clause
)));
}
#trashed_guard
if #has_policy {
return Err(rullst_orm::Error::Validation(
"delete_all() cannot authorize a policy-protected model; load the records and call each model's delete() inside Orm::transaction(...)".to_string()
));
}
let query_str = self.__rullst_delete_all_sql(rullst_orm::Orm::driver().unwrap_or_default());
let query_bindings = self.scope_bindings.iter().chain(self.bindings.iter());
if rullst_orm::schema::is_query_log_enabled() {
println!("[SQL Debug] {:?} | Bindings: [{} parameter(s) redacted for security]", query_str, self.scope_bindings.len() + self.bindings.len());
}
let result = {
let mut query = rullst_orm::_sqlx::query(rullst_orm::_sqlx::AssertSqlSafe(query_str.as_str()));
for binding in query_bindings {
query = rullst_orm::value::BindRullstValue::bind_rullst_value(query, binding);
}
let timeout = rullst_orm::schema::get_query_timeout();
if let Some(t) = timeout {
tokio::time::timeout(t, query.execute(executor))
.await
.map_err(|_| rullst_orm::Error::DatabaseError("Query execution timed out".to_string()))??
} else {
query.execute(executor).await?
}
};
Ok(result.rows_affected())
}
fn __rullst_delete_all_sql(&self, driver: &str) -> String {
#delete_all_logic
let first_where = self.push_wheres(&mut query_str);
self.push_soft_deletes(&mut query_str, first_where);
if driver == "postgres" {
rullst_orm::replace_placeholders(&query_str)
} else {
query_str
}
}
fn __rullst_unsupported_delete_clause(&self) -> Option<&'static str> {
if self.__rullst_explicit_limit().is_some() {
Some("limit()")
} else if self.offset.is_some() {
Some("offset()")
} else if self.order_by.is_some() && !self.__rullst_has_relevance_order() {
Some("order_by()")
} else if !self.joins.is_empty() || !self.join_bindings.is_empty() {
Some("joins")
} else if self.group_by.is_some() || !self.havings.is_empty() {
Some("group_by()/having")
} else if !self.ctes.is_empty() {
Some("CTEs")
} else {
None
}
}
}
}