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
7#[cfg(feature = "perf-attribution")]
8use crate::db::executor::ScalarExecutePhaseAttribution;
9use crate::{
10    db::{
11        DbSession, EntityResponse, LoadQueryResult, PagedGroupedExecutionWithTrace,
12        PagedLoadExecutionWithTrace, PersistedRow, Query, QueryError, QueryTracePlan,
13        access::AccessStrategy,
14        commit::CommitSchemaFingerprint,
15        cursor::{
16            CursorPlanError, decode_optional_cursor_token, decode_optional_grouped_cursor_token,
17        },
18        diagnostics::ExecutionTrace,
19        executor::{
20            ExecutionFamily, GroupedCursorPage, LoadExecutor, PreparedExecutionPlan,
21            SharedPreparedExecutionPlan,
22        },
23        query::builder::{
24            PreparedFluentAggregateExplainStrategy, PreparedFluentProjectionStrategy,
25        },
26        query::explain::{
27            ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor, ExplainPlan,
28        },
29        query::{
30            intent::{CompiledQuery, PlannedQuery, StructuralQuery},
31            plan::{AccessPlannedQuery, QueryMode, VisibleIndexes},
32        },
33    },
34    error::InternalError,
35    model::entity::EntityModel,
36    traits::{CanisterKind, EntityKind, EntityValue, Path},
37};
38#[cfg(feature = "perf-attribution")]
39use candid::CandidType;
40use icydb_utils::Xxh3;
41#[cfg(feature = "perf-attribution")]
42use serde::Deserialize;
43use std::{cell::RefCell, collections::HashMap, hash::BuildHasherDefault};
44
45type CacheBuildHasher = BuildHasherDefault<Xxh3>;
46
47// Bump this when the shared lower query-plan cache key meaning changes in a
48// way that must force old in-heap entries to miss instead of aliasing.
49const SHARED_QUERY_PLAN_CACHE_METHOD_VERSION: u8 = 1;
50
51#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52pub(in crate::db) enum QueryPlanVisibility {
53    StoreNotReady,
54    StoreReady,
55}
56
57#[derive(Clone, Debug, Eq, Hash, PartialEq)]
58pub(in crate::db) struct QueryPlanCacheKey {
59    cache_method_version: u8,
60    entity_path: &'static str,
61    schema_fingerprint: CommitSchemaFingerprint,
62    visibility: QueryPlanVisibility,
63    structural_query: crate::db::query::intent::StructuralQueryCacheKey,
64}
65
66#[derive(Clone, Debug)]
67pub(in crate::db) struct QueryPlanCacheEntry {
68    logical_plan: AccessPlannedQuery,
69    prepared_plan: SharedPreparedExecutionPlan,
70}
71
72impl QueryPlanCacheEntry {
73    #[must_use]
74    pub(in crate::db) const fn new(
75        logical_plan: AccessPlannedQuery,
76        prepared_plan: SharedPreparedExecutionPlan,
77    ) -> Self {
78        Self {
79            logical_plan,
80            prepared_plan,
81        }
82    }
83
84    #[must_use]
85    pub(in crate::db) const fn logical_plan(&self) -> &AccessPlannedQuery {
86        &self.logical_plan
87    }
88
89    #[must_use]
90    pub(in crate::db) fn typed_prepared_plan<E: EntityKind>(&self) -> PreparedExecutionPlan<E> {
91        self.prepared_plan.typed_clone::<E>()
92    }
93}
94
95pub(in crate::db) type QueryPlanCache =
96    HashMap<QueryPlanCacheKey, QueryPlanCacheEntry, CacheBuildHasher>;
97
98thread_local! {
99    // Keep one in-heap query-plan cache per store registry so fresh `DbSession`
100    // facades can share prepared logical plans across update/query calls while
101    // tests and multi-registry host processes remain isolated by registry
102    // identity.
103    static QUERY_PLAN_CACHES: RefCell<HashMap<usize, QueryPlanCache, CacheBuildHasher>> =
104        RefCell::new(HashMap::default());
105}
106
107#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
108pub(in crate::db) struct QueryPlanCacheAttribution {
109    pub hits: u64,
110    pub misses: u64,
111}
112
113impl QueryPlanCacheAttribution {
114    #[must_use]
115    const fn hit() -> Self {
116        Self { hits: 1, misses: 0 }
117    }
118
119    #[must_use]
120    const fn miss() -> Self {
121        Self { hits: 0, misses: 1 }
122    }
123}
124
125///
126/// QueryExecutionAttribution
127///
128/// QueryExecutionAttribution records the top-level compile/execute split for
129/// typed/fluent query execution at the session boundary.
130///
131#[cfg(feature = "perf-attribution")]
132#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
133pub struct QueryExecutionAttribution {
134    pub compile_local_instructions: u64,
135    pub runtime_local_instructions: u64,
136    pub finalize_local_instructions: u64,
137    pub direct_data_row_scan_local_instructions: u64,
138    pub direct_data_row_order_window_local_instructions: u64,
139    pub direct_data_row_page_window_local_instructions: u64,
140    pub response_decode_local_instructions: u64,
141    pub execute_local_instructions: u64,
142    pub total_local_instructions: u64,
143    pub shared_query_plan_cache_hits: u64,
144    pub shared_query_plan_cache_misses: u64,
145}
146
147#[cfg(feature = "perf-attribution")]
148#[expect(
149    clippy::missing_const_for_fn,
150    reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
151)]
152fn read_query_local_instruction_counter() -> u64 {
153    #[cfg(target_arch = "wasm32")]
154    {
155        canic_cdk::api::performance_counter(1)
156    }
157
158    #[cfg(not(target_arch = "wasm32"))]
159    {
160        0
161    }
162}
163
164#[cfg(feature = "perf-attribution")]
165fn measure_query_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
166    let start = read_query_local_instruction_counter();
167    let result = run();
168    let delta = read_query_local_instruction_counter().saturating_sub(start);
169
170    (delta, result)
171}
172
173impl<C: CanisterKind> DbSession<C> {
174    #[cfg(feature = "perf-attribution")]
175    const fn empty_scalar_execute_phase_attribution() -> ScalarExecutePhaseAttribution {
176        ScalarExecutePhaseAttribution {
177            runtime_local_instructions: 0,
178            finalize_local_instructions: 0,
179            direct_data_row_scan_local_instructions: 0,
180            direct_data_row_order_window_local_instructions: 0,
181            direct_data_row_page_window_local_instructions: 0,
182        }
183    }
184
185    fn query_plan_cache_scope_id(&self) -> usize {
186        self.db.cache_scope_id()
187    }
188
189    fn with_query_plan_cache<R>(&self, f: impl FnOnce(&mut QueryPlanCache) -> R) -> R {
190        let scope_id = self.query_plan_cache_scope_id();
191
192        QUERY_PLAN_CACHES.with(|caches| {
193            let mut caches = caches.borrow_mut();
194            let cache = caches.entry(scope_id).or_default();
195
196            f(cache)
197        })
198    }
199
200    const fn visible_indexes_for_model(
201        model: &'static EntityModel,
202        visibility: QueryPlanVisibility,
203    ) -> VisibleIndexes<'static> {
204        match visibility {
205            QueryPlanVisibility::StoreReady => VisibleIndexes::planner_visible(model.indexes()),
206            QueryPlanVisibility::StoreNotReady => VisibleIndexes::none(),
207        }
208    }
209
210    #[cfg(test)]
211    pub(in crate::db) fn query_plan_cache_len(&self) -> usize {
212        self.with_query_plan_cache(|cache| cache.len())
213    }
214
215    #[cfg(test)]
216    pub(in crate::db) fn clear_query_plan_cache_for_tests(&self) {
217        self.with_query_plan_cache(QueryPlanCache::clear);
218    }
219
220    pub(in crate::db) fn query_plan_visibility_for_store_path(
221        &self,
222        store_path: &'static str,
223    ) -> Result<QueryPlanVisibility, QueryError> {
224        let store = self
225            .db
226            .recovered_store(store_path)
227            .map_err(QueryError::execute)?;
228        let visibility = if store.index_state() == crate::db::IndexState::Ready {
229            QueryPlanVisibility::StoreReady
230        } else {
231            QueryPlanVisibility::StoreNotReady
232        };
233
234        Ok(visibility)
235    }
236
237    pub(in crate::db) fn cached_query_plan_entry_for_authority(
238        &self,
239        authority: crate::db::executor::EntityAuthority,
240        schema_fingerprint: CommitSchemaFingerprint,
241        query: &StructuralQuery,
242    ) -> Result<(QueryPlanCacheEntry, QueryPlanCacheAttribution), QueryError> {
243        let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
244        let cache_key =
245            QueryPlanCacheKey::for_authority(authority, schema_fingerprint, visibility, query);
246
247        {
248            let cached = self.with_query_plan_cache(|cache| cache.get(&cache_key).cloned());
249            if let Some(entry) = cached {
250                return Ok((entry, QueryPlanCacheAttribution::hit()));
251            }
252        }
253
254        let visible_indexes = Self::visible_indexes_for_model(authority.model(), visibility);
255        let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
256        let entry = QueryPlanCacheEntry::new(
257            plan.clone(),
258            SharedPreparedExecutionPlan::from_plan(authority, plan),
259        );
260        self.with_query_plan_cache(|cache| {
261            cache.insert(cache_key, entry.clone());
262        });
263
264        Ok((entry, QueryPlanCacheAttribution::miss()))
265    }
266
267    #[cfg(test)]
268    pub(in crate::db) fn query_plan_cache_key_for_tests(
269        authority: crate::db::executor::EntityAuthority,
270        schema_fingerprint: CommitSchemaFingerprint,
271        visibility: QueryPlanVisibility,
272        query: &StructuralQuery,
273        cache_method_version: u8,
274    ) -> QueryPlanCacheKey {
275        QueryPlanCacheKey::for_authority_with_method_version(
276            authority,
277            schema_fingerprint,
278            visibility,
279            query,
280            cache_method_version,
281        )
282    }
283
284    pub(in crate::db) fn cached_structural_plan_for_authority(
285        &self,
286        authority: crate::db::executor::EntityAuthority,
287        schema_fingerprint: CommitSchemaFingerprint,
288        query: &StructuralQuery,
289    ) -> Result<AccessPlannedQuery, QueryError> {
290        let (entry, _) =
291            self.cached_query_plan_entry_for_authority(authority, schema_fingerprint, query)?;
292
293        Ok(entry.logical_plan().clone())
294    }
295
296    // Resolve the planner-visible index slice for one typed query exactly once
297    // at the session boundary before handing execution/planning off to query-owned logic.
298    fn with_query_visible_indexes<E, T>(
299        &self,
300        query: &Query<E>,
301        op: impl FnOnce(
302            &Query<E>,
303            &crate::db::query::plan::VisibleIndexes<'static>,
304        ) -> Result<T, QueryError>,
305    ) -> Result<T, QueryError>
306    where
307        E: EntityKind<Canister = C>,
308    {
309        let visibility = self.query_plan_visibility_for_store_path(E::Store::PATH)?;
310        let visible_indexes = Self::visible_indexes_for_model(E::MODEL, visibility);
311
312        op(query, &visible_indexes)
313    }
314
315    // Resolve one typed structural query onto the shared lower plan cache so
316    // typed/fluent callers do not each duplicate the entity metadata plumbing.
317    fn cached_structural_plan_for_entity<E>(
318        &self,
319        query: &StructuralQuery,
320    ) -> Result<AccessPlannedQuery, QueryError>
321    where
322        E: EntityKind<Canister = C>,
323    {
324        self.cached_structural_plan_for_authority(
325            crate::db::executor::EntityAuthority::for_type::<E>(),
326            crate::db::schema::commit_schema_fingerprint_for_entity::<E>(),
327            query,
328        )
329    }
330
331    pub(in crate::db::session) fn cached_prepared_query_plan_for_entity<E>(
332        &self,
333        query: &StructuralQuery,
334    ) -> Result<(PreparedExecutionPlan<E>, QueryPlanCacheAttribution), QueryError>
335    where
336        E: EntityKind<Canister = C>,
337    {
338        let (entry, attribution) = self.cached_query_plan_entry_for_authority(
339            crate::db::executor::EntityAuthority::for_type::<E>(),
340            crate::db::schema::commit_schema_fingerprint_for_entity::<E>(),
341            query,
342        )?;
343
344        Ok((entry.typed_prepared_plan::<E>(), attribution))
345    }
346
347    // Compile one typed query using only the indexes currently visible for the
348    // query's recovered store.
349    pub(in crate::db) fn compile_query_with_visible_indexes<E>(
350        &self,
351        query: &Query<E>,
352    ) -> Result<CompiledQuery<E>, QueryError>
353    where
354        E: EntityKind<Canister = C>,
355    {
356        let plan = self.cached_structural_plan_for_entity::<E>(query.structural())?;
357
358        Ok(Query::<E>::compiled_query_from_plan(plan))
359    }
360
361    // Build one logical planned-query shell using only the indexes currently
362    // visible for the query's recovered store.
363    pub(in crate::db) fn planned_query_with_visible_indexes<E>(
364        &self,
365        query: &Query<E>,
366    ) -> Result<PlannedQuery<E>, QueryError>
367    where
368        E: EntityKind<Canister = C>,
369    {
370        let plan = self.cached_structural_plan_for_entity::<E>(query.structural())?;
371
372        Ok(Query::<E>::planned_query_from_plan(plan))
373    }
374
375    // Project one logical explain payload using only planner-visible indexes.
376    pub(in crate::db) fn explain_query_with_visible_indexes<E>(
377        &self,
378        query: &Query<E>,
379    ) -> Result<ExplainPlan, QueryError>
380    where
381        E: EntityKind<Canister = C>,
382    {
383        self.with_query_visible_indexes(query, |query, visible_indexes| {
384            query.explain_with_visible_indexes(visible_indexes)
385        })
386    }
387
388    // Hash one typed query plan using only the indexes currently visible for
389    // the query's recovered store.
390    pub(in crate::db) fn query_plan_hash_hex_with_visible_indexes<E>(
391        &self,
392        query: &Query<E>,
393    ) -> Result<String, QueryError>
394    where
395        E: EntityKind<Canister = C>,
396    {
397        self.with_query_visible_indexes(query, |query, visible_indexes| {
398            query.plan_hash_hex_with_visible_indexes(visible_indexes)
399        })
400    }
401
402    // Explain one load execution shape using only planner-visible
403    // indexes from the recovered store state.
404    pub(in crate::db) fn explain_query_execution_with_visible_indexes<E>(
405        &self,
406        query: &Query<E>,
407    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
408    where
409        E: EntityValue + EntityKind<Canister = C>,
410    {
411        self.with_query_visible_indexes(query, |query, visible_indexes| {
412            query.explain_execution_with_visible_indexes(visible_indexes)
413        })
414    }
415
416    // Render one load execution descriptor as deterministic text using
417    // only planner-visible indexes from the recovered store state.
418    pub(in crate::db) fn explain_query_execution_text_with_visible_indexes<E>(
419        &self,
420        query: &Query<E>,
421    ) -> Result<String, QueryError>
422    where
423        E: EntityValue + EntityKind<Canister = C>,
424    {
425        self.with_query_visible_indexes(query, |query, visible_indexes| {
426            query.explain_execution_text_with_visible_indexes(visible_indexes)
427        })
428    }
429
430    // Render one load execution descriptor as canonical JSON using
431    // only planner-visible indexes from the recovered store state.
432    pub(in crate::db) fn explain_query_execution_json_with_visible_indexes<E>(
433        &self,
434        query: &Query<E>,
435    ) -> Result<String, QueryError>
436    where
437        E: EntityValue + EntityKind<Canister = C>,
438    {
439        self.with_query_visible_indexes(query, |query, visible_indexes| {
440            query.explain_execution_json_with_visible_indexes(visible_indexes)
441        })
442    }
443
444    // Render one load execution descriptor plus route diagnostics using
445    // only planner-visible indexes from the recovered store state.
446    pub(in crate::db) fn explain_query_execution_verbose_with_visible_indexes<E>(
447        &self,
448        query: &Query<E>,
449    ) -> Result<String, QueryError>
450    where
451        E: EntityValue + EntityKind<Canister = C>,
452    {
453        self.with_query_visible_indexes(query, |query, visible_indexes| {
454            query.explain_execution_verbose_with_visible_indexes(visible_indexes)
455        })
456    }
457
458    // Explain one prepared fluent aggregate terminal using only
459    // planner-visible indexes from the recovered store state.
460    pub(in crate::db) fn explain_query_prepared_aggregate_terminal_with_visible_indexes<E, S>(
461        &self,
462        query: &Query<E>,
463        strategy: &S,
464    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
465    where
466        E: EntityValue + EntityKind<Canister = C>,
467        S: PreparedFluentAggregateExplainStrategy,
468    {
469        self.with_query_visible_indexes(query, |query, visible_indexes| {
470            query
471                .explain_prepared_aggregate_terminal_with_visible_indexes(visible_indexes, strategy)
472        })
473    }
474
475    // Explain one `bytes_by(field)` terminal using only planner-visible
476    // indexes from the recovered store state.
477    pub(in crate::db) fn explain_query_bytes_by_with_visible_indexes<E>(
478        &self,
479        query: &Query<E>,
480        target_field: &str,
481    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
482    where
483        E: EntityValue + EntityKind<Canister = C>,
484    {
485        self.with_query_visible_indexes(query, |query, visible_indexes| {
486            query.explain_bytes_by_with_visible_indexes(visible_indexes, target_field)
487        })
488    }
489
490    // Explain one prepared fluent projection terminal using only
491    // planner-visible indexes from the recovered store state.
492    pub(in crate::db) fn explain_query_prepared_projection_terminal_with_visible_indexes<E>(
493        &self,
494        query: &Query<E>,
495        strategy: &PreparedFluentProjectionStrategy,
496    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
497    where
498        E: EntityValue + EntityKind<Canister = C>,
499    {
500        self.with_query_visible_indexes(query, |query, visible_indexes| {
501            query.explain_prepared_projection_terminal_with_visible_indexes(
502                visible_indexes,
503                strategy,
504            )
505        })
506    }
507
508    // Validate that one execution strategy is admissible for scalar paged load
509    // execution and fail closed on grouped/primary-key-only routes.
510    fn ensure_scalar_paged_execution_family(family: ExecutionFamily) -> Result<(), QueryError> {
511        match family {
512            ExecutionFamily::PrimaryKey => Err(QueryError::invariant(
513                CursorPlanError::cursor_requires_explicit_or_grouped_ordering_message(),
514            )),
515            ExecutionFamily::Ordered => Ok(()),
516            ExecutionFamily::Grouped => Err(QueryError::invariant(
517                "grouped queries execute via execute(), not page().execute()",
518            )),
519        }
520    }
521
522    // Validate that one execution strategy is admissible for the grouped
523    // execution surface.
524    fn ensure_grouped_execution_family(family: ExecutionFamily) -> Result<(), QueryError> {
525        match family {
526            ExecutionFamily::Grouped => Ok(()),
527            ExecutionFamily::PrimaryKey | ExecutionFamily::Ordered => Err(QueryError::invariant(
528                "grouped execution requires grouped logical plans",
529            )),
530        }
531    }
532
533    /// Execute one scalar load/delete query and return materialized response rows.
534    pub fn execute_query<E>(&self, query: &Query<E>) -> Result<EntityResponse<E>, QueryError>
535    where
536        E: PersistedRow<Canister = C> + EntityValue,
537    {
538        // Phase 1: compile typed intent into one prepared execution-plan contract.
539        let mode = query.mode();
540        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query.structural())?;
541
542        // Phase 2: delegate execution to the shared compiled-plan entry path.
543        self.execute_query_dyn(mode, plan)
544    }
545
546    /// Execute one typed query while reporting the compile/execute split at
547    /// the shared fluent query seam.
548    #[cfg(feature = "perf-attribution")]
549    #[doc(hidden)]
550    pub fn execute_query_result_with_attribution<E>(
551        &self,
552        query: &Query<E>,
553    ) -> Result<(LoadQueryResult<E>, QueryExecutionAttribution), QueryError>
554    where
555        E: PersistedRow<Canister = C> + EntityValue,
556    {
557        // Phase 1: measure compile work at the typed/fluent boundary,
558        // including the shared lower query-plan cache lookup/build exactly
559        // once. This preserves honest hit/miss attribution without
560        // double-building plans on one-shot cache misses.
561        let (compile_local_instructions, plan_and_cache) = measure_query_stage(|| {
562            self.cached_prepared_query_plan_for_entity::<E>(query.structural())
563        });
564        let (plan, cache_attribution) = plan_and_cache?;
565
566        // Phase 2: execute one query result using the prepared plan produced
567        // by the compile/cache boundary above.
568        let (execute_local_instructions, result) = measure_query_stage(|| {
569            if query.has_grouping() {
570                self.execute_grouped_plan_with_trace(plan, None)
571                    .map(|(page, trace)| {
572                        let next_cursor = page
573                            .next_cursor
574                            .map(|token| {
575                                let Some(token) = token.as_grouped() else {
576                                    return Err(
577                                        QueryError::grouped_paged_emitted_scalar_continuation(),
578                                    );
579                                };
580
581                                token.encode().map_err(|err| {
582                                    QueryError::serialize_internal(format!(
583                                        "failed to serialize grouped continuation cursor: {err}"
584                                    ))
585                                })
586                            })
587                            .transpose()?;
588
589                        Ok::<(LoadQueryResult<E>, ScalarExecutePhaseAttribution, u64), QueryError>(
590                            (
591                                LoadQueryResult::Grouped(PagedGroupedExecutionWithTrace::new(
592                                    page.rows,
593                                    next_cursor,
594                                    trace,
595                                )),
596                                Self::empty_scalar_execute_phase_attribution(),
597                                0,
598                            ),
599                        )
600                    })?
601            } else {
602                match query.mode() {
603                    QueryMode::Load(_) => {
604                        let (rows, phase_attribution, response_decode_local_instructions) = self
605                            .load_executor::<E>()
606                            .execute_with_phase_attribution(plan)
607                            .map_err(QueryError::execute)?;
608
609                        Ok::<(LoadQueryResult<E>, ScalarExecutePhaseAttribution, u64), QueryError>(
610                            (
611                                LoadQueryResult::Rows(rows),
612                                phase_attribution,
613                                response_decode_local_instructions,
614                            ),
615                        )
616                    }
617                    QueryMode::Delete(_) => {
618                        let result = self.execute_query_dyn(query.mode(), plan)?;
619
620                        Ok((
621                            LoadQueryResult::Rows(result),
622                            Self::empty_scalar_execute_phase_attribution(),
623                            0,
624                        ))
625                    }
626                }
627            }
628        });
629        let (result, execute_phase_attribution, response_decode_local_instructions) = result?;
630        let total_local_instructions =
631            compile_local_instructions.saturating_add(execute_local_instructions);
632
633        Ok((
634            result,
635            QueryExecutionAttribution {
636                compile_local_instructions,
637                runtime_local_instructions: execute_phase_attribution.runtime_local_instructions,
638                finalize_local_instructions: execute_phase_attribution.finalize_local_instructions,
639                direct_data_row_scan_local_instructions: execute_phase_attribution
640                    .direct_data_row_scan_local_instructions,
641                direct_data_row_order_window_local_instructions: execute_phase_attribution
642                    .direct_data_row_order_window_local_instructions,
643                direct_data_row_page_window_local_instructions: execute_phase_attribution
644                    .direct_data_row_page_window_local_instructions,
645                response_decode_local_instructions,
646                execute_local_instructions,
647                total_local_instructions,
648                shared_query_plan_cache_hits: cache_attribution.hits,
649                shared_query_plan_cache_misses: cache_attribution.misses,
650            },
651        ))
652    }
653
654    // Execute one typed query through the unified row/grouped result surface so
655    // higher layers do not need to branch on grouped shape themselves.
656    #[doc(hidden)]
657    pub fn execute_query_result<E>(
658        &self,
659        query: &Query<E>,
660    ) -> Result<LoadQueryResult<E>, QueryError>
661    where
662        E: PersistedRow<Canister = C> + EntityValue,
663    {
664        if query.has_grouping() {
665            return self
666                .execute_grouped(query, None)
667                .map(LoadQueryResult::Grouped);
668        }
669
670        self.execute_query(query).map(LoadQueryResult::Rows)
671    }
672
673    /// Execute one typed delete query and return only the affected-row count.
674    #[doc(hidden)]
675    pub fn execute_delete_count<E>(&self, query: &Query<E>) -> Result<u32, QueryError>
676    where
677        E: PersistedRow<Canister = C> + EntityValue,
678    {
679        // Phase 1: fail closed if the caller routes a non-delete query here.
680        if !query.mode().is_delete() {
681            return Err(QueryError::unsupported_query(
682                "delete count execution requires delete query mode",
683            ));
684        }
685
686        // Phase 2: compile typed delete intent into one prepared execution-plan contract.
687        let plan = self
688            .compile_query_with_visible_indexes(query)?
689            .into_prepared_execution_plan();
690
691        // Phase 3: execute the shared delete core while skipping response-row materialization.
692        self.with_metrics(|| self.delete_executor::<E>().execute_count(plan))
693            .map_err(QueryError::execute)
694    }
695
696    /// Execute one scalar query from one pre-built prepared execution contract.
697    ///
698    /// This is the shared compiled-plan entry boundary used by the typed
699    /// `execute_query(...)` surface and adjacent query execution facades.
700    pub(in crate::db) fn execute_query_dyn<E>(
701        &self,
702        mode: QueryMode,
703        plan: PreparedExecutionPlan<E>,
704    ) -> Result<EntityResponse<E>, QueryError>
705    where
706        E: PersistedRow<Canister = C> + EntityValue,
707    {
708        let result = match mode {
709            QueryMode::Load(_) => self.with_metrics(|| self.load_executor::<E>().execute(plan)),
710            QueryMode::Delete(_) => self.with_metrics(|| self.delete_executor::<E>().execute(plan)),
711        };
712
713        result.map_err(QueryError::execute)
714    }
715
716    // Shared load-query terminal wrapper: build plan, run under metrics, map
717    // execution errors into query-facing errors.
718    pub(in crate::db) fn execute_load_query_with<E, T>(
719        &self,
720        query: &Query<E>,
721        op: impl FnOnce(LoadExecutor<E>, PreparedExecutionPlan<E>) -> Result<T, InternalError>,
722    ) -> Result<T, QueryError>
723    where
724        E: PersistedRow<Canister = C> + EntityValue,
725    {
726        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query.structural())?;
727
728        self.with_metrics(|| op(self.load_executor::<E>(), plan))
729            .map_err(QueryError::execute)
730    }
731
732    /// Build one trace payload for a query without executing it.
733    ///
734    /// This lightweight surface is intended for developer diagnostics:
735    /// plan hash, access strategy summary, and planner/executor route shape.
736    pub fn trace_query<E>(&self, query: &Query<E>) -> Result<QueryTracePlan, QueryError>
737    where
738        E: EntityKind<Canister = C>,
739    {
740        let compiled = self.compile_query_with_visible_indexes(query)?;
741        let explain = compiled.explain();
742        let plan_hash = compiled.plan_hash_hex();
743
744        let (executable, _) =
745            self.cached_prepared_query_plan_for_entity::<E>(query.structural())?;
746        let access_strategy = AccessStrategy::from_plan(executable.access()).debug_summary();
747        let execution_family = match query.mode() {
748            QueryMode::Load(_) => Some(executable.execution_family().map_err(QueryError::execute)?),
749            QueryMode::Delete(_) => None,
750        };
751
752        Ok(QueryTracePlan::new(
753            plan_hash,
754            access_strategy,
755            execution_family,
756            explain,
757        ))
758    }
759
760    /// Execute one scalar paged load query and return optional continuation cursor plus trace.
761    pub(crate) fn execute_load_query_paged_with_trace<E>(
762        &self,
763        query: &Query<E>,
764        cursor_token: Option<&str>,
765    ) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
766    where
767        E: PersistedRow<Canister = C> + EntityValue,
768    {
769        // Phase 1: build/validate prepared execution plan and reject grouped plans.
770        let plan = self
771            .cached_prepared_query_plan_for_entity::<E>(query.structural())?
772            .0;
773        Self::ensure_scalar_paged_execution_family(
774            plan.execution_family().map_err(QueryError::execute)?,
775        )?;
776
777        // Phase 2: decode external cursor token and validate it against plan surface.
778        let cursor_bytes = decode_optional_cursor_token(cursor_token)
779            .map_err(QueryError::from_cursor_plan_error)?;
780        let cursor = plan
781            .prepare_cursor(cursor_bytes.as_deref())
782            .map_err(QueryError::from_executor_plan_error)?;
783
784        // Phase 3: execute one traced page and encode outbound continuation token.
785        let (page, trace) = self
786            .with_metrics(|| {
787                self.load_executor::<E>()
788                    .execute_paged_with_cursor_traced(plan, cursor)
789            })
790            .map_err(QueryError::execute)?;
791        let next_cursor = page
792            .next_cursor
793            .map(|token| {
794                let Some(token) = token.as_scalar() else {
795                    return Err(QueryError::scalar_paged_emitted_grouped_continuation());
796                };
797
798                token.encode().map_err(|err| {
799                    QueryError::serialize_internal(format!(
800                        "failed to serialize continuation cursor: {err}"
801                    ))
802                })
803            })
804            .transpose()?;
805
806        Ok(PagedLoadExecutionWithTrace::new(
807            page.items,
808            next_cursor,
809            trace,
810        ))
811    }
812
813    /// Execute one grouped query page with optional grouped continuation cursor.
814    ///
815    /// This is the explicit grouped execution boundary; scalar load APIs reject
816    /// grouped plans to preserve scalar response contracts.
817    pub(in crate::db) fn execute_grouped<E>(
818        &self,
819        query: &Query<E>,
820        cursor_token: Option<&str>,
821    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
822    where
823        E: PersistedRow<Canister = C> + EntityValue,
824    {
825        let (page, trace) = self.execute_grouped_page_with_trace(query, cursor_token)?;
826        let next_cursor = page
827            .next_cursor
828            .map(|token| {
829                let Some(token) = token.as_grouped() else {
830                    return Err(QueryError::grouped_paged_emitted_scalar_continuation());
831                };
832
833                token.encode().map_err(|err| {
834                    QueryError::serialize_internal(format!(
835                        "failed to serialize grouped continuation cursor: {err}"
836                    ))
837                })
838            })
839            .transpose()?;
840
841        Ok(PagedGroupedExecutionWithTrace::new(
842            page.rows,
843            next_cursor,
844            trace,
845        ))
846    }
847
848    // Execute the canonical grouped query core and return the raw grouped page
849    // plus optional execution trace before outward cursor formatting.
850    fn execute_grouped_page_with_trace<E>(
851        &self,
852        query: &Query<E>,
853        cursor_token: Option<&str>,
854    ) -> Result<(GroupedCursorPage, Option<ExecutionTrace>), QueryError>
855    where
856        E: PersistedRow<Canister = C> + EntityValue,
857    {
858        // Phase 1: build the prepared execution plan once from the typed query.
859        let plan = self
860            .cached_prepared_query_plan_for_entity::<E>(query.structural())?
861            .0;
862
863        // Phase 2: reuse the shared prepared grouped execution path.
864        self.execute_grouped_plan_with_trace(plan, cursor_token)
865    }
866
867    // Execute one grouped prepared plan page with optional grouped cursor.
868    fn execute_grouped_plan_with_trace<E>(
869        &self,
870        plan: PreparedExecutionPlan<E>,
871        cursor_token: Option<&str>,
872    ) -> Result<(GroupedCursorPage, Option<ExecutionTrace>), QueryError>
873    where
874        E: PersistedRow<Canister = C> + EntityValue,
875    {
876        // Phase 1: validate the prepared plan shape before decoding cursors.
877        Self::ensure_grouped_execution_family(
878            plan.execution_family().map_err(QueryError::execute)?,
879        )?;
880
881        // Phase 2: decode external grouped cursor token and validate against plan.
882        let cursor = decode_optional_grouped_cursor_token(cursor_token)
883            .map_err(QueryError::from_cursor_plan_error)?;
884        let cursor = plan
885            .prepare_grouped_cursor_token(cursor)
886            .map_err(QueryError::from_executor_plan_error)?;
887
888        // Phase 3: execute one grouped page while preserving the structural
889        // grouped cursor payload for whichever outward cursor format the caller needs.
890        self.with_metrics(|| {
891            self.load_executor::<E>()
892                .execute_grouped_paged_with_cursor_traced(plan, cursor)
893        })
894        .map_err(QueryError::execute)
895    }
896}
897
898impl QueryPlanCacheKey {
899    fn for_authority(
900        authority: crate::db::executor::EntityAuthority,
901        schema_fingerprint: CommitSchemaFingerprint,
902        visibility: QueryPlanVisibility,
903        query: &StructuralQuery,
904    ) -> Self {
905        Self::for_authority_with_method_version(
906            authority,
907            schema_fingerprint,
908            visibility,
909            query,
910            SHARED_QUERY_PLAN_CACHE_METHOD_VERSION,
911        )
912    }
913
914    fn for_authority_with_method_version(
915        authority: crate::db::executor::EntityAuthority,
916        schema_fingerprint: CommitSchemaFingerprint,
917        visibility: QueryPlanVisibility,
918        query: &StructuralQuery,
919        cache_method_version: u8,
920    ) -> Self {
921        Self {
922            cache_method_version,
923            entity_path: authority.entity_path(),
924            schema_fingerprint,
925            visibility,
926            structural_query: query.structural_cache_key(),
927        }
928    }
929}