fraiseql_core/cache/mod.rs
1//! Query result caching for FraiseQL v2.
2//!
3//! # Overview
4//!
5//! This module provides transparent W-TinyLFU query result caching with view-based
6//! and entity-based invalidation. Cache entries are automatically invalidated when
7//! mutations modify the underlying data.
8//!
9//! # Scope
10//!
11//! - **W-TinyLFU result caching** with per-entry TTL (via moka)
12//! - **Lock-free reads** — cache hits do not acquire any shared lock
13//! - **View-based invalidation** and **entity-based invalidation** via O(k) reverse indexes
14//! - **Security-aware cache key generation** (prevents data leakage)
15//! - **Integration with `DatabaseAdapter`** via wrapper
16//!
17//! # Architecture
18//!
19//! ```text
20//! ┌─────────────────────┐
21//! │ GraphQL Query │
22//! │ + Variables │
23//! │ + WHERE Clause │
24//! └──────────┬──────────┘
25//! │
26//! ↓ generate_cache_key()
27//! ┌─────────────────────┐
28//! │ ahash Cache Key │ ← Includes variables for security
29//! └──────────┬──────────┘
30//! │
31//! ↓ QueryResultCache::get()
32//! ┌─────────────────────┐
33//! │ Cache Hit? │
34//! │ - Check TTL (moka) │
35//! │ - W-TinyLFU policy │
36//! └──────────┬──────────┘
37//! │
38//! ┌─────┴─────┐
39//! │ │
40//! HIT MISS
41//! │ │
42//! ↓ ↓ execute_query()
43//! Return Database Query
44//! Cached + Store Result
45//! Result + Track Views
46//!
47//! Mutation:
48//! ┌─────────────────────┐
49//! │ Mutation executed │
50//! │ "createUser" │
51//! └──────────┬──────────┘
52//! │
53//! ↓ InvalidationContext::for_mutation()
54//! ┌─────────────────────┐
55//! │ Modified Views: │
56//! │ - v_user │
57//! └──────────┬──────────┘
58//! │
59//! ↓ cache.invalidate_views()
60//! ┌─────────────────────┐
61//! │ Remove all caches │
62//! │ reading from v_user │
63//! └─────────────────────┘
64//! ```
65//!
66//! # Configuration
67//!
68//! ```rust
69//! use fraiseql_core::cache::CacheConfig;
70//!
71//! // Production configuration
72//! let config = CacheConfig {
73//! enabled: true,
74//! max_entries: 50_000,
75//! ttl_seconds: 86_400, // 24 hours
76//! cache_list_queries: true,
77//! ..Default::default()
78//! };
79//!
80//! // Development (disable for deterministic tests)
81//! let config = CacheConfig::disabled();
82//! ```
83//!
84//! # Usage Example
85//!
86//! ```no_run
87//! use fraiseql_core::cache::{CachedDatabaseAdapter, QueryResultCache, CacheConfig, InvalidationContext};
88//! use fraiseql_core::db::{postgres::PostgresAdapter, DatabaseAdapter};
89//!
90//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
91//! // Create database adapter
92//! let db_adapter = PostgresAdapter::new("postgresql://localhost/db").await?;
93//!
94//! // Wrap with caching
95//! let cache = QueryResultCache::new(CacheConfig::default());
96//! let adapter = CachedDatabaseAdapter::new(
97//! db_adapter,
98//! cache,
99//! "1.0.0".to_string() // schema version
100//! );
101//!
102//! // Use as normal DatabaseAdapter - caching is transparent
103//! let users = adapter
104//! .execute_where_query("v_user", None, Some(10), None, None)
105//! .await?;
106//!
107//! println!("Found {} users", users.len());
108//!
109//! // After mutation, invalidate
110//! let invalidation = InvalidationContext::for_mutation(
111//! "createUser",
112//! vec!["v_user".to_string()]
113//! );
114//! adapter.invalidate_views(&invalidation.modified_views)?;
115//! # Ok(())
116//! # }
117//! ```
118//!
119//! # Performance
120//!
121//! - **Cache hit latency**: ~0.05ms (P99 < 0.5ms) — lock-free read path
122//! - **Expected hit rate**: 60-80% for typical workloads (higher than LRU under skewed access)
123//! - **Memory usage**: ~100 MB for default config (10,000 entries @ 10 KB avg)
124//! - **Speedup**: 50-200x faster than database queries
125//!
126//! # Security
127//!
128//! Cache keys include variable values to prevent data leakage between users.
129//! Different users with different query variables get different cache entries.
130//!
131//! **Example**:
132//! ```text
133//! User A: query { user(id: 1) } → Cache key: abc123...
134//! User B: query { user(id: 2) } → Cache key: def456... (DIFFERENT)
135//! ```
136//!
137//! This prevents User B from accidentally seeing User A's cached data.
138//!
139//! # View-Based Invalidation
140//!
141//! Invalidation operates at the **view/table level**:
142//!
143//! - **Mutation modifies `v_user`** → Invalidate ALL caches reading from `v_user`
144//! - **Expected hit rate**: 60-70% (some over-invalidation)
145//!
146//! **Example**:
147//! ```text
148//! Cache Entry 1: query { user(id: 1) } → reads v_user
149//! Cache Entry 2: query { user(id: 2) } → reads v_user
150//! Cache Entry 3: query { post(id: 100) } → reads v_post
151//!
152//! Mutation: updateUser(id: 1)
153//! → Invalidates Entry 1 AND Entry 2 (even though Entry 2 not affected)
154//! → Entry 3 remains cached
155//! ```
156//!
157//! # Cache Security Requirements
158//!
159//! The cache is safe in single-tenant deployments with no additional configuration.
160//! In **multi-tenant deployments**, two requirements must be met to prevent data
161//! leakage between tenants:
162//!
163//! 1. **Row-Level Security (RLS) must be active.** The cache key includes the per-request WHERE
164//! clause injected by FraiseQL's RLS policy engine. Different users with different RLS
165//! predicates receive different cache entries. If RLS is disabled or returns an empty clause,
166//! all users share the same key for identical queries and variables — Tenant A's data appears in
167//! Tenant B's responses.
168//!
169//! 2. **Schema content hash must be used as the schema version.** Use
170//! `CompiledSchema::content_hash()` (not `env!("CARGO_PKG_VERSION")`) when constructing
171//! `CachedDatabaseAdapter`. This ensures that any schema change automatically invalidates all
172//! cached entries, preventing stale-schema hits after deployment.
173//!
174//! The server emits a startup `warn!` when caching is enabled but no RLS policies
175//! are declared in the compiled schema. This warning is informational in
176//! single-tenant deployments and a critical security indicator in multi-tenant ones.
177//!
178//! # Future Enhancements
179//!
180//! - **Entity-level tracking**: Track by `User:123`, not just `v_user`
181//! - **Cascade integration**: Parse mutation metadata for precise invalidation
182//! - **Selective invalidation**: Only invalidate affected entity IDs
183//! - **Expected hit rate**: 90-95% with entity-level tracking
184//!
185//! # Module Organization
186//!
187//! - **`adapter`**: `CachedDatabaseAdapter` wrapper for transparent caching
188//! - **`config`**: Cache configuration with memory-safe bounds
189//! - **`key`**: Security-critical cache key generation (includes APQ integration)
190//! - **`result`**: W-TinyLFU cache storage (moka) with per-entry TTL, reverse indexes, and metrics
191//! - **`dependency_tracker`**: Bidirectional view↔cache mapping
192//! - **`invalidation`**: Public invalidation API with structured contexts
193
194mod adapter;
195mod config;
196mod dependency_tracker;
197mod fact_table_cache;
198mod invalidation;
199mod invalidation_api;
200mod key;
201mod relay_cache;
202mod result;
203
204// Cascading invalidation with transitive dependencies
205pub mod cascade_invalidator;
206
207// Entity-level caching modules
208pub mod cascade_metadata;
209pub mod cascade_response_parser;
210pub mod entity_key;
211pub mod query_analyzer;
212pub mod uuid_extractor;
213
214// Fact table aggregation caching
215pub mod fact_table_version;
216
217// Public exports
218pub use adapter::{CachedDatabaseAdapter, view_name_to_entity_type};
219pub use cascade_invalidator::{CascadeInvalidator, InvalidationStats};
220pub use cascade_metadata::CascadeMetadata;
221pub use cascade_response_parser::CascadeResponseParser;
222pub use config::{CacheConfig, RlsEnforcement};
223// Export dependency tracker (used in doctests and advanced use cases)
224pub use dependency_tracker::DependencyTracker;
225pub use entity_key::EntityKey;
226pub use fact_table_version::{
227 FactTableCacheConfig, FactTableVersionProvider, FactTableVersionStrategy, VERSION_TABLE_SCHEMA,
228};
229pub use invalidation::{InvalidationContext, InvalidationReason};
230pub use key::{
231 extract_accessed_views, generate_cache_key, generate_projection_query_key,
232 generate_view_query_key,
233};
234pub use query_analyzer::{QueryAnalyzer, QueryCardinality, QueryEntityProfile};
235pub use result::{CacheMetrics, CachedResult, QueryResultCache};
236pub use uuid_extractor::UUIDExtractor;