fraiseql_core/cache/dependency_tracker.rs
1//! Dependency tracking for cache invalidation.
2//!
3//! Tracks which cache entries depend on which database views/tables to enable
4//! efficient view-based invalidation when mutations occur.
5//!
6//! # Current Scope
7//!
8//! - View-based tracking (not entity-level)
9//! - Bidirectional mapping (cache ↔ views)
10//! - Simple dependency management
11//!
12//! # Future Enhancements
13//!
14//! - Entity-level tracking (`User:123` not just `v_user`)
15//! - Cascade integration (parse mutation metadata)
16//! - Coherency validation (ensure no stale reads)
17
18use std::collections::{HashMap, HashSet};
19
20/// Tracks which cache entries depend on which views/tables.
21///
22/// Maintains bidirectional mappings between cache keys and the database
23/// views/tables they access. This enables efficient lookup during invalidation:
24/// "Which cache entries read from `v_user`?"
25///
26/// # Example
27///
28/// ```
29/// use fraiseql_core::cache::DependencyTracker;
30///
31/// let mut tracker = DependencyTracker::new();
32///
33/// // Record that cache entry accesses v_user
34/// tracker.record_access(
35/// "cache_key_abc123".to_string(),
36/// vec!["v_user".to_string()]
37/// );
38///
39/// // Find all caches that read from v_user
40/// let affected = tracker.get_dependent_caches("v_user");
41/// assert!(affected.contains(&"cache_key_abc123".to_string()));
42/// ```
43#[derive(Debug)]
44pub struct DependencyTracker {
45 /// Cache key → list of views accessed.
46 ///
47 /// Forward mapping for removing cache entries.
48 cache_to_views: HashMap<String, Vec<String>>,
49
50 /// View name → set of cache keys that read it.
51 ///
52 /// Reverse mapping for finding affected caches during invalidation.
53 view_to_caches: HashMap<String, HashSet<String>>,
54}
55
56impl DependencyTracker {
57 /// Create new dependency tracker.
58 ///
59 /// # Example
60 ///
61 /// ```
62 /// use fraiseql_core::cache::DependencyTracker;
63 ///
64 /// let tracker = DependencyTracker::new();
65 /// ```
66 #[must_use]
67 pub fn new() -> Self {
68 Self {
69 cache_to_views: HashMap::new(),
70 view_to_caches: HashMap::new(),
71 }
72 }
73
74 /// Record that a cache entry accesses certain views.
75 ///
76 /// Updates both forward and reverse mappings.
77 ///
78 /// # Arguments
79 ///
80 /// * `cache_key` - Cache key (from `generate_cache_key()`)
81 /// * `views` - List of views accessed by the query
82 ///
83 /// # Example
84 ///
85 /// ```
86 /// use fraiseql_core::cache::DependencyTracker;
87 ///
88 /// let mut tracker = DependencyTracker::new();
89 ///
90 /// tracker.record_access(
91 /// "key1".to_string(),
92 /// vec!["v_user".to_string(), "v_post".to_string()]
93 /// );
94 /// ```
95 #[allow(clippy::needless_pass_by_value)] // Reason: both cache_key and views are inserted into internal HashMaps and consumed
96 pub fn record_access(&mut self, cache_key: String, views: Vec<String>) {
97 // If updating existing entry, remove old reverse mappings first
98 if let Some(old_views) = self.cache_to_views.get(&cache_key) {
99 for old_view in old_views {
100 if let Some(caches) = self.view_to_caches.get_mut(old_view) {
101 caches.remove(&cache_key);
102 // Clean up empty sets
103 if caches.is_empty() {
104 self.view_to_caches.remove(old_view);
105 }
106 }
107 }
108 }
109
110 // Store cache → views mapping (forward)
111 self.cache_to_views.insert(cache_key.clone(), views.clone());
112
113 // Update view → caches reverse mapping
114 for view in views {
115 self.view_to_caches.entry(view).or_default().insert(cache_key.clone());
116 }
117 }
118
119 /// Get all cache keys that access a view.
120 ///
121 /// Used during invalidation to find affected cache entries.
122 ///
123 /// # Arguments
124 ///
125 /// * `view` - View/table name
126 ///
127 /// # Returns
128 ///
129 /// List of cache keys that read from this view
130 ///
131 /// # Example
132 ///
133 /// ```
134 /// use fraiseql_core::cache::DependencyTracker;
135 ///
136 /// let mut tracker = DependencyTracker::new();
137 /// tracker.record_access("key1".to_string(), vec!["v_user".to_string()]);
138 ///
139 /// let affected = tracker.get_dependent_caches("v_user");
140 /// assert_eq!(affected.len(), 1);
141 /// assert!(affected.contains(&"key1".to_string()));
142 /// ```
143 #[must_use]
144 pub fn get_dependent_caches(&self, view: &str) -> Vec<String> {
145 self.view_to_caches
146 .get(view)
147 .map(|set| set.iter().cloned().collect())
148 .unwrap_or_default()
149 }
150
151 /// Remove a cache entry from tracking.
152 ///
153 /// Called when cache entry is evicted (LRU) or invalidated.
154 /// Cleans up both forward and reverse mappings.
155 ///
156 /// # Arguments
157 ///
158 /// * `cache_key` - Cache key to remove
159 ///
160 /// # Example
161 ///
162 /// ```
163 /// use fraiseql_core::cache::DependencyTracker;
164 ///
165 /// let mut tracker = DependencyTracker::new();
166 /// tracker.record_access("key1".to_string(), vec!["v_user".to_string()]);
167 ///
168 /// tracker.remove_cache("key1");
169 ///
170 /// let affected = tracker.get_dependent_caches("v_user");
171 /// assert_eq!(affected.len(), 0);
172 /// ```
173 pub fn remove_cache(&mut self, cache_key: &str) {
174 if let Some(views) = self.cache_to_views.remove(cache_key) {
175 // Remove from view → caches mappings
176 for view in views {
177 if let Some(caches) = self.view_to_caches.get_mut(&view) {
178 caches.remove(cache_key);
179 // Clean up empty sets
180 if caches.is_empty() {
181 self.view_to_caches.remove(&view);
182 }
183 }
184 }
185 }
186 }
187
188 /// Clear all tracking data.
189 ///
190 /// Used for testing and cache flush.
191 ///
192 /// # Example
193 ///
194 /// ```
195 /// use fraiseql_core::cache::DependencyTracker;
196 ///
197 /// let mut tracker = DependencyTracker::new();
198 /// tracker.record_access("key1".to_string(), vec!["v_user".to_string()]);
199 ///
200 /// tracker.clear();
201 ///
202 /// assert_eq!(tracker.cache_count(), 0);
203 /// ```
204 pub fn clear(&mut self) {
205 self.cache_to_views.clear();
206 self.view_to_caches.clear();
207 }
208
209 /// Get total number of tracked cache entries.
210 ///
211 /// # Example
212 ///
213 /// ```
214 /// use fraiseql_core::cache::DependencyTracker;
215 ///
216 /// let mut tracker = DependencyTracker::new();
217 /// tracker.record_access("key1".to_string(), vec!["v_user".to_string()]);
218 /// tracker.record_access("key2".to_string(), vec!["v_post".to_string()]);
219 ///
220 /// assert_eq!(tracker.cache_count(), 2);
221 /// ```
222 #[must_use]
223 pub fn cache_count(&self) -> usize {
224 self.cache_to_views.len()
225 }
226
227 /// Get total number of tracked views.
228 ///
229 /// # Example
230 ///
231 /// ```
232 /// use fraiseql_core::cache::DependencyTracker;
233 ///
234 /// let mut tracker = DependencyTracker::new();
235 /// tracker.record_access("key1".to_string(), vec!["v_user".to_string()]);
236 /// tracker.record_access("key2".to_string(), vec!["v_user".to_string(), "v_post".to_string()]);
237 ///
238 /// assert_eq!(tracker.view_count(), 2); // v_user and v_post
239 /// ```
240 #[must_use]
241 pub fn view_count(&self) -> usize {
242 self.view_to_caches.len()
243 }
244
245 /// Get all tracked views.
246 ///
247 /// Used for debugging and monitoring.
248 ///
249 /// # Example
250 ///
251 /// ```
252 /// use fraiseql_core::cache::DependencyTracker;
253 ///
254 /// let mut tracker = DependencyTracker::new();
255 /// tracker.record_access("key1".to_string(), vec!["v_user".to_string()]);
256 ///
257 /// let views = tracker.get_all_views();
258 /// assert!(views.contains(&"v_user".to_string()));
259 /// ```
260 #[must_use]
261 pub fn get_all_views(&self) -> Vec<String> {
262 self.view_to_caches.keys().cloned().collect()
263 }
264}
265
266impl Default for DependencyTracker {
267 fn default() -> Self {
268 Self::new()
269 }
270}