icydb-core 0.259.3

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: db::session::sql::cache
//! Responsibility: SQL compiled-command cache identity and storage.
//! Does not own: SQL parsing, lowering, execution, or result shaping.
//! Boundary: keeps syntax-bound SQL cache state separate from shared query-plan cache state.

use crate::{
    db::{
        DbSession,
        schema::{AcceptedSchemaRevision, AcceptedSchemaRuntimeRootIdentity, SchemaVersion},
        session::{
            AcceptedSchemaCatalogContext,
            bounded_cache::BoundedCache,
            sql::compiled::{CompiledSqlCommand, SqlCompiledSchemaFingerprint},
        },
    },
    traits::CanisterKind,
};
use std::{cell::RefCell, collections::HashMap, rc::Rc};

// This cache deliberately stays on syntax-bound SQL statement identity for the
// front-end prepared/template lane. Grouped semantic canonicalization and
// grouped structural/cache identity do not flow into this key.
const SQL_COMPILED_COMMAND_CACHE_MAX_ENTRIES: usize = 1024;

///
/// SqlCompiledCommandSurface
///
/// SqlCompiledCommandSurface separates SQL query and mutation API cache lanes so
/// identical text cannot alias across public session surfaces with different
/// admissible statement families.
///

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(in crate::db::session::sql) enum SqlCompiledCommandSurface {
    Query,
    Mutation,
}

///
/// SqlCompiledCommandCacheKey
///
/// SqlCompiledCommandCacheKey pins one compiled SQL artifact to the exact
/// session-local semantic boundary that produced it.
/// The key is intentionally conservative: surface kind, entity path, schema
/// runtime-root identity, entity schema revision/version, schema fingerprint,
/// and raw SQL text must all match before execution can reuse a prior compile.
///

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(in crate::db) struct SqlCompiledCommandCacheKey {
    surface: SqlCompiledCommandSurface,
    entity_path: Rc<str>,
    accepted_runtime_root_identity: AcceptedSchemaRuntimeRootIdentity,
    accepted_schema_revision: AcceptedSchemaRevision,
    schema_version: SchemaVersion,
    schema_fingerprint: SqlCompiledSchemaFingerprint,
    sql: String,
}

pub(in crate::db) type SqlCompiledCommandCache =
    BoundedCache<SqlCompiledCommandCacheKey, CompiledSqlCommand>;

///
/// SqlCompiledCommandCacheContext
///
/// SqlCompiledCommandCacheContext carries the accepted-schema facts needed by
/// one SQL compile lookup. The cache key uses the accepted schema fingerprint;
/// miss compilation uses the paired `EntityAuthority` and `SchemaInfo` so
/// read-side predicate canonicalization observes the same live schema authority.
///

#[derive(Debug)]
pub(in crate::db::session::sql) struct SqlCompiledCommandCacheContext {
    key: SqlCompiledCommandCacheKey,
    catalog: AcceptedSchemaCatalogContext,
}

impl SqlCompiledCommandCacheContext {
    #[must_use]
    pub(in crate::db::session::sql) fn from_catalog(
        surface: SqlCompiledCommandSurface,
        sql: &str,
        catalog: AcceptedSchemaCatalogContext,
    ) -> Self {
        Self {
            key: SqlCompiledCommandCacheKey::new(
                surface,
                catalog.identity().entity_path(),
                catalog.runtime_root_identity(),
                catalog.revision(),
                catalog.schema_version(),
                SqlCompiledSchemaFingerprint::from_catalog(&catalog),
                sql,
            ),
            catalog,
        }
    }

    #[must_use]
    pub(in crate::db::session::sql) fn into_cache_inputs(
        self,
    ) -> (SqlCompiledCommandCacheKey, AcceptedSchemaCatalogContext) {
        (self.key, self.catalog)
    }
}

thread_local! {
    // Keep SQL-facing caches in canister-lifetime heap state keyed by the
    // store registry identity so state-changing canister calls can warm
    // query-facing SQL reuse without leaking entries across unrelated
    // registries in tests.
    static SQL_COMPILED_COMMAND_CACHES: RefCell<HashMap<usize, SqlCompiledCommandCache>> =
        RefCell::new(HashMap::default());
}

impl SqlCompiledCommandCacheKey {
    fn new(
        surface: SqlCompiledCommandSurface,
        entity_path: impl Into<Rc<str>>,
        accepted_runtime_root_identity: AcceptedSchemaRuntimeRootIdentity,
        accepted_schema_revision: AcceptedSchemaRevision,
        schema_version: SchemaVersion,
        schema_fingerprint: SqlCompiledSchemaFingerprint,
        sql: &str,
    ) -> Self {
        Self {
            surface,
            entity_path: entity_path.into(),
            accepted_runtime_root_identity,
            accepted_schema_revision,
            schema_version,
            schema_fingerprint,
            sql: sql.to_string(),
        }
    }
}

impl<C: CanisterKind> DbSession<C> {
    #[cfg(test)]
    pub(in crate::db::session) fn sql_compiled_cache_len_for_tests(&self) -> usize {
        self.with_sql_compiled_command_cache(|cache| cache.len())
    }

    pub(in crate::db::session::sql) fn with_sql_compiled_command_cache<R>(
        &self,
        f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
    ) -> R {
        let scope_id = self.db.cache_scope_id();

        SQL_COMPILED_COMMAND_CACHES.with(|caches| {
            let mut caches = caches.borrow_mut();
            let cache = caches.entry(scope_id).or_insert_with(|| {
                SqlCompiledCommandCache::new(SQL_COMPILED_COMMAND_CACHE_MAX_ENTRIES)
            });

            f(cache)
        })
    }
}