Skip to main content

icydb_core/db/session/query/
explain.rs

1//! Module: db::session::query::explain
2//! Responsibility: read-only query explain, trace, and plan-hash surfaces.
3//! Does not own: execution, cursor decoding, fluent terminal execution, or diagnostics attribution.
4//! Boundary: maps cached session-visible plans into query-facing diagnostic DTOs.
5
6use crate::{
7    db::{
8        DbSession, Query, QueryError, QueryTracePlan, TraceExecutionFamily,
9        access::summarize_executable_access_plan,
10        executor::ExecutionFamily,
11        query::builder::{AggregateExplain, ProjectionExplain},
12        query::explain::{
13            ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor, ExplainPlan,
14        },
15        query::plan::{AccessPlannedQuery, QueryMode, VisibleIndexes},
16        session::query::{QueryPlanCacheAttribution, query_plan_cache_reuse_event},
17    },
18    traits::{CanisterKind, EntityKind, EntityValue},
19};
20
21// Translate executor route-family selection into the query-owned trace label
22// at the session boundary so trace DTOs do not depend on executor types.
23const fn trace_execution_family_from_executor(family: ExecutionFamily) -> TraceExecutionFamily {
24    match family {
25        ExecutionFamily::PrimaryKey => TraceExecutionFamily::PrimaryKey,
26        ExecutionFamily::Ordered => TraceExecutionFamily::Ordered,
27        ExecutionFamily::Grouped => TraceExecutionFamily::Grouped,
28    }
29}
30
31impl<C: CanisterKind> DbSession<C> {
32    // Borrow the cached logical plan for read-only query diagnostics so explain
33    // and hash surfaces do not clone the full access-planned query.
34    fn try_map_cached_logical_query_plan<E, T>(
35        &self,
36        query: &Query<E>,
37        map: impl FnOnce(&AccessPlannedQuery) -> Result<T, QueryError>,
38    ) -> Result<T, QueryError>
39    where
40        E: EntityKind<Canister = C>,
41    {
42        self.try_map_cached_shared_query_plan_ref_for_entity::<E, T>(query, |prepared_plan| {
43            map(prepared_plan.logical_plan())
44        })
45    }
46
47    // Reuse the same cached logical plan as execution explain, then freeze the
48    // explain-only access-choice facts for the effective session-visible index
49    // slice before route facts are assembled.
50    fn cached_execution_explain_plan<E>(
51        &self,
52        query: &Query<E>,
53        visible_indexes: &VisibleIndexes<'_>,
54    ) -> Result<(AccessPlannedQuery, QueryPlanCacheAttribution), QueryError>
55    where
56        E: EntityKind<Canister = C>,
57    {
58        let (prepared_plan, cache_attribution) =
59            self.cached_shared_query_plan_for_entity::<E>(query)?;
60        let mut plan = prepared_plan.logical_plan().clone();
61
62        plan.finalize_access_choice_for_model_with_indexes(
63            query.structural().model(),
64            visible_indexes.as_slice(),
65        );
66
67        Ok((plan, cache_attribution))
68    }
69
70    // Project one logical explain payload using only planner-visible indexes.
71    pub(in crate::db) fn explain_query_with_visible_indexes<E>(
72        &self,
73        query: &Query<E>,
74    ) -> Result<ExplainPlan, QueryError>
75    where
76        E: EntityKind<Canister = C>,
77    {
78        self.try_map_cached_logical_query_plan(query, |plan| Ok(plan.explain()))
79    }
80
81    // Hash one typed query plan using only the indexes currently visible for
82    // the query's recovered store.
83    pub(in crate::db) fn query_plan_hash_hex_with_visible_indexes<E>(
84        &self,
85        query: &Query<E>,
86    ) -> Result<String, QueryError>
87    where
88        E: EntityKind<Canister = C>,
89    {
90        self.try_map_cached_logical_query_plan(query, |plan| Ok(plan.fingerprint().to_string()))
91    }
92
93    // Explain one load execution shape using only planner-visible
94    // indexes from the recovered store state.
95    pub(in crate::db) fn explain_query_execution_with_visible_indexes<E>(
96        &self,
97        query: &Query<E>,
98    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
99    where
100        E: EntityValue + EntityKind<Canister = C>,
101    {
102        self.with_query_visible_indexes(query, |query, visible_indexes| {
103            let (plan, _) = self.cached_execution_explain_plan::<E>(query, visible_indexes)?;
104
105            query
106                .structural()
107                .explain_execution_descriptor_from_plan(&plan)
108        })
109    }
110
111    // Render one load execution descriptor plus route diagnostics using
112    // only planner-visible indexes from the recovered store state.
113    pub(in crate::db) fn explain_query_execution_verbose_with_visible_indexes<E>(
114        &self,
115        query: &Query<E>,
116    ) -> Result<String, QueryError>
117    where
118        E: EntityValue + EntityKind<Canister = C>,
119    {
120        self.with_query_visible_indexes(query, |query, visible_indexes| {
121            let (plan, cache_attribution) =
122                self.cached_execution_explain_plan::<E>(query, visible_indexes)?;
123
124            query
125                .structural()
126                .finalized_execution_diagnostics_from_plan_with_descriptor_mutator(
127                    &plan,
128                    Some(query_plan_cache_reuse_event(cache_attribution)),
129                    |_| {},
130                )
131                .map(|diagnostics| diagnostics.render_text_verbose())
132        })
133    }
134
135    // Explain one prepared fluent aggregate terminal from the same cached
136    // prepared plan used by execution.
137    pub(in crate::db) fn explain_query_prepared_aggregate_terminal_with_visible_indexes<E, S>(
138        &self,
139        query: &Query<E>,
140        strategy: &S,
141    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
142    where
143        E: EntityValue + EntityKind<Canister = C>,
144        S: AggregateExplain,
145    {
146        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
147
148        plan.explain_prepared_aggregate_terminal(strategy)
149    }
150
151    // Explain one `bytes_by(field)` terminal from the same cached prepared
152    // plan used by execution.
153    pub(in crate::db) fn explain_query_bytes_by_with_visible_indexes<E>(
154        &self,
155        query: &Query<E>,
156        target_field: &str,
157    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
158    where
159        E: EntityValue + EntityKind<Canister = C>,
160    {
161        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
162
163        plan.explain_bytes_by_terminal(target_field)
164    }
165
166    // Explain one prepared fluent projection terminal from the same cached
167    // prepared plan used by execution.
168    pub(in crate::db) fn explain_query_prepared_projection_terminal_with_visible_indexes<E, S>(
169        &self,
170        query: &Query<E>,
171        strategy: &S,
172    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
173    where
174        E: EntityValue + EntityKind<Canister = C>,
175        S: ProjectionExplain,
176    {
177        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
178
179        plan.explain_prepared_projection_terminal(strategy)
180    }
181
182    /// Build one trace payload for a query without executing it.
183    ///
184    /// This lightweight surface is intended for developer diagnostics:
185    /// plan hash, access strategy summary, and planner/executor route shape.
186    pub fn trace_query<E>(&self, query: &Query<E>) -> Result<QueryTracePlan, QueryError>
187    where
188        E: EntityKind<Canister = C>,
189    {
190        let (prepared_plan, cache_attribution) =
191            self.cached_prepared_query_plan_for_entity::<E>(query)?;
192        let logical_plan = prepared_plan.logical_plan();
193        let explain = logical_plan.explain();
194        let plan_hash = logical_plan.fingerprint().to_string();
195        let executable_access = prepared_plan.access().executable_contract();
196        let access_strategy = summarize_executable_access_plan(&executable_access);
197        let execution_family = match prepared_plan.mode() {
198            QueryMode::Load(_) => Some(trace_execution_family_from_executor(
199                prepared_plan
200                    .execution_family()
201                    .map_err(QueryError::execute)?,
202            )),
203            QueryMode::Delete(_) => None,
204        };
205        let reuse = query_plan_cache_reuse_event(cache_attribution);
206
207        Ok(QueryTracePlan::new(
208            plan_hash,
209            access_strategy,
210            execution_family,
211            reuse,
212            explain,
213        ))
214    }
215}