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