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    // Render one standalone model-only execution explain JSON payload from the
250    // same finalized diagnostics artifact used by verbose text explain.
251    fn explain_execution_json_from_plan(
252        &self,
253        plan: &AccessPlannedQuery,
254    ) -> Result<String, QueryError> {
255        self.finalized_execution_diagnostics_from_model_only_plan(plan, None)
256            .map(|diagnostics| diagnostics.render_json_canonical())
257    }
258
259    // Freeze one explain-only access-choice snapshot from accepted
260    // planner-visible indexes before building descriptor diagnostics.
261    fn finalize_explain_access_choice_for_visible_indexes(
262        &self,
263        plan: &mut AccessPlannedQuery,
264        visible_indexes: &VisibleIndexes<'_>,
265    ) {
266        if let Some(schema_info) = visible_indexes.accepted_schema_info() {
267            plan.finalize_access_choice_for_model_with_semantic_indexes_and_schema(
268                self.model(),
269                visible_indexes.accepted_semantic_index_contracts(),
270                schema_info,
271            );
272            return;
273        }
274
275        plan.finalize_access_choice_for_model_only_with_indexes(
276            self.model(),
277            visible_indexes.generated_model_only_indexes(),
278        );
279    }
280
281    // Freeze one explicit model-only access-choice snapshot for standalone
282    // query explain surfaces that intentionally do not have accepted runtime
283    // schema authority.
284    fn finalize_explain_access_choice_for_model_only(&self, plan: &mut AccessPlannedQuery) {
285        plan.finalize_access_choice_for_model_only_with_indexes(
286            self.model(),
287            self.model().indexes(),
288        );
289    }
290
291    // Build and freeze one explicit model-only access-plan snapshot for
292    // standalone query explain surfaces.
293    fn finalized_model_only_explain_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
294        let mut plan = self.build_plan()?;
295        self.finalize_explain_access_choice_for_model_only(&mut plan);
296
297        Ok(plan)
298    }
299
300    // Build and freeze one access-plan snapshot using a caller-provided
301    // visible-index slice for runtime/session explain surfaces.
302    fn finalized_visible_indexes_explain_plan(
303        &self,
304        visible_indexes: &VisibleIndexes<'_>,
305    ) -> Result<AccessPlannedQuery, QueryError> {
306        let mut plan = self.build_plan_with_visible_indexes(visible_indexes)?;
307        self.finalize_explain_access_choice_for_visible_indexes(&mut plan, visible_indexes);
308
309        Ok(plan)
310    }
311
312    // Build one explicit model-only execution descriptor for standalone query
313    // surfaces that are not bound to a recovered store/accepted schema.
314    fn explain_execution_descriptor_for_model_only(
315        &self,
316    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
317        let plan = self.finalized_model_only_explain_plan()?;
318
319        self.explain_execution_descriptor_from_model_only_plan(&plan)
320    }
321
322    // Build one execution descriptor using the caller-resolved accepted visible
323    // indexes for runtime/session explain.
324    fn explain_execution_descriptor_for_visible_indexes(
325        &self,
326        visible_indexes: &VisibleIndexes<'_>,
327    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
328        let plan = self.finalized_visible_indexes_explain_plan(visible_indexes)?;
329
330        self.explain_execution_descriptor_from_model_only_plan(&plan)
331    }
332
333    // Render one explicit model-only verbose execution payload for standalone
334    // query surfaces that are not bound to a recovered store/accepted schema.
335    fn render_execution_verbose_for_model_only(&self) -> Result<String, QueryError> {
336        let plan = self.finalized_model_only_explain_plan()?;
337
338        self.explain_execution_verbose_from_plan(&plan)
339    }
340
341    // Render one explicit model-only execution JSON payload for standalone
342    // query surfaces that are not bound to a recovered store/accepted schema.
343    fn render_execution_json_for_model_only(&self) -> Result<String, QueryError> {
344        let plan = self.finalized_model_only_explain_plan()?;
345
346        self.explain_execution_json_from_plan(&plan)
347    }
348
349    // Render one verbose execution payload using the caller-resolved accepted
350    // visible indexes for runtime/session explain.
351    fn explain_execution_verbose_for_visible_indexes(
352        &self,
353        visible_indexes: &VisibleIndexes<'_>,
354    ) -> Result<String, QueryError> {
355        let plan = self.finalized_visible_indexes_explain_plan(visible_indexes)?;
356
357        self.explain_execution_verbose_from_plan(&plan)
358    }
359
360    // Render one finalized execution JSON payload using the caller-resolved
361    // accepted visible indexes for runtime/session explain.
362    fn explain_execution_json_for_visible_indexes(
363        &self,
364        visible_indexes: &VisibleIndexes<'_>,
365    ) -> Result<String, QueryError> {
366        let plan = self.finalized_visible_indexes_explain_plan(visible_indexes)?;
367
368        self.explain_execution_json_from_plan(&plan)
369    }
370
371    /// Explain one model-only load execution shape through the structural query core.
372    #[inline(never)]
373    pub(in crate::db) fn explain_execution_for_model_only(
374        &self,
375    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
376        self.explain_execution_descriptor_for_model_only()
377    }
378
379    /// Explain one load execution shape using a caller-visible index slice.
380    #[inline(never)]
381    pub(in crate::db) fn explain_execution_with_visible_indexes(
382        &self,
383        visible_indexes: &VisibleIndexes<'_>,
384    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
385        self.explain_execution_descriptor_for_visible_indexes(visible_indexes)
386    }
387
388    /// Render one model-only verbose scalar load execution payload through the
389    /// shared structural descriptor and route-diagnostics paths.
390    #[inline(never)]
391    pub(in crate::db) fn explain_execution_verbose_for_model_only(
392        &self,
393    ) -> Result<String, QueryError> {
394        self.render_execution_verbose_for_model_only()
395    }
396
397    /// Render the model-only execution explain artifact as finalized JSON.
398    #[inline(never)]
399    pub(in crate::db) fn explain_execution_json_for_model_only(
400        &self,
401    ) -> Result<String, QueryError> {
402        self.render_execution_json_for_model_only()
403    }
404
405    /// Render one verbose scalar load execution payload using visible indexes.
406    #[inline(never)]
407    pub(in crate::db) fn explain_execution_verbose_with_visible_indexes(
408        &self,
409        visible_indexes: &VisibleIndexes<'_>,
410    ) -> Result<String, QueryError> {
411        self.explain_execution_verbose_for_visible_indexes(visible_indexes)
412    }
413
414    /// Render the visible-index execution explain artifact as finalized JSON.
415    #[inline(never)]
416    pub(in crate::db) fn explain_execution_json_with_visible_indexes(
417        &self,
418        visible_indexes: &VisibleIndexes<'_>,
419    ) -> Result<String, QueryError> {
420        self.explain_execution_json_for_visible_indexes(visible_indexes)
421    }
422
423    /// Explain one aggregate terminal execution route without running it.
424    #[inline(never)]
425    #[cfg(test)]
426    pub(in crate::db) fn explain_aggregate_terminal_with_visible_indexes(
427        &self,
428        visible_indexes: &VisibleIndexes<'_>,
429        aggregate: AggregateRouteShape<'_>,
430    ) -> Result<ExplainAggregateTerminalPlan, QueryError> {
431        let plan = self.build_plan_with_visible_indexes(visible_indexes)?;
432        let query_explain = plan.explain();
433        let terminal = aggregate.kind();
434        let execution = assemble_aggregate_terminal_execution_descriptor(&plan, aggregate)
435            .map_err(QueryError::execute)?;
436
437        Ok(ExplainAggregateTerminalPlan::new(
438            query_explain,
439            terminal,
440            execution,
441        ))
442    }
443}
444
445impl SharedPreparedExecutionPlan {
446    /// Explain one cached prepared aggregate terminal route without running it.
447    pub(in crate::db) fn explain_prepared_aggregate_terminal<S>(
448        &self,
449        strategy: &S,
450    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
451    where
452        S: AggregateExplain,
453    {
454        let Some(kind) = strategy.explain_aggregate_kind() else {
455            return Err(QueryError::invariant());
456        };
457        let aggregate = self
458            .authority_ref()
459            .aggregate_route_shape(kind, strategy.explain_projected_field())
460            .map_err(QueryError::execute)?;
461        let execution =
462            assemble_aggregate_terminal_execution_descriptor(self.logical_plan(), aggregate)
463                .map_err(QueryError::execute)?;
464
465        Ok(ExplainAggregateTerminalPlan::new(
466            self.logical_plan().explain(),
467            kind,
468            execution,
469        ))
470    }
471
472    fn explain_prepared_terminal_load_descriptor(
473        &self,
474        terminal_label: &str,
475        field_label: &str,
476    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
477        let mut descriptor = assemble_load_execution_node_descriptor_for_authority(
478            self.authority_ref(),
479            self.logical_plan(),
480        )
481        .map_err(QueryError::execute)?;
482
483        descriptor
484            .node_properties
485            .insert(property_keys::TERMINAL, Value::from(terminal_label));
486        descriptor.node_properties.insert(
487            property_keys::TERMINAL_FIELD,
488            Value::from(field_label.to_string()),
489        );
490
491        Ok(descriptor)
492    }
493
494    /// Explain one cached prepared `bytes_by(field)` terminal route without running it.
495    pub(in crate::db) fn explain_bytes_by_terminal(
496        &self,
497        target_field: &str,
498    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
499        let mut descriptor =
500            self.explain_prepared_terminal_load_descriptor("bytes_by", target_field)?;
501        let projection_mode = self.bytes_by_projection_mode(target_field);
502
503        descriptor.node_properties.insert(
504            property_keys::TERMINAL_PROJECTION_MODE,
505            Value::from(projection_mode.label()),
506        );
507        descriptor.node_properties.insert(
508            property_keys::TERMINAL_INDEX_ONLY,
509            Value::from(projection_mode.is_index_only()),
510        );
511
512        Ok(descriptor)
513    }
514
515    /// Explain one cached prepared projection terminal route without running it.
516    pub(in crate::db) fn explain_prepared_projection_terminal<S>(
517        &self,
518        strategy: &S,
519    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
520    where
521        S: ProjectionExplain,
522    {
523        let projection_descriptor = strategy.explain_projection_descriptor();
524        let mut descriptor = self.explain_prepared_terminal_load_descriptor(
525            projection_descriptor.terminal_label(),
526            projection_descriptor.field_label(),
527        )?;
528        descriptor.node_properties.insert(
529            property_keys::TERMINAL_OUTPUT,
530            Value::from(projection_descriptor.output_label()),
531        );
532
533        Ok(descriptor)
534    }
535}
536
537impl<E> Query<E>
538where
539    E: EntityValue + EntityKind,
540{
541    // Resolve the structural execution descriptor through either the explicit
542    // model-only lane or one caller-provided visible-index slice.
543    fn explain_execution_descriptor_for_model_only_or_visible_indexes(
544        &self,
545        visible_indexes: Option<&VisibleIndexes<'_>>,
546    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
547        match visible_indexes {
548            Some(visible_indexes) => self
549                .structural()
550                .explain_execution_with_visible_indexes(visible_indexes),
551            None => self.structural().explain_execution_for_model_only(),
552        }
553    }
554
555    // Render one descriptor-derived execution surface after resolving the
556    // visibility slice once at the typed query boundary.
557    fn render_execution_descriptor_for_visibility(
558        &self,
559        visible_indexes: Option<&VisibleIndexes<'_>>,
560        render: impl FnOnce(ExplainExecutionNodeDescriptor) -> String,
561    ) -> Result<String, QueryError> {
562        let descriptor =
563            self.explain_execution_descriptor_for_model_only_or_visible_indexes(visible_indexes)?;
564
565        Ok(render(descriptor))
566    }
567
568    // Render one verbose execution explain payload after choosing the explicit
569    // model-only lane or the accepted visible-index lane once.
570    fn explain_execution_verbose_for_model_only_or_visible_indexes(
571        &self,
572        visible_indexes: Option<&VisibleIndexes<'_>>,
573    ) -> Result<String, QueryError> {
574        match visible_indexes {
575            Some(visible_indexes) => self
576                .structural()
577                .explain_execution_verbose_with_visible_indexes(visible_indexes),
578            None => self.structural().explain_execution_verbose_for_model_only(),
579        }
580    }
581
582    // Render finalized execution JSON after choosing the explicit model-only
583    // lane or the accepted visible-index lane once.
584    fn explain_execution_json_for_model_only_or_visible_indexes(
585        &self,
586        visible_indexes: Option<&VisibleIndexes<'_>>,
587    ) -> Result<String, QueryError> {
588        match visible_indexes {
589            Some(visible_indexes) => self
590                .structural()
591                .explain_execution_json_with_visible_indexes(visible_indexes),
592            None => self.structural().explain_execution_json_for_model_only(),
593        }
594    }
595
596    /// Explain executor-selected load execution shape without running it.
597    pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
598        self.explain_execution_descriptor_for_model_only_or_visible_indexes(None)
599    }
600
601    /// Explain executor-selected load execution shape with caller-visible indexes.
602    #[cfg(test)]
603    pub(in crate::db) fn explain_execution_with_visible_indexes(
604        &self,
605        visible_indexes: &VisibleIndexes<'_>,
606    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
607        self.explain_execution_descriptor_for_model_only_or_visible_indexes(Some(visible_indexes))
608    }
609
610    /// Explain executor-selected load execution shape as deterministic text.
611    pub fn explain_execution_text(&self) -> Result<String, QueryError> {
612        self.render_execution_descriptor_for_visibility(None, |descriptor| {
613            descriptor.render_text_tree()
614        })
615    }
616
617    /// Explain executor-selected load execution shape as canonical JSON.
618    pub fn explain_execution_json(&self) -> Result<String, QueryError> {
619        self.explain_execution_json_for_model_only_or_visible_indexes(None)
620    }
621
622    /// Explain executor-selected load execution shape with route diagnostics.
623    #[inline(never)]
624    pub fn explain_execution_verbose(&self) -> Result<String, QueryError> {
625        self.explain_execution_verbose_for_model_only_or_visible_indexes(None)
626    }
627
628    /// Explain one aggregate terminal execution route without running it.
629    #[cfg(test)]
630    #[inline(never)]
631    pub(in crate::db) fn explain_aggregate_terminal(
632        &self,
633        aggregate: AggregateExpr,
634    ) -> Result<ExplainAggregateTerminalPlan, QueryError> {
635        self.structural()
636            .explain_aggregate_terminal_with_visible_indexes(
637                &VisibleIndexes::generated_model_only(E::MODEL.indexes()),
638                AggregateRouteShape::new_from_fields(
639                    aggregate.kind(),
640                    aggregate.target_field(),
641                    E::MODEL.fields(),
642                    model_primary_key_names(E::MODEL).as_slice(),
643                ),
644            )
645    }
646}
647
648fn model_primary_key_names(model: &EntityModel) -> Vec<&'static str> {
649    model
650        .primary_key_model()
651        .fields()
652        .iter()
653        .map(crate::model::field::FieldModel::name)
654        .collect()
655}
656
657// Render the logical ORDER pushdown label for verbose execution diagnostics.
658fn plan_order_pushdown_label(order_pushdown: &ExplainOrderPushdown) -> String {
659    match order_pushdown {
660        ExplainOrderPushdown::MissingModelContext => "missing_model_context".to_string(),
661        ExplainOrderPushdown::EligibleSecondaryIndex { index, prefix_len } => {
662            format!("eligible(index={index},prefix_len={prefix_len})")
663        }
664        ExplainOrderPushdown::Rejected(reason) => format!("rejected({reason:?})"),
665    }
666}