use anyhow::{bail, Result};
use aries_askar::entry::TagFilter;
use aries_askar::{ErrorKind, Store};
#[derive(Debug, Default, Clone)]
pub struct BatchOutcome {
pub deleted: usize,
pub already_gone: usize,
pub failed: usize,
pub first_error: Option<String>,
}
impl BatchOutcome {
pub fn progressed(&self) -> bool {
self.deleted + self.already_gone > 0
}
pub fn report(&self, category: &str) {
if self.already_gone > 0 {
println!(
" {} {category} already deleted (skipped — idempotent).",
self.already_gone
);
}
if self.failed > 0 {
eprintln!(
" {} {category} delete(s) failed: {}",
self.failed,
self.first_error.as_deref().unwrap_or("<no detail>")
);
}
}
}
pub async fn delete_pairs(
store: &Store,
profile: Option<String>,
pairs: &[(&str, String)],
label: &str,
strict: bool,
) -> Result<BatchOutcome> {
let mut out = BatchOutcome::default();
if pairs.is_empty() {
return Ok(out);
}
let mut txn = store.transaction(profile).await?;
for (category, name) in pairs {
match txn.remove(category, name).await {
Ok(()) => out.deleted += 1,
Err(e) if e.kind() == ErrorKind::NotFound => out.already_gone += 1,
Err(e) => {
out.failed += 1;
if out.first_error.is_none() {
out.first_error = Some(format!("{category}/{name}: {e}"));
}
}
}
}
if !out.progressed() {
let _ = txn.rollback().await;
bail!(
"{label}: 0/{} deletes succeeded — possible poison record or lock. Aborting. \
First error: {}",
pairs.len(),
out.first_error.as_deref().unwrap_or("<no detail>")
);
}
if strict && out.failed > 0 {
let _ = txn.rollback().await;
bail!(
"{label}: {}/{} deletes failed — aborting so the tenant is not checkpointed as \
complete. Re-run to retry. First error: {}",
out.failed,
pairs.len(),
out.first_error.as_deref().unwrap_or("<no detail>")
);
}
txn.commit().await?;
Ok(out)
}
pub async fn delete_ids(
store: &Store,
profile: Option<String>,
category: &'static str,
ids: &[String],
label: &str,
strict: bool,
) -> Result<BatchOutcome> {
let pairs: Vec<(&str, String)> = ids.iter().map(|id| (category, id.clone())).collect();
delete_pairs(store, profile, &pairs, label, strict).await
}
pub async fn remove_all(
store: &Store,
profile: Option<String>,
category: &str,
tag_filter: Option<TagFilter>,
) -> Result<i64> {
let mut txn = store.transaction(profile).await?;
let removed = txn.remove_all(Some(category), tag_filter).await?;
txn.commit().await?;
Ok(removed)
}
pub async fn throttle(ms: u64) {
if ms > 0 {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
}
}