1use std::collections::{HashMap, HashSet, VecDeque};
7
8use serde::{Deserialize, Serialize};
9
10#[allow(unused_imports)] use crate::error::FraiseQLError;
12use crate::error::Result;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CascadeInvalidator {
49 view_dependencies: HashMap<String, HashSet<String>>,
52
53 dependents: HashMap<String, HashSet<String>>,
56
57 stats: InvalidationStats,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct InvalidationStats {
64 pub total_cascades: u64,
66
67 pub total_invalidated: u64,
69
70 pub average_affected: f64,
72
73 pub max_affected: usize,
75}
76
77impl Default for InvalidationStats {
78 fn default() -> Self {
79 Self {
80 total_cascades: 0,
81 total_invalidated: 0,
82 average_affected: 0.0,
83 max_affected: 0,
84 }
85 }
86}
87
88impl CascadeInvalidator {
89 #[must_use]
91 pub fn new() -> Self {
92 Self {
93 view_dependencies: HashMap::new(),
94 dependents: HashMap::new(),
95 stats: InvalidationStats::default(),
96 }
97 }
98
99 pub fn add_dependency(&mut self, dependent_view: &str, dependency_view: &str) -> Result<()> {
124 if dependent_view == dependency_view {
125 return Err(crate::error::FraiseQLError::Validation {
126 message: "View cannot depend on itself".to_string(),
127 path: Some("cascade_invalidator::add_dependency".to_string()),
128 });
129 }
130
131 if self.can_reach(dependency_view, dependent_view) {
136 return Err(crate::error::FraiseQLError::Validation {
137 message: format!(
138 "Adding dependency '{}' → '{}' would create a cycle",
139 dependent_view, dependency_view
140 ),
141 path: Some("cascade_invalidator::add_dependency".to_string()),
142 });
143 }
144
145 let dependent = dependent_view.to_string();
146 let dependency = dependency_view.to_string();
147
148 self.view_dependencies
150 .entry(dependent.clone())
151 .or_default()
152 .insert(dependency.clone());
153
154 self.dependents.entry(dependency).or_default().insert(dependent);
156
157 Ok(())
158 }
159
160 fn can_reach(&self, from: &str, target: &str) -> bool {
170 let mut visited = HashSet::new();
171 let mut queue = VecDeque::new();
172 queue.push_back(from.to_string());
173
174 while let Some(current) = queue.pop_front() {
175 if current == target {
176 return true;
177 }
178 if !visited.insert(current.clone()) {
179 continue; }
181 if let Some(deps) = self.view_dependencies.get(¤t) {
183 for dep in deps {
184 if !visited.contains(dep.as_str()) {
185 queue.push_back(dep.clone());
186 }
187 }
188 }
189 }
190
191 false
192 }
193
194 pub fn cascade_invalidate(&mut self, view: &str) -> Result<HashSet<String>> {
235 let mut invalidated = HashSet::new();
236 let mut queue = VecDeque::new();
237
238 queue.push_back(view.to_string());
239 invalidated.insert(view.to_string());
240
241 while let Some(current_view) = queue.pop_front() {
243 if let Some(dependent_views) = self.dependents.get(¤t_view) {
244 for dependent in dependent_views {
245 if !invalidated.contains(dependent) {
246 invalidated.insert(dependent.clone());
247 queue.push_back(dependent.clone());
248 }
249 }
250 }
251 }
252
253 self.stats.total_cascades += 1;
255 #[allow(clippy::cast_possible_truncation)]
256 {
258 self.stats.total_invalidated += invalidated.len() as u64;
259 }
260 self.stats.max_affected = self.stats.max_affected.max(invalidated.len());
261 if self.stats.total_cascades > 0 {
262 #[allow(clippy::cast_precision_loss)]
263 {
266 self.stats.average_affected =
267 self.stats.total_invalidated as f64 / self.stats.total_cascades as f64;
268 }
269 }
270
271 Ok(invalidated)
272 }
273
274 pub fn get_direct_dependents(&self, view: &str) -> HashSet<String> {
286 self.dependents.get(view).cloned().unwrap_or_default()
287 }
288
289 pub fn get_direct_dependencies(&self, view: &str) -> HashSet<String> {
299 self.view_dependencies.get(view).cloned().unwrap_or_default()
300 }
301
302 pub fn get_transitive_dependents(&self, view: &str) -> HashSet<String> {
314 let mut result = HashSet::new();
315 let mut queue = VecDeque::new();
316
317 queue.push_back(view.to_string());
318 result.insert(view.to_string());
319
320 while let Some(current) = queue.pop_front() {
321 if let Some(deps) = self.dependents.get(¤t) {
322 for dep in deps {
323 if !result.contains(dep) {
324 result.insert(dep.clone());
325 queue.push_back(dep.clone());
326 }
327 }
328 }
329 }
330
331 result
332 }
333
334 pub fn has_dependency_path(&self, dependent: &str, dependency: &str) -> bool {
347 let mut visited = HashSet::new();
348 let mut queue = VecDeque::new();
349
350 queue.push_back(dependent.to_string());
351
352 while let Some(current) = queue.pop_front() {
353 if visited.contains(¤t) {
354 continue;
355 }
356 visited.insert(current.clone());
357
358 if let Some(deps) = self.view_dependencies.get(¤t) {
359 for dep in deps {
360 if dep == dependency {
361 return true;
362 }
363 queue.push_back(dep.clone());
364 }
365 }
366 }
367
368 false
369 }
370
371 pub fn stats(&self) -> InvalidationStats {
373 self.stats.clone()
374 }
375
376 pub fn clear(&mut self) {
378 self.view_dependencies.clear();
379 self.dependents.clear();
380 self.stats = InvalidationStats::default();
381 }
382
383 pub fn view_count(&self) -> usize {
385 let mut views = HashSet::new();
386 views.extend(self.dependents.keys().cloned());
387 views.extend(self.view_dependencies.keys().cloned());
388 views.len()
389 }
390
391 pub fn dependency_count(&self) -> usize {
393 self.view_dependencies.values().map(|deps| deps.len()).sum()
394 }
395}
396
397impl Default for CascadeInvalidator {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 #![allow(clippy::unwrap_used)] use super::*;
408
409 #[test]
410 fn test_add_single_dependency() {
411 let mut invalidator = CascadeInvalidator::new();
412 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
413
414 assert!(invalidator.get_direct_dependencies("v_user").contains("v_raw_user"));
415 assert!(invalidator.get_direct_dependents("v_raw_user").contains("v_user"));
416 }
417
418 #[test]
419 fn test_self_dependency_fails() {
420 let mut invalidator = CascadeInvalidator::new();
421 let result = invalidator.add_dependency("v_user", "v_user");
422 assert!(
423 matches!(result, Err(crate::error::FraiseQLError::Validation { .. })),
424 "expected Validation error for self-dependency, got: {result:?}"
425 );
426 }
427
428 #[test]
429 fn test_cascade_invalidate_single_level() {
430 let mut invalidator = CascadeInvalidator::new();
431 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
432
433 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
434 assert_eq!(invalidated.len(), 2);
435 assert!(invalidated.contains("v_raw_user"));
436 assert!(invalidated.contains("v_user"));
437 }
438
439 #[test]
440 fn test_cascade_invalidate_multiple_levels() {
441 let mut invalidator = CascadeInvalidator::new();
442 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
443 invalidator.add_dependency("v_analytics", "v_user").unwrap();
444 invalidator.add_dependency("v_dashboard", "v_analytics").unwrap();
445
446 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
447 assert_eq!(invalidated.len(), 4);
448 assert!(invalidated.contains("v_raw_user"));
449 assert!(invalidated.contains("v_user"));
450 assert!(invalidated.contains("v_analytics"));
451 assert!(invalidated.contains("v_dashboard"));
452 }
453
454 #[test]
455 fn test_cascade_invalidate_branching() {
456 let mut invalidator = CascadeInvalidator::new();
457 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
458 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
459 invalidator.add_dependency("v_analytics", "v_user").unwrap();
460 invalidator.add_dependency("v_dashboard", "v_post").unwrap();
461
462 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
463 assert_eq!(invalidated.len(), 5);
464 assert!(invalidated.contains("v_raw_user"));
465 assert!(invalidated.contains("v_user"));
466 assert!(invalidated.contains("v_post"));
467 assert!(invalidated.contains("v_analytics"));
468 assert!(invalidated.contains("v_dashboard"));
469 }
470
471 #[test]
472 fn test_get_direct_dependents() {
473 let mut invalidator = CascadeInvalidator::new();
474 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
475 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
476
477 let dependents = invalidator.get_direct_dependents("v_raw_user");
478 assert_eq!(dependents.len(), 2);
479 assert!(dependents.contains("v_user"));
480 assert!(dependents.contains("v_post"));
481 }
482
483 #[test]
484 fn test_get_direct_dependencies() {
485 let mut invalidator = CascadeInvalidator::new();
486 invalidator.add_dependency("v_analytics", "v_user").unwrap();
487 invalidator.add_dependency("v_analytics", "v_post").unwrap();
488
489 let deps = invalidator.get_direct_dependencies("v_analytics");
490 assert_eq!(deps.len(), 2);
491 assert!(deps.contains("v_user"));
492 assert!(deps.contains("v_post"));
493 }
494
495 #[test]
496 fn test_get_transitive_dependents() {
497 let mut invalidator = CascadeInvalidator::new();
498 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
499 invalidator.add_dependency("v_analytics", "v_user").unwrap();
500 invalidator.add_dependency("v_dashboard", "v_analytics").unwrap();
501
502 let transitive = invalidator.get_transitive_dependents("v_raw_user");
503 assert_eq!(transitive.len(), 4);
504 assert!(transitive.contains("v_raw_user"));
505 assert!(transitive.contains("v_user"));
506 assert!(transitive.contains("v_analytics"));
507 assert!(transitive.contains("v_dashboard"));
508 }
509
510 #[test]
511 fn test_has_dependency_path() {
512 let mut invalidator = CascadeInvalidator::new();
513 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
514 invalidator.add_dependency("v_analytics", "v_user").unwrap();
515
516 assert!(invalidator.has_dependency_path("v_analytics", "v_raw_user"));
517 assert!(invalidator.has_dependency_path("v_analytics", "v_user"));
518 assert!(invalidator.has_dependency_path("v_user", "v_raw_user"));
519 assert!(!invalidator.has_dependency_path("v_raw_user", "v_analytics"));
520 assert!(!invalidator.has_dependency_path("v_raw_user", "v_user"));
521 }
522
523 #[test]
524 fn test_stats_tracking() {
525 let mut invalidator = CascadeInvalidator::new();
526 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
527 invalidator.add_dependency("v_analytics", "v_user").unwrap();
528
529 invalidator.cascade_invalidate("v_raw_user").unwrap();
530 invalidator.cascade_invalidate("v_user").unwrap();
531
532 let stats = invalidator.stats();
533 assert_eq!(stats.total_cascades, 2);
534 assert_eq!(stats.total_invalidated, 5); assert_eq!(stats.max_affected, 3);
536 }
537
538 #[test]
539 fn test_clear() {
540 let mut invalidator = CascadeInvalidator::new();
541 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
542 assert_eq!(invalidator.view_count(), 2);
543
544 invalidator.clear();
545 assert_eq!(invalidator.view_count(), 0);
546 assert_eq!(invalidator.dependency_count(), 0);
547 }
548
549 #[test]
550 fn test_view_and_dependency_count() {
551 let mut invalidator = CascadeInvalidator::new();
552 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
553 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
554 invalidator.add_dependency("v_analytics", "v_user").unwrap();
555
556 assert_eq!(invalidator.view_count(), 4);
557 assert_eq!(invalidator.dependency_count(), 3);
558 }
559
560 #[test]
561 fn test_diamond_dependency() {
562 let mut invalidator = CascadeInvalidator::new();
563 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
565 invalidator.add_dependency("v_post", "v_raw_user").unwrap();
566 invalidator.add_dependency("v_analytics", "v_user").unwrap();
567 invalidator.add_dependency("v_analytics", "v_post").unwrap();
568
569 let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
570 assert_eq!(invalidated.len(), 4);
572 assert!(invalidated.contains("v_raw_user"));
573 assert!(invalidated.contains("v_user"));
574 assert!(invalidated.contains("v_post"));
575 assert!(invalidated.contains("v_analytics"));
576 }
577
578 #[test]
579 fn test_multiple_independent_chains() {
580 let mut invalidator = CascadeInvalidator::new();
581 invalidator.add_dependency("v_user_1", "v_raw_1").unwrap();
583 invalidator.add_dependency("v_analytics_1", "v_user_1").unwrap();
584 invalidator.add_dependency("v_user_2", "v_raw_2").unwrap();
586 invalidator.add_dependency("v_analytics_2", "v_user_2").unwrap();
587
588 let invalidated = invalidator.cascade_invalidate("v_raw_1").unwrap();
589 assert_eq!(invalidated.len(), 3); assert!(!invalidated.contains("v_raw_2"));
591 assert!(!invalidated.contains("v_user_2"));
592 }
593
594 #[test]
595 fn test_cycle_detection_via_has_dependency_path() {
596 let mut invalidator = CascadeInvalidator::new();
597 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
598 invalidator.add_dependency("v_analytics", "v_user").unwrap();
599
600 assert!(!invalidator.has_dependency_path("v_raw_user", "v_analytics"));
602 }
603
604 #[test]
605 fn test_indirect_cycle_detection() {
606 let mut invalidator = CascadeInvalidator::new();
607 invalidator.add_dependency("B", "A").unwrap();
609 invalidator.add_dependency("C", "B").unwrap();
610
611 let result = invalidator.add_dependency("A", "C");
614 assert!(
615 matches!(result, Err(crate::error::FraiseQLError::Validation { .. })),
616 "expected Validation error for indirect cycle A→C→B→A, got: {result:?}"
617 );
618 let msg = result.unwrap_err().to_string();
619 assert!(msg.contains("cycle"), "error message should mention cycle, got: {msg}");
620 }
621
622 #[test]
623 fn test_three_node_cycle_rejected() {
624 let mut invalidator = CascadeInvalidator::new();
625 invalidator.add_dependency("B", "A").unwrap(); invalidator.add_dependency("C", "B").unwrap(); let result = invalidator.add_dependency("A", "C");
629 assert!(
630 matches!(result, Err(crate::error::FraiseQLError::Validation { .. })),
631 "expected Validation error for three-node cycle A→C→B→A, got: {result:?}"
632 );
633 }
634
635 #[test]
636 fn test_serialization() {
637 let mut invalidator = CascadeInvalidator::new();
638 invalidator.add_dependency("v_user", "v_raw_user").unwrap();
639
640 let json = serde_json::to_string(&invalidator).expect("serialize should work");
641 let restored: CascadeInvalidator =
642 serde_json::from_str(&json).expect("deserialize should work");
643
644 assert_eq!(
645 restored.get_direct_dependents("v_raw_user"),
646 invalidator.get_direct_dependents("v_raw_user")
647 );
648 }
649
650 #[test]
651 fn cascade_invalidator_is_send_sync() {
652 fn assert_send_sync<T: Send + Sync>() {}
655 assert_send_sync::<CascadeInvalidator>();
656 }
657
658 #[test]
659 fn cascade_invalidate_takes_shared_ref() {
660 let mut inv = CascadeInvalidator::new();
661 inv.add_dependency("v_b", "v_a").unwrap();
662 let result = inv.cascade_invalidate("v_a").unwrap();
664 assert!(result.contains("v_b"));
665 }
666}