Skip to main content

icydb_core/db/session/query/
execution.rs

1//! Module: db::session::query::execution
2//! Responsibility: canonical query execution dispatch and executor error mapping.
3//! Does not own: diagnostics attribution, cursor decoding, fluent adaptation, or explain surfaces.
4//! Boundary: maps prepared plans into executor calls and query-facing response/error types.
5
6#[cfg(feature = "diagnostics")]
7use crate::db::executor::{GroupedExecutePhaseAttribution, ScalarExecutePhaseAttribution};
8use crate::{
9    db::{
10        DbSession, EntityResponse, LoadQueryResult, PersistedRow, Query, QueryError,
11        diagnostics::ExecutionTrace,
12        executor::{
13            ExecutionFamily, ExecutorPlanError, LoadExecutor, PreparedExecutionPlan,
14            StructuralGroupedProjectionResult,
15        },
16        query::plan::QueryMode,
17        schema::AcceptedEnumCatalogHandle,
18        session::finalize_structural_grouped_projection_result,
19    },
20    error::InternalError,
21    traits::CanisterKind,
22    types::Id,
23    value::Value,
24};
25
26///
27/// PreparedQueryExecutionOutcome
28///
29/// PreparedQueryExecutionOutcome is the private shared result shape for one
30/// prepared query execution. Normal execution and diagnostics attribution use
31/// it to share scalar/grouped/delete dispatch without exposing executor DTOs
32/// outside the session query module.
33///
34#[expect(
35    clippy::large_enum_variant,
36    reason = "the private grouped outcome keeps its execution trace and optional diagnostics attribution inline so attribution does not measure a boundary-only box allocation"
37)]
38pub(in crate::db::session::query) enum PreparedQueryExecutionOutcome<E>
39where
40    E: PersistedRow,
41{
42    Scalar {
43        rows: EntityResponse<E>,
44        #[cfg(feature = "diagnostics")]
45        phase: Option<ScalarExecutePhaseAttribution>,
46        #[cfg(feature = "diagnostics")]
47        response_decode_local_instructions: u64,
48    },
49    Grouped {
50        result: StructuralGroupedProjectionResult,
51        trace: Option<ExecutionTrace>,
52        #[cfg(feature = "diagnostics")]
53        phase: Option<GroupedExecutePhaseAttribution>,
54    },
55    Delete {
56        rows: EntityResponse<E>,
57    },
58    DeleteCount {
59        row_count: u32,
60    },
61}
62
63/// Runtime output paired with the exact accepted catalog retained by the
64/// guarded plan that produced it.
65pub(in crate::db) struct AcceptedExecutionOutput<T> {
66    value: T,
67    enum_catalog: AcceptedEnumCatalogHandle,
68}
69
70pub(in crate::db) type AcceptedValuesOutput = AcceptedExecutionOutput<Vec<Value>>;
71pub(in crate::db) type AcceptedIdValuesOutput<E> = AcceptedExecutionOutput<Vec<(Id<E>, Value)>>;
72pub(in crate::db) type AcceptedOptionalValueOutput = AcceptedExecutionOutput<Option<Value>>;
73
74impl<T> AcceptedExecutionOutput<T> {
75    #[must_use]
76    pub(in crate::db) const fn new(value: T, enum_catalog: AcceptedEnumCatalogHandle) -> Self {
77        Self {
78            value,
79            enum_catalog,
80        }
81    }
82
83    #[must_use]
84    pub(in crate::db) fn into_parts(self) -> (T, AcceptedEnumCatalogHandle) {
85        (self.value, self.enum_catalog)
86    }
87
88    #[must_use]
89    pub(in crate::db) fn into_value(self) -> T {
90        self.value
91    }
92}
93
94///
95/// PreparedQueryExecutionOutput
96///
97/// PreparedQueryExecutionOutput tells the shared prepared-plan path whether a
98/// delete query should materialize deleted rows or use the count-only executor
99/// terminal. The mode exists so `execute_delete_count` can share the same
100/// session dispatch core without forcing row allocation.
101///
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub(in crate::db::session::query) enum PreparedQueryExecutionOutput {
105    Rows,
106    DeleteCount,
107}
108
109// Convert executor plan-surface failures at the session boundary so query error
110// types do not import executor-owned error enums.
111pub(in crate::db::session) fn query_error_from_executor_plan_error(
112    err: ExecutorPlanError,
113) -> QueryError {
114    match err {
115        ExecutorPlanError::Cursor(err) => QueryError::from_cursor_plan_error(*err),
116    }
117}
118
119impl<C: CanisterKind> DbSession<C> {
120    // Fail closed before a cached prepared plan reaches row access when its
121    // retained catalog authority is no longer the store's current root.
122    pub(in crate::db::session) fn ensure_prepared_query_plan_is_current<E>(
123        &self,
124        plan: &PreparedExecutionPlan<E>,
125    ) -> Result<(), QueryError>
126    where
127        E: PersistedRow<Canister = C>,
128    {
129        let authority = plan
130            .accepted_schema_authority()
131            .map_err(QueryError::execute)?;
132
133        self.ensure_accepted_schema_authority_is_current::<E>(authority)
134            .map_err(QueryError::execute)
135    }
136
137    // Validate that one execution strategy is admissible for scalar paged load
138    // execution and fail closed on grouped/primary-key-only routes.
139    pub(in crate::db::session::query) fn ensure_scalar_paged_execution_family(
140        family: ExecutionFamily,
141    ) -> Result<(), QueryError> {
142        match family {
143            ExecutionFamily::Ordered => Ok(()),
144            ExecutionFamily::PrimaryKey | ExecutionFamily::Grouped => Err(QueryError::invariant()),
145        }
146    }
147
148    // Validate that one execution strategy is admissible for the grouped
149    // execution surface.
150    pub(in crate::db::session::query) fn ensure_grouped_execution_family(
151        family: ExecutionFamily,
152    ) -> Result<(), QueryError> {
153        match family {
154            ExecutionFamily::Grouped => Ok(()),
155            ExecutionFamily::PrimaryKey | ExecutionFamily::Ordered => Err(QueryError::invariant()),
156        }
157    }
158
159    /// Execute one scalar load query through a rows-only dispatch path.
160    ///
161    /// This keeps row-only fluent terminals from retaining grouped and delete
162    /// executor branches through the broad `LoadQueryResult` boundary.
163    pub fn execute_scalar_query_rows<E>(
164        &self,
165        query: &Query<E>,
166    ) -> Result<EntityResponse<E>, QueryError>
167    where
168        E: PersistedRow<Canister = C>,
169    {
170        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
171        self.ensure_prepared_query_plan_is_current(&plan)?;
172
173        if plan.is_grouped() {
174            return Err(QueryError::invariant());
175        }
176
177        match plan.mode() {
178            QueryMode::Load(_) => self
179                .with_metrics(|| self.load_executor::<E>().execute(plan))
180                .map_err(QueryError::execute),
181            QueryMode::Delete(_) => Err(QueryError::unsupported_query()),
182        }
183    }
184
185    /// Execute one typed delete query and materialize the deleted rows.
186    #[doc(hidden)]
187    pub fn execute_delete_rows<E>(&self, query: &Query<E>) -> Result<EntityResponse<E>, QueryError>
188    where
189        E: PersistedRow<Canister = C>,
190    {
191        // Phase 1: fail closed if the caller routes a non-delete query here.
192        if !query.mode().is_delete() {
193            return Err(QueryError::unsupported_query());
194        }
195
196        // Phase 2: resolve one cached prepared execution-plan contract from
197        // the shared lower boundary.
198        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
199
200        // Phase 3: execute through the shared prepared-plan path while keeping
201        // the row-returning delete terminal explicit.
202        match self.execute_prepared(plan, false, PreparedQueryExecutionOutput::Rows)? {
203            PreparedQueryExecutionOutcome::Delete { rows } => Ok(rows),
204            PreparedQueryExecutionOutcome::Scalar { .. }
205            | PreparedQueryExecutionOutcome::Grouped { .. }
206            | PreparedQueryExecutionOutcome::DeleteCount { .. } => Err(QueryError::invariant()),
207        }
208    }
209
210    // Execute one typed query through the unified row/grouped result surface so
211    // higher layers do not need to branch on grouped shape themselves.
212    #[doc(hidden)]
213    pub fn execute_query_result<E>(
214        &self,
215        query: &Query<E>,
216    ) -> Result<LoadQueryResult<E>, QueryError>
217    where
218        E: PersistedRow<Canister = C>,
219    {
220        // Phase 1: compile typed intent into one prepared execution-plan
221        // contract shared by scalar, grouped, and delete execution.
222        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
223
224        // Phase 2: execute through the canonical prepared-plan path and adapt
225        // the private executor outcome into the public session result shape.
226        self.execute_prepared(plan, false, PreparedQueryExecutionOutput::Rows)
227            .and_then(Self::load_result_from_prepared_outcome)
228    }
229
230    /// Execute one typed delete query and return only the affected-row count.
231    #[doc(hidden)]
232    pub fn execute_delete_count<E>(&self, query: &Query<E>) -> Result<u32, QueryError>
233    where
234        E: PersistedRow<Canister = C>,
235    {
236        // Phase 1: fail closed if the caller routes a non-delete query here.
237        if !query.mode().is_delete() {
238            return Err(QueryError::unsupported_query());
239        }
240
241        // Phase 2: resolve one cached prepared execution-plan contract directly
242        // from the shared lower boundary instead of rebuilding it through the
243        // typed compiled-query wrapper.
244        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
245
246        // Phase 3: execute through the shared prepared-plan path while keeping
247        // the count-only delete terminal that skips response-row materialization.
248        match self.execute_prepared(plan, false, PreparedQueryExecutionOutput::DeleteCount)? {
249            PreparedQueryExecutionOutcome::DeleteCount { row_count } => Ok(row_count),
250            PreparedQueryExecutionOutcome::Scalar { .. }
251            | PreparedQueryExecutionOutcome::Grouped { .. }
252            | PreparedQueryExecutionOutcome::Delete { .. } => Err(QueryError::invariant()),
253        }
254    }
255
256    // Execute one prepared plan through the shared scalar/grouped/delete
257    // dispatch. Diagnostics can request phase-attribution executor entrypoints;
258    // normal execution keeps the existing non-attribution calls.
259    pub(in crate::db::session::query) fn execute_prepared<E>(
260        &self,
261        plan: PreparedExecutionPlan<E>,
262        collect_attribution: bool,
263        output: PreparedQueryExecutionOutput,
264    ) -> Result<PreparedQueryExecutionOutcome<E>, QueryError>
265    where
266        E: PersistedRow<Canister = C>,
267    {
268        #[cfg(not(feature = "diagnostics"))]
269        let _ = collect_attribution;
270
271        if plan.is_grouped() {
272            if output == PreparedQueryExecutionOutput::DeleteCount {
273                return Err(QueryError::invariant());
274            }
275
276            #[cfg(feature = "diagnostics")]
277            if collect_attribution {
278                let (result, trace, phase) =
279                    self.execute_grouped_with_phase_attribution(plan, None)?;
280
281                return Ok(PreparedQueryExecutionOutcome::Grouped {
282                    result,
283                    trace,
284                    phase: Some(phase),
285                });
286            }
287
288            let (result, trace) = self.execute_grouped_with_trace(plan, None)?;
289
290            return Ok(PreparedQueryExecutionOutcome::Grouped {
291                result,
292                trace,
293                #[cfg(feature = "diagnostics")]
294                phase: None,
295            });
296        }
297
298        self.ensure_prepared_query_plan_is_current(&plan)?;
299
300        match plan.mode() {
301            QueryMode::Load(_) => {
302                if output == PreparedQueryExecutionOutput::DeleteCount {
303                    return Err(QueryError::invariant());
304                }
305
306                #[cfg(feature = "diagnostics")]
307                if collect_attribution {
308                    let (rows, phase, response_decode_local_instructions) = self
309                        .load_executor::<E>()
310                        .execute_with_phase_attribution(plan)
311                        .map_err(QueryError::execute)?;
312
313                    return Ok(PreparedQueryExecutionOutcome::Scalar {
314                        rows,
315                        phase: Some(phase),
316                        response_decode_local_instructions,
317                    });
318                }
319
320                let rows = self
321                    .with_metrics(|| self.load_executor::<E>().execute(plan))
322                    .map_err(QueryError::execute)?;
323
324                Ok(PreparedQueryExecutionOutcome::Scalar {
325                    rows,
326                    #[cfg(feature = "diagnostics")]
327                    phase: None,
328                    #[cfg(feature = "diagnostics")]
329                    response_decode_local_instructions: 0,
330                })
331            }
332            QueryMode::Delete(_) => match output {
333                PreparedQueryExecutionOutput::Rows => {
334                    let rows = self
335                        .with_metrics(|| self.delete_executor::<E>().execute(plan))
336                        .map_err(QueryError::execute)?;
337
338                    Ok(PreparedQueryExecutionOutcome::Delete { rows })
339                }
340                PreparedQueryExecutionOutput::DeleteCount => {
341                    let row_count = self
342                        .with_metrics(|| self.delete_executor::<E>().execute_count(plan))
343                        .map_err(QueryError::execute)?;
344
345                    Ok(PreparedQueryExecutionOutcome::DeleteCount { row_count })
346                }
347            },
348        }
349    }
350
351    // Adapt the canonical prepared-plan outcome to the public load-query
352    // result shape. This is the only non-diagnostics adapter that understands
353    // the private scalar/grouped/delete execution outcome variants.
354    fn load_result_from_prepared_outcome<E>(
355        outcome: PreparedQueryExecutionOutcome<E>,
356    ) -> Result<LoadQueryResult<E>, QueryError>
357    where
358        E: PersistedRow<Canister = C>,
359    {
360        match outcome {
361            PreparedQueryExecutionOutcome::Scalar { rows, .. }
362            | PreparedQueryExecutionOutcome::Delete { rows } => Ok(LoadQueryResult::Rows(rows)),
363            PreparedQueryExecutionOutcome::Grouped { result, trace, .. } => {
364                finalize_structural_grouped_projection_result(result, trace)
365                    .map(LoadQueryResult::Grouped)
366            }
367            PreparedQueryExecutionOutcome::DeleteCount { .. } => Err(QueryError::invariant()),
368        }
369    }
370
371    // Shared load-query terminal wrapper: build plan, run under metrics, map
372    // execution errors into query-facing errors.
373    pub(in crate::db) fn execute_with_plan<E, T>(
374        &self,
375        query: &Query<E>,
376        op: impl FnOnce(LoadExecutor<E>, PreparedExecutionPlan<E>) -> Result<T, InternalError>,
377    ) -> Result<T, QueryError>
378    where
379        E: PersistedRow<Canister = C>,
380    {
381        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
382        self.ensure_prepared_query_plan_is_current(&plan)?;
383
384        self.with_metrics(|| op(self.load_executor::<E>(), plan))
385            .map_err(QueryError::execute)
386    }
387
388    // Execute one value-producing operation while retaining the exact catalog
389    // handle carried by the guarded plan for later outward rendering.
390    pub(in crate::db) fn execute_with_plan_and_catalog<E, T>(
391        &self,
392        query: &Query<E>,
393        op: impl FnOnce(LoadExecutor<E>, PreparedExecutionPlan<E>) -> Result<T, InternalError>,
394    ) -> Result<AcceptedExecutionOutput<T>, QueryError>
395    where
396        E: PersistedRow<Canister = C>,
397    {
398        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
399        self.ensure_prepared_query_plan_is_current(&plan)?;
400        let enum_catalog = plan
401            .accepted_enum_catalog_handle()
402            .map_err(QueryError::execute)?
403            .clone();
404        let value = self
405            .with_metrics(|| op(self.load_executor::<E>(), plan))
406            .map_err(QueryError::execute)?;
407
408        Ok(AcceptedExecutionOutput::new(value, enum_catalog))
409    }
410}