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        session::finalize_structural_grouped_projection_result,
18    },
19    error::InternalError,
20    traits::{CanisterKind, EntityValue},
21};
22
23///
24/// PreparedQueryExecutionOutcome
25///
26/// PreparedQueryExecutionOutcome is the private shared result shape for one
27/// prepared query execution. Normal execution and diagnostics attribution use
28/// it to share scalar/grouped/delete dispatch without exposing executor DTOs
29/// outside the session query module.
30///
31#[cfg_attr(
32    not(feature = "diagnostics"),
33    expect(
34        clippy::large_enum_variant,
35        reason = "non-diagnostics builds keep the grouped execution trace inline to avoid boxing a private session-boundary outcome"
36    )
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///
64/// PreparedQueryExecutionOutput
65///
66/// PreparedQueryExecutionOutput tells the shared prepared-plan path whether a
67/// delete query should materialize deleted rows or use the count-only executor
68/// terminal. The mode exists so `execute_delete_count` can share the same
69/// session dispatch core without forcing row allocation.
70///
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub(in crate::db::session::query) enum PreparedQueryExecutionOutput {
74    Rows,
75    DeleteCount,
76}
77
78// Convert executor plan-surface failures at the session boundary so query error
79// types do not import executor-owned error enums.
80pub(in crate::db::session) fn query_error_from_executor_plan_error(
81    err: ExecutorPlanError,
82) -> QueryError {
83    match err {
84        ExecutorPlanError::Cursor(err) => QueryError::from_cursor_plan_error(*err),
85    }
86}
87
88impl<C: CanisterKind> DbSession<C> {
89    // Validate that one execution strategy is admissible for scalar paged load
90    // execution and fail closed on grouped/primary-key-only routes.
91    pub(in crate::db::session::query) fn ensure_scalar_paged_execution_family(
92        family: ExecutionFamily,
93    ) -> Result<(), QueryError> {
94        match family {
95            ExecutionFamily::Ordered => Ok(()),
96            ExecutionFamily::PrimaryKey | ExecutionFamily::Grouped => Err(QueryError::invariant()),
97        }
98    }
99
100    // Validate that one execution strategy is admissible for the grouped
101    // execution surface.
102    pub(in crate::db::session::query) fn ensure_grouped_execution_family(
103        family: ExecutionFamily,
104    ) -> Result<(), QueryError> {
105        match family {
106            ExecutionFamily::Grouped => Ok(()),
107            ExecutionFamily::PrimaryKey | ExecutionFamily::Ordered => Err(QueryError::invariant()),
108        }
109    }
110
111    /// Execute one scalar load query through a rows-only dispatch path.
112    ///
113    /// This keeps row-only fluent terminals from retaining grouped and delete
114    /// executor branches through the broad `LoadQueryResult` boundary.
115    pub fn execute_scalar_query_rows<E>(
116        &self,
117        query: &Query<E>,
118    ) -> Result<EntityResponse<E>, QueryError>
119    where
120        E: PersistedRow<Canister = C> + EntityValue,
121    {
122        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
123
124        if plan.is_grouped() {
125            return Err(QueryError::invariant());
126        }
127
128        match plan.mode() {
129            QueryMode::Load(_) => self
130                .with_metrics(|| self.load_executor::<E>().execute(plan))
131                .map_err(QueryError::execute),
132            QueryMode::Delete(_) => Err(QueryError::unsupported_query()),
133        }
134    }
135
136    /// Execute one typed delete query and materialize the deleted rows.
137    #[doc(hidden)]
138    pub fn execute_delete_rows<E>(&self, query: &Query<E>) -> Result<EntityResponse<E>, QueryError>
139    where
140        E: PersistedRow<Canister = C> + EntityValue,
141    {
142        // Phase 1: fail closed if the caller routes a non-delete query here.
143        if !query.mode().is_delete() {
144            return Err(QueryError::unsupported_query());
145        }
146
147        // Phase 2: resolve one cached prepared execution-plan contract from
148        // the shared lower boundary.
149        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
150
151        // Phase 3: execute through the shared prepared-plan path while keeping
152        // the row-returning delete terminal explicit.
153        match self.execute_prepared(plan, false, PreparedQueryExecutionOutput::Rows)? {
154            PreparedQueryExecutionOutcome::Delete { rows } => Ok(rows),
155            PreparedQueryExecutionOutcome::Scalar { .. }
156            | PreparedQueryExecutionOutcome::Grouped { .. }
157            | PreparedQueryExecutionOutcome::DeleteCount { .. } => Err(QueryError::invariant()),
158        }
159    }
160
161    // Execute one typed query through the unified row/grouped result surface so
162    // higher layers do not need to branch on grouped shape themselves.
163    #[doc(hidden)]
164    pub fn execute_query_result<E>(
165        &self,
166        query: &Query<E>,
167    ) -> Result<LoadQueryResult<E>, QueryError>
168    where
169        E: PersistedRow<Canister = C> + EntityValue,
170    {
171        // Phase 1: compile typed intent into one prepared execution-plan
172        // contract shared by scalar, grouped, and delete execution.
173        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
174
175        // Phase 2: execute through the canonical prepared-plan path and adapt
176        // the private executor outcome into the public session result shape.
177        self.execute_prepared(plan, false, PreparedQueryExecutionOutput::Rows)
178            .and_then(Self::load_result_from_prepared_outcome)
179    }
180
181    /// Execute one typed delete query and return only the affected-row count.
182    #[doc(hidden)]
183    pub fn execute_delete_count<E>(&self, query: &Query<E>) -> Result<u32, QueryError>
184    where
185        E: PersistedRow<Canister = C> + EntityValue,
186    {
187        // Phase 1: fail closed if the caller routes a non-delete query here.
188        if !query.mode().is_delete() {
189            return Err(QueryError::unsupported_query());
190        }
191
192        // Phase 2: resolve one cached prepared execution-plan contract directly
193        // from the shared lower boundary instead of rebuilding it through the
194        // typed compiled-query wrapper.
195        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
196
197        // Phase 3: execute through the shared prepared-plan path while keeping
198        // the count-only delete terminal that skips response-row materialization.
199        match self.execute_prepared(plan, false, PreparedQueryExecutionOutput::DeleteCount)? {
200            PreparedQueryExecutionOutcome::DeleteCount { row_count } => Ok(row_count),
201            PreparedQueryExecutionOutcome::Scalar { .. }
202            | PreparedQueryExecutionOutcome::Grouped { .. }
203            | PreparedQueryExecutionOutcome::Delete { .. } => Err(QueryError::invariant()),
204        }
205    }
206
207    // Execute one prepared plan through the shared scalar/grouped/delete
208    // dispatch. Diagnostics can request phase-attribution executor entrypoints;
209    // normal execution keeps the existing non-attribution calls.
210    pub(in crate::db::session::query) fn execute_prepared<E>(
211        &self,
212        plan: PreparedExecutionPlan<E>,
213        collect_attribution: bool,
214        output: PreparedQueryExecutionOutput,
215    ) -> Result<PreparedQueryExecutionOutcome<E>, QueryError>
216    where
217        E: PersistedRow<Canister = C> + EntityValue,
218    {
219        #[cfg(not(feature = "diagnostics"))]
220        let _ = collect_attribution;
221
222        if plan.is_grouped() {
223            if output == PreparedQueryExecutionOutput::DeleteCount {
224                return Err(QueryError::invariant());
225            }
226
227            #[cfg(feature = "diagnostics")]
228            if collect_attribution {
229                let (result, trace, phase) =
230                    self.execute_grouped_with_phase_attribution(plan, None)?;
231
232                return Ok(PreparedQueryExecutionOutcome::Grouped {
233                    result,
234                    trace,
235                    phase: Some(phase),
236                });
237            }
238
239            let (result, trace) = self.execute_grouped_with_trace(plan, None)?;
240
241            return Ok(PreparedQueryExecutionOutcome::Grouped {
242                result,
243                trace,
244                #[cfg(feature = "diagnostics")]
245                phase: None,
246            });
247        }
248
249        match plan.mode() {
250            QueryMode::Load(_) => {
251                if output == PreparedQueryExecutionOutput::DeleteCount {
252                    return Err(QueryError::invariant());
253                }
254
255                #[cfg(feature = "diagnostics")]
256                if collect_attribution {
257                    let (rows, phase, response_decode_local_instructions) = self
258                        .load_executor::<E>()
259                        .execute_with_phase_attribution(plan)
260                        .map_err(QueryError::execute)?;
261
262                    return Ok(PreparedQueryExecutionOutcome::Scalar {
263                        rows,
264                        phase: Some(phase),
265                        response_decode_local_instructions,
266                    });
267                }
268
269                let rows = self
270                    .with_metrics(|| self.load_executor::<E>().execute(plan))
271                    .map_err(QueryError::execute)?;
272
273                Ok(PreparedQueryExecutionOutcome::Scalar {
274                    rows,
275                    #[cfg(feature = "diagnostics")]
276                    phase: None,
277                    #[cfg(feature = "diagnostics")]
278                    response_decode_local_instructions: 0,
279                })
280            }
281            QueryMode::Delete(_) => match output {
282                PreparedQueryExecutionOutput::Rows => {
283                    let rows = self
284                        .with_metrics(|| self.delete_executor::<E>().execute(plan))
285                        .map_err(QueryError::execute)?;
286
287                    Ok(PreparedQueryExecutionOutcome::Delete { rows })
288                }
289                PreparedQueryExecutionOutput::DeleteCount => {
290                    let row_count = self
291                        .with_metrics(|| self.delete_executor::<E>().execute_count(plan))
292                        .map_err(QueryError::execute)?;
293
294                    Ok(PreparedQueryExecutionOutcome::DeleteCount { row_count })
295                }
296            },
297        }
298    }
299
300    // Adapt the canonical prepared-plan outcome to the public load-query
301    // result shape. This is the only non-diagnostics adapter that understands
302    // the private scalar/grouped/delete execution outcome variants.
303    fn load_result_from_prepared_outcome<E>(
304        outcome: PreparedQueryExecutionOutcome<E>,
305    ) -> Result<LoadQueryResult<E>, QueryError>
306    where
307        E: PersistedRow<Canister = C> + EntityValue,
308    {
309        match outcome {
310            PreparedQueryExecutionOutcome::Scalar { rows, .. }
311            | PreparedQueryExecutionOutcome::Delete { rows } => Ok(LoadQueryResult::Rows(rows)),
312            PreparedQueryExecutionOutcome::Grouped { result, trace, .. } => {
313                finalize_structural_grouped_projection_result(result, trace)
314                    .map(LoadQueryResult::Grouped)
315            }
316            PreparedQueryExecutionOutcome::DeleteCount { .. } => Err(QueryError::invariant()),
317        }
318    }
319
320    // Shared load-query terminal wrapper: build plan, run under metrics, map
321    // execution errors into query-facing errors.
322    pub(in crate::db) fn execute_with_plan<E, T>(
323        &self,
324        query: &Query<E>,
325        op: impl FnOnce(LoadExecutor<E>, PreparedExecutionPlan<E>) -> Result<T, InternalError>,
326    ) -> Result<T, QueryError>
327    where
328        E: PersistedRow<Canister = C> + EntityValue,
329    {
330        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
331
332        self.with_metrics(|| op(self.load_executor::<E>(), plan))
333            .map_err(QueryError::execute)
334    }
335}