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