fraiseql_core/cache/invalidation_api.rs
1//! Invalidation methods for `CachedDatabaseAdapter`.
2//!
3//! This module provides view-level and entity-level cache invalidation,
4//! including cascade expansion via `CascadeInvalidator`.
5
6use fraiseql_db::ViewName;
7
8use super::adapter::CachedDatabaseAdapter;
9use crate::{db::DatabaseAdapter, error::Result};
10
11impl<A: DatabaseAdapter> CachedDatabaseAdapter<A> {
12 /// Invalidate cache entries that read from specified views.
13 ///
14 /// Call this after mutations to ensure cache consistency. All cache entries
15 /// that accessed any of the modified views will be removed.
16 ///
17 /// # Arguments
18 ///
19 /// * `views` - List of views/tables that were modified
20 ///
21 /// # Returns
22 ///
23 /// Number of cache entries invalidated
24 ///
25 /// # Errors
26 ///
27 /// Returns error if cache mutex is poisoned (very rare).
28 ///
29 /// # Example
30 ///
31 /// ```rust,no_run
32 /// # use fraiseql_core::cache::CachedDatabaseAdapter;
33 /// # use fraiseql_core::db::postgres::PostgresAdapter;
34 /// # use fraiseql_db::ViewName;
35 /// # async fn example(adapter: CachedDatabaseAdapter<PostgresAdapter>) -> Result<(), Box<dyn std::error::Error>> {
36 /// // After creating a user
37 /// let count = adapter.invalidate_views(&[ViewName::from("v_user")])?;
38 /// println!("Invalidated {} cache entries", count);
39 /// # Ok(())
40 /// # }
41 /// ```
42 pub fn invalidate_views(&self, views: &[ViewName]) -> Result<u64> {
43 // When caching is disabled, both the shard scan and the CascadeInvalidator
44 // mutex are unnecessary — skip them entirely to avoid serializing mutations.
45 if !self.cache.is_enabled() {
46 return Ok(0);
47 }
48
49 // Expand the view list with transitive dependents when a cascade
50 // invalidator is configured. `CascadeInvalidator` works in terms of raw
51 // `String` view names today; promote each `ViewName` to `String` for the
52 // cascade walk and convert the expanded result back at the end.
53 if let Some(cascader) = &self.cascade_invalidator {
54 let mut expanded: std::collections::HashSet<String> =
55 views.iter().map(|v| v.as_str().to_owned()).collect();
56 let mut guard = cascader.lock().map_err(|e| crate::error::FraiseQLError::Internal {
57 message: format!("Cascade invalidator lock poisoned: {e}"),
58 source: None,
59 })?;
60 for view in views {
61 let transitive = guard.cascade_invalidate(view.as_str())?;
62 expanded.extend(transitive);
63 }
64 let expanded_views: Vec<ViewName> = expanded.into_iter().map(ViewName::from).collect();
65 return self.cache.invalidate_views(&expanded_views);
66 }
67 self.cache.invalidate_views(views)
68 }
69
70 /// Evict only list (multi-row) cache entries for the given views.
71 ///
72 /// Unlike `invalidate_views()`, leaves single-entity point-lookup entries
73 /// intact. Used for CREATE mutations: creating a new entity does not affect
74 /// queries that fetch a *different* existing entity by UUID.
75 ///
76 /// Expands the view list with transitive dependents when a
77 /// `CascadeInvalidator` is configured (same logic as `invalidate_views()`).
78 ///
79 /// # Returns
80 ///
81 /// Number of cache entries evicted.
82 ///
83 /// # Errors
84 ///
85 /// Returns error if the cascade invalidator lock is poisoned.
86 pub fn invalidate_list_queries(&self, views: &[ViewName]) -> Result<u64> {
87 if !self.cache.is_enabled() {
88 return Ok(0);
89 }
90
91 if let Some(cascader) = &self.cascade_invalidator {
92 let mut expanded: std::collections::HashSet<String> =
93 views.iter().map(|v| v.as_str().to_owned()).collect();
94 let mut guard = cascader.lock().map_err(|e| crate::error::FraiseQLError::Internal {
95 message: format!("Cascade invalidator lock poisoned: {e}"),
96 source: None,
97 })?;
98 for view in views {
99 let transitive = guard.cascade_invalidate(view.as_str())?;
100 expanded.extend(transitive);
101 }
102 let expanded_views: Vec<ViewName> = expanded.into_iter().map(ViewName::from).collect();
103 return self.cache.invalidate_list_queries(&expanded_views);
104 }
105 self.cache.invalidate_list_queries(views)
106 }
107
108 /// Evict cache entries that contain the given entity UUID.
109 ///
110 /// Delegates to `QueryResultCache::invalidate_by_entity`. Only entries
111 /// whose entity-ID index (built at `put()` time) contains the given UUID
112 /// are removed; all other entries remain warm.
113 ///
114 /// # Returns
115 ///
116 /// Number of cache entries evicted.
117 ///
118 /// # Errors
119 ///
120 /// Returns error if the cache mutex is poisoned.
121 pub fn invalidate_by_entity(&self, entity_type: &str, entity_id: &str) -> Result<u64> {
122 self.cache.invalidate_by_entity(entity_type, entity_id)
123 }
124}