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