use std::{collections::HashSet, hash::Hash, mem::size_of};
use crate::{Budget, EvaluationError, Location, QueryError, QueryErrorCategory};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NativeSetOperator {
Union,
Intersect,
Except,
}
impl NativeSetOperator {
pub fn from_keyword(keyword: &str) -> Option<Self> {
match keyword {
"union" => Some(Self::Union),
"intersect" => Some(Self::Intersect),
"except" => Some(Self::Except),
_ => None,
}
}
}
pub fn combine_native_set<T: Eq + Hash, E>(
operator: NativeSetOperator,
left: impl ExactSizeIterator<Item = T>,
right: impl ExactSizeIterator<Item = T>,
ordered_candidates: impl IntoIterator<Item = Result<T, EvaluationError<E>>>,
budget: &mut Budget,
location: Location,
mut identity_work: impl FnMut(&T, &mut Budget) -> Result<(), EvaluationError<E>>,
) -> Result<Vec<T>, EvaluationError<E>> {
let mut left = membership(left, budget, &location, &mut identity_work)?;
let mut right = membership(right, budget, &location, &mut identity_work)?;
admit_slots::<T>(0, budget, &location)?;
let mut selected = Vec::new();
for candidate in ordered_candidates {
budget.charge(1, location.clone())?;
let candidate = candidate?;
budget.charge(1, location.clone())?;
identity_work(&candidate, budget)?;
let in_left = left.remove(&candidate);
budget.charge(1, location.clone())?;
identity_work(&candidate, budget)?;
let in_right = right.remove(&candidate);
let keep = match operator {
NativeSetOperator::Union => in_left || in_right,
NativeSetOperator::Intersect => in_left && in_right,
NativeSetOperator::Except => in_left && !in_right,
};
if keep {
let count = selected
.len()
.checked_add(1)
.ok_or_else(|| storage_error(&location))?;
budget.check_items(count, location.clone())?;
check_storage::<T>(count, &location)?;
budget.charge(1, location.clone())?;
budget.charge_results(1, location.clone())?;
selected
.try_reserve(1)
.map_err(|_| storage_error(&location))?;
selected.push(candidate);
}
}
Ok(selected)
}
fn membership<T: Eq + Hash, E>(
items: impl ExactSizeIterator<Item = T>,
budget: &mut Budget,
location: &Location,
identity_work: &mut impl FnMut(&T, &mut Budget) -> Result<(), EvaluationError<E>>,
) -> Result<HashSet<T>, EvaluationError<E>> {
admit_slots::<T>(items.len(), budget, location)?;
let mut membership = HashSet::new();
membership
.try_reserve(items.len())
.map_err(|_| storage_error(location))?;
for item in items {
budget.charge(1, location.clone())?;
identity_work(&item, budget)?;
membership.insert(item);
}
Ok(membership)
}
fn admit_slots<T>(
count: usize,
budget: &mut Budget,
location: &Location,
) -> Result<(), QueryError> {
check_storage::<T>(count, location)?;
let units = count
.checked_add(1)
.ok_or_else(|| storage_error(location))?;
budget.charge(units, location.clone())?;
budget.charge_results(units, location.clone())
}
fn check_storage<T>(count: usize, location: &Location) -> Result<(), QueryError> {
count
.checked_mul(size_of::<T>().max(1))
.filter(|bytes| *bytes <= isize::MAX as usize)
.map(|_| ())
.ok_or_else(|| storage_error(location))
}
fn storage_error(location: &Location) -> QueryError {
QueryError {
location: location.clone(),
message: "native set storage limit exceeded".into(),
category: QueryErrorCategory::ResultLimit,
}
}