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