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::{EntityAuthority, 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<
55        (
56            AccessPlannedQuery,
57            EntityAuthority,
58            QueryPlanCacheAttribution,
59        ),
60        QueryError,
61    >
62    where
63        E: EntityKind<Canister = C>,
64    {
65        let (prepared_plan, cache_attribution) =
66            self.cached_shared_query_plan_for_entity::<E>(query)?;
67        let mut plan = prepared_plan.logical_plan().clone();
68        let authority = prepared_plan.authority();
69        let schema_info = authority.accepted_schema_info().ok_or_else(|| {
70            QueryError::invariant(
71                "session query execution explain lost accepted schema authority before access-choice finalization",
72            )
73        })?;
74
75        plan.finalize_access_choice_for_model_with_accepted_indexes_and_schema(
76            query.structural().model(),
77            visible_indexes.accepted_field_path_indexes(),
78            visible_indexes.accepted_expression_indexes(),
79            schema_info,
80        );
81
82        Ok((plan, authority, cache_attribution))
83    }
84
85    // Project one logical explain payload using only planner-visible indexes.
86    pub(in crate::db) fn explain_query_with_visible_indexes<E>(
87        &self,
88        query: &Query<E>,
89    ) -> Result<ExplainPlan, QueryError>
90    where
91        E: EntityKind<Canister = C>,
92    {
93        self.with_query_visible_indexes(query, |query, visible_indexes| {
94            self.try_map_cached_logical_query_plan(query, |plan| {
95                let mut plan = plan.clone();
96                let schema_info = visible_indexes.accepted_schema_info().ok_or_else(|| {
97                    QueryError::invariant(
98                        "session query explain lost accepted schema visibility before access-choice finalization",
99                    )
100                })?;
101                plan.finalize_access_choice_for_model_with_accepted_indexes_and_schema(
102                    query.structural().model(),
103                    visible_indexes.accepted_field_path_indexes(),
104                    visible_indexes.accepted_expression_indexes(),
105                    schema_info,
106                );
107
108                Ok(plan.explain())
109            })
110        })
111    }
112
113    // Hash one typed query plan using only the indexes currently visible for
114    // the query's recovered store.
115    pub(in crate::db) fn query_plan_hash_hex_with_visible_indexes<E>(
116        &self,
117        query: &Query<E>,
118    ) -> Result<String, QueryError>
119    where
120        E: EntityKind<Canister = C>,
121    {
122        self.try_map_cached_logical_query_plan(query, |plan| Ok(plan.fingerprint().to_string()))
123    }
124
125    // Explain one load execution shape using only planner-visible
126    // indexes from the recovered store state.
127    pub(in crate::db) fn explain_query_execution_with_visible_indexes<E>(
128        &self,
129        query: &Query<E>,
130    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
131    where
132        E: EntityValue + EntityKind<Canister = C>,
133    {
134        self.with_query_visible_indexes(query, |query, visible_indexes| {
135            let (plan, authority, _) =
136                self.cached_execution_explain_plan::<E>(query, visible_indexes)?;
137
138            query
139                .structural()
140                .explain_execution_descriptor_from_plan_with_authority(&plan, &authority)
141        })
142    }
143
144    // Render one load execution descriptor plus route diagnostics using
145    // only planner-visible indexes from the recovered store state.
146    pub(in crate::db) fn explain_query_execution_verbose_with_visible_indexes<E>(
147        &self,
148        query: &Query<E>,
149    ) -> Result<String, QueryError>
150    where
151        E: EntityValue + EntityKind<Canister = C>,
152    {
153        self.with_query_visible_indexes(query, |query, visible_indexes| {
154            let (plan, authority, cache_attribution) =
155                self.cached_execution_explain_plan::<E>(query, visible_indexes)?;
156
157            query
158                .structural()
159                .finalized_execution_diagnostics_from_plan_with_authority_and_descriptor_mutator(
160                    &plan,
161                    &authority,
162                    Some(query_plan_cache_reuse_event(cache_attribution)),
163                    |_| {},
164                )
165                .map(|diagnostics| diagnostics.render_text_verbose())
166        })
167    }
168
169    // Explain one prepared fluent aggregate terminal from the same cached
170    // prepared plan used by execution.
171    pub(in crate::db) fn explain_query_prepared_aggregate_terminal_with_visible_indexes<E, S>(
172        &self,
173        query: &Query<E>,
174        strategy: &S,
175    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
176    where
177        E: EntityValue + EntityKind<Canister = C>,
178        S: AggregateExplain,
179    {
180        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
181
182        plan.explain_prepared_aggregate_terminal(strategy)
183    }
184
185    // Explain one `bytes_by(field)` terminal from the same cached prepared
186    // plan used by execution.
187    pub(in crate::db) fn explain_query_bytes_by_with_visible_indexes<E>(
188        &self,
189        query: &Query<E>,
190        target_field: &str,
191    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
192    where
193        E: EntityValue + EntityKind<Canister = C>,
194    {
195        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
196
197        plan.explain_bytes_by_terminal(target_field)
198    }
199
200    // Explain one prepared fluent projection terminal from the same cached
201    // prepared plan used by execution.
202    pub(in crate::db) fn explain_query_prepared_projection_terminal_with_visible_indexes<E, S>(
203        &self,
204        query: &Query<E>,
205        strategy: &S,
206    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
207    where
208        E: EntityValue + EntityKind<Canister = C>,
209        S: ProjectionExplain,
210    {
211        let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query)?;
212
213        plan.explain_prepared_projection_terminal(strategy)
214    }
215
216    /// Build one trace payload for a query without executing it.
217    ///
218    /// This lightweight surface is intended for developer diagnostics:
219    /// plan hash, access strategy summary, and planner/executor route shape.
220    pub fn trace_query<E>(&self, query: &Query<E>) -> Result<QueryTracePlan, QueryError>
221    where
222        E: EntityKind<Canister = C>,
223    {
224        let (prepared_plan, cache_attribution) =
225            self.cached_prepared_query_plan_for_entity::<E>(query)?;
226        let logical_plan = prepared_plan.logical_plan();
227        let explain = logical_plan.explain();
228        let plan_hash = logical_plan.fingerprint().to_string();
229        let executable_access = prepared_plan.access().executable_contract();
230        let access_strategy = summarize_executable_access_plan(&executable_access);
231        let execution_family = match prepared_plan.mode() {
232            QueryMode::Load(_) => Some(trace_execution_family_from_executor(
233                prepared_plan
234                    .execution_family()
235                    .map_err(QueryError::execute)?,
236            )),
237            QueryMode::Delete(_) => None,
238        };
239        let reuse = query_plan_cache_reuse_event(cache_attribution);
240
241        Ok(QueryTracePlan::new(
242            plan_hash,
243            access_strategy,
244            execution_family,
245            reuse,
246            explain,
247        ))
248    }
249}