Skip to main content

Module cache

Module cache 

Source
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 DatabaseAdapter via 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 from v_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:

  1. 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.

  2. Schema content hash must be used as the schema version. Use CompiledSchema::content_hash() (not env!("CARGO_PKG_VERSION")) when constructing CachedDatabaseAdapter. 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 just v_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: CachedDatabaseAdapter wrapper for transparent caching
  • config: Cache configuration with memory-safe bounds
  • key: Security-critical cache key generation (includes APQ integration)
  • result: W-TinyLFU cache storage (moka) with per-entry TTL, reverse indexes, and metrics
  • dependency_tracker: Bidirectional view↔cache mapping
  • invalidation: 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§

CacheConfig
Cache configuration - disabled by default as of v2.0.0-rc.12.
CacheMetrics
Cache metrics for monitoring.
CachedDatabaseAdapter
Cached database adapter wrapper.
CachedResult
Cached query result with metadata.
DependencyTracker
Tracks which cache entries depend on which views/tables.
InvalidationContext
Context for cache invalidation operations.
QueryResultCache
Thread-safe W-TinyLFU cache for query results.

Enums§

InvalidationReason
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.