use std::collections::{HashMap, HashSet, VecDeque};
use serde::{Deserialize, Serialize};
#[allow(unused_imports)] use crate::error::FraiseQLError;
use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CascadeInvalidator {
view_dependencies: HashMap<String, HashSet<String>>,
dependents: HashMap<String, HashSet<String>>,
stats: InvalidationStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvalidationStats {
pub total_cascades: u64,
pub total_invalidated: u64,
pub average_affected: f64,
pub max_affected: usize,
}
impl Default for InvalidationStats {
fn default() -> Self {
Self {
total_cascades: 0,
total_invalidated: 0,
average_affected: 0.0,
max_affected: 0,
}
}
}
impl CascadeInvalidator {
#[must_use]
pub fn new() -> Self {
Self {
view_dependencies: HashMap::new(),
dependents: HashMap::new(),
stats: InvalidationStats::default(),
}
}
pub fn add_dependency(&mut self, dependent_view: &str, dependency_view: &str) -> Result<()> {
if dependent_view == dependency_view {
return Err(crate::error::FraiseQLError::Validation {
message: "View cannot depend on itself".to_string(),
path: Some("cascade_invalidator::add_dependency".to_string()),
});
}
if self.can_reach(dependency_view, dependent_view) {
return Err(crate::error::FraiseQLError::Validation {
message: format!(
"Adding dependency '{}' → '{}' would create a cycle",
dependent_view, dependency_view
),
path: Some("cascade_invalidator::add_dependency".to_string()),
});
}
let dependent = dependent_view.to_string();
let dependency = dependency_view.to_string();
self.view_dependencies
.entry(dependent.clone())
.or_default()
.insert(dependency.clone());
self.dependents.entry(dependency).or_default().insert(dependent);
Ok(())
}
fn can_reach(&self, from: &str, target: &str) -> bool {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(from.to_string());
while let Some(current) = queue.pop_front() {
if current == target {
return true;
}
if !visited.insert(current.clone()) {
continue; }
if let Some(deps) = self.view_dependencies.get(¤t) {
for dep in deps {
if !visited.contains(dep.as_str()) {
queue.push_back(dep.clone());
}
}
}
}
false
}
pub fn cascade_invalidate(&mut self, view: &str) -> Result<HashSet<String>> {
let mut invalidated = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(view.to_string());
invalidated.insert(view.to_string());
while let Some(current_view) = queue.pop_front() {
if let Some(dependent_views) = self.dependents.get(¤t_view) {
for dependent in dependent_views {
if !invalidated.contains(dependent) {
invalidated.insert(dependent.clone());
queue.push_back(dependent.clone());
}
}
}
}
self.stats.total_cascades += 1;
#[allow(clippy::cast_possible_truncation)]
{
self.stats.total_invalidated += invalidated.len() as u64;
}
self.stats.max_affected = self.stats.max_affected.max(invalidated.len());
if self.stats.total_cascades > 0 {
#[allow(clippy::cast_precision_loss)]
{
self.stats.average_affected =
self.stats.total_invalidated as f64 / self.stats.total_cascades as f64;
}
}
Ok(invalidated)
}
#[must_use]
pub fn get_direct_dependents(&self, view: &str) -> HashSet<String> {
self.dependents.get(view).cloned().unwrap_or_default()
}
#[must_use]
pub fn get_direct_dependencies(&self, view: &str) -> HashSet<String> {
self.view_dependencies.get(view).cloned().unwrap_or_default()
}
#[must_use]
pub fn get_transitive_dependents(&self, view: &str) -> HashSet<String> {
let mut result = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(view.to_string());
result.insert(view.to_string());
while let Some(current) = queue.pop_front() {
if let Some(deps) = self.dependents.get(¤t) {
for dep in deps {
if !result.contains(dep) {
result.insert(dep.clone());
queue.push_back(dep.clone());
}
}
}
}
result
}
#[must_use]
pub fn has_dependency_path(&self, dependent: &str, dependency: &str) -> bool {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(dependent.to_string());
while let Some(current) = queue.pop_front() {
if visited.contains(¤t) {
continue;
}
visited.insert(current.clone());
if let Some(deps) = self.view_dependencies.get(¤t) {
for dep in deps {
if dep == dependency {
return true;
}
queue.push_back(dep.clone());
}
}
}
false
}
#[must_use]
pub fn stats(&self) -> InvalidationStats {
self.stats.clone()
}
pub fn clear(&mut self) {
self.view_dependencies.clear();
self.dependents.clear();
self.stats = InvalidationStats::default();
}
#[must_use]
pub fn view_count(&self) -> usize {
let mut views = HashSet::new();
views.extend(self.dependents.keys().cloned());
views.extend(self.view_dependencies.keys().cloned());
views.len()
}
#[must_use]
pub fn dependency_count(&self) -> usize {
self.view_dependencies.values().map(|deps| deps.len()).sum()
}
}
impl Default for CascadeInvalidator {
fn default() -> Self {
Self::new()
}
}