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