Skip to main content

icydb_core/db/executor/explain/
mod.rs

1//! Module: db::executor::explain
2//! Responsibility: assemble executor-owned EXPLAIN descriptor payloads.
3//! Does not own: explain rendering formats or logical plan projection.
4//! Boundary: centralized execution-plan-to-descriptor mapping used by EXPLAIN surfaces.
5
6mod descriptor;
7
8#[cfg(test)]
9use crate::db::executor::planning::route::AggregateRouteShape;
10#[cfg(test)]
11use crate::db::query::builder::AggregateExpr;
12use crate::{
13    db::{
14        Query, TraceReuseEvent,
15        executor::{EntityAuthority, SharedPreparedExecutionPlan},
16        query::{
17            admission::{QueryAdmissionLane, QueryAdmissionPolicy, QueryAdmissionSummary},
18            builder::{AggregateExplain, ProjectionExplain},
19            explain::{
20                ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor,
21                ExplainExecutionNodeType, ExplainOrderPushdown, FinalizedQueryDiagnostics,
22                property_keys,
23            },
24            intent::{QueryError, StructuralQuery},
25            plan::{AccessPlannedQuery, VisibleIndexes},
26        },
27    },
28    model::entity::EntityModel,
29    traits::{EntityKind, EntityValue},
30    value::Value,
31};
32
33#[cfg(all(test, feature = "sql-explain"))]
34pub(in crate::db) use descriptor::assemble_load_execution_node_descriptor;
35use descriptor::assemble_load_execution_verbose_diagnostics_from_route_facts;
36#[cfg(feature = "sql-explain")]
37pub(in crate::db) use descriptor::assemble_scalar_aggregate_execution_descriptor_with_projection;
38pub(in crate::db) use descriptor::{
39    assemble_aggregate_terminal_execution_descriptor,
40    assemble_load_execution_node_descriptor_for_authority,
41    assemble_load_execution_node_descriptor_from_route_facts,
42    freeze_load_execution_route_facts_for_authority,
43    freeze_load_execution_route_facts_for_model_only,
44};
45
46struct DescriptorStagePresence {
47    present: [bool; Self::STAGE_COUNT],
48}
49
50impl DescriptorStagePresence {
51    const STAGE_COUNT: usize = 4;
52    const TOP_N_SEEK: usize = 0;
53    const INDEX_RANGE_LIMIT_PUSHDOWN: usize = 1;
54    const INDEX_PREDICATE_PREFILTER: usize = 2;
55    const RESIDUAL_FILTER: usize = 3;
56
57    fn from_descriptor(descriptor: &ExplainExecutionNodeDescriptor) -> Self {
58        let mut presence = Self {
59            present: [false; Self::STAGE_COUNT],
60        };
61
62        descriptor.for_each_preorder(&mut |node| match node.node_type() {
63            ExplainExecutionNodeType::TopNSeek => presence.present[Self::TOP_N_SEEK] = true,
64            ExplainExecutionNodeType::IndexRangeLimitPushdown => {
65                presence.present[Self::INDEX_RANGE_LIMIT_PUSHDOWN] = true;
66            }
67            ExplainExecutionNodeType::IndexPredicatePrefilter => {
68                presence.present[Self::INDEX_PREDICATE_PREFILTER] = true;
69            }
70            ExplainExecutionNodeType::ResidualFilter => {
71                presence.present[Self::RESIDUAL_FILTER] = true;
72            }
73            _ => {}
74        });
75
76        presence
77    }
78
79    const fn has_top_n_seek(&self) -> bool {
80        self.present[Self::TOP_N_SEEK]
81    }
82
83    const fn has_index_range_limit_pushdown(&self) -> bool {
84        self.present[Self::INDEX_RANGE_LIMIT_PUSHDOWN]
85    }
86
87    const fn has_index_predicate_prefilter(&self) -> bool {
88        self.present[Self::INDEX_PREDICATE_PREFILTER]
89    }
90
91    const fn has_residual_filter(&self) -> bool {
92        self.present[Self::RESIDUAL_FILTER]
93    }
94}
95
96impl StructuralQuery {
97    // Assemble one finalized diagnostics artifact from route facts that were
98    // already frozen by the caller-selected schema authority.
99    fn finalized_execution_diagnostics_from_route_facts(
100        plan: &AccessPlannedQuery,
101        route_facts: &crate::db::executor::explain::descriptor::LoadExecutionRouteFacts,
102        reuse: Option<TraceReuseEvent>,
103    ) -> Result<FinalizedQueryDiagnostics, QueryError> {
104        let descriptor =
105            assemble_load_execution_node_descriptor_from_route_facts(plan, route_facts)
106                .map_err(QueryError::execute)?;
107        let route_diagnostics =
108            assemble_load_execution_verbose_diagnostics_from_route_facts(plan, route_facts)
109                .map_err(QueryError::execute)?;
110        let explain = plan.explain();
111
112        // Phase 1: add descriptor-stage summaries for key execution operators.
113        let stage_presence = DescriptorStagePresence::from_descriptor(&descriptor);
114        let mut logical_diagnostics = Vec::new();
115        logical_diagnostics.push(format!(
116            "diag.d.has_top_n_seek={}",
117            stage_presence.has_top_n_seek()
118        ));
119        logical_diagnostics.push(format!(
120            "diag.d.has_index_range_limit_pushdown={}",
121            stage_presence.has_index_range_limit_pushdown()
122        ));
123        logical_diagnostics.push(format!(
124            "diag.d.has_index_predicate_prefilter={}",
125            stage_presence.has_index_predicate_prefilter()
126        ));
127        logical_diagnostics.push(format!(
128            "diag.d.has_residual_filter={}",
129            stage_presence.has_residual_filter()
130        ));
131
132        // Phase 2: append logical-plan diagnostics relevant to verbose explain.
133        logical_diagnostics.push(format!("diag.p.mode={:?}", explain.mode()));
134        logical_diagnostics.push(format!(
135            "diag.p.order_pushdown={}",
136            plan_order_pushdown_label(explain.order_pushdown())
137        ));
138        logical_diagnostics.push(format!(
139            "diag.p.predicate_pushdown={}",
140            plan.predicate_pushdown_label()
141        ));
142        logical_diagnostics.push(format!(
143            "diag.p.predicate_pushdown_outcome={}",
144            plan.predicate_pushdown_outcome_label()
145        ));
146        logical_diagnostics.push(format!(
147            "diag.p.predicate_pushdown_reason={}",
148            plan.predicate_pushdown_reason_label()
149        ));
150        logical_diagnostics.push(format!("diag.p.distinct={}", explain.distinct()));
151        logical_diagnostics.push(format!("diag.p.page={:?}", explain.page()));
152        logical_diagnostics.push(format!("diag.p.consistency={:?}", explain.consistency()));
153
154        let admission = QueryAdmissionPolicy::diagnostic_explain().evaluate(
155            QueryAdmissionSummary::from_plan(QueryAdmissionLane::DiagnosticExplain, plan),
156        );
157
158        Ok(FinalizedQueryDiagnostics::new(
159            descriptor,
160            route_diagnostics,
161            logical_diagnostics,
162            reuse,
163        )
164        .with_admission(admission))
165    }
166
167    // Assemble one model-only execution descriptor from a previously built
168    // access plan so standalone text/json/verbose explain surfaces do not each
169    // rebuild it.
170    pub(in crate::db) fn explain_execution_descriptor_from_model_only_plan(
171        &self,
172        plan: &AccessPlannedQuery,
173    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
174        let primary_key_names = model_primary_key_names(self.model());
175        let route_facts = freeze_load_execution_route_facts_for_model_only(
176            self.model().fields(),
177            primary_key_names.as_slice(),
178            plan,
179        )
180        .map_err(QueryError::execute)?;
181
182        assemble_load_execution_node_descriptor_from_route_facts(plan, &route_facts)
183            .map_err(QueryError::execute)
184    }
185
186    // Assemble one execution descriptor from accepted executor authority.
187    pub(in crate::db) fn explain_execution_descriptor_from_plan_with_authority(
188        &self,
189        plan: &AccessPlannedQuery,
190        authority: &EntityAuthority,
191    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
192        debug_assert_eq!(self.model().path(), authority.entity_path());
193        let route_facts = freeze_load_execution_route_facts_for_authority(authority, plan)
194            .map_err(QueryError::execute)?;
195
196        assemble_load_execution_node_descriptor_from_route_facts(plan, &route_facts)
197            .map_err(QueryError::execute)
198    }
199
200    // Render one standalone model-only verbose execution explain payload from
201    // a single access plan, freezing one immutable diagnostics artifact instead
202    // of returning one wrapper-owned line list that callers still have to
203    // extend locally.
204    fn finalized_execution_diagnostics_from_model_only_plan(
205        &self,
206        plan: &AccessPlannedQuery,
207        reuse: Option<TraceReuseEvent>,
208    ) -> Result<FinalizedQueryDiagnostics, QueryError> {
209        let primary_key_names = model_primary_key_names(self.model());
210        let route_facts = freeze_load_execution_route_facts_for_model_only(
211            self.model().fields(),
212            primary_key_names.as_slice(),
213            plan,
214        )
215        .map_err(QueryError::execute)?;
216
217        Self::finalized_execution_diagnostics_from_route_facts(plan, &route_facts, reuse)
218    }
219
220    /// Freeze one immutable diagnostics artifact through accepted executor
221    /// authority while still allowing one caller-owned descriptor mutation.
222    pub(in crate::db) fn finalized_execution_diagnostics_from_plan_with_authority_and_descriptor_mutator(
223        &self,
224        plan: &AccessPlannedQuery,
225        authority: &EntityAuthority,
226        reuse: Option<TraceReuseEvent>,
227        mutate_descriptor: impl FnOnce(&mut ExplainExecutionNodeDescriptor),
228    ) -> Result<FinalizedQueryDiagnostics, QueryError> {
229        debug_assert_eq!(self.model().path(), authority.entity_path());
230        let route_facts = freeze_load_execution_route_facts_for_authority(authority, plan)
231            .map_err(QueryError::execute)?;
232        let mut diagnostics =
233            Self::finalized_execution_diagnostics_from_route_facts(plan, &route_facts, reuse)?;
234        mutate_descriptor(&mut diagnostics.execution);
235
236        Ok(diagnostics)
237    }
238
239    // Render one verbose execution explain payload using only the canonical
240    // diagnostics artifact owned by this executor boundary.
241    fn explain_execution_verbose_from_plan(
242        &self,
243        plan: &AccessPlannedQuery,
244    ) -> Result<String, QueryError> {
245        self.finalized_execution_diagnostics_from_model_only_plan(plan, None)
246            .map(|diagnostics| diagnostics.render_text_verbose())
247    }
248
249    // Freeze one explain-only access-choice snapshot from accepted
250    // planner-visible indexes before building descriptor diagnostics.
251    fn finalize_explain_access_choice_for_visible_indexes(
252        &self,
253        plan: &mut AccessPlannedQuery,
254        visible_indexes: &VisibleIndexes<'_>,
255    ) {
256        if let Some(schema_info) = visible_indexes.accepted_schema_info() {
257            plan.finalize_access_choice_for_model_with_semantic_indexes_and_schema(
258                self.model(),
259                visible_indexes.accepted_semantic_index_contracts(),
260                schema_info,
261            );
262            return;
263        }
264
265        plan.finalize_access_choice_for_model_only_with_indexes(
266            self.model(),
267            visible_indexes.generated_model_only_indexes(),
268        );
269    }
270
271    // Freeze one explicit model-only access-choice snapshot for standalone
272    // query explain surfaces that intentionally do not have accepted runtime
273    // schema authority.
274    fn finalize_explain_access_choice_for_model_only(&self, plan: &mut AccessPlannedQuery) {
275        plan.finalize_access_choice_for_model_only_with_indexes(
276            self.model(),
277            self.model().indexes(),
278        );
279    }
280
281    // Build and freeze one explicit model-only access-plan snapshot for
282    // standalone query explain surfaces.
283    fn finalized_model_only_explain_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
284        let mut plan = self.build_plan()?;
285        self.finalize_explain_access_choice_for_model_only(&mut plan);
286
287        Ok(plan)
288    }
289
290    // Build and freeze one access-plan snapshot using a caller-provided
291    // visible-index slice for runtime/session explain surfaces.
292    fn finalized_visible_indexes_explain_plan(
293        &self,
294        visible_indexes: &VisibleIndexes<'_>,
295    ) -> Result<AccessPlannedQuery, QueryError> {
296        let mut plan = self.build_plan_with_visible_indexes(visible_indexes)?;
297        self.finalize_explain_access_choice_for_visible_indexes(&mut plan, visible_indexes);
298
299        Ok(plan)
300    }
301
302    // Build one explicit model-only execution descriptor for standalone query
303    // surfaces that are not bound to a recovered store/accepted schema.
304    fn explain_execution_descriptor_for_model_only(
305        &self,
306    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
307        let plan = self.finalized_model_only_explain_plan()?;
308
309        self.explain_execution_descriptor_from_model_only_plan(&plan)
310    }
311
312    // Build one execution descriptor using the caller-resolved accepted visible
313    // indexes for runtime/session explain.
314    fn explain_execution_descriptor_for_visible_indexes(
315        &self,
316        visible_indexes: &VisibleIndexes<'_>,
317    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
318        let plan = self.finalized_visible_indexes_explain_plan(visible_indexes)?;
319
320        self.explain_execution_descriptor_from_model_only_plan(&plan)
321    }
322
323    // Render one explicit model-only verbose execution payload for standalone
324    // query surfaces that are not bound to a recovered store/accepted schema.
325    fn render_execution_verbose_for_model_only(&self) -> Result<String, QueryError> {
326        let plan = self.finalized_model_only_explain_plan()?;
327
328        self.explain_execution_verbose_from_plan(&plan)
329    }
330
331    // Render one verbose execution payload using the caller-resolved accepted
332    // visible indexes for runtime/session explain.
333    fn explain_execution_verbose_for_visible_indexes(
334        &self,
335        visible_indexes: &VisibleIndexes<'_>,
336    ) -> Result<String, QueryError> {
337        let plan = self.finalized_visible_indexes_explain_plan(visible_indexes)?;
338
339        self.explain_execution_verbose_from_plan(&plan)
340    }
341
342    /// Explain one model-only load execution shape through the structural query core.
343    #[inline(never)]
344    pub(in crate::db) fn explain_execution_for_model_only(
345        &self,
346    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
347        self.explain_execution_descriptor_for_model_only()
348    }
349
350    /// Explain one load execution shape using a caller-visible index slice.
351    #[inline(never)]
352    pub(in crate::db) fn explain_execution_with_visible_indexes(
353        &self,
354        visible_indexes: &VisibleIndexes<'_>,
355    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
356        self.explain_execution_descriptor_for_visible_indexes(visible_indexes)
357    }
358
359    /// Render one model-only verbose scalar load execution payload through the
360    /// shared structural descriptor and route-diagnostics paths.
361    #[inline(never)]
362    pub(in crate::db) fn explain_execution_verbose_for_model_only(
363        &self,
364    ) -> Result<String, QueryError> {
365        self.render_execution_verbose_for_model_only()
366    }
367
368    /// Render one verbose scalar load execution payload using visible indexes.
369    #[inline(never)]
370    pub(in crate::db) fn explain_execution_verbose_with_visible_indexes(
371        &self,
372        visible_indexes: &VisibleIndexes<'_>,
373    ) -> Result<String, QueryError> {
374        self.explain_execution_verbose_for_visible_indexes(visible_indexes)
375    }
376
377    /// Explain one aggregate terminal execution route without running it.
378    #[inline(never)]
379    #[cfg(test)]
380    pub(in crate::db) fn explain_aggregate_terminal_with_visible_indexes(
381        &self,
382        visible_indexes: &VisibleIndexes<'_>,
383        aggregate: AggregateRouteShape<'_>,
384    ) -> Result<ExplainAggregateTerminalPlan, QueryError> {
385        let plan = self.build_plan_with_visible_indexes(visible_indexes)?;
386        let query_explain = plan.explain();
387        let terminal = aggregate.kind();
388        let execution = assemble_aggregate_terminal_execution_descriptor(&plan, aggregate)
389            .map_err(QueryError::execute)?;
390
391        Ok(ExplainAggregateTerminalPlan::new(
392            query_explain,
393            terminal,
394            execution,
395        ))
396    }
397}
398
399impl SharedPreparedExecutionPlan {
400    /// Explain one cached prepared aggregate terminal route without running it.
401    pub(in crate::db) fn explain_prepared_aggregate_terminal<S>(
402        &self,
403        strategy: &S,
404    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
405    where
406        S: AggregateExplain,
407    {
408        let Some(kind) = strategy.explain_aggregate_kind() else {
409            return Err(QueryError::invariant());
410        };
411        let aggregate = self
412            .authority_ref()
413            .aggregate_route_shape(kind, strategy.explain_projected_field())
414            .map_err(QueryError::execute)?;
415        let execution =
416            assemble_aggregate_terminal_execution_descriptor(self.logical_plan(), aggregate)
417                .map_err(QueryError::execute)?;
418
419        Ok(ExplainAggregateTerminalPlan::new(
420            self.logical_plan().explain(),
421            kind,
422            execution,
423        ))
424    }
425
426    fn explain_prepared_terminal_load_descriptor(
427        &self,
428        terminal_label: &str,
429        field_label: &str,
430    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
431        let mut descriptor = assemble_load_execution_node_descriptor_for_authority(
432            self.authority_ref(),
433            self.logical_plan(),
434        )
435        .map_err(QueryError::execute)?;
436
437        descriptor
438            .node_properties
439            .insert(property_keys::TERMINAL, Value::from(terminal_label));
440        descriptor.node_properties.insert(
441            property_keys::TERMINAL_FIELD,
442            Value::from(field_label.to_string()),
443        );
444
445        Ok(descriptor)
446    }
447
448    /// Explain one cached prepared `bytes_by(field)` terminal route without running it.
449    pub(in crate::db) fn explain_bytes_by_terminal(
450        &self,
451        target_field: &str,
452    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
453        let mut descriptor =
454            self.explain_prepared_terminal_load_descriptor("bytes_by", target_field)?;
455        let projection_mode = self.bytes_by_projection_mode(target_field);
456
457        descriptor.node_properties.insert(
458            property_keys::TERMINAL_PROJECTION_MODE,
459            Value::from(projection_mode.label()),
460        );
461        descriptor.node_properties.insert(
462            property_keys::TERMINAL_INDEX_ONLY,
463            Value::from(projection_mode.is_index_only()),
464        );
465
466        Ok(descriptor)
467    }
468
469    /// Explain one cached prepared projection terminal route without running it.
470    pub(in crate::db) fn explain_prepared_projection_terminal<S>(
471        &self,
472        strategy: &S,
473    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
474    where
475        S: ProjectionExplain,
476    {
477        let projection_descriptor = strategy.explain_projection_descriptor();
478        let mut descriptor = self.explain_prepared_terminal_load_descriptor(
479            projection_descriptor.terminal_label(),
480            projection_descriptor.field_label(),
481        )?;
482        descriptor.node_properties.insert(
483            property_keys::TERMINAL_OUTPUT,
484            Value::from(projection_descriptor.output_label()),
485        );
486
487        Ok(descriptor)
488    }
489}
490
491impl<E> Query<E>
492where
493    E: EntityValue + EntityKind,
494{
495    // Resolve the structural execution descriptor through either the explicit
496    // model-only lane or one caller-provided visible-index slice.
497    fn explain_execution_descriptor_for_model_only_or_visible_indexes(
498        &self,
499        visible_indexes: Option<&VisibleIndexes<'_>>,
500    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
501        match visible_indexes {
502            Some(visible_indexes) => self
503                .structural()
504                .explain_execution_with_visible_indexes(visible_indexes),
505            None => self.structural().explain_execution_for_model_only(),
506        }
507    }
508
509    // Render one descriptor-derived execution surface after resolving the
510    // visibility slice once at the typed query boundary.
511    fn render_execution_descriptor_for_visibility(
512        &self,
513        visible_indexes: Option<&VisibleIndexes<'_>>,
514        render: impl FnOnce(ExplainExecutionNodeDescriptor) -> String,
515    ) -> Result<String, QueryError> {
516        let descriptor =
517            self.explain_execution_descriptor_for_model_only_or_visible_indexes(visible_indexes)?;
518
519        Ok(render(descriptor))
520    }
521
522    // Render one verbose execution explain payload after choosing the explicit
523    // model-only lane or the accepted visible-index lane once.
524    fn explain_execution_verbose_for_model_only_or_visible_indexes(
525        &self,
526        visible_indexes: Option<&VisibleIndexes<'_>>,
527    ) -> Result<String, QueryError> {
528        match visible_indexes {
529            Some(visible_indexes) => self
530                .structural()
531                .explain_execution_verbose_with_visible_indexes(visible_indexes),
532            None => self.structural().explain_execution_verbose_for_model_only(),
533        }
534    }
535
536    /// Explain executor-selected load execution shape without running it.
537    pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
538        self.explain_execution_descriptor_for_model_only_or_visible_indexes(None)
539    }
540
541    /// Explain executor-selected load execution shape with caller-visible indexes.
542    #[cfg(test)]
543    pub(in crate::db) fn explain_execution_with_visible_indexes(
544        &self,
545        visible_indexes: &VisibleIndexes<'_>,
546    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
547        self.explain_execution_descriptor_for_model_only_or_visible_indexes(Some(visible_indexes))
548    }
549
550    /// Explain executor-selected load execution shape as deterministic text.
551    pub fn explain_execution_text(&self) -> Result<String, QueryError> {
552        self.render_execution_descriptor_for_visibility(None, |descriptor| {
553            descriptor.render_text_tree()
554        })
555    }
556
557    /// Explain executor-selected load execution shape as canonical JSON.
558    pub fn explain_execution_json(&self) -> Result<String, QueryError> {
559        self.render_execution_descriptor_for_visibility(None, |descriptor| {
560            descriptor.render_json_canonical()
561        })
562    }
563
564    /// Explain executor-selected load execution shape with route diagnostics.
565    #[inline(never)]
566    pub fn explain_execution_verbose(&self) -> Result<String, QueryError> {
567        self.explain_execution_verbose_for_model_only_or_visible_indexes(None)
568    }
569
570    /// Explain one aggregate terminal execution route without running it.
571    #[cfg(test)]
572    #[inline(never)]
573    pub(in crate::db) fn explain_aggregate_terminal(
574        &self,
575        aggregate: AggregateExpr,
576    ) -> Result<ExplainAggregateTerminalPlan, QueryError> {
577        self.structural()
578            .explain_aggregate_terminal_with_visible_indexes(
579                &VisibleIndexes::generated_model_only(E::MODEL.indexes()),
580                AggregateRouteShape::new_from_fields(
581                    aggregate.kind(),
582                    aggregate.target_field(),
583                    E::MODEL.fields(),
584                    model_primary_key_names(E::MODEL).as_slice(),
585                ),
586            )
587    }
588}
589
590fn model_primary_key_names(model: &EntityModel) -> Vec<&'static str> {
591    model
592        .primary_key_model()
593        .fields()
594        .iter()
595        .map(crate::model::field::FieldModel::name)
596        .collect()
597}
598
599// Render the logical ORDER pushdown label for verbose execution diagnostics.
600fn plan_order_pushdown_label(order_pushdown: &ExplainOrderPushdown) -> String {
601    match order_pushdown {
602        ExplainOrderPushdown::MissingModelContext => "missing_model_context".to_string(),
603        ExplainOrderPushdown::EligibleSecondaryIndex { index, prefix_len } => {
604            format!("eligible(index={index},prefix_len={prefix_len})")
605        }
606        ExplainOrderPushdown::Rejected(reason) => format!("rejected({reason:?})"),
607    }
608}