Skip to main content

icydb_core/db/session/
mod.rs

1//! Module: session
2//! Responsibility: user-facing query/write execution facade over db executors.
3//! Does not own: planning semantics, cursor validation rules, or storage mutation protocol.
4//! Boundary: converts fluent/query intent calls into executor operations and response DTOs.
5
6mod accepted_schema;
7mod bounded_cache;
8mod catalog;
9mod query;
10mod response;
11#[cfg(feature = "sql")]
12mod sql;
13///
14/// TESTS
15///
16#[cfg(all(test, feature = "sql"))]
17mod tests;
18mod write;
19
20use crate::{
21    db::{
22        Db, EntityRuntimeHooks, FluentDeleteQuery, FluentLoadQuery, MissingRowPolicy, PersistedRow,
23        Query, StoreRegistry, WriteBatchResponse,
24        commit::CommitSchemaFingerprint,
25        executor::{DeleteExecutor, LoadExecutor, SaveExecutor},
26        schema::{AcceptedRowDecodeContract, SchemaInfo},
27    },
28    error::InternalError,
29    metrics::sink::{ExecKind, MetricsSink, record_exec_error_for_path, with_metrics_sink},
30    traits::{CanisterKind, EntityKind, EntityValue},
31    value::Value,
32};
33use std::thread::LocalKey;
34
35#[cfg(all(test, feature = "sql"))]
36pub(in crate::db) use accepted_schema::AcceptedCatalogRuntimeCounterSnapshot;
37pub(in crate::db) use accepted_schema::AcceptedSchemaCatalogContext;
38#[cfg(feature = "diagnostics")]
39pub use query::{
40    DirectDataRowAttribution, FluentTerminalExecutionAttribution, GroupedCountAttribution,
41    GroupedExecutionAttribution, KernelRowAttribution, QueryExecutionAttribution,
42    ScalarAggregateAttribution,
43};
44pub(in crate::db) use response::finalize_scalar_paged_execution;
45pub(in crate::db) use response::finalize_structural_grouped_projection_result;
46#[cfg(feature = "sql")]
47pub(in crate::db) use response::sql_grouped_cursor_from_bytes;
48#[cfg(feature = "sql")]
49pub use sql::{
50    SqlAdminBulkDeletePlan, SqlAdminBulkUpdatePlan, SqlDdlExecutionStatus, SqlDdlMutationKind,
51    SqlDdlPreparationReport, SqlDeleteExposurePolicy, SqlDeletePolicyContext,
52    SqlDeletePolicyRejection, SqlDeletePolicyReport, SqlDeleteStatementClassification,
53    SqlPublicBoundedDeletePlan, SqlPublicBoundedUpdatePlan, SqlPublicPrimaryKeyDeletePlan,
54    SqlPublicPrimaryKeyUpdatePlan, SqlSessionCurrentDeletePlan, SqlSessionCurrentUpdatePlan,
55    SqlStatementDispatch, SqlStatementResult, SqlStatementShellSurface, SqlStatementSurface,
56    SqlUpdateAssignmentPolicy, SqlUpdateExposurePolicy, SqlUpdatePolicyContext,
57    SqlUpdatePolicyRejection, SqlUpdatePolicyReport, SqlUpdateStatementClassification,
58    SqlValidatedDeletePlan, SqlValidatedUpdatePlan, SqlWriteExecutionBounds, SqlWriteOrderProof,
59    SqlWriteReturningBounds, SqlWriteReturningShape, SqlWriteStatementShape, SqlWriteWhereProof,
60    classify_sql_delete_policy, classify_sql_update_policy, sql_statement_dispatch,
61    sql_statement_entity_name, sql_statement_shell_surface, sql_statement_surface,
62};
63#[cfg(all(feature = "sql", feature = "diagnostics"))]
64pub use sql::{
65    SqlCompileAttribution, SqlExecutionAttribution, SqlHybridCoveringAttribution,
66    SqlOutputBlobAttribution, SqlPureCoveringAttribution, SqlQueryCacheAttribution,
67    SqlQueryExecutionAttribution, SqlScalarAggregateAttribution,
68};
69#[cfg(all(feature = "sql", feature = "diagnostics"))]
70pub use sql::{SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics};
71
72///
73/// DbSession
74///
75/// Session-scoped database handle with policy (debug, metrics) and execution routing.
76///
77
78pub struct DbSession<C: CanisterKind> {
79    db: Db<C>,
80    debug: bool,
81    metrics: Option<&'static dyn MetricsSink>,
82}
83
84impl<C: CanisterKind> DbSession<C> {
85    /// Construct one session facade for a database handle.
86    #[must_use]
87    pub(crate) const fn new(db: Db<C>) -> Self {
88        Self {
89            db,
90            debug: false,
91            metrics: None,
92        }
93    }
94
95    /// Construct one session facade from store registry and runtime hooks.
96    #[must_use]
97    pub const fn new_with_hooks(
98        store: &'static LocalKey<StoreRegistry>,
99        entity_runtime_hooks: &'static [EntityRuntimeHooks<C>],
100    ) -> Self {
101        Self::new(Db::new_with_hooks(store, entity_runtime_hooks))
102    }
103
104    /// Enable debug execution behavior where supported by executors.
105    #[must_use]
106    pub const fn debug(mut self) -> Self {
107        self.debug = true;
108        self
109    }
110
111    /// Attach one metrics sink for all session-executed operations.
112    #[must_use]
113    pub const fn metrics_sink(mut self, sink: &'static dyn MetricsSink) -> Self {
114        self.metrics = Some(sink);
115        self
116    }
117
118    // Shared fluent load wrapper construction keeps the session boundary in
119    // one place when load entry points differ only by missing-row policy.
120    const fn fluent_load_query<E>(&self, consistency: MissingRowPolicy) -> FluentLoadQuery<'_, E>
121    where
122        E: EntityKind<Canister = C>,
123    {
124        FluentLoadQuery::new(self, Query::new(consistency))
125    }
126
127    // Shared fluent delete wrapper construction keeps the delete-mode handoff
128    // explicit at the session boundary instead of reassembling the same query
129    // shell in each public entry point.
130    fn fluent_delete_query<E>(&self, consistency: MissingRowPolicy) -> FluentDeleteQuery<'_, E>
131    where
132        E: PersistedRow<Canister = C>,
133    {
134        FluentDeleteQuery::new(self, Query::new(consistency).delete())
135    }
136
137    fn with_metrics<T>(&self, f: impl FnOnce() -> T) -> T {
138        if let Some(sink) = self.metrics {
139            with_metrics_sink(sink, f)
140        } else {
141            f()
142        }
143    }
144
145    // Shared save-facade wrapper keeps metrics wiring and response shaping uniform.
146    fn execute_save_with<E, T, R>(
147        &self,
148        op: impl FnOnce(SaveExecutor<E>) -> Result<T, InternalError>,
149        map: impl FnOnce(T) -> R,
150    ) -> Result<R, InternalError>
151    where
152        E: PersistedRow<Canister = C> + EntityValue,
153    {
154        let (contract, schema_info, schema_fingerprint) = match self
155            .with_metrics(|| self.ensure_generated_compatible_accepted_save_schema::<E>())
156        {
157            Ok(authority) => authority,
158            Err(error) => {
159                self.with_metrics(|| record_exec_error_for_path(ExecKind::Save, E::PATH, &error));
160
161                return Err(error);
162            }
163        };
164        let value = self.with_metrics(|| {
165            op(self.save_executor::<E>(contract, schema_info, schema_fingerprint))
166        })?;
167
168        Ok(map(value))
169    }
170
171    // Execute save work after the caller has already proven that the accepted
172    // row contract is generated-compatible. SQL and structural writes use this
173    // after their pre-staging schema guard so mutation staging and save
174    // execution do not rerun schema-store reconciliation in the same statement.
175    fn execute_save_with_checked_accepted_row_contract<E, T, R>(
176        &self,
177        accepted_row_decode_contract: AcceptedRowDecodeContract,
178        accepted_schema_info: SchemaInfo,
179        accepted_schema_fingerprint: CommitSchemaFingerprint,
180        op: impl FnOnce(SaveExecutor<E>) -> Result<T, InternalError>,
181        map: impl FnOnce(T) -> R,
182    ) -> Result<R, InternalError>
183    where
184        E: PersistedRow<Canister = C> + EntityValue,
185    {
186        let value = self.with_metrics(|| {
187            op(self.save_executor::<E>(
188                accepted_row_decode_contract,
189                accepted_schema_info,
190                accepted_schema_fingerprint,
191            ))
192        })?;
193
194        Ok(map(value))
195    }
196
197    // Shared save-facade wrappers keep response shape explicit at call sites.
198    fn execute_save_entity<E>(
199        &self,
200        op: impl FnOnce(SaveExecutor<E>) -> Result<E, InternalError>,
201    ) -> Result<E, InternalError>
202    where
203        E: PersistedRow<Canister = C> + EntityValue,
204    {
205        self.execute_save_with(op, std::convert::identity)
206    }
207
208    fn execute_save_batch<E>(
209        &self,
210        op: impl FnOnce(SaveExecutor<E>) -> Result<Vec<E>, InternalError>,
211    ) -> Result<WriteBatchResponse<E>, InternalError>
212    where
213        E: PersistedRow<Canister = C> + EntityValue,
214    {
215        self.execute_save_with(op, WriteBatchResponse::new)
216    }
217
218    // ---------------------------------------------------------------------
219    // Query entry points (public, fluent)
220    // ---------------------------------------------------------------------
221
222    /// Start a fluent load query with default missing-row policy (`Ignore`).
223    #[must_use]
224    pub const fn load<E>(&self) -> FluentLoadQuery<'_, E>
225    where
226        E: EntityKind<Canister = C>,
227    {
228        self.fluent_load_query(MissingRowPolicy::Ignore)
229    }
230
231    /// Start a fluent load query with explicit missing-row policy.
232    #[must_use]
233    pub const fn load_with_consistency<E>(
234        &self,
235        consistency: MissingRowPolicy,
236    ) -> FluentLoadQuery<'_, E>
237    where
238        E: EntityKind<Canister = C>,
239    {
240        self.fluent_load_query(consistency)
241    }
242
243    /// Start a fluent delete query with default missing-row policy (`Ignore`).
244    #[must_use]
245    pub fn delete<E>(&self) -> FluentDeleteQuery<'_, E>
246    where
247        E: PersistedRow<Canister = C>,
248    {
249        self.fluent_delete_query(MissingRowPolicy::Ignore)
250    }
251
252    /// Start a fluent delete query with explicit missing-row policy.
253    #[must_use]
254    pub fn delete_with_consistency<E>(
255        &self,
256        consistency: MissingRowPolicy,
257    ) -> FluentDeleteQuery<'_, E>
258    where
259        E: PersistedRow<Canister = C>,
260    {
261        self.fluent_delete_query(consistency)
262    }
263
264    /// Return one constant scalar row equivalent to SQL `SELECT 1`.
265    ///
266    /// This terminal bypasses query planning and access routing entirely.
267    #[must_use]
268    pub const fn select_one(&self) -> Value {
269        Value::Int64(1)
270    }
271
272    // ---------------------------------------------------------------------
273    // Low-level executors (crate-internal; execution primitives)
274    // ---------------------------------------------------------------------
275
276    #[must_use]
277    pub(in crate::db) const fn load_executor<E>(&self) -> LoadExecutor<E>
278    where
279        E: EntityKind<Canister = C> + EntityValue,
280    {
281        LoadExecutor::new(self.db, self.debug)
282    }
283
284    #[must_use]
285    pub(in crate::db) const fn delete_executor<E>(&self) -> DeleteExecutor<E>
286    where
287        E: PersistedRow<Canister = C> + EntityValue,
288    {
289        DeleteExecutor::new(self.db)
290    }
291
292    #[must_use]
293    pub(in crate::db) const fn save_executor<E>(
294        &self,
295        accepted_row_decode_contract: AcceptedRowDecodeContract,
296        accepted_schema_info: SchemaInfo,
297        accepted_schema_fingerprint: CommitSchemaFingerprint,
298    ) -> SaveExecutor<E>
299    where
300        E: PersistedRow<Canister = C> + EntityValue,
301    {
302        SaveExecutor::new_with_accepted_contract(
303            self.db,
304            self.debug,
305            accepted_row_decode_contract,
306            accepted_schema_info,
307            accepted_schema_fingerprint,
308        )
309    }
310}