use std::collections::HashSet;
use std::future::Future;
use std::pin::Pin;
use crate::core::Model;
use crate::query::QuerySet;
use crate::sql::{delete_pool, CounterPool, ExecError, Pool};
type ResultFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, ExecError>> + Send + 'a>>;
pub trait Prunable: Model {
fn prune_queryset() -> QuerySet<Self>;
}
#[doc(hidden)]
pub struct PrunableEntry {
pub name: &'static str,
pub count: fn(&Pool) -> ResultFuture<'_, i64>,
pub prune: fn(&Pool) -> ResultFuture<'_, u64>,
}
inventory::collect!(PrunableEntry);
#[doc(hidden)]
pub fn __count_thunk<T>(pool: &Pool) -> ResultFuture<'_, i64>
where
T: Prunable + Send + 'static,
QuerySet<T>: CounterPool<T>,
{
Box::pin(async move { T::prune_queryset().count(pool).await })
}
#[doc(hidden)]
pub fn __prune_thunk<T>(pool: &Pool) -> ResultFuture<'_, u64>
where
T: Prunable + Send + 'static,
{
Box::pin(async move {
let query = T::prune_queryset().compile_delete()?;
delete_pool(pool, &query).await
})
}
#[macro_export]
macro_rules! register_prunable {
($t:ty) => {
$crate::inventory::submit! {
$crate::prunable::PrunableEntry {
name: <$t as $crate::core::Model>::SCHEMA.table,
count: $crate::prunable::__count_thunk::<$t>,
prune: $crate::prunable::__prune_thunk::<$t>,
}
}
};
}
#[derive(Debug, Clone, Default)]
pub struct PruneOptions {
pub only: Vec<String>,
pub except: Vec<String>,
}
impl PruneOptions {
fn allows(&self, name: &str) -> bool {
if self.except.iter().any(|s| s == name) {
return false;
}
if self.only.is_empty() {
return true;
}
self.only.iter().any(|s| s == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PruneReport {
pub table: String,
pub rows: u64,
}
pub async fn prune_all(pool: &Pool, opts: &PruneOptions) -> Result<Vec<PruneReport>, ExecError> {
let mut reports = Vec::new();
for entry in inventory::iter::<PrunableEntry> {
if !opts.allows(entry.name) {
continue;
}
let rows = (entry.prune)(pool).await?;
reports.push(PruneReport {
table: entry.name.to_owned(),
rows,
});
}
Ok(reports)
}
pub async fn prune_pretend(
pool: &Pool,
opts: &PruneOptions,
) -> Result<Vec<PruneReport>, ExecError> {
let mut reports = Vec::new();
for entry in inventory::iter::<PrunableEntry> {
if !opts.allows(entry.name) {
continue;
}
let rows = (entry.count)(pool).await?;
reports.push(PruneReport {
table: entry.name.to_owned(),
rows: u64::try_from(rows.max(0)).unwrap_or(0),
});
}
Ok(reports)
}
#[must_use]
pub fn registered_names() -> Vec<&'static str> {
let mut seen: HashSet<&'static str> = HashSet::new();
let mut out = Vec::new();
for entry in inventory::iter::<PrunableEntry> {
if seen.insert(entry.name) {
out.push(entry.name);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn options_allow_when_empty() {
let opts = PruneOptions::default();
assert!(opts.allows("foo"));
assert!(opts.allows("bar"));
}
#[test]
fn options_only_restricts_to_listed() {
let opts = PruneOptions {
only: vec!["foo".into()],
..PruneOptions::default()
};
assert!(opts.allows("foo"));
assert!(!opts.allows("bar"));
}
#[test]
fn options_except_excludes_listed() {
let opts = PruneOptions {
except: vec!["foo".into()],
..PruneOptions::default()
};
assert!(!opts.allows("foo"));
assert!(opts.allows("bar"));
}
#[test]
fn options_except_takes_precedence_over_only() {
let opts = PruneOptions {
only: vec!["foo".into(), "bar".into()],
except: vec!["foo".into()],
};
assert!(!opts.allows("foo"));
assert!(opts.allows("bar"));
assert!(!opts.allows("baz"));
}
}