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 sql;
8///
9/// TESTS
10///
11#[cfg(test)]
12mod tests;
13mod write;
14
15use crate::{
16    db::{
17        Db, EntitySchemaDescription, FluentDeleteQuery, FluentLoadQuery, IntegrityReport,
18        MigrationPlan, MigrationRunOutcome, MissingRowPolicy, PlanError, Query, QueryError,
19        StorageReport, StoreRegistry, WriteBatchResponse,
20        commit::EntityRuntimeHooks,
21        cursor::decode_optional_cursor_token,
22        executor::{DeleteExecutor, ExecutorPlanError, LoadExecutor, SaveExecutor},
23        schema::{describe_entity_model, show_indexes_for_model},
24    },
25    error::InternalError,
26    metrics::sink::{MetricsSink, with_metrics_sink},
27    traits::{CanisterKind, EntityKind, EntityValue},
28    value::Value,
29};
30use std::thread::LocalKey;
31
32pub use sql::SqlStatementRoute;
33
34// Map executor-owned plan-surface failures into query-owned plan errors.
35fn map_executor_plan_error(err: ExecutorPlanError) -> QueryError {
36    match err {
37        ExecutorPlanError::Cursor(err) => QueryError::from(PlanError::from(*err)),
38    }
39}
40
41// Decode one optional external cursor token and map decode failures into the
42// query-plan cursor error boundary.
43fn decode_optional_cursor_bytes(cursor_token: Option<&str>) -> Result<Option<Vec<u8>>, QueryError> {
44    decode_optional_cursor_token(cursor_token).map_err(|err| QueryError::from(PlanError::from(err)))
45}
46
47///
48/// DbSession
49///
50/// Session-scoped database handle with policy (debug, metrics) and execution routing.
51///
52
53pub struct DbSession<C: CanisterKind> {
54    db: Db<C>,
55    debug: bool,
56    metrics: Option<&'static dyn MetricsSink>,
57}
58
59impl<C: CanisterKind> DbSession<C> {
60    /// Construct one session facade for a database handle.
61    #[must_use]
62    pub(crate) const fn new(db: Db<C>) -> Self {
63        Self {
64            db,
65            debug: false,
66            metrics: None,
67        }
68    }
69
70    /// Construct one session facade from store registry and runtime hooks.
71    #[must_use]
72    pub const fn new_with_hooks(
73        store: &'static LocalKey<StoreRegistry>,
74        entity_runtime_hooks: &'static [EntityRuntimeHooks<C>],
75    ) -> Self {
76        Self::new(Db::new_with_hooks(store, entity_runtime_hooks))
77    }
78
79    /// Enable debug execution behavior where supported by executors.
80    #[must_use]
81    pub const fn debug(mut self) -> Self {
82        self.debug = true;
83        self
84    }
85
86    /// Attach one metrics sink for all session-executed operations.
87    #[must_use]
88    pub const fn metrics_sink(mut self, sink: &'static dyn MetricsSink) -> Self {
89        self.metrics = Some(sink);
90        self
91    }
92
93    fn with_metrics<T>(&self, f: impl FnOnce() -> T) -> T {
94        if let Some(sink) = self.metrics {
95            with_metrics_sink(sink, f)
96        } else {
97            f()
98        }
99    }
100
101    // Shared save-facade wrapper keeps metrics wiring and response shaping uniform.
102    fn execute_save_with<E, T, R>(
103        &self,
104        op: impl FnOnce(SaveExecutor<E>) -> Result<T, InternalError>,
105        map: impl FnOnce(T) -> R,
106    ) -> Result<R, InternalError>
107    where
108        E: EntityKind<Canister = C> + EntityValue,
109    {
110        let value = self.with_metrics(|| op(self.save_executor::<E>()))?;
111
112        Ok(map(value))
113    }
114
115    // Shared save-facade wrappers keep response shape explicit at call sites.
116    fn execute_save_entity<E>(
117        &self,
118        op: impl FnOnce(SaveExecutor<E>) -> Result<E, InternalError>,
119    ) -> Result<E, InternalError>
120    where
121        E: EntityKind<Canister = C> + EntityValue,
122    {
123        self.execute_save_with(op, std::convert::identity)
124    }
125
126    fn execute_save_batch<E>(
127        &self,
128        op: impl FnOnce(SaveExecutor<E>) -> Result<Vec<E>, InternalError>,
129    ) -> Result<WriteBatchResponse<E>, InternalError>
130    where
131        E: EntityKind<Canister = C> + EntityValue,
132    {
133        self.execute_save_with(op, WriteBatchResponse::new)
134    }
135
136    fn execute_save_view<E>(
137        &self,
138        op: impl FnOnce(SaveExecutor<E>) -> Result<E::ViewType, InternalError>,
139    ) -> Result<E::ViewType, InternalError>
140    where
141        E: EntityKind<Canister = C> + EntityValue,
142    {
143        self.execute_save_with(op, std::convert::identity)
144    }
145
146    // ---------------------------------------------------------------------
147    // Query entry points (public, fluent)
148    // ---------------------------------------------------------------------
149
150    /// Start a fluent load query with default missing-row policy (`Ignore`).
151    #[must_use]
152    pub const fn load<E>(&self) -> FluentLoadQuery<'_, E>
153    where
154        E: EntityKind<Canister = C>,
155    {
156        FluentLoadQuery::new(self, Query::new(MissingRowPolicy::Ignore))
157    }
158
159    /// Start a fluent load query with explicit missing-row policy.
160    #[must_use]
161    pub const fn load_with_consistency<E>(
162        &self,
163        consistency: MissingRowPolicy,
164    ) -> FluentLoadQuery<'_, E>
165    where
166        E: EntityKind<Canister = C>,
167    {
168        FluentLoadQuery::new(self, Query::new(consistency))
169    }
170
171    /// Start a fluent delete query with default missing-row policy (`Ignore`).
172    #[must_use]
173    pub fn delete<E>(&self) -> FluentDeleteQuery<'_, E>
174    where
175        E: EntityKind<Canister = C>,
176    {
177        FluentDeleteQuery::new(self, Query::new(MissingRowPolicy::Ignore).delete())
178    }
179
180    /// Start a fluent delete query with explicit missing-row policy.
181    #[must_use]
182    pub fn delete_with_consistency<E>(
183        &self,
184        consistency: MissingRowPolicy,
185    ) -> FluentDeleteQuery<'_, E>
186    where
187        E: EntityKind<Canister = C>,
188    {
189        FluentDeleteQuery::new(self, Query::new(consistency).delete())
190    }
191
192    /// Return one constant scalar row equivalent to SQL `SELECT 1`.
193    ///
194    /// This terminal bypasses query planning and access routing entirely.
195    #[must_use]
196    pub const fn select_one(&self) -> Value {
197        Value::Int(1)
198    }
199
200    /// Return one stable, human-readable index listing for the entity schema.
201    ///
202    /// Output format mirrors SQL-style introspection:
203    /// - `PRIMARY KEY (field)`
204    /// - `INDEX name (field_a, field_b)`
205    /// - `UNIQUE INDEX name (field_a, field_b)`
206    #[must_use]
207    pub fn show_indexes<E>(&self) -> Vec<String>
208    where
209        E: EntityKind<Canister = C>,
210    {
211        show_indexes_for_model(E::MODEL)
212    }
213
214    /// Return one structured schema description for the entity.
215    ///
216    /// This is a typed `DESCRIBE`-style introspection surface consumed by
217    /// developer tooling and pre-EXPLAIN debugging.
218    #[must_use]
219    pub fn describe_entity<E>(&self) -> EntitySchemaDescription
220    where
221        E: EntityKind<Canister = C>,
222    {
223        describe_entity_model(E::MODEL)
224    }
225
226    /// Build one point-in-time storage report for observability endpoints.
227    pub fn storage_report(
228        &self,
229        name_to_path: &[(&'static str, &'static str)],
230    ) -> Result<StorageReport, InternalError> {
231        self.db.storage_report(name_to_path)
232    }
233
234    /// Build one point-in-time integrity scan report for observability endpoints.
235    pub fn integrity_report(&self) -> Result<IntegrityReport, InternalError> {
236        self.db.integrity_report()
237    }
238
239    /// Execute one bounded migration run with durable internal cursor state.
240    ///
241    /// Migration progress is persisted internally so upgrades/restarts can
242    /// resume from the last successful step without external cursor ownership.
243    pub fn execute_migration_plan(
244        &self,
245        plan: &MigrationPlan,
246        max_steps: usize,
247    ) -> Result<MigrationRunOutcome, InternalError> {
248        self.with_metrics(|| self.db.execute_migration_plan(plan, max_steps))
249    }
250
251    // ---------------------------------------------------------------------
252    // Low-level executors (crate-internal; execution primitives)
253    // ---------------------------------------------------------------------
254
255    #[must_use]
256    pub(in crate::db) const fn load_executor<E>(&self) -> LoadExecutor<E>
257    where
258        E: EntityKind<Canister = C> + EntityValue,
259    {
260        LoadExecutor::new(self.db, self.debug)
261    }
262
263    #[must_use]
264    pub(in crate::db) const fn delete_executor<E>(&self) -> DeleteExecutor<E>
265    where
266        E: EntityKind<Canister = C> + EntityValue,
267    {
268        DeleteExecutor::new(self.db, self.debug)
269    }
270
271    #[must_use]
272    pub(in crate::db) const fn save_executor<E>(&self) -> SaveExecutor<E>
273    where
274        E: EntityKind<Canister = C> + EntityValue,
275    {
276        SaveExecutor::new(self.db, self.debug)
277    }
278}