Skip to main content

fraiseql_core/cache/
mod.rs

1//! Query result caching for FraiseQL v2.
2//!
3//! # Overview
4//!
5//! This module provides transparent LRU-based query result caching with view-based
6//! invalidation. Cache entries are automatically invalidated when mutations modify
7//! the underlying data.
8//!
9//! # Scope
10//!
11//! - **LRU-based result caching** with TTL expiry
12//! - **View-based invalidation** (not entity-level)
13//! - **Security-aware cache key generation** (prevents data leakage)
14//! - **Integration with `DatabaseAdapter`** via wrapper
15//!
16//! # Architecture
17//!
18//! ```text
19//! ┌─────────────────────┐
20//! │ GraphQL Query       │
21//! │ + Variables         │
22//! │ + WHERE Clause      │
23//! └──────────┬──────────┘
24//!            │
25//!            ↓ generate_cache_key()
26//! ┌─────────────────────┐
27//! │ SHA-256 Cache Key   │ ← Includes variables for security
28//! └──────────┬──────────┘
29//!            │
30//!            ↓ QueryResultCache::get()
31//! ┌─────────────────────┐
32//! │ Cache Hit?          │
33//! │ - Check TTL         │
34//! │ - Check LRU         │
35//! └──────────┬──────────┘
36//!            │
37//!      ┌─────┴─────┐
38//!      │           │
39//!     HIT         MISS
40//!      │           │
41//!      ↓           ↓ execute_query()
42//! Return      Database Query
43//! Cached      + Store Result
44//! Result      + Track Views
45//!
46//! Mutation:
47//! ┌─────────────────────┐
48//! │ Mutation executed   │
49//! │ "createUser"        │
50//! └──────────┬──────────┘
51//!            │
52//!            ↓ InvalidationContext::for_mutation()
53//! ┌─────────────────────┐
54//! │ Modified Views:     │
55//! │ - v_user            │
56//! └──────────┬──────────┘
57//!            │
58//!            ↓ cache.invalidate_views()
59//! ┌─────────────────────┐
60//! │ Remove all caches   │
61//! │ reading from v_user │
62//! └─────────────────────┘
63//! ```
64//!
65//! # Configuration
66//!
67//! ```rust
68//! use fraiseql_core::cache::CacheConfig;
69//!
70//! // Production configuration
71//! let config = CacheConfig {
72//!     enabled: true,
73//!     max_entries: 50_000,
74//!     ttl_seconds: 86_400,  // 24 hours
75//!     cache_list_queries: true,
76//! };
77//!
78//! // Development (disable for deterministic tests)
79//! let config = CacheConfig::disabled();
80//! ```
81//!
82//! # Usage Example
83//!
84//! ```ignore
85//! use fraiseql_core::cache::{CachedDatabaseAdapter, QueryResultCache, CacheConfig, InvalidationContext};
86//! use fraiseql_core::db::postgres::PostgresAdapter;
87//!
88//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
89//! // Create database adapter
90//! let db_adapter = PostgresAdapter::new("postgresql://localhost/db").await?;
91//!
92//! // Wrap with caching
93//! let cache = QueryResultCache::new(CacheConfig::default());
94//! let adapter = CachedDatabaseAdapter::new(
95//!     db_adapter,
96//!     cache,
97//!     "1.0.0".to_string()  // schema version
98//! );
99//!
100//! // Use as normal DatabaseAdapter - caching is transparent
101//! let users = adapter
102//!     .execute_where_query("v_user", None, Some(10), None)
103//!     .await?;
104//!
105//! println!("Found {} users", users.len());
106//!
107//! // After mutation, invalidate
108//! let invalidation = InvalidationContext::for_mutation(
109//!     "createUser",
110//!     vec!["v_user".to_string()]
111//! );
112//! adapter.invalidate_views(&invalidation.modified_views)?;
113//! # Ok(())
114//! # }
115//! ```
116//!
117//! # Performance
118//!
119//! - **Cache hit latency**: ~0.1ms (P99 < 1ms)
120//! - **Expected hit rate**: 60-80% for typical workloads
121//! - **Memory usage**: ~100 MB for default config (10,000 entries @ 10 KB avg)
122//! - **Speedup**: 50-200x faster than database queries
123//!
124//! # Security
125//!
126//! Cache keys include variable values to prevent data leakage between users.
127//! Different users with different query variables get different cache entries.
128//!
129//! **Example**:
130//! ```text
131//! User A: query { user(id: 1) } → Cache key: abc123...
132//! User B: query { user(id: 2) } → Cache key: def456... (DIFFERENT)
133//! ```
134//!
135//! This prevents User B from accidentally seeing User A's cached data.
136//!
137//! # View-Based Invalidation
138//!
139//! Invalidation operates at the **view/table level**:
140//!
141//! - **Mutation modifies `v_user`** → Invalidate ALL caches reading from `v_user`
142//! - **Expected hit rate**: 60-70% (some over-invalidation)
143//!
144//! **Example**:
145//! ```text
146//! Cache Entry 1: query { user(id: 1) }     → reads v_user
147//! Cache Entry 2: query { user(id: 2) }     → reads v_user
148//! Cache Entry 3: query { post(id: 100) }   → reads v_post
149//!
150//! Mutation: updateUser(id: 1)
151//! → Invalidates Entry 1 AND Entry 2 (even though Entry 2 not affected)
152//! → Entry 3 remains cached
153//! ```
154//!
155//! # Future Enhancements
156//!
157//! - **Entity-level tracking**: Track by `User:123`, not just `v_user`
158//! - **Cascade integration**: Parse mutation metadata for precise invalidation
159//! - **Selective invalidation**: Only invalidate affected entity IDs
160//! - **Expected hit rate**: 90-95% with entity-level tracking
161//!
162//! # Module Organization
163//!
164//! - **`adapter`**: `CachedDatabaseAdapter` wrapper for transparent caching
165//! - **`config`**: Cache configuration with memory-safe bounds
166//! - **`key`**: Security-critical cache key generation (includes APQ integration)
167//! - **`result`**: LRU cache storage with TTL and metrics
168//! - **`dependency_tracker`**: Bidirectional view↔cache mapping
169//! - **`invalidation`**: Public invalidation API with structured contexts
170
171mod adapter;
172mod config;
173mod dependency_tracker;
174mod invalidation;
175mod key;
176mod result;
177
178// Cascading invalidation with transitive dependencies
179pub mod cascade_invalidator;
180
181// Entity-level caching modules
182pub mod cascade_metadata;
183pub mod cascade_response_parser;
184pub mod entity_key;
185pub mod query_analyzer;
186pub mod uuid_extractor;
187
188// Fact table aggregation caching
189pub mod fact_table_version;
190
191// Public exports
192pub use adapter::CachedDatabaseAdapter;
193pub use cascade_invalidator::{CascadeInvalidator, InvalidationStats};
194pub use cascade_metadata::CascadeMetadata;
195pub use cascade_response_parser::CascadeResponseParser;
196pub use config::CacheConfig;
197// Export dependency tracker (used in doctests and advanced use cases)
198pub use dependency_tracker::DependencyTracker;
199pub use entity_key::EntityKey;
200pub use fact_table_version::{
201    FactTableCacheConfig, FactTableVersionProvider, FactTableVersionStrategy, VERSION_TABLE_SCHEMA,
202};
203pub use invalidation::{InvalidationContext, InvalidationReason};
204pub use key::{extract_accessed_views, generate_cache_key};
205pub use query_analyzer::{QueryAnalyzer, QueryCardinality, QueryEntityProfile};
206pub use result::{CacheMetrics, CachedResult, QueryResultCache};
207pub use uuid_extractor::UUIDExtractor;