Skip to main content

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}