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 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;