use crate::{
db::{
DbSession, Query, QueryError, TraceReuseArtifactClass, TraceReuseEvent,
commit::CommitSchemaFingerprint,
executor::{EntityAuthority, PreparedExecutionPlan, SharedPreparedExecutionPlan},
predicate::predicate_fingerprint_normalized,
query::{
intent::StructuralQuery,
plan::{AccessPlannedQuery, VisibleIndexes},
},
},
model::entity::EntityModel,
traits::{CanisterKind, EntityKind, Path},
};
use std::{cell::RefCell, collections::HashMap};
const SHARED_QUERY_PLAN_CACHE_METHOD_VERSION: u8 = 2;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(in crate::db) enum QueryPlanVisibility {
StoreNotReady,
StoreReady,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(in crate::db) struct QueryPlanCacheKey {
cache_method_version: u8,
entity_path: &'static str,
schema_fingerprint: CommitSchemaFingerprint,
visibility: QueryPlanVisibility,
structural_query: crate::db::query::intent::StructuralQueryCacheKey,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(in crate::db) struct QueryPlanCacheAttribution {
pub hits: u64,
pub misses: u64,
}
pub(in crate::db) type QueryPlanCache = HashMap<QueryPlanCacheKey, SharedPreparedExecutionPlan>;
thread_local! {
static QUERY_PLAN_CACHES: RefCell<HashMap<usize, QueryPlanCache>> =
RefCell::new(HashMap::default());
}
impl QueryPlanCacheAttribution {
#[must_use]
const fn hit() -> Self {
Self { hits: 1, misses: 0 }
}
#[must_use]
const fn miss() -> Self {
Self { hits: 0, misses: 1 }
}
}
pub(in crate::db::session) const fn query_plan_cache_reuse_event(
attribution: QueryPlanCacheAttribution,
) -> TraceReuseEvent {
if attribution.hits > 0 {
TraceReuseEvent::hit(TraceReuseArtifactClass::SharedPreparedQueryPlan)
} else {
TraceReuseEvent::miss(TraceReuseArtifactClass::SharedPreparedQueryPlan)
}
}
impl<C: CanisterKind> DbSession<C> {
fn with_query_plan_cache<R>(&self, f: impl FnOnce(&mut QueryPlanCache) -> R) -> R {
let scope_id = self.db.cache_scope_id();
QUERY_PLAN_CACHES.with(|caches| {
let mut caches = caches.borrow_mut();
let cache = caches.entry(scope_id).or_default();
f(cache)
})
}
pub(in crate::db::session) const fn visible_indexes_for_model(
model: &'static EntityModel,
visibility: QueryPlanVisibility,
) -> VisibleIndexes<'static> {
match visibility {
QueryPlanVisibility::StoreReady => VisibleIndexes::planner_visible(model.indexes()),
QueryPlanVisibility::StoreNotReady => VisibleIndexes::none(),
}
}
#[cfg(test)]
pub(in crate::db) fn query_plan_cache_len(&self) -> usize {
self.with_query_plan_cache(|cache| cache.len())
}
#[cfg(test)]
pub(in crate::db) fn clear_query_plan_cache_for_tests(&self) {
self.with_query_plan_cache(QueryPlanCache::clear);
}
pub(in crate::db) fn query_plan_visibility_for_store_path(
&self,
store_path: &'static str,
) -> Result<QueryPlanVisibility, QueryError> {
let store = self
.db
.recovered_store(store_path)
.map_err(QueryError::execute)?;
let visibility = if store.index_state() == crate::db::IndexState::Ready {
QueryPlanVisibility::StoreReady
} else {
QueryPlanVisibility::StoreNotReady
};
Ok(visibility)
}
pub(in crate::db) fn cached_shared_query_plan_for_authority(
&self,
authority: EntityAuthority,
schema_fingerprint: CommitSchemaFingerprint,
query: &StructuralQuery,
) -> Result<(SharedPreparedExecutionPlan, QueryPlanCacheAttribution), QueryError> {
let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
if query.trivial_scalar_load_fast_path_eligible() {
return self.cached_trivial_scalar_load_plan_for_authority(
authority,
schema_fingerprint,
visibility,
query,
);
}
let visible_indexes = Self::visible_indexes_for_model(authority.model(), visibility);
let planning_state = query.prepare_scalar_planning_state()?;
let normalized_predicate_fingerprint = planning_state
.normalized_predicate()
.map(predicate_fingerprint_normalized);
let cache_key =
QueryPlanCacheKey::for_authority_with_normalized_predicate_fingerprint_and_method_version(
authority,
schema_fingerprint,
visibility,
query,
normalized_predicate_fingerprint,
SHARED_QUERY_PLAN_CACHE_METHOD_VERSION,
);
{
let cached = self.with_query_plan_cache(|cache| cache.get(&cache_key).cloned());
if let Some(prepared_plan) = cached {
return Ok((prepared_plan, QueryPlanCacheAttribution::hit()));
}
}
let plan = query.build_plan_with_visible_indexes_from_scalar_planning_state(
&visible_indexes,
planning_state,
)?;
let prepared_plan = SharedPreparedExecutionPlan::from_plan(authority, plan);
self.with_query_plan_cache(|cache| {
cache.insert(cache_key, prepared_plan.clone());
});
Ok((prepared_plan, QueryPlanCacheAttribution::miss()))
}
fn cached_trivial_scalar_load_plan_for_authority(
&self,
authority: EntityAuthority,
schema_fingerprint: CommitSchemaFingerprint,
visibility: QueryPlanVisibility,
query: &StructuralQuery,
) -> Result<(SharedPreparedExecutionPlan, QueryPlanCacheAttribution), QueryError> {
let cache_key =
QueryPlanCacheKey::for_authority_with_normalized_predicate_fingerprint_and_method_version(
authority,
schema_fingerprint,
visibility,
query,
None,
SHARED_QUERY_PLAN_CACHE_METHOD_VERSION,
);
{
let cached = self.with_query_plan_cache(|cache| cache.get(&cache_key).cloned());
if let Some(prepared_plan) = cached {
return Ok((prepared_plan, QueryPlanCacheAttribution::hit()));
}
}
let Some(plan) = query.try_build_trivial_scalar_load_plan()? else {
return Err(QueryError::invariant(
"trivial scalar load fast path lost eligibility during plan build",
));
};
let prepared_plan = SharedPreparedExecutionPlan::from_plan(authority, plan);
self.with_query_plan_cache(|cache| {
cache.insert(cache_key, prepared_plan.clone());
});
Ok((prepared_plan, QueryPlanCacheAttribution::miss()))
}
#[cfg(test)]
pub(in crate::db) fn query_plan_cache_key_for_tests(
authority: EntityAuthority,
schema_fingerprint: CommitSchemaFingerprint,
visibility: QueryPlanVisibility,
query: &StructuralQuery,
cache_method_version: u8,
) -> QueryPlanCacheKey {
QueryPlanCacheKey::for_authority_with_method_version(
authority,
schema_fingerprint,
visibility,
query,
cache_method_version,
)
}
pub(in crate::db::session) fn with_query_visible_indexes<E, T>(
&self,
query: &Query<E>,
op: impl FnOnce(&Query<E>, &VisibleIndexes<'static>) -> Result<T, QueryError>,
) -> Result<T, QueryError>
where
E: EntityKind<Canister = C>,
{
let visibility = self.query_plan_visibility_for_store_path(E::Store::PATH)?;
let visible_indexes = Self::visible_indexes_for_model(E::MODEL, visibility);
op(query, &visible_indexes)
}
pub(in crate::db::session) fn cached_prepared_query_plan_for_entity<E>(
&self,
query: &Query<E>,
) -> Result<(PreparedExecutionPlan<E>, QueryPlanCacheAttribution), QueryError>
where
E: EntityKind<Canister = C>,
{
let (prepared_plan, attribution) = self.cached_shared_query_plan_for_entity::<E>(query)?;
Ok((prepared_plan.typed_clone::<E>(), attribution))
}
pub(in crate::db::session) fn cached_shared_query_plan_for_entity<E>(
&self,
query: &Query<E>,
) -> Result<(SharedPreparedExecutionPlan, QueryPlanCacheAttribution), QueryError>
where
E: EntityKind<Canister = C>,
{
self.cached_shared_query_plan_for_authority(
EntityAuthority::for_type::<E>(),
crate::db::schema::commit_schema_fingerprint_for_entity::<E>(),
query.structural(),
)
}
pub(in crate::db::session) fn try_map_cached_shared_query_plan_ref_for_entity<E, T>(
&self,
query: &Query<E>,
map: impl FnOnce(&SharedPreparedExecutionPlan) -> Result<T, QueryError>,
) -> Result<T, QueryError>
where
E: EntityKind<Canister = C>,
{
let (prepared_plan, _) = self.cached_shared_query_plan_for_entity::<E>(query)?;
map(&prepared_plan)
}
pub(in crate::db::session) fn try_map_cached_shared_query_plan_ref_for_authority<T>(
&self,
authority: EntityAuthority,
schema_fingerprint: CommitSchemaFingerprint,
query: &StructuralQuery,
map: impl FnOnce(&SharedPreparedExecutionPlan) -> Result<T, QueryError>,
) -> Result<(T, QueryPlanCacheAttribution), QueryError> {
let (prepared_plan, attribution) =
self.cached_shared_query_plan_for_authority(authority, schema_fingerprint, query)?;
let mapped = map(&prepared_plan)?;
Ok((mapped, attribution))
}
pub(in crate::db::session) fn map_cached_shared_query_plan_for_entity<E, T>(
&self,
query: &Query<E>,
map: impl FnOnce(AccessPlannedQuery) -> T,
) -> Result<T, QueryError>
where
E: EntityKind<Canister = C>,
{
let (prepared_plan, _) = self.cached_shared_query_plan_for_entity::<E>(query)?;
Ok(map(prepared_plan.logical_plan().clone()))
}
}
impl QueryPlanCacheKey {
const fn from_authority_parts(
authority: EntityAuthority,
schema_fingerprint: CommitSchemaFingerprint,
visibility: QueryPlanVisibility,
structural_query: crate::db::query::intent::StructuralQueryCacheKey,
cache_method_version: u8,
) -> Self {
Self {
cache_method_version,
entity_path: authority.entity_path(),
schema_fingerprint,
visibility,
structural_query,
}
}
#[cfg(test)]
fn for_authority_with_method_version(
authority: EntityAuthority,
schema_fingerprint: CommitSchemaFingerprint,
visibility: QueryPlanVisibility,
query: &StructuralQuery,
cache_method_version: u8,
) -> Self {
Self::from_authority_parts(
authority,
schema_fingerprint,
visibility,
query.structural_cache_key(),
cache_method_version,
)
}
fn for_authority_with_normalized_predicate_fingerprint_and_method_version(
authority: EntityAuthority,
schema_fingerprint: CommitSchemaFingerprint,
visibility: QueryPlanVisibility,
query: &StructuralQuery,
normalized_predicate_fingerprint: Option<[u8; 32]>,
cache_method_version: u8,
) -> Self {
Self::from_authority_parts(
authority,
schema_fingerprint,
visibility,
query.structural_cache_key_with_normalized_predicate_fingerprint(
normalized_predicate_fingerprint,
),
cache_method_version,
)
}
}