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::{
11            EntityAuthority, ExecutionFamily, initial_read_plan_requires_materialized_sort,
12        },
13        query::admission::{
14            QueryAdmissionPolicy, QueryAdmissionSummary, QueryMaterializationSummary,
15        },
16        query::builder::{AggregateExplain, ProjectionExplain},
17        query::explain::{
18            ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor, ExplainPlan,
19        },
20        query::plan::{AccessPlannedQuery, QueryMode, VisibleIndexes},
21        session::query::{QueryPlanCacheAttribution, query_plan_cache_reuse_event},
22    },
23    traits::{CanisterKind, EntityKind, EntityValue},
24};
25
26// Translate executor route-family selection into the query-owned trace label
27// at the session boundary so trace DTOs do not depend on executor types.
28const fn trace_execution_family_from_executor(family: ExecutionFamily) -> TraceExecutionFamily {
29    match family {
30        ExecutionFamily::PrimaryKey => TraceExecutionFamily::PrimaryKey,
31        ExecutionFamily::Ordered => TraceExecutionFamily::Ordered,
32        ExecutionFamily::Grouped => TraceExecutionFamily::Grouped,
33    }
34}
35
36impl<C: CanisterKind> DbSession<C> {
37    // Reuse one cached logical plan, then freeze the explain-only
38    // access-choice facts for the effective session-visible index slice before
39    // descriptor or route facts are assembled.
40    fn cached_finalized_explain_plan<E>(
41        &self,
42        query: &Query<E>,
43        visible_indexes: &VisibleIndexes<'_>,
44    ) -> Result<
45        (
46            AccessPlannedQuery,
47            EntityAuthority,
48            QueryPlanCacheAttribution,
49        ),
50        QueryError,
51    >
52    where
53        E: EntityKind<Canister = C>,
54    {
55        let (prepared_plan, cache_attribution) =
56            self.cached_shared_query_plan_for_entity::<E>(query)?;
57        let mut plan = prepared_plan.logical_plan().clone();
58        let authority = prepared_plan.authority();
59        let schema_info = authority
60            .accepted_schema_info()
61            .ok_or_else(QueryError::invariant)?;
62
63        plan.finalize_access_choice_for_model_with_semantic_indexes_and_schema(
64            query.structural().model(),
65            visible_indexes.accepted_semantic_index_contracts(),
66            schema_info,
67        );
68
69        Ok((plan, authority, 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.with_query_visible_indexes(query, |query, visible_indexes| {
81            let (plan, _, _) = self.cached_finalized_explain_plan::<E>(query, visible_indexes)?;
82
83            Ok(plan.explain())
84        })
85    }
86
87    // Hash one typed query plan using only the indexes currently visible for
88    // the query's recovered store.
89    pub(in crate::db) fn query_plan_hash_hex_with_visible_indexes<E>(
90        &self,
91        query: &Query<E>,
92    ) -> Result<String, QueryError>
93    where
94        E: EntityKind<Canister = C>,
95    {
96        let (prepared_plan, _) = self.cached_shared_query_plan_for_entity::<E>(query)?;
97
98        Ok(prepared_plan.plan_hash_hex())
99    }
100
101    /// Require one typed/fluent query plan to satisfy the default bounded read policy.
102    #[doc(hidden)]
103    pub fn ensure_default_query_read_admission<E>(
104        &self,
105        query: &Query<E>,
106    ) -> Result<QueryAdmissionSummary, QueryError>
107    where
108        E: EntityKind<Canister = C>,
109    {
110        self.ensure_query_read_admission_policy(
111            query,
112            &QueryAdmissionPolicy::default_bounded_read(),
113        )
114    }
115
116    /// Evaluate one typed/fluent query plan against a read-admission policy.
117    ///
118    /// This does not execute rows or prove a final response-byte size. Public
119    /// endpoints that return typed data must still enforce their outward
120    /// response budget after shaping the response.
121    pub(in crate::db) fn evaluate_query_read_admission_policy<E>(
122        &self,
123        query: &Query<E>,
124        policy: &QueryAdmissionPolicy,
125    ) -> Result<QueryAdmissionSummary, QueryError>
126    where
127        E: EntityKind<Canister = C>,
128    {
129        let (prepared_plan, _) = self.cached_shared_query_plan_for_entity::<E>(query)?;
130        let mut summary =
131            QueryAdmissionSummary::from_plan(policy.lane(), prepared_plan.logical_plan());
132
133        if initial_read_plan_requires_materialized_sort(&prepared_plan)
134            .map_err(QueryError::execute)?
135        {
136            let returned_row_bound = summary.returned_row_bound();
137            let returned_row_bound_kind = summary.returned_row_bound_kind();
138            summary = summary.with_materialization(QueryMaterializationSummary::sort(
139                returned_row_bound,
140                returned_row_bound_kind,
141            ));
142        }
143
144        Ok(policy.evaluate(summary))
145    }
146
147    /// Require one typed/fluent query plan to be admitted by a read-admission policy.
148    ///
149    /// This returns the admitted plan summary on success, or the same compact
150    /// read-admission `QueryError` family used by policy-bound SQL reads.
151    pub(in crate::db) fn ensure_query_read_admission_policy<E>(
152        &self,
153        query: &Query<E>,
154        policy: &QueryAdmissionPolicy,
155    ) -> Result<QueryAdmissionSummary, QueryError>
156    where
157        E: EntityKind<Canister = C>,
158    {
159        let admission = self.evaluate_query_read_admission_policy(query, policy)?;
160
161        if let Some(rejection) = admission.rejection() {
162            Err(QueryError::from(rejection.code()))
163        } else {
164            Ok(admission)
165        }
166    }
167
168    // Explain one load execution shape using only planner-visible
169    // indexes from the recovered store state.
170    pub(in crate::db) fn explain_query_execution_with_visible_indexes<E>(
171        &self,
172        query: &Query<E>,
173    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
174    where
175        E: EntityValue + EntityKind<Canister = C>,
176    {
177        self.with_query_visible_indexes(query, |query, visible_indexes| {
178            let (plan, authority, _) =
179                self.cached_finalized_explain_plan::<E>(query, visible_indexes)?;
180
181            query
182                .structural()
183                .explain_execution_descriptor_from_plan_with_authority(&plan, &authority)
184        })
185    }
186
187    // Render one load execution descriptor plus route diagnostics using
188    // only planner-visible indexes from the recovered store state.
189    pub(in crate::db) fn explain_query_execution_verbose_with_visible_indexes<E>(
190        &self,
191        query: &Query<E>,
192    ) -> Result<String, QueryError>
193    where
194        E: EntityValue + EntityKind<Canister = C>,
195    {
196        self.with_query_visible_indexes(query, |query, visible_indexes| {
197            let (plan, authority, cache_attribution) =
198                self.cached_finalized_explain_plan::<E>(query, visible_indexes)?;
199
200            query
201                .structural()
202                .finalized_execution_diagnostics_from_plan_with_authority_and_descriptor_mutator(
203                    &plan,
204                    &authority,
205                    Some(query_plan_cache_reuse_event(cache_attribution)),
206                    |_| {},
207                )
208                .map(|diagnostics| diagnostics.render_text_verbose())
209        })
210    }
211
212    // Render one finalized load execution JSON payload using only
213    // planner-visible indexes from the recovered store state.
214    pub(in crate::db) fn explain_query_execution_json_with_visible_indexes<E>(
215        &self,
216        query: &Query<E>,
217    ) -> Result<String, QueryError>
218    where
219        E: EntityValue + EntityKind<Canister = C>,
220    {
221        self.with_query_visible_indexes(query, |query, visible_indexes| {
222            let (plan, authority, cache_attribution) =
223                self.cached_finalized_explain_plan::<E>(query, visible_indexes)?;
224
225            query
226                .structural()
227                .finalized_execution_diagnostics_from_plan_with_authority_and_descriptor_mutator(
228                    &plan,
229                    &authority,
230                    Some(query_plan_cache_reuse_event(cache_attribution)),
231                    |_| {},
232                )
233                .map(|diagnostics| diagnostics.render_json_canonical())
234        })
235    }
236
237    // Explain one prepared fluent aggregate terminal from the same cached
238    // prepared plan used by execution.
239    pub(in crate::db) fn explain_query_prepared_aggregate_terminal_with_visible_indexes<E, S>(
240        &self,
241        query: &Query<E>,
242        strategy: &S,
243    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
244    where
245        E: EntityKind<Canister = C>,
246        S: AggregateExplain,
247    {
248        let (plan, _) = self.cached_shared_query_plan_for_entity::<E>(query)?;
249
250        plan.explain_prepared_aggregate_terminal(strategy)
251    }
252
253    // Explain one `bytes_by(field)` terminal from the same cached prepared
254    // plan used by execution.
255    pub(in crate::db) fn explain_query_bytes_by_with_visible_indexes<E>(
256        &self,
257        query: &Query<E>,
258        target_field: &str,
259    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
260    where
261        E: EntityKind<Canister = C>,
262    {
263        let (plan, _) = self.cached_shared_query_plan_for_entity::<E>(query)?;
264
265        plan.explain_bytes_by_terminal(target_field)
266    }
267
268    // Explain one prepared fluent projection terminal from the same cached
269    // prepared plan used by execution.
270    pub(in crate::db) fn explain_query_prepared_projection_terminal_with_visible_indexes<E, S>(
271        &self,
272        query: &Query<E>,
273        strategy: &S,
274    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
275    where
276        E: EntityKind<Canister = C>,
277        S: ProjectionExplain,
278    {
279        let (plan, _) = self.cached_shared_query_plan_for_entity::<E>(query)?;
280
281        plan.explain_prepared_projection_terminal(strategy)
282    }
283
284    /// Build one trace payload for a query without executing it.
285    ///
286    /// This lightweight surface is intended for developer diagnostics:
287    /// plan hash, access strategy summary, and planner/executor route shape.
288    pub fn trace_query<E>(&self, query: &Query<E>) -> Result<QueryTracePlan, QueryError>
289    where
290        E: EntityKind<Canister = C>,
291    {
292        let (prepared_plan, cache_attribution) =
293            self.cached_shared_query_plan_for_entity::<E>(query)?;
294        let logical_plan = prepared_plan.logical_plan();
295        let explain = logical_plan.explain();
296        let plan_hash = prepared_plan.plan_hash_hex();
297        let executable_access = prepared_plan.access().executable_contract();
298        let access_strategy = summarize_executable_access_plan(&executable_access);
299        let execution_family = match prepared_plan.mode() {
300            QueryMode::Load(_) => Some(trace_execution_family_from_executor(
301                prepared_plan
302                    .execution_family()
303                    .map_err(QueryError::execute)?,
304            )),
305            QueryMode::Delete(_) => None,
306        };
307        let reuse = query_plan_cache_reuse_event(cache_attribution);
308
309        Ok(QueryTracePlan::new(
310            plan_hash,
311            access_strategy,
312            execution_family,
313            reuse,
314            explain,
315        ))
316    }
317}