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 query;
7mod response;
8#[cfg(feature = "sql")]
9mod sql;
10///
11/// TESTS
12///
13#[cfg(all(test, feature = "sql"))]
14mod tests;
15mod write;
16
17use crate::{
18    db::{
19        Db, EntityFieldDescription, EntityRuntimeHooks, EntitySchemaDescription, FluentDeleteQuery,
20        FluentLoadQuery, IndexState, IntegrityReport, MigrationPlan, MigrationRegistry,
21        MigrationRunOutcome, MissingRowPolicy, PersistedRow, Query, QueryError,
22        SchemaMigrationDescriptor, SchemaMigrationExecutionOutcome, SchemaMigrationPlanner,
23        StorageReport, StoreRegistry, WriteBatchResponse,
24        executor::{DeleteExecutor, LoadExecutor, SaveExecutor},
25        query::plan::VisibleIndexes,
26        schema::{
27            describe_entity_fields, describe_entity_model, show_indexes_for_model,
28            show_indexes_for_model_with_runtime_state,
29        },
30    },
31    error::InternalError,
32    metrics::sink::{MetricsSink, with_metrics_sink},
33    model::entity::EntityModel,
34    traits::{CanisterKind, EntityKind, EntityValue, Path},
35    value::Value,
36};
37use std::thread::LocalKey;
38
39#[cfg(feature = "diagnostics")]
40pub use query::QueryExecutionAttribution;
41pub(in crate::db) use response::{
42    finalize_grouped_paged_execution, finalize_scalar_paged_execution,
43    sql_grouped_cursor_from_bytes,
44};
45#[cfg(all(feature = "sql", feature = "diagnostics"))]
46pub use sql::SqlQueryExecutionAttribution;
47#[cfg(feature = "sql")]
48pub use sql::SqlStatementResult;
49#[cfg(all(feature = "sql", feature = "diagnostics"))]
50pub use sql::{SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics};
51
52///
53/// DbSession
54///
55/// Session-scoped database handle with policy (debug, metrics) and execution routing.
56///
57
58pub struct DbSession<C: CanisterKind> {
59    db: Db<C>,
60    debug: bool,
61    metrics: Option<&'static dyn MetricsSink>,
62}
63
64impl<C: CanisterKind> DbSession<C> {
65    /// Construct one session facade for a database handle.
66    #[must_use]
67    pub(crate) const fn new(db: Db<C>) -> Self {
68        Self {
69            db,
70            debug: false,
71            metrics: None,
72        }
73    }
74
75    /// Construct one session facade from store registry and runtime hooks.
76    #[must_use]
77    pub const fn new_with_hooks(
78        store: &'static LocalKey<StoreRegistry>,
79        entity_runtime_hooks: &'static [EntityRuntimeHooks<C>],
80    ) -> Self {
81        Self::new(Db::new_with_hooks(store, entity_runtime_hooks))
82    }
83
84    /// Enable debug execution behavior where supported by executors.
85    #[must_use]
86    pub const fn debug(mut self) -> Self {
87        self.debug = true;
88        self
89    }
90
91    /// Attach one metrics sink for all session-executed operations.
92    #[must_use]
93    pub const fn metrics_sink(mut self, sink: &'static dyn MetricsSink) -> Self {
94        self.metrics = Some(sink);
95        self
96    }
97
98    // Shared fluent load wrapper construction keeps the session boundary in
99    // one place when load entry points differ only by missing-row policy.
100    const fn fluent_load_query<E>(&self, consistency: MissingRowPolicy) -> FluentLoadQuery<'_, E>
101    where
102        E: EntityKind<Canister = C>,
103    {
104        FluentLoadQuery::new(self, Query::new(consistency))
105    }
106
107    // Shared fluent delete wrapper construction keeps the delete-mode handoff
108    // explicit at the session boundary instead of reassembling the same query
109    // shell in each public entry point.
110    fn fluent_delete_query<E>(&self, consistency: MissingRowPolicy) -> FluentDeleteQuery<'_, E>
111    where
112        E: PersistedRow<Canister = C>,
113    {
114        FluentDeleteQuery::new(self, Query::new(consistency).delete())
115    }
116
117    fn with_metrics<T>(&self, f: impl FnOnce() -> T) -> T {
118        if let Some(sink) = self.metrics {
119            with_metrics_sink(sink, f)
120        } else {
121            f()
122        }
123    }
124
125    // Shared save-facade wrapper keeps metrics wiring and response shaping uniform.
126    fn execute_save_with<E, T, R>(
127        &self,
128        op: impl FnOnce(SaveExecutor<E>) -> Result<T, InternalError>,
129        map: impl FnOnce(T) -> R,
130    ) -> Result<R, InternalError>
131    where
132        E: PersistedRow<Canister = C> + EntityValue,
133    {
134        let value = self.with_metrics(|| op(self.save_executor::<E>()))?;
135
136        Ok(map(value))
137    }
138
139    // Shared save-facade wrappers keep response shape explicit at call sites.
140    fn execute_save_entity<E>(
141        &self,
142        op: impl FnOnce(SaveExecutor<E>) -> Result<E, InternalError>,
143    ) -> Result<E, InternalError>
144    where
145        E: PersistedRow<Canister = C> + EntityValue,
146    {
147        self.execute_save_with(op, std::convert::identity)
148    }
149
150    fn execute_save_batch<E>(
151        &self,
152        op: impl FnOnce(SaveExecutor<E>) -> Result<Vec<E>, InternalError>,
153    ) -> Result<WriteBatchResponse<E>, InternalError>
154    where
155        E: PersistedRow<Canister = C> + EntityValue,
156    {
157        self.execute_save_with(op, WriteBatchResponse::new)
158    }
159
160    // ---------------------------------------------------------------------
161    // Query entry points (public, fluent)
162    // ---------------------------------------------------------------------
163
164    /// Start a fluent load query with default missing-row policy (`Ignore`).
165    #[must_use]
166    pub const fn load<E>(&self) -> FluentLoadQuery<'_, E>
167    where
168        E: EntityKind<Canister = C>,
169    {
170        self.fluent_load_query(MissingRowPolicy::Ignore)
171    }
172
173    /// Start a fluent load query with explicit missing-row policy.
174    #[must_use]
175    pub const fn load_with_consistency<E>(
176        &self,
177        consistency: MissingRowPolicy,
178    ) -> FluentLoadQuery<'_, E>
179    where
180        E: EntityKind<Canister = C>,
181    {
182        self.fluent_load_query(consistency)
183    }
184
185    /// Start a fluent delete query with default missing-row policy (`Ignore`).
186    #[must_use]
187    pub fn delete<E>(&self) -> FluentDeleteQuery<'_, E>
188    where
189        E: PersistedRow<Canister = C>,
190    {
191        self.fluent_delete_query(MissingRowPolicy::Ignore)
192    }
193
194    /// Start a fluent delete query with explicit missing-row policy.
195    #[must_use]
196    pub fn delete_with_consistency<E>(
197        &self,
198        consistency: MissingRowPolicy,
199    ) -> FluentDeleteQuery<'_, E>
200    where
201        E: PersistedRow<Canister = C>,
202    {
203        self.fluent_delete_query(consistency)
204    }
205
206    /// Return one constant scalar row equivalent to SQL `SELECT 1`.
207    ///
208    /// This terminal bypasses query planning and access routing entirely.
209    #[must_use]
210    pub const fn select_one(&self) -> Value {
211        Value::Int(1)
212    }
213
214    /// Return one stable, human-readable index listing for the entity schema.
215    ///
216    /// Output format mirrors SQL-style introspection:
217    /// - `PRIMARY KEY (field)`
218    /// - `INDEX name (field_a, field_b)`
219    /// - `UNIQUE INDEX name (field_a, field_b)`
220    #[must_use]
221    pub fn show_indexes<E>(&self) -> Vec<String>
222    where
223        E: EntityKind<Canister = C>,
224    {
225        self.show_indexes_for_store_model(E::Store::PATH, E::MODEL)
226    }
227
228    /// Return one stable, human-readable index listing for one schema model.
229    ///
230    /// This model-only helper is schema-owned and intentionally does not
231    /// attach runtime lifecycle state because it does not carry store
232    /// placement context.
233    #[must_use]
234    pub fn show_indexes_for_model(&self, model: &'static EntityModel) -> Vec<String> {
235        show_indexes_for_model(model)
236    }
237
238    // Return one stable, human-readable index listing for one resolved
239    // store/model pair, attaching the current runtime lifecycle state when the
240    // registry can resolve the backing store handle.
241    pub(in crate::db) fn show_indexes_for_store_model(
242        &self,
243        store_path: &str,
244        model: &'static EntityModel,
245    ) -> Vec<String> {
246        let runtime_state = self
247            .db
248            .with_store_registry(|registry| registry.try_get_store(store_path).ok())
249            .map(|store| store.index_state());
250
251        show_indexes_for_model_with_runtime_state(model, runtime_state)
252    }
253
254    /// Return one stable list of field descriptors for the entity schema.
255    #[must_use]
256    pub fn show_columns<E>(&self) -> Vec<EntityFieldDescription>
257    where
258        E: EntityKind<Canister = C>,
259    {
260        self.show_columns_for_model(E::MODEL)
261    }
262
263    /// Return one stable list of field descriptors for one schema model.
264    #[must_use]
265    pub fn show_columns_for_model(
266        &self,
267        model: &'static EntityModel,
268    ) -> Vec<EntityFieldDescription> {
269        describe_entity_fields(model)
270    }
271
272    /// Return one stable list of runtime-registered entity names.
273    #[must_use]
274    pub fn show_entities(&self) -> Vec<String> {
275        self.db.runtime_entity_names()
276    }
277
278    /// Return one stable list of runtime-registered entity names.
279    ///
280    /// `SHOW TABLES` is only an alias for `SHOW ENTITIES`, so the typed
281    /// metadata surface keeps the same alias relationship.
282    #[must_use]
283    pub fn show_tables(&self) -> Vec<String> {
284        self.show_entities()
285    }
286
287    // Resolve the exact secondary-index set that is visible to planner-owned
288    // query planning for one recovered store/model pair.
289    fn visible_indexes_for_store_model(
290        &self,
291        store_path: &str,
292        model: &'static EntityModel,
293    ) -> Result<VisibleIndexes<'static>, QueryError> {
294        // Phase 1: resolve the recovered store state once at the session
295        // boundary so query/executor planning does not reopen lifecycle checks.
296        let store = self
297            .db
298            .recovered_store(store_path)
299            .map_err(QueryError::execute)?;
300        let state = store.index_state();
301        if state != IndexState::Ready {
302            return Ok(VisibleIndexes::none());
303        }
304        debug_assert_eq!(state, IndexState::Ready);
305
306        // Phase 2: planner-visible indexes are exactly the model-owned index
307        // declarations once the recovered store is query-visible.
308        Ok(VisibleIndexes::planner_visible(model.indexes()))
309    }
310
311    /// Return one structured schema description for the entity.
312    ///
313    /// This is a typed `DESCRIBE`-style introspection surface consumed by
314    /// developer tooling and pre-EXPLAIN debugging.
315    #[must_use]
316    pub fn describe_entity<E>(&self) -> EntitySchemaDescription
317    where
318        E: EntityKind<Canister = C>,
319    {
320        self.describe_entity_model(E::MODEL)
321    }
322
323    /// Return one structured schema description for one schema model.
324    #[must_use]
325    pub fn describe_entity_model(&self, model: &'static EntityModel) -> EntitySchemaDescription {
326        describe_entity_model(model)
327    }
328
329    /// Build one point-in-time storage report for observability endpoints.
330    pub fn storage_report(
331        &self,
332        name_to_path: &[(&'static str, &'static str)],
333    ) -> Result<StorageReport, InternalError> {
334        self.db.storage_report(name_to_path)
335    }
336
337    /// Build one point-in-time storage report using default entity-path labels.
338    pub fn storage_report_default(&self) -> Result<StorageReport, InternalError> {
339        self.db.storage_report_default()
340    }
341
342    /// Build one point-in-time integrity scan report for observability endpoints.
343    pub fn integrity_report(&self) -> Result<IntegrityReport, InternalError> {
344        self.db.integrity_report()
345    }
346
347    /// Execute one bounded migration run with durable internal cursor state.
348    ///
349    /// Migration progress is persisted internally so upgrades/restarts can
350    /// resume from the last successful step without external cursor ownership.
351    pub fn execute_migration_plan(
352        &self,
353        plan: &MigrationPlan,
354        max_steps: usize,
355    ) -> Result<MigrationRunOutcome, InternalError> {
356        self.with_metrics(|| self.db.execute_migration_plan(plan, max_steps))
357    }
358
359    /// Execute one schema-evolution descriptor through the migration derivation layer.
360    ///
361    /// The schema-evolution layer validates descriptor/model compatibility and
362    /// prevents already-applied migrations from reaching the lower row-op
363    /// migration engine.
364    pub fn execute_schema_migration_descriptor(
365        &self,
366        registry: &mut MigrationRegistry,
367        planner: &SchemaMigrationPlanner,
368        descriptor: &SchemaMigrationDescriptor,
369        max_steps: usize,
370    ) -> Result<SchemaMigrationExecutionOutcome, InternalError> {
371        self.with_metrics(|| {
372            self.db
373                .execute_schema_migration_descriptor(registry, planner, descriptor, max_steps)
374        })
375    }
376
377    // ---------------------------------------------------------------------
378    // Low-level executors (crate-internal; execution primitives)
379    // ---------------------------------------------------------------------
380
381    #[must_use]
382    pub(in crate::db) const fn load_executor<E>(&self) -> LoadExecutor<E>
383    where
384        E: EntityKind<Canister = C> + EntityValue,
385    {
386        LoadExecutor::new(self.db, self.debug)
387    }
388
389    #[must_use]
390    pub(in crate::db) const fn delete_executor<E>(&self) -> DeleteExecutor<E>
391    where
392        E: PersistedRow<Canister = C> + EntityValue,
393    {
394        DeleteExecutor::new(self.db)
395    }
396
397    #[must_use]
398    pub(in crate::db) const fn save_executor<E>(&self) -> SaveExecutor<E>
399    where
400        E: PersistedRow<Canister = C> + EntityValue,
401    {
402        SaveExecutor::new(self.db, self.debug)
403    }
404}