use std::sync::Arc;
use fraiseql_db::types::ReadRouting;
use super::CachedDatabaseAdapter;
use crate::{
backend::{
ComposedLevel, DatabaseAdapter, ProjectionRequest, WhereClause,
types::{JsonbValue, sql_hints::OrderByClause},
},
cache::{
by_id::by_id_tag,
collections::composed_tags,
key::{
generate_composed_query_key, generate_projection_query_key, generate_view_query_key,
},
},
error::Result,
};
#[must_use]
pub fn view_name_to_entity_type(view: &str) -> Option<String> {
let after_prefix = view.split_once('_')?.1;
if after_prefix.is_empty() {
return None;
}
let pascal = after_prefix
.split('_')
.map(|segment| {
let mut chars = segment.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<String>();
Some(pascal)
}
impl<A: DatabaseAdapter> CachedDatabaseAdapter<A> {
pub(super) fn bypasses_cache(&self, request: &ProjectionRequest<'_>) -> bool {
let opted_in = self.opt_in_mode || request.source_args.is_some();
!self.cache.is_enabled() || (opted_in && !self.cacheable_views.contains(request.view))
}
#[tracing::instrument(skip_all, fields(cache.view = request.view))]
pub(super) async fn execute_with_projection_impl(
&self,
request: &ProjectionRequest<'_>,
session_vars: &[(&str, &str)],
) -> Result<Arc<Vec<JsonbValue>>> {
if self.bypasses_cache(request) {
return self
.adapter
.execute_with_projection_arc_with_session(request, session_vars, ReadRouting::Any)
.await;
}
let cache_key = generate_projection_query_key(
request,
session_vars,
crate::runtime::scoped_request_locale().as_deref(),
&self.schema_version,
);
if let Some(cached_arc) = self.cache.get(cache_key)? {
return Ok(cached_arc);
}
let fence = self.cache.invalidation_generation();
let arc = self
.adapter
.execute_with_projection_arc_with_session(request, session_vars, ReadRouting::Any)
.await?;
self.store(cache_key, &arc, request.view, request.where_clause, fence)?;
Ok(arc)
}
fn store(
&self,
cache_key: u64,
rows: &Arc<Vec<JsonbValue>>,
view: &str,
where_clause: Option<&WhereClause>,
fence: u64,
) -> Result<()> {
let ttl = self.view_ttl_overrides.get(view).copied();
let views = self.accessed_views_for(view);
match by_id_tag(&self.by_id_sources, view, where_clause, &views) {
Some(row) => {
self.cache.put_by_id(cache_key, Arc::clone(rows), views, ttl, row, Some(fence))
},
None => self.cache.put_arc(
cache_key,
Arc::clone(rows),
views,
ttl,
view_name_to_entity_type(view).as_deref(),
Some(fence),
),
}
}
pub(super) fn composed_policy(&self, read: &ComposedLevel) -> ComposedPolicy {
let mut views = Vec::new();
collect_views(read, &mut views);
if self.opt_in_mode && !views.iter().all(|v| self.cacheable_views.contains(*v)) {
return ComposedPolicy::Uncached;
}
ComposedPolicy::Cached {
ttl: views.iter().filter_map(|v| self.view_ttl_overrides.get(*v).copied()).min(),
}
}
pub(super) async fn execute_composed_impl(
&self,
read: &ComposedLevel,
session_vars: &[(&str, &str)],
routing: ReadRouting,
) -> Result<Arc<Vec<JsonbValue>>> {
let policy = if self.cache.is_enabled() {
self.composed_policy(read)
} else {
ComposedPolicy::Uncached
};
let ComposedPolicy::Cached { ttl } = policy else {
return self.adapter.execute_composed_with_session(read, session_vars, routing).await;
};
let cache_key = generate_composed_query_key(
read,
session_vars,
crate::runtime::scoped_request_locale().as_deref(),
&self.schema_version,
);
if let Some(cached) = self.cache.get(cache_key)? {
return Ok(cached);
}
let fence = self.cache.invalidation_generation();
let map = self.collections.current();
let rows = self.adapter.execute_composed_with_session(read, session_vars, routing).await?;
let (views, tags) =
composed_tags(read, &rows, map.as_deref(), |view| self.accessed_views_for(view));
self.cache.put_composed(
cache_key,
Arc::clone(&rows),
views,
ttl,
None,
tags,
Some(fence),
)?;
Ok(rows)
}
#[tracing::instrument(skip_all, fields(cache.view = view))]
pub(super) async fn execute_where_query_impl(
&self,
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
session_vars: &[(&str, &str)],
) -> Result<Arc<Vec<JsonbValue>>> {
if !self.cache.is_enabled() || (self.opt_in_mode && !self.cacheable_views.contains(view)) {
return self
.adapter
.execute_where_query_arc_with_session(
view,
where_clause,
limit,
offset,
order_by,
session_vars,
ReadRouting::Any,
)
.await;
}
let cache_key = generate_view_query_key(
view,
where_clause,
limit,
offset,
order_by,
session_vars,
crate::runtime::scoped_request_locale().as_deref(),
&self.schema_version,
);
if let Some(cached_arc) = self.cache.get(cache_key)? {
return Ok(cached_arc);
}
let fence = self.cache.invalidation_generation();
let arc = self
.adapter
.execute_where_query_arc_with_session(
view,
where_clause,
limit,
offset,
order_by,
session_vars,
ReadRouting::Any,
)
.await?;
self.store(cache_key, &arc, view, where_clause, fence)?;
Ok(arc)
}
}
fn collect_views<'a>(level: &'a ComposedLevel, views: &mut Vec<&'a str>) {
views.push(&level.view);
for embed in &level.embeds {
collect_views(&embed.level, views);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ComposedPolicy {
Uncached,
Cached {
ttl: Option<u64>,
},
}