1use std::collections::{HashMap, HashSet, VecDeque};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct CascadeInvalidator {
47 view_dependencies: HashMap<String, HashSet<String>>,
50
51 dependents: HashMap<String, HashSet<String>>,
54
55 stats: InvalidationStats,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct InvalidationStats {
62 pub total_cascades: u64,
64
65 pub total_invalidated: u64,
67
68 pub average_affected: f64,
70
71 pub max_affected: usize,
73}
74
75impl Default for InvalidationStats {
76 fn default() -> Self {
77 Self {
78 total_cascades: 0,
79 total_invalidated: 0,
80 average_affected: 0.0,
81 max_affected: 0,
82 }
83 }
84}
85
86impl CascadeInvalidator {
87 #[must_use]
89 pub fn new() -> Self {
90 Self {
91 view_dependencies: HashMap::new(),
92 dependents: HashMap::new(),
93 stats: InvalidationStats::default(),
94 }
95 }
96
97 pub fn add_dependency(&mut self, dependent_view: &str, dependency_view: &str) -> Result<()> {
116 if dependent_view == dependency_view {
117 return Err(crate::error::FraiseQLError::Validation {
118 message: "View cannot depend on itself".to_string(),
119 path: Some("cascade_invalidator::add_dependency".to_string()),
120 });
121 }
122
123 let dependent = dependent_view.to_string();
124 let dependency = dependency_view.to_string();
125
126 self.view_dependencies
128 .entry(dependent.clone())
129 .or_insert_with(HashSet::new)
130 .insert(dependency.clone());
131
132 self.dependents.entry(dependency).or_insert_with(HashSet::new).insert(dependent);
134
135 Ok(())
136 }
137
138 pub fn cascade_invalidate(&mut self, view: &str) -> Result<HashSet<String>> {
173 let mut invalidated = HashSet::new();
174 let mut queue = VecDeque::new();
175
176 queue.push_back(view.to_string());
177 invalidated.insert(view.to_string());
178
179 while let Some(current_view) = queue.pop_front() {
181 if let Some(dependent_views) = self.dependents.get(¤t_view) {
182 for dependent in dependent_views {
183 if !invalidated.contains(dependent) {
184 invalidated.insert(dependent.clone());
185 queue.push_back(dependent.clone());
186 }
187 }
188 }
189 }
190
191 self.stats.total_cascades += 1;
193 self.stats.total_invalidated += invalidated.len() as u64;
194 self.stats.max_affected = self.stats.max_affected.max(invalidated.len());
195 if self.stats.total_cascades > 0 {
196 self.stats.average_affected =
197 self.stats.total_invalidated as f64 / self.stats.total_cascades as f64;
198 }
199
200 Ok(invalidated)
201 }
202
203 pub fn get_direct_dependents(&self, view: &str) -> HashSet<String> {
215 self.dependents.get(view).cloned().unwrap_or_default()
216 }
217
218 pub fn get_direct_dependencies(&self, view: &str) -> HashSet<String> {
228 self.view_dependencies.get(view).cloned().unwrap_or_default()
229 }
230
231 pub fn get_transitive_dependents(&self, view: &str) -> HashSet<String> {
243 let mut result = HashSet::new();
244 let mut queue = VecDeque::new();
245
246 queue.push_back(view.to_string());
247 result.insert(view.to_string());
248
249 while let Some(current) = queue.pop_front() {
250 if let Some(deps) = self.dependents.get(¤t) {
251 for dep in deps {
252 if !result.contains(dep) {
253 result.insert(dep.clone());
254 queue.push_back(dep.clone());
255 }
256 }
257 }
258 }
259
260 result
261 }
262
263 pub fn has_dependency_path(&self, dependent: &str, dependency: &str) -> bool {
276 let mut visited = HashSet::new();
277 let mut queue = VecDeque::new();
278
279 queue.push_back(dependent.to_string());
280
281 while let Some(current) = queue.pop_front() {
282 if visited.contains(¤t) {
283 continue;
284 }
285 visited.insert(current.clone());
286
287 if let Some(deps) = self.view_dependencies.get(¤t) {
288 for dep in deps {
289 if dep == dependency {
290 return true;
291 }
292 queue.push_back(dep.clone());
293 }
294 }
295 }
296
297 false
298 }
299
300 pub fn stats(&self) -> InvalidationStats {
302 self.stats.clone()
303 }
304
305 pub fn clear(&mut self) {
307 self.view_dependencies.clear();
308 self.dependents.clear();
309 self.stats = InvalidationStats::default();
310 }
311
312 pub fn view_count(&self) -> usize {
314 let mut views = HashSet::new();
315 views.extend(self.dependents.keys().cloned());
316 views.extend(self.view_dependencies.keys().cloned());
317 views.len()
318 }
319
320 pub fn dependency_count(&self) -> usize {
322 self.view_dependencies.values().map(|deps| deps.len()).sum()
323 }
324}
325
326impl Default for CascadeInvalidator {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_add_single_dependency() {
338 let mut invalidator = CascadeInvalidator::new();
339 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
340
341 assert!(invalidator.get_direct_dependencies("v_user").contains("v_raw_user"));
342 assert!(invalidator.get_direct_dependents("v_raw_user").contains("v_user"));
343 }
344
345 #[test]
346 fn test_self_dependency_fails() {
347 let mut invalidator = CascadeInvalidator::new();
348 let result = invalidator.add_dependency("v_user", "v_user");
349 assert!(result.is_err());
350 }
351
352 #[test]
353 fn test_cascade_invalidate_single_level() {
354 let mut invalidator = CascadeInvalidator::new();
355 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
356
357 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
358 assert_eq!(invalidated.len(), 2);
359 assert!(invalidated.contains("v_raw_user"));
360 assert!(invalidated.contains("v_user"));
361 }
362
363 #[test]
364 fn test_cascade_invalidate_multiple_levels() {
365 let mut invalidator = CascadeInvalidator::new();
366 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
367 invalidator.add_dependency("v_analytics", "v_user").unwrap();
368 invalidator.add_dependency("v_dashboard", "v_analytics").unwrap();
369
370 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
371 assert_eq!(invalidated.len(), 4);
372 assert!(invalidated.contains("v_raw_user"));
373 assert!(invalidated.contains("v_user"));
374 assert!(invalidated.contains("v_analytics"));
375 assert!(invalidated.contains("v_dashboard"));
376 }
377
378 #[test]
379 fn test_cascade_invalidate_branching() {
380 let mut invalidator = CascadeInvalidator::new();
381 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
382 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
383 invalidator.add_dependency("v_analytics", "v_user").unwrap();
384 invalidator.add_dependency("v_dashboard", "v_post").unwrap();
385
386 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
387 assert_eq!(invalidated.len(), 5);
388 assert!(invalidated.contains("v_raw_user"));
389 assert!(invalidated.contains("v_user"));
390 assert!(invalidated.contains("v_post"));
391 assert!(invalidated.contains("v_analytics"));
392 assert!(invalidated.contains("v_dashboard"));
393 }
394
395 #[test]
396 fn test_get_direct_dependents() {
397 let mut invalidator = CascadeInvalidator::new();
398 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
399 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
400
401 let dependents = invalidator.get_direct_dependents("v_raw_user");
402 assert_eq!(dependents.len(), 2);
403 assert!(dependents.contains("v_user"));
404 assert!(dependents.contains("v_post"));
405 }
406
407 #[test]
408 fn test_get_direct_dependencies() {
409 let mut invalidator = CascadeInvalidator::new();
410 invalidator.add_dependency("v_analytics", "v_user").unwrap();
411 invalidator.add_dependency("v_analytics", "v_post").unwrap();
412
413 let deps = invalidator.get_direct_dependencies("v_analytics");
414 assert_eq!(deps.len(), 2);
415 assert!(deps.contains("v_user"));
416 assert!(deps.contains("v_post"));
417 }
418
419 #[test]
420 fn test_get_transitive_dependents() {
421 let mut invalidator = CascadeInvalidator::new();
422 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
423 invalidator.add_dependency("v_analytics", "v_user").unwrap();
424 invalidator.add_dependency("v_dashboard", "v_analytics").unwrap();
425
426 let transitive = invalidator.get_transitive_dependents("v_raw_user");
427 assert_eq!(transitive.len(), 4);
428 assert!(transitive.contains("v_raw_user"));
429 assert!(transitive.contains("v_user"));
430 assert!(transitive.contains("v_analytics"));
431 assert!(transitive.contains("v_dashboard"));
432 }
433
434 #[test]
435 fn test_has_dependency_path() {
436 let mut invalidator = CascadeInvalidator::new();
437 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
438 invalidator.add_dependency("v_analytics", "v_user").unwrap();
439
440 assert!(invalidator.has_dependency_path("v_analytics", "v_raw_user"));
441 assert!(invalidator.has_dependency_path("v_analytics", "v_user"));
442 assert!(invalidator.has_dependency_path("v_user", "v_raw_user"));
443 assert!(!invalidator.has_dependency_path("v_raw_user", "v_analytics"));
444 assert!(!invalidator.has_dependency_path("v_raw_user", "v_user"));
445 }
446
447 #[test]
448 fn test_stats_tracking() {
449 let mut invalidator = CascadeInvalidator::new();
450 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
451 invalidator.add_dependency("v_analytics", "v_user").unwrap();
452
453 invalidator.cascade_invalidate("v_raw_user").unwrap();
454 invalidator.cascade_invalidate("v_user").unwrap();
455
456 let stats = invalidator.stats();
457 assert_eq!(stats.total_cascades, 2);
458 assert_eq!(stats.total_invalidated, 5); assert_eq!(stats.max_affected, 3);
460 }
461
462 #[test]
463 fn test_clear() {
464 let mut invalidator = CascadeInvalidator::new();
465 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
466 assert_eq!(invalidator.view_count(), 2);
467
468 invalidator.clear();
469 assert_eq!(invalidator.view_count(), 0);
470 assert_eq!(invalidator.dependency_count(), 0);
471 }
472
473 #[test]
474 fn test_view_and_dependency_count() {
475 let mut invalidator = CascadeInvalidator::new();
476 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
477 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
478 invalidator.add_dependency("v_analytics", "v_user").unwrap();
479
480 assert_eq!(invalidator.view_count(), 4);
481 assert_eq!(invalidator.dependency_count(), 3);
482 }
483
484 #[test]
485 fn test_diamond_dependency() {
486 let mut invalidator = CascadeInvalidator::new();
487 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
489 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
490 invalidator.add_dependency("v_analytics", "v_user").unwrap();
491 invalidator.add_dependency("v_analytics", "v_post").unwrap();
492
493 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
494 assert_eq!(invalidated.len(), 4);
496 assert!(invalidated.contains("v_raw_user"));
497 assert!(invalidated.contains("v_user"));
498 assert!(invalidated.contains("v_post"));
499 assert!(invalidated.contains("v_analytics"));
500 }
501
502 #[test]
503 fn test_multiple_independent_chains() {
504 let mut invalidator = CascadeInvalidator::new();
505 invalidator.add_dependency("v_user_1", "v_raw_1").unwrap();
507 invalidator.add_dependency("v_analytics_1", "v_user_1").unwrap();
508 invalidator.add_dependency("v_user_2", "v_raw_2").unwrap();
510 invalidator.add_dependency("v_analytics_2", "v_user_2").unwrap();
511
512 let invalidated = invalidator.cascade_invalidate("v_raw_1").unwrap();
513 assert_eq!(invalidated.len(), 3); assert!(!invalidated.contains("v_raw_2"));
515 assert!(!invalidated.contains("v_user_2"));
516 }
517
518 #[test]
519 fn test_cycle_detection_via_has_dependency_path() {
520 let mut invalidator = CascadeInvalidator::new();
521 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
522 invalidator.add_dependency("v_analytics", "v_user").unwrap();
523 assert!(!invalidator.has_dependency_path("v_raw_user", "v_analytics"));
527 }
528
529 #[test]
530 fn test_serialization() {
531 let mut invalidator = CascadeInvalidator::new();
532 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
533
534 let json = serde_json::to_string(&invalidator).expect("serialize should work");
535 let restored: CascadeInvalidator =
536 serde_json::from_str(&json).expect("deserialize should work");
537
538 assert_eq!(
539 restored.get_direct_dependents("v_raw_user"),
540 invalidator.get_direct_dependents("v_raw_user")
541 );
542 }
543}