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