Skip to main content

CachedDatabaseAdapter

Struct CachedDatabaseAdapter 

Source
pub struct CachedDatabaseAdapter<A: DatabaseAdapter> { /* private fields */ }
Expand description

Cached database adapter wrapper.

Wraps any DatabaseAdapter implementation with transparent query result caching. Cache keys include query, variables, WHERE clause, and schema version for security and correctness.

§Cache Behavior

  • Cache Hit: Returns cached result in ~0.1ms (50-200x faster than database)
  • Cache Miss: Executes query via underlying adapter, stores result in cache
  • Invalidation: Call invalidate_views() after mutations to clear affected caches

§Thread Safety

This adapter is Send + Sync and can be safely shared across async tasks. The underlying cache uses Arc<Mutex<>> for thread-safe access.

§Example

use fraiseql_core::cache::{CachedDatabaseAdapter, QueryResultCache, CacheConfig, InvalidationContext};
use fraiseql_core::db::{postgres::PostgresAdapter, DatabaseAdapter};

let db = PostgresAdapter::new("postgresql://localhost/db").await?;
let cache = QueryResultCache::new(CacheConfig::default());
let adapter = CachedDatabaseAdapter::new(db, cache, "1.0.0".to_string());

// First query - cache miss (slower)
let users1 = adapter.execute_where_query("v_user", None, None, None, None).await?;

// Second query - cache hit (fast!)
let users2 = adapter.execute_where_query("v_user", None, None, None, None).await?;

// After mutation, invalidate
let invalidation = InvalidationContext::for_mutation(
    "createUser",
    vec!["v_user".to_string()]
);
adapter.invalidate_views(&invalidation.modified_views)?;

Implementations§

Source§

impl<A: DatabaseAdapter> CachedDatabaseAdapter<A>

Source

pub fn new(adapter: A, cache: QueryResultCache, schema_version: String) -> Self

Create new cached database adapter.

§Arguments
  • adapter - Underlying database adapter to wrap
  • cache - Query result cache instance
  • schema_version - Uniquely identifies the compiled schema. Use schema.content_hash() (NOT env!("CARGO_PKG_VERSION")) so that any schema content change automatically invalidates cached entries across deploys.
§Example
use fraiseql_core::cache::{CachedDatabaseAdapter, QueryResultCache, CacheConfig};
use fraiseql_core::db::postgres::PostgresAdapter;
use fraiseql_core::schema::CompiledSchema;

let db = PostgresAdapter::new("postgresql://localhost/db").await?;
let cache = QueryResultCache::new(CacheConfig::default());
let adapter = CachedDatabaseAdapter::new(
    db,
    cache,
    schema.content_hash()  // Use content hash for automatic invalidation
);
Source

pub fn with_view_ttl_overrides(self, overrides: HashMap<String, u64>) -> Self

Set per-view TTL overrides.

Maps sql_source (view name) → TTL in seconds. Built at server startup from compiled QueryDefinition::cache_ttl_seconds entries.

§Example
let overrides = std::collections::HashMap::from([
    ("v_country".to_string(), 3600_u64),      // 1 h for reference data
    ("v_live_price".to_string(), 0_u64),      // no TTL — mutation-invalidated only
]);
let adapter = CachedDatabaseAdapter::new(db, cache, "1.0.0".to_string())
    .with_view_ttl_overrides(overrides);
Source

pub fn with_cascade_invalidator(self, invalidator: CascadeInvalidator) -> Self

Set a cascade invalidator for transitive view dependency expansion.

When set, invalidate_views() uses BFS to expand the initial view list to include all views that transitively depend on the invalidated views.

§Example
let mut cascade = CascadeInvalidator::new();
cascade.add_dependency("v_user_stats", "v_user")?;
cascade.add_dependency("v_dashboard", "v_user_stats")?;
let adapter = CachedDatabaseAdapter::new(db, cache, "1.0.0".to_string())
    .with_cascade_invalidator(cascade);
Source

pub fn with_ttl_overrides_from_schema(self, schema: &CompiledSchema) -> Self

Populate per-view TTL overrides from a compiled schema.

For each query that has cache_ttl_seconds set and a non-null sql_source, this maps the view name → TTL so the cache adapter uses the per-query TTL instead of the global default.

§Example
let adapter = CachedDatabaseAdapter::new(db, cache, "1.0.0".to_string())
    .with_ttl_overrides_from_schema(&schema);
Source

pub fn with_fact_table_config( adapter: A, cache: QueryResultCache, schema_version: String, fact_table_config: FactTableCacheConfig, ) -> Self

Create new cached database adapter with fact table caching configuration.

§Arguments
  • adapter - Underlying database adapter to wrap
  • cache - Query result cache instance
  • schema_version - Current schema version (e.g., git hash, semver)
  • fact_table_config - Configuration for fact table aggregation caching
§Example
use fraiseql_core::cache::{
    CachedDatabaseAdapter, QueryResultCache, CacheConfig,
    FactTableCacheConfig, FactTableVersionStrategy,
};
use fraiseql_core::db::postgres::PostgresAdapter;

let db = PostgresAdapter::new("postgresql://localhost/db").await?;
let cache = QueryResultCache::new(CacheConfig::default());

// Configure fact table caching strategies
let mut ft_config = FactTableCacheConfig::default();
ft_config.set_strategy("tf_sales", FactTableVersionStrategy::VersionTable);
ft_config.set_strategy("tf_events", FactTableVersionStrategy::time_based(300));

let adapter = CachedDatabaseAdapter::with_fact_table_config(
    db,
    cache,
    "1.0.0".to_string(),
    ft_config,
);
Source

pub const fn inner(&self) -> &A

Get reference to underlying adapter.

Useful for accessing adapter-specific methods not in the DatabaseAdapter trait.

§Example
// Access PostgreSQL-specific functionality
let pg_adapter = adapter.inner();
Source

pub fn cache(&self) -> &QueryResultCache

Get reference to cache.

Useful for metrics and monitoring.

§Example
let metrics = adapter.cache().metrics()?;
println!("Cache hit rate: {:.1}%", metrics.hit_rate() * 100.0);
Source

pub fn schema_version(&self) -> &str

Get schema version.

§Example
println!("Schema version: {}", adapter.schema_version());
Source

pub const fn fact_table_config(&self) -> &FactTableCacheConfig

Get fact table cache configuration.

Source

pub fn version_provider(&self) -> &FactTableVersionProvider

Get the version provider for fact tables.

Source

pub async fn validate_rls_active(&self) -> Result<()>

Verify that Row-Level Security is active on the database connection.

Call this during server initialization when both caching and multi-tenancy (schema.is_multi_tenant()) are enabled. Without RLS, users sharing the same query parameters will receive the same cached response regardless of tenant.

§What this checks

Runs SELECT current_setting('row_security', true) AS rls_setting. The result must be 'on' or 'force' for the check to pass. Non-PostgreSQL databases (which return an error or unsupported) are treated as “RLS not active”.

§Errors

Returns FraiseQLError::Configuration if RLS appears inactive.

Source

pub async fn enforce_rls(&self, enforcement: RlsEnforcement) -> Result<()>

Apply the RLS enforcement policy from CacheConfig.

Runs validate_rls_active and handles the result according to enforcement:

§Errors

Returns the error from validate_rls_active when enforcement is Error.

Source§

impl<A: DatabaseAdapter> CachedDatabaseAdapter<A>

Source

pub async fn execute_aggregation_query( &self, sql: &str, ) -> Result<Vec<HashMap<String, Value>>>

Execute aggregation query with caching based on fact table versioning strategy.

This method provides transparent caching for aggregation queries on fact tables. The caching behavior depends on the configured strategy for the fact table.

§Arguments
  • sql - The aggregation SQL query
§Returns

Query results (from cache or database)

§Example

// This query will be cached according to tf_sales strategy
let results = adapter.execute_aggregation_query(
    "SELECT SUM(revenue) FROM tf_sales WHERE year = 2024"
).await?;
§Errors

Returns FraiseQLError if the underlying database query fails.

Source§

impl<A: DatabaseAdapter> CachedDatabaseAdapter<A>

Source

pub fn invalidate_views(&self, views: &[String]) -> Result<u64>

Invalidate cache entries that read from specified views.

Call this after mutations to ensure cache consistency. All cache entries that accessed any of the modified views will be removed.

§Arguments
  • views - List of views/tables that were modified
§Returns

Number of cache entries invalidated

§Errors

Returns error if cache mutex is poisoned (very rare).

§Example
// After creating a user
let count = adapter.invalidate_views(&["v_user".to_string()])?;
println!("Invalidated {} cache entries", count);
Source

pub fn invalidate_cascade_entities( &self, cascade_response: &Value, parser: &CascadeResponseParser, ) -> Result<u64>

Invalidate cache entries based on GraphQL Cascade response entities.

This is the entity-aware invalidation method that provides more precise invalidation. Instead of invalidating all caches reading from a view, only caches that depend on the affected entities are invalidated.

§Arguments
  • cascade_response - GraphQL mutation response with cascade field
  • parser - CascadeResponseParser to extract entities
§Returns

Number of cache entries invalidated

§Example
let cascade_response = json!({
    "createPost": {
        "cascade": {
            "updated": [
                { "__typename": "User", "id": "uuid-1" }
            ]
        }
    }
});

let parser = CascadeResponseParser::new();
let count = adapter.invalidate_cascade_entities(&cascade_response, &parser)?;
println!("Invalidated {} cache entries", count);
§Note on Performance

This method replaces view-level invalidation with entity-level invalidation. Instead of clearing all caches that touch a view (e.g., v_user), only caches that touch the specific entities are cleared (e.g., User:uuid-1).

Expected improvement:

  • View-level: 60-70% hit rate (many false positives)
  • Entity-level: 90-95% hit rate (only true positives)
§Errors

Returns FraiseQLError if the cascade response cannot be parsed.

Source

pub fn invalidate_list_queries(&self, views: &[String]) -> Result<u64>

Evict only list (multi-row) cache entries for the given views.

Unlike invalidate_views(), leaves single-entity point-lookup entries intact. Used for CREATE mutations: creating a new entity does not affect queries that fetch a different existing entity by UUID.

Expands the view list with transitive dependents when a CascadeInvalidator is configured (same logic as invalidate_views()).

§Returns

Number of cache entries evicted.

§Errors

Returns error if the cascade invalidator lock is poisoned.

Source

pub fn invalidate_by_entity( &self, entity_type: &str, entity_id: &str, ) -> Result<u64>

Evict cache entries that contain the given entity UUID.

Delegates to QueryResultCache::invalidate_by_entity. Only entries whose entity-ID index (built at put() time) contains the given UUID are removed; all other entries remain warm.

§Returns

Number of cache entries evicted.

§Errors

Returns error if the cache mutex is poisoned.

Trait Implementations§

Source§

impl<A: DatabaseAdapter + Clone> Clone for CachedDatabaseAdapter<A>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<A: DatabaseAdapter> DatabaseAdapter for CachedDatabaseAdapter<A>

Source§

fn execute_with_projection<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view: &'life1 str, projection: Option<&'life2 SqlProjectionHint>, where_clause: Option<&'life3 WhereClause>, limit: Option<u32>, offset: Option<u32>, _order_by: Option<&'life4 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Vec<JsonbValue>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait,

Execute a WHERE query with SQL field projection optimization. Read more
Source§

fn execute_where_query<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, _order_by: Option<&'life3 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Vec<JsonbValue>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait,

Execute a WHERE query against a view and return JSONB rows. Read more
Source§

fn execute_with_projection_arc<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view: &'life1 str, projection: Option<&'life2 SqlProjectionHint>, where_clause: Option<&'life3 WhereClause>, limit: Option<u32>, offset: Option<u32>, _order_by: Option<&'life4 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait,

Like execute_with_projection but returns the result wrapped in an Arc. Read more
Source§

fn execute_where_query_arc<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, _order_by: Option<&'life3 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait,

Like execute_where_query but returns the result wrapped in an Arc. Read more
Source§

fn database_type(&self) -> DatabaseType

Get database type (for logging/metrics). Read more
Source§

fn health_check<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Health check - verify database connectivity. Read more
Source§

fn pool_metrics(&self) -> PoolMetrics

Get connection pool metrics. Read more
Source§

fn execute_raw_query<'life0, 'life1, 'async_trait>( &'life0 self, sql: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Execute raw SQL query and return rows as JSON objects. Read more
Source§

fn execute_parameterized_aggregate<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, sql: &'life1 str, params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Execute a parameterized aggregate SQL query (GROUP BY / HAVING / window). Read more
Source§

fn execute_function_call<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, function_name: &'life1 str, args: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Execute a database function call and return all columns as rows. Read more
Source§

fn invalidate_views<'life0, 'life1, 'async_trait>( &'life0 self, views: &'life1 [String], ) -> Pin<Box<dyn Future<Output = Result<u64>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Invalidate cached query results for the specified views. Read more
Source§

fn invalidate_by_entity<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, entity_type: &'life1 str, entity_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<u64>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Evict cache entries that contain the given entity UUID. Read more
Source§

fn invalidate_list_queries<'life0, 'life1, 'async_trait>( &'life0 self, views: &'life1 [String], ) -> Pin<Box<dyn Future<Output = Result<u64>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Evict only list (multi-row) cache entries for the given views. Read more
Source§

fn bump_fact_table_versions<'life0, 'life1, 'async_trait>( &'life0 self, tables: &'life1 [String], ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Bump fact table version counters after a successful mutation. Read more
Source§

fn execute_row_query<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Vec<Vec<ColumnValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait,

Execute a row-shaped query against a view, returning typed column values. Read more
Source§

fn supports_mutations(&self) -> bool

Returns true if this adapter supports GraphQL mutation operations. Read more
Source§

fn capabilities(&self) -> DatabaseCapabilities

Get database capabilities. Read more
Source§

fn explain_query<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _sql: &'life1 str, _params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = Result<Value, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Run the database’s EXPLAIN on a SQL statement without executing it. Read more
Source§

fn explain_where_query<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _view: &'life1 str, _where_clause: Option<&'life2 WhereClause>, _limit: Option<u32>, _offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Value, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Run EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) against a view with the same parameterized WHERE clause that execute_where_query would use. Read more
Source§

fn mutation_strategy(&self) -> MutationStrategy

Returns the mutation strategy used by this adapter. Read more
Source§

fn execute_direct_mutation<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _ctx: &'life1 DirectMutationContext<'life2>, ) -> Pin<Box<dyn Future<Output = Result<Vec<Value>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Execute a direct SQL mutation (INSERT/UPDATE/DELETE) and return the mutation response rows as JSON objects. Read more
Source§

impl<A: RelayDatabaseAdapter + DatabaseAdapter> RelayDatabaseAdapter for CachedDatabaseAdapter<A>

Source§

async fn execute_relay_page( &self, view: &str, cursor_column: &str, after: Option<CursorValue>, before: Option<CursorValue>, limit: u32, forward: bool, where_clause: Option<&WhereClause>, order_by: Option<&[OrderByClause]>, include_total_count: bool, ) -> Result<RelayPageResult>

Execute keyset (cursor-based) pagination against a JSONB view. Read more
Source§

impl<A: SupportsMutations + Send + Sync> SupportsMutations for CachedDatabaseAdapter<A>

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more