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 cache entries that contain the given entity UUID.
71    ///
72    /// Delegates to `QueryResultCache::invalidate_by_entity`. Only entries
73    /// whose entity-ID index (built at `put()` time) contains the given UUID
74    /// are removed; all other entries remain warm.
75    ///
76    /// # Returns
77    ///
78    /// Number of cache entries evicted.
79    ///
80    /// # Errors
81    ///
82    /// Returns error if the cache mutex is poisoned.
83    pub fn invalidate_by_entity(&self, entity_type: &str, entity_id: &str) -> Result<u64> {
84        self.cache.invalidate_by_entity(entity_type, entity_id)
85    }
86}