Expand description
Query result caching for FraiseQL v2.
§Overview
This module provides transparent W-TinyLFU query result caching with view-based and entity-based invalidation. Cache entries are automatically invalidated when mutations modify the underlying data.
§Scope
- W-TinyLFU result caching with per-entry TTL (via moka)
- Lock-free reads — cache hits do not acquire any shared lock
- View-based invalidation and entity-based invalidation via O(k) reverse indexes
- Security-aware cache key generation (prevents data leakage)
- Integration with
DatabaseAdaptervia wrapper
§Architecture
┌─────────────────────┐
│ GraphQL Query │
│ + Variables │
│ + WHERE Clause │
└──────────┬──────────┘
│
↓ generate_cache_key()
┌─────────────────────┐
│ ahash Cache Key │ ← Includes variables for security
└──────────┬──────────┘
│
↓ QueryResultCache::get()
┌─────────────────────┐
│ Cache Hit? │
│ - Check TTL (moka) │
│ - W-TinyLFU policy │
└──────────┬──────────┘
│
┌─────┴─────┐
│ │
HIT MISS
│ │
↓ ↓ execute_query()
Return Database Query
Cached + Store Result
Result + Track Views
Mutation:
┌─────────────────────┐
│ Mutation executed │
│ "createUser" │
└──────────┬──────────┘
│
↓ InvalidationContext::for_mutation()
┌─────────────────────┐
│ Modified Views: │
│ - v_user │
└──────────┬──────────┘
│
↓ cache.invalidate_views()
┌─────────────────────┐
│ Remove all caches │
│ reading from v_user │
└─────────────────────┘§Configuration
use fraiseql_core::cache::CacheConfig;
// Production configuration
let config = CacheConfig {
enabled: true,
max_entries: 50_000,
ttl_seconds: 86_400, // 24 hours
cache_list_queries: true,
..Default::default()
};
// Development (disable for deterministic tests)
let config = CacheConfig::disabled();§Usage Example
use fraiseql_core::cache::{CachedDatabaseAdapter, QueryResultCache, CacheConfig, InvalidationContext};
use fraiseql_core::db::{postgres::PostgresAdapter, DatabaseAdapter};
// Create database adapter
let db_adapter = PostgresAdapter::new("postgresql://localhost/db").await?;
// Wrap with caching
let cache = QueryResultCache::new(CacheConfig::default());
let adapter = CachedDatabaseAdapter::new(
db_adapter,
cache,
"1.0.0".to_string() // schema version
);
// Use as normal DatabaseAdapter - caching is transparent
let users = adapter
.execute_where_query("v_user", None, Some(10), None, None)
.await?;
println!("Found {} users", users.len());
// After mutation, invalidate
let invalidation = InvalidationContext::for_mutation(
"createUser",
vec!["v_user".to_string()]
);
adapter.invalidate_views(&invalidation.modified_views)?;§Performance
- Cache hit latency: ~0.05ms (P99 < 0.5ms) — lock-free read path
- Expected hit rate: 60-80% for typical workloads (higher than LRU under skewed access)
- Memory usage: ~100 MB for default config (10,000 entries @ 10 KB avg)
- Speedup: 50-200x faster than database queries
§Security
Cache keys include variable values to prevent data leakage between users. Different users with different query variables get different cache entries.
Example:
User A: query { user(id: 1) } → Cache key: abc123...
User B: query { user(id: 2) } → Cache key: def456... (DIFFERENT)This prevents User B from accidentally seeing User A’s cached data.
§View-Based Invalidation
Invalidation operates at the view/table level:
- Mutation modifies
v_user→ Invalidate ALL caches reading fromv_user - Expected hit rate: 60-70% (some over-invalidation)
Example:
Cache Entry 1: query { user(id: 1) } → reads v_user
Cache Entry 2: query { user(id: 2) } → reads v_user
Cache Entry 3: query { post(id: 100) } → reads v_post
Mutation: updateUser(id: 1)
→ Invalidates Entry 1 AND Entry 2 (even though Entry 2 not affected)
→ Entry 3 remains cached§Cache Security Requirements
The cache is safe in single-tenant deployments with no additional configuration. In multi-tenant deployments, two requirements must be met to prevent data leakage between tenants:
-
Row-Level Security (RLS) must be active. The cache key includes the per-request WHERE clause injected by FraiseQL’s RLS policy engine. Different users with different RLS predicates receive different cache entries. If RLS is disabled or returns an empty clause, all users share the same key for identical queries and variables — Tenant A’s data appears in Tenant B’s responses.
-
Schema content hash must be used as the schema version. Use
CompiledSchema::content_hash()(notenv!("CARGO_PKG_VERSION")) when constructingCachedDatabaseAdapter. This ensures that any schema change automatically invalidates all cached entries, preventing stale-schema hits after deployment.
The server emits a startup warn! when caching is enabled but no RLS policies
are declared in the compiled schema. This warning is informational in
single-tenant deployments and a critical security indicator in multi-tenant ones.
§Future Enhancements
- Entity-level tracking: Track by
User:123, not justv_user - Cascade integration: Parse mutation metadata for precise invalidation
- Selective invalidation: Only invalidate affected entity IDs
- Expected hit rate: 90-95% with entity-level tracking
§Module Organization
adapter:CachedDatabaseAdapterwrapper for transparent cachingconfig: Cache configuration with memory-safe boundskey: Security-critical cache key generation (includes APQ integration)result: W-TinyLFU cache storage (moka) with per-entry TTL, reverse indexes, and metricsdependency_tracker: Bidirectional view↔cache mappinginvalidation: Public invalidation API with structured contexts
Re-exports§
pub use cascade_invalidator::CascadeInvalidator;pub use cascade_invalidator::InvalidationStats;pub use cascade_metadata::CascadeMetadata;pub use cascade_response_parser::CascadeResponseParser;pub use entity_key::EntityKey;pub use fact_table_version::FactTableCacheConfig;pub use fact_table_version::FactTableVersionProvider;pub use fact_table_version::FactTableVersionStrategy;pub use fact_table_version::VERSION_TABLE_SCHEMA;pub use query_analyzer::QueryAnalyzer;pub use query_analyzer::QueryCardinality;pub use query_analyzer::QueryEntityProfile;pub use uuid_extractor::UUIDExtractor;
Modules§
- cascade_
invalidator - Cascading cache invalidation for transitive view dependencies.
- cascade_
metadata - Cascade metadata for mapping mutations to entity types.
- cascade_
response_ parser - Parser for GraphQL Cascade responses to extract entity invalidation data.
- entity_
key - Type-safe entity keys for entity-level cache invalidation.
- fact_
table_ version - Fact table versioning for aggregation query caching.
- query_
analyzer - Query analyzer for extracting entity constraints from compiled queries.
- uuid_
extractor - UUID extraction from mutation responses for entity-level cache invalidation.
Structs§
- Cache
Config - Cache configuration - disabled by default as of v2.0.0-rc.12.
- Cache
Metrics - Cache metrics for monitoring.
- Cached
Database Adapter - Cached database adapter wrapper.
- Cached
Result - Cached query result with metadata.
- Dependency
Tracker - Tracks which cache entries depend on which views/tables.
- Invalidation
Context - Context for cache invalidation operations.
- Query
Result Cache - Thread-safe W-TinyLFU cache for query results.
Enums§
- Invalidation
Reason - Reason for cache invalidation.
- RlsEnforcement
- Controls what happens when caching is enabled in a multi-tenant deployment but Row-Level Security does not appear to be active.
Functions§
- extract_
accessed_ views - Extract accessed views from query definition.
- generate_
cache_ key - Generate cache key for query result.
- generate_
projection_ query_ key - Fast cache key for a projection query — zero heap allocations.
- generate_
view_ query_ key - Fast cache key for a view query — zero heap allocations.
- view_
name_ to_ entity_ type - Derives the GraphQL entity type name from a database view name.