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