Skip to main content

icydb_core/db/session/
query.rs

1//! Module: db::session::query
2//! Responsibility: session-bound query planning, explain, and cursor execution
3//! helpers that recover store visibility before delegating to query-owned logic.
4//! Does not own: query intent construction or executor runtime semantics.
5//! Boundary: resolves session visibility and cursor policy before handing work to the planner/executor.
6
7use crate::{
8    db::{
9        DbSession, EntityResponse, LoadQueryResult, PagedGroupedExecutionWithTrace,
10        PagedLoadExecutionWithTrace, PersistedRow, Query, QueryError, QueryTracePlan,
11        access::AccessStrategy,
12        cursor::{
13            CursorPlanError, decode_optional_cursor_token, decode_optional_grouped_cursor_token,
14        },
15        diagnostics::ExecutionTrace,
16        executor::{ExecutionFamily, GroupedCursorPage, LoadExecutor, PreparedExecutionPlan},
17        query::builder::{
18            PreparedFluentAggregateExplainStrategy, PreparedFluentProjectionStrategy,
19        },
20        query::explain::{
21            ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor, ExplainPlan,
22        },
23        query::intent::{CompiledQuery, PlannedQuery},
24        query::plan::QueryMode,
25    },
26    error::InternalError,
27    traits::{CanisterKind, EntityKind, EntityValue, Path},
28};
29
30impl<C: CanisterKind> DbSession<C> {
31    // Resolve the planner-visible index slice for one typed query exactly once
32    // at the session boundary before handing execution/planning off to query-owned logic.
33    fn with_query_visible_indexes<E, T>(
34        &self,
35        query: &Query<E>,
36        op: impl FnOnce(
37            &Query<E>,
38            &crate::db::query::plan::VisibleIndexes<'static>,
39        ) -> Result<T, QueryError>,
40    ) -> Result<T, QueryError>
41    where
42        E: EntityKind<Canister = C>,
43    {
44        let visible_indexes = self.visible_indexes_for_store_model(E::Store::PATH, E::MODEL)?;
45
46        op(query, &visible_indexes)
47    }
48
49    // Compile one typed query using only the indexes currently visible for the
50    // query's recovered store.
51    pub(in crate::db) fn compile_query_with_visible_indexes<E>(
52        &self,
53        query: &Query<E>,
54    ) -> Result<CompiledQuery<E>, QueryError>
55    where
56        E: EntityKind<Canister = C>,
57    {
58        self.with_query_visible_indexes(query, |query, visible_indexes| {
59            query.plan_with_visible_indexes(visible_indexes)
60        })
61    }
62
63    // Build one logical planned-query shell using only the indexes currently
64    // visible for the query's recovered store.
65    pub(in crate::db) fn planned_query_with_visible_indexes<E>(
66        &self,
67        query: &Query<E>,
68    ) -> Result<PlannedQuery<E>, QueryError>
69    where
70        E: EntityKind<Canister = C>,
71    {
72        self.with_query_visible_indexes(query, |query, visible_indexes| {
73            query.planned_with_visible_indexes(visible_indexes)
74        })
75    }
76
77    // Project one logical explain payload using only planner-visible indexes.
78    pub(in crate::db) fn explain_query_with_visible_indexes<E>(
79        &self,
80        query: &Query<E>,
81    ) -> Result<ExplainPlan, QueryError>
82    where
83        E: EntityKind<Canister = C>,
84    {
85        self.with_query_visible_indexes(query, |query, visible_indexes| {
86            query.explain_with_visible_indexes(visible_indexes)
87        })
88    }
89
90    // Hash one typed query plan using only the indexes currently visible for
91    // the query's recovered store.
92    pub(in crate::db) fn query_plan_hash_hex_with_visible_indexes<E>(
93        &self,
94        query: &Query<E>,
95    ) -> Result<String, QueryError>
96    where
97        E: EntityKind<Canister = C>,
98    {
99        self.with_query_visible_indexes(query, |query, visible_indexes| {
100            query.plan_hash_hex_with_visible_indexes(visible_indexes)
101        })
102    }
103
104    // Explain one load execution shape using only planner-visible
105    // indexes from the recovered store state.
106    pub(in crate::db) fn explain_query_execution_with_visible_indexes<E>(
107        &self,
108        query: &Query<E>,
109    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
110    where
111        E: EntityValue + EntityKind<Canister = C>,
112    {
113        self.with_query_visible_indexes(query, |query, visible_indexes| {
114            query.explain_execution_with_visible_indexes(visible_indexes)
115        })
116    }
117
118    // Render one load execution descriptor as deterministic text using
119    // only planner-visible indexes from the recovered store state.
120    pub(in crate::db) fn explain_query_execution_text_with_visible_indexes<E>(
121        &self,
122        query: &Query<E>,
123    ) -> Result<String, QueryError>
124    where
125        E: EntityValue + EntityKind<Canister = C>,
126    {
127        self.with_query_visible_indexes(query, |query, visible_indexes| {
128            query.explain_execution_text_with_visible_indexes(visible_indexes)
129        })
130    }
131
132    // Render one load execution descriptor as canonical JSON using
133    // only planner-visible indexes from the recovered store state.
134    pub(in crate::db) fn explain_query_execution_json_with_visible_indexes<E>(
135        &self,
136        query: &Query<E>,
137    ) -> Result<String, QueryError>
138    where
139        E: EntityValue + EntityKind<Canister = C>,
140    {
141        self.with_query_visible_indexes(query, |query, visible_indexes| {
142            query.explain_execution_json_with_visible_indexes(visible_indexes)
143        })
144    }
145
146    // Render one load execution descriptor plus route diagnostics using
147    // only planner-visible indexes from the recovered store state.
148    pub(in crate::db) fn explain_query_execution_verbose_with_visible_indexes<E>(
149        &self,
150        query: &Query<E>,
151    ) -> Result<String, QueryError>
152    where
153        E: EntityValue + EntityKind<Canister = C>,
154    {
155        self.with_query_visible_indexes(query, |query, visible_indexes| {
156            query.explain_execution_verbose_with_visible_indexes(visible_indexes)
157        })
158    }
159
160    // Explain one prepared fluent aggregate terminal using only
161    // planner-visible indexes from the recovered store state.
162    pub(in crate::db) fn explain_query_prepared_aggregate_terminal_with_visible_indexes<E, S>(
163        &self,
164        query: &Query<E>,
165        strategy: &S,
166    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
167    where
168        E: EntityValue + EntityKind<Canister = C>,
169        S: PreparedFluentAggregateExplainStrategy,
170    {
171        self.with_query_visible_indexes(query, |query, visible_indexes| {
172            query
173                .explain_prepared_aggregate_terminal_with_visible_indexes(visible_indexes, strategy)
174        })
175    }
176
177    // Explain one `bytes_by(field)` terminal using only planner-visible
178    // indexes from the recovered store state.
179    pub(in crate::db) fn explain_query_bytes_by_with_visible_indexes<E>(
180        &self,
181        query: &Query<E>,
182        target_field: &str,
183    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
184    where
185        E: EntityValue + EntityKind<Canister = C>,
186    {
187        self.with_query_visible_indexes(query, |query, visible_indexes| {
188            query.explain_bytes_by_with_visible_indexes(visible_indexes, target_field)
189        })
190    }
191
192    // Explain one prepared fluent projection terminal using only
193    // planner-visible indexes from the recovered store state.
194    pub(in crate::db) fn explain_query_prepared_projection_terminal_with_visible_indexes<E>(
195        &self,
196        query: &Query<E>,
197        strategy: &PreparedFluentProjectionStrategy,
198    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
199    where
200        E: EntityValue + EntityKind<Canister = C>,
201    {
202        self.with_query_visible_indexes(query, |query, visible_indexes| {
203            query.explain_prepared_projection_terminal_with_visible_indexes(
204                visible_indexes,
205                strategy,
206            )
207        })
208    }
209
210    // Validate that one execution strategy is admissible for scalar paged load
211    // execution and fail closed on grouped/primary-key-only routes.
212    fn ensure_scalar_paged_execution_family(family: ExecutionFamily) -> Result<(), QueryError> {
213        match family {
214            ExecutionFamily::PrimaryKey => Err(QueryError::invariant(
215                CursorPlanError::cursor_requires_explicit_or_grouped_ordering_message(),
216            )),
217            ExecutionFamily::Ordered => Ok(()),
218            ExecutionFamily::Grouped => Err(QueryError::invariant(
219                "grouped queries execute via execute(), not page().execute()",
220            )),
221        }
222    }
223
224    // Validate that one execution strategy is admissible for the grouped
225    // execution surface.
226    fn ensure_grouped_execution_family(family: ExecutionFamily) -> Result<(), QueryError> {
227        match family {
228            ExecutionFamily::Grouped => Ok(()),
229            ExecutionFamily::PrimaryKey | ExecutionFamily::Ordered => Err(QueryError::invariant(
230                "grouped execution requires grouped logical plans",
231            )),
232        }
233    }
234
235    /// Execute one scalar load/delete query and return materialized response rows.
236    pub fn execute_query<E>(&self, query: &Query<E>) -> Result<EntityResponse<E>, QueryError>
237    where
238        E: PersistedRow<Canister = C> + EntityValue,
239    {
240        // Phase 1: compile typed intent into one prepared execution-plan contract.
241        let mode = query.mode();
242        let plan = self
243            .compile_query_with_visible_indexes(query)?
244            .into_prepared_execution_plan();
245
246        // Phase 2: delegate execution to the shared compiled-plan entry path.
247        self.execute_query_dyn(mode, plan)
248    }
249
250    // Execute one typed query through the unified row/grouped result surface so
251    // higher layers do not need to branch on grouped shape themselves.
252    #[doc(hidden)]
253    pub fn execute_query_result<E>(
254        &self,
255        query: &Query<E>,
256    ) -> Result<LoadQueryResult<E>, QueryError>
257    where
258        E: PersistedRow<Canister = C> + EntityValue,
259    {
260        if query.has_grouping() {
261            return self
262                .execute_grouped(query, None)
263                .map(LoadQueryResult::Grouped);
264        }
265
266        self.execute_query(query).map(LoadQueryResult::Rows)
267    }
268
269    /// Execute one typed delete query and return only the affected-row count.
270    #[doc(hidden)]
271    pub fn execute_delete_count<E>(&self, query: &Query<E>) -> Result<u32, QueryError>
272    where
273        E: PersistedRow<Canister = C> + EntityValue,
274    {
275        // Phase 1: fail closed if the caller routes a non-delete query here.
276        if !query.mode().is_delete() {
277            return Err(QueryError::unsupported_query(
278                "delete count execution requires delete query mode",
279            ));
280        }
281
282        // Phase 2: compile typed delete intent into one prepared execution-plan contract.
283        let plan = self
284            .compile_query_with_visible_indexes(query)?
285            .into_prepared_execution_plan();
286
287        // Phase 3: execute the shared delete core while skipping response-row materialization.
288        self.with_metrics(|| self.delete_executor::<E>().execute_count(plan))
289            .map_err(QueryError::execute)
290    }
291
292    /// Execute one scalar query from one pre-built prepared execution contract.
293    ///
294    /// This is the shared compiled-plan entry boundary used by the typed
295    /// `execute_query(...)` surface and adjacent query execution facades.
296    pub(in crate::db) fn execute_query_dyn<E>(
297        &self,
298        mode: QueryMode,
299        plan: PreparedExecutionPlan<E>,
300    ) -> Result<EntityResponse<E>, QueryError>
301    where
302        E: PersistedRow<Canister = C> + EntityValue,
303    {
304        let result = match mode {
305            QueryMode::Load(_) => self.with_metrics(|| self.load_executor::<E>().execute(plan)),
306            QueryMode::Delete(_) => self.with_metrics(|| self.delete_executor::<E>().execute(plan)),
307        };
308
309        result.map_err(QueryError::execute)
310    }
311
312    // Shared load-query terminal wrapper: build plan, run under metrics, map
313    // execution errors into query-facing errors.
314    pub(in crate::db) fn execute_load_query_with<E, T>(
315        &self,
316        query: &Query<E>,
317        op: impl FnOnce(LoadExecutor<E>, PreparedExecutionPlan<E>) -> Result<T, InternalError>,
318    ) -> Result<T, QueryError>
319    where
320        E: PersistedRow<Canister = C> + EntityValue,
321    {
322        let plan = self
323            .compile_query_with_visible_indexes(query)?
324            .into_prepared_execution_plan();
325
326        self.with_metrics(|| op(self.load_executor::<E>(), plan))
327            .map_err(QueryError::execute)
328    }
329
330    /// Build one trace payload for a query without executing it.
331    ///
332    /// This lightweight surface is intended for developer diagnostics:
333    /// plan hash, access strategy summary, and planner/executor route shape.
334    pub fn trace_query<E>(&self, query: &Query<E>) -> Result<QueryTracePlan, QueryError>
335    where
336        E: EntityKind<Canister = C>,
337    {
338        let compiled = self.compile_query_with_visible_indexes(query)?;
339        let explain = compiled.explain();
340        let plan_hash = compiled.plan_hash_hex();
341
342        let executable = compiled.into_prepared_execution_plan();
343        let access_strategy = AccessStrategy::from_plan(executable.access()).debug_summary();
344        let execution_family = match query.mode() {
345            QueryMode::Load(_) => Some(executable.execution_family().map_err(QueryError::execute)?),
346            QueryMode::Delete(_) => None,
347        };
348
349        Ok(QueryTracePlan::new(
350            plan_hash,
351            access_strategy,
352            execution_family,
353            explain,
354        ))
355    }
356
357    /// Execute one scalar paged load query and return optional continuation cursor plus trace.
358    pub(crate) fn execute_load_query_paged_with_trace<E>(
359        &self,
360        query: &Query<E>,
361        cursor_token: Option<&str>,
362    ) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
363    where
364        E: PersistedRow<Canister = C> + EntityValue,
365    {
366        // Phase 1: build/validate prepared execution plan and reject grouped plans.
367        let plan = self
368            .compile_query_with_visible_indexes(query)?
369            .into_prepared_execution_plan();
370        Self::ensure_scalar_paged_execution_family(
371            plan.execution_family().map_err(QueryError::execute)?,
372        )?;
373
374        // Phase 2: decode external cursor token and validate it against plan surface.
375        let cursor_bytes = decode_optional_cursor_token(cursor_token)
376            .map_err(QueryError::from_cursor_plan_error)?;
377        let cursor = plan
378            .prepare_cursor(cursor_bytes.as_deref())
379            .map_err(QueryError::from_executor_plan_error)?;
380
381        // Phase 3: execute one traced page and encode outbound continuation token.
382        let (page, trace) = self
383            .with_metrics(|| {
384                self.load_executor::<E>()
385                    .execute_paged_with_cursor_traced(plan, cursor)
386            })
387            .map_err(QueryError::execute)?;
388        let next_cursor = page
389            .next_cursor
390            .map(|token| {
391                let Some(token) = token.as_scalar() else {
392                    return Err(QueryError::scalar_paged_emitted_grouped_continuation());
393                };
394
395                token.encode().map_err(|err| {
396                    QueryError::serialize_internal(format!(
397                        "failed to serialize continuation cursor: {err}"
398                    ))
399                })
400            })
401            .transpose()?;
402
403        Ok(PagedLoadExecutionWithTrace::new(
404            page.items,
405            next_cursor,
406            trace,
407        ))
408    }
409
410    /// Execute one grouped query page with optional grouped continuation cursor.
411    ///
412    /// This is the explicit grouped execution boundary; scalar load APIs reject
413    /// grouped plans to preserve scalar response contracts.
414    pub(in crate::db) fn execute_grouped<E>(
415        &self,
416        query: &Query<E>,
417        cursor_token: Option<&str>,
418    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
419    where
420        E: PersistedRow<Canister = C> + EntityValue,
421    {
422        let (page, trace) = self.execute_grouped_page_with_trace(query, cursor_token)?;
423        let next_cursor = page
424            .next_cursor
425            .map(|token| {
426                let Some(token) = token.as_grouped() else {
427                    return Err(QueryError::grouped_paged_emitted_scalar_continuation());
428                };
429
430                token.encode().map_err(|err| {
431                    QueryError::serialize_internal(format!(
432                        "failed to serialize grouped continuation cursor: {err}"
433                    ))
434                })
435            })
436            .transpose()?;
437
438        Ok(PagedGroupedExecutionWithTrace::new(
439            page.rows,
440            next_cursor,
441            trace,
442        ))
443    }
444
445    // Execute the canonical grouped query core and return the raw grouped page
446    // plus optional execution trace before outward cursor formatting.
447    fn execute_grouped_page_with_trace<E>(
448        &self,
449        query: &Query<E>,
450        cursor_token: Option<&str>,
451    ) -> Result<(GroupedCursorPage, Option<ExecutionTrace>), QueryError>
452    where
453        E: PersistedRow<Canister = C> + EntityValue,
454    {
455        // Phase 1: build/validate prepared execution plan and require grouped shape.
456        let plan = self
457            .compile_query_with_visible_indexes(query)?
458            .into_prepared_execution_plan();
459        Self::ensure_grouped_execution_family(
460            plan.execution_family().map_err(QueryError::execute)?,
461        )?;
462
463        // Phase 2: decode external grouped cursor token and validate against plan.
464        let cursor = decode_optional_grouped_cursor_token(cursor_token)
465            .map_err(QueryError::from_cursor_plan_error)?;
466        let cursor = plan
467            .prepare_grouped_cursor_token(cursor)
468            .map_err(QueryError::from_executor_plan_error)?;
469
470        // Phase 3: execute one grouped page while preserving the structural
471        // grouped cursor payload for whichever outward cursor format the caller needs.
472        self.with_metrics(|| {
473            self.load_executor::<E>()
474                .execute_grouped_paged_with_cursor_traced(plan, cursor)
475        })
476        .map_err(QueryError::execute)
477    }
478}