Skip to main content

icydb_core/db/query/explain/
mod.rs

1//! Module: query::explain
2//! Responsibility: deterministic, read-only projection of logical query plans.
3//! Does not own: plan execution or semantic validation.
4//! Boundary: diagnostics/explain surface over intent/planner outputs.
5
6use crate::{
7    db::{
8        access::{
9            AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
10            SecondaryOrderPushdownRejection,
11        },
12        predicate::{
13            CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate, normalize,
14        },
15        query::{
16            access::{AccessPathVisitor, visit_explain_access_path},
17            plan::{
18                AccessPlanProjection, AccessPlannedQuery, AggregateKind, DeleteLimitSpec,
19                GroupHavingClause, GroupHavingSpec, GroupHavingSymbol, GroupedPlanStrategyHint,
20                LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
21                grouped_plan_strategy_hint_for_plan, project_access_plan,
22            },
23        },
24    },
25    model::entity::EntityModel,
26    traits::FieldValue,
27    value::Value,
28};
29use std::{collections::BTreeMap, fmt::Write, ops::Bound};
30
31///
32/// ExplainPlan
33///
34/// Stable, deterministic representation of a planned query for observability.
35///
36
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ExplainPlan {
39    pub(crate) mode: QueryMode,
40    pub(crate) access: ExplainAccessPath,
41    pub(crate) predicate: ExplainPredicate,
42    predicate_model: Option<Predicate>,
43    pub(crate) order_by: ExplainOrderBy,
44    pub(crate) distinct: bool,
45    pub(crate) grouping: ExplainGrouping,
46    pub(crate) order_pushdown: ExplainOrderPushdown,
47    pub(crate) page: ExplainPagination,
48    pub(crate) delete_limit: ExplainDeleteLimit,
49    pub(crate) consistency: MissingRowPolicy,
50}
51
52///
53/// ExplainAggregateTerminalRoute
54///
55/// Executor-projected scalar aggregate terminal route label for explain output.
56/// Keeps seek-edge fast-path labels explicit without exposing route internals.
57///
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum ExplainAggregateTerminalRoute {
60    Standard,
61    IndexSeekFirst { fetch: usize },
62    IndexSeekLast { fetch: usize },
63}
64
65///
66/// ExplainAggregateTerminalPlan
67///
68/// Combined explain payload for one scalar aggregate terminal request.
69/// Includes logical explain projection plus executor route label.
70///
71#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct ExplainAggregateTerminalPlan {
73    pub(crate) query: ExplainPlan,
74    pub(crate) terminal: AggregateKind,
75    pub(crate) route: ExplainAggregateTerminalRoute,
76    pub(crate) execution: ExplainExecutionDescriptor,
77}
78
79///
80/// ExplainExecutionOrderingSource
81///
82/// Stable ordering-origin projection used by terminal execution explain output.
83/// This keeps index-seek labels and materialized fallback labels explicit.
84///
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum ExplainExecutionOrderingSource {
87    AccessOrder,
88    Materialized,
89    IndexSeekFirst { fetch: usize },
90    IndexSeekLast { fetch: usize },
91}
92
93///
94/// ExplainExecutionMode
95///
96/// Stable execution-mode projection used by execution explain descriptors.
97///
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum ExplainExecutionMode {
100    Streaming,
101    Materialized,
102}
103
104///
105/// ExplainExecutionDescriptor
106///
107/// Stable scalar execution descriptor consumed by terminal EXPLAIN surfaces.
108/// This keeps execution authority projection centralized and avoids ad-hoc
109/// terminal-specific explain branching at call sites.
110///
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct ExplainExecutionDescriptor {
113    pub(crate) access_strategy: ExplainAccessPath,
114    pub(crate) covering_projection: bool,
115    pub(crate) aggregation: AggregateKind,
116    pub(crate) execution_mode: ExplainExecutionMode,
117    pub(crate) ordering_source: ExplainExecutionOrderingSource,
118    pub(crate) limit: Option<u32>,
119    pub(crate) cursor: bool,
120    pub(crate) node_properties: BTreeMap<String, Value>,
121}
122
123///
124/// ExplainExecutionNodeType
125///
126/// Stable execution-node vocabulary for EXPLAIN descriptor projection.
127///
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum ExplainExecutionNodeType {
130    ByKeyLookup,
131    ByKeysLookup,
132    PrimaryKeyRangeScan,
133    IndexPrefixScan,
134    IndexRangeScan,
135    IndexMultiLookup,
136    FullScan,
137    Union,
138    Intersection,
139    IndexPredicatePrefilter,
140    ResidualPredicateFilter,
141    OrderByAccessSatisfied,
142    OrderByMaterializedSort,
143    DistinctPreOrdered,
144    DistinctMaterialized,
145    ProjectionMaterialized,
146    ProjectionIndexOnly,
147    LimitOffset,
148    CursorResume,
149    IndexRangeLimitPushdown,
150    TopNSeek,
151    AggregateCount,
152    AggregateExists,
153    AggregateMin,
154    AggregateMax,
155    AggregateFirst,
156    AggregateLast,
157    AggregateSum,
158    AggregateSeekFirst,
159    AggregateSeekLast,
160    GroupedAggregateHashMaterialized,
161    GroupedAggregateOrderedMaterialized,
162    SecondaryOrderPushdown,
163}
164
165///
166/// ExplainExecutionNodeDescriptor
167///
168/// Canonical execution-node descriptor used by EXPLAIN text/verbose/json
169/// renderers. Optional fields are node-family specific and are additive.
170///
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct ExplainExecutionNodeDescriptor {
173    pub(crate) node_type: ExplainExecutionNodeType,
174    pub(crate) execution_mode: ExplainExecutionMode,
175    pub(crate) access_strategy: Option<ExplainAccessPath>,
176    pub(crate) predicate_pushdown: Option<String>,
177    pub(crate) residual_predicate: Option<ExplainPredicate>,
178    pub(crate) projection: Option<String>,
179    pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
180    pub(crate) limit: Option<u32>,
181    pub(crate) cursor: Option<bool>,
182    pub(crate) covering_scan: Option<bool>,
183    pub(crate) rows_expected: Option<u64>,
184    pub(crate) children: Vec<Self>,
185    pub(crate) node_properties: BTreeMap<String, Value>,
186}
187
188impl ExplainPlan {
189    /// Return query mode projected by this explain plan.
190    #[must_use]
191    pub const fn mode(&self) -> QueryMode {
192        self.mode
193    }
194
195    /// Borrow projected access-path shape.
196    #[must_use]
197    pub const fn access(&self) -> &ExplainAccessPath {
198        &self.access
199    }
200
201    /// Borrow projected predicate shape.
202    #[must_use]
203    pub const fn predicate(&self) -> &ExplainPredicate {
204        &self.predicate
205    }
206
207    /// Borrow projected ORDER BY shape.
208    #[must_use]
209    pub const fn order_by(&self) -> &ExplainOrderBy {
210        &self.order_by
211    }
212
213    /// Return whether DISTINCT is enabled.
214    #[must_use]
215    pub const fn distinct(&self) -> bool {
216        self.distinct
217    }
218
219    /// Borrow projected grouped-shape metadata.
220    #[must_use]
221    pub const fn grouping(&self) -> &ExplainGrouping {
222        &self.grouping
223    }
224
225    /// Borrow projected ORDER pushdown status.
226    #[must_use]
227    pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
228        &self.order_pushdown
229    }
230
231    /// Borrow projected pagination status.
232    #[must_use]
233    pub const fn page(&self) -> &ExplainPagination {
234        &self.page
235    }
236
237    /// Borrow projected delete-limit status.
238    #[must_use]
239    pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
240        &self.delete_limit
241    }
242
243    /// Return missing-row consistency policy.
244    #[must_use]
245    pub const fn consistency(&self) -> MissingRowPolicy {
246        self.consistency
247    }
248}
249
250impl ExplainAggregateTerminalPlan {
251    /// Borrow the underlying query explain payload.
252    #[must_use]
253    pub const fn query(&self) -> &ExplainPlan {
254        &self.query
255    }
256
257    /// Return terminal aggregate kind.
258    #[must_use]
259    pub const fn terminal(&self) -> AggregateKind {
260        self.terminal
261    }
262
263    /// Return projected aggregate terminal route.
264    #[must_use]
265    pub const fn route(&self) -> ExplainAggregateTerminalRoute {
266        self.route
267    }
268
269    /// Borrow projected execution descriptor.
270    #[must_use]
271    pub const fn execution(&self) -> &ExplainExecutionDescriptor {
272        &self.execution
273    }
274
275    #[must_use]
276    pub(in crate::db) const fn new(
277        query: ExplainPlan,
278        terminal: AggregateKind,
279        execution: ExplainExecutionDescriptor,
280    ) -> Self {
281        let route = execution.route();
282
283        Self {
284            query,
285            terminal,
286            route,
287            execution,
288        }
289    }
290}
291
292impl ExplainExecutionDescriptor {
293    /// Borrow projected access strategy.
294    #[must_use]
295    pub const fn access_strategy(&self) -> &ExplainAccessPath {
296        &self.access_strategy
297    }
298
299    /// Return whether projection can be served from index payload only.
300    #[must_use]
301    pub const fn covering_projection(&self) -> bool {
302        self.covering_projection
303    }
304
305    /// Return projected aggregate kind.
306    #[must_use]
307    pub const fn aggregation(&self) -> AggregateKind {
308        self.aggregation
309    }
310
311    /// Return projected execution mode.
312    #[must_use]
313    pub const fn execution_mode(&self) -> ExplainExecutionMode {
314        self.execution_mode
315    }
316
317    /// Return projected ordering source.
318    #[must_use]
319    pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
320        self.ordering_source
321    }
322
323    /// Return projected execution limit.
324    #[must_use]
325    pub const fn limit(&self) -> Option<u32> {
326        self.limit
327    }
328
329    /// Return whether continuation was applied.
330    #[must_use]
331    pub const fn cursor(&self) -> bool {
332        self.cursor
333    }
334
335    /// Borrow projected execution node properties.
336    #[must_use]
337    pub const fn node_properties(&self) -> &BTreeMap<String, Value> {
338        &self.node_properties
339    }
340
341    #[must_use]
342    pub(in crate::db) const fn route(&self) -> ExplainAggregateTerminalRoute {
343        match self.ordering_source {
344            ExplainExecutionOrderingSource::IndexSeekFirst { fetch } => {
345                ExplainAggregateTerminalRoute::IndexSeekFirst { fetch }
346            }
347            ExplainExecutionOrderingSource::IndexSeekLast { fetch } => {
348                ExplainAggregateTerminalRoute::IndexSeekLast { fetch }
349            }
350            ExplainExecutionOrderingSource::AccessOrder
351            | ExplainExecutionOrderingSource::Materialized => {
352                ExplainAggregateTerminalRoute::Standard
353            }
354        }
355    }
356}
357
358impl ExplainAggregateTerminalPlan {
359    #[must_use]
360    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
361        ExplainExecutionNodeDescriptor {
362            node_type: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
363            execution_mode: self.execution.execution_mode,
364            access_strategy: Some(self.execution.access_strategy.clone()),
365            predicate_pushdown: None,
366            residual_predicate: None,
367            projection: None,
368            ordering_source: Some(self.execution.ordering_source),
369            limit: self.execution.limit,
370            cursor: Some(self.execution.cursor),
371            covering_scan: Some(self.execution.covering_projection),
372            rows_expected: None,
373            children: Vec::new(),
374            node_properties: self.execution.node_properties.clone(),
375        }
376    }
377}
378
379const fn aggregate_execution_node_type(
380    terminal: AggregateKind,
381    ordering_source: ExplainExecutionOrderingSource,
382) -> ExplainExecutionNodeType {
383    match ordering_source {
384        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
385            ExplainExecutionNodeType::AggregateSeekFirst
386        }
387        ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
388            ExplainExecutionNodeType::AggregateSeekLast
389        }
390        ExplainExecutionOrderingSource::AccessOrder
391        | ExplainExecutionOrderingSource::Materialized => match terminal {
392            AggregateKind::Count => ExplainExecutionNodeType::AggregateCount,
393            AggregateKind::Exists => ExplainExecutionNodeType::AggregateExists,
394            AggregateKind::Min => ExplainExecutionNodeType::AggregateMin,
395            AggregateKind::Max => ExplainExecutionNodeType::AggregateMax,
396            AggregateKind::First => ExplainExecutionNodeType::AggregateFirst,
397            AggregateKind::Last => ExplainExecutionNodeType::AggregateLast,
398            AggregateKind::Sum => ExplainExecutionNodeType::AggregateSum,
399        },
400    }
401}
402
403impl ExplainExecutionNodeType {
404    #[must_use]
405    pub const fn as_str(self) -> &'static str {
406        match self {
407            Self::ByKeyLookup => "ByKeyLookup",
408            Self::ByKeysLookup => "ByKeysLookup",
409            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
410            Self::IndexPrefixScan => "IndexPrefixScan",
411            Self::IndexRangeScan => "IndexRangeScan",
412            Self::IndexMultiLookup => "IndexMultiLookup",
413            Self::FullScan => "FullScan",
414            Self::Union => "Union",
415            Self::Intersection => "Intersection",
416            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
417            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
418            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
419            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
420            Self::DistinctPreOrdered => "DistinctPreOrdered",
421            Self::DistinctMaterialized => "DistinctMaterialized",
422            Self::ProjectionMaterialized => "ProjectionMaterialized",
423            Self::ProjectionIndexOnly => "ProjectionIndexOnly",
424            Self::LimitOffset => "LimitOffset",
425            Self::CursorResume => "CursorResume",
426            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
427            Self::TopNSeek => "TopNSeek",
428            Self::AggregateCount => "AggregateCount",
429            Self::AggregateExists => "AggregateExists",
430            Self::AggregateMin => "AggregateMin",
431            Self::AggregateMax => "AggregateMax",
432            Self::AggregateFirst => "AggregateFirst",
433            Self::AggregateLast => "AggregateLast",
434            Self::AggregateSum => "AggregateSum",
435            Self::AggregateSeekFirst => "AggregateSeekFirst",
436            Self::AggregateSeekLast => "AggregateSeekLast",
437            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
438            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
439            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
440        }
441    }
442}
443
444impl ExplainExecutionNodeDescriptor {
445    /// Return node type.
446    #[must_use]
447    pub const fn node_type(&self) -> ExplainExecutionNodeType {
448        self.node_type
449    }
450
451    /// Return execution mode.
452    #[must_use]
453    pub const fn execution_mode(&self) -> ExplainExecutionMode {
454        self.execution_mode
455    }
456
457    /// Borrow optional access strategy annotation.
458    #[must_use]
459    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
460        self.access_strategy.as_ref()
461    }
462
463    /// Borrow optional predicate pushdown annotation.
464    #[must_use]
465    pub fn predicate_pushdown(&self) -> Option<&str> {
466        self.predicate_pushdown.as_deref()
467    }
468
469    /// Borrow optional residual predicate annotation.
470    #[must_use]
471    pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
472        self.residual_predicate.as_ref()
473    }
474
475    /// Borrow optional projection annotation.
476    #[must_use]
477    pub fn projection(&self) -> Option<&str> {
478        self.projection.as_deref()
479    }
480
481    /// Return optional ordering source annotation.
482    #[must_use]
483    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
484        self.ordering_source
485    }
486
487    /// Return optional limit annotation.
488    #[must_use]
489    pub const fn limit(&self) -> Option<u32> {
490        self.limit
491    }
492
493    /// Return optional continuation annotation.
494    #[must_use]
495    pub const fn cursor(&self) -> Option<bool> {
496        self.cursor
497    }
498
499    /// Return optional covering-scan annotation.
500    #[must_use]
501    pub const fn covering_scan(&self) -> Option<bool> {
502        self.covering_scan
503    }
504
505    /// Return optional row-count expectation annotation.
506    #[must_use]
507    pub const fn rows_expected(&self) -> Option<u64> {
508        self.rows_expected
509    }
510
511    /// Borrow child execution nodes.
512    #[must_use]
513    pub const fn children(&self) -> &[Self] {
514        self.children.as_slice()
515    }
516
517    /// Borrow node properties.
518    #[must_use]
519    pub const fn node_properties(&self) -> &BTreeMap<String, Value> {
520        &self.node_properties
521    }
522
523    #[must_use]
524    pub fn render_text_tree(&self) -> String {
525        let mut lines = Vec::new();
526        self.render_text_tree_into(0, &mut lines);
527        lines.join("\n")
528    }
529
530    #[must_use]
531    pub fn render_json_canonical(&self) -> String {
532        let mut out = String::new();
533        write_execution_node_json(self, &mut out);
534        out
535    }
536
537    #[must_use]
538    pub fn render_text_tree_verbose(&self) -> String {
539        let mut lines = Vec::new();
540        self.render_text_tree_verbose_into(0, &mut lines);
541        lines.join("\n")
542    }
543
544    fn render_text_tree_into(&self, depth: usize, lines: &mut Vec<String>) {
545        let mut line = format!(
546            "{}{} execution_mode={}",
547            "  ".repeat(depth),
548            self.node_type.as_str(),
549            execution_mode_label(self.execution_mode)
550        );
551
552        if let Some(access_strategy) = self.access_strategy.as_ref() {
553            let _ = write!(line, " access={}", access_strategy_label(access_strategy));
554        }
555        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
556            let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
557        }
558        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
559            let _ = write!(line, " residual_predicate={residual_predicate:?}");
560        }
561        if let Some(projection) = self.projection.as_ref() {
562            let _ = write!(line, " projection={projection}");
563        }
564        if let Some(ordering_source) = self.ordering_source {
565            let _ = write!(
566                line,
567                " ordering_source={}",
568                ordering_source_label(ordering_source)
569            );
570        }
571        if let Some(limit) = self.limit {
572            let _ = write!(line, " limit={limit}");
573        }
574        if let Some(cursor) = self.cursor {
575            let _ = write!(line, " cursor={cursor}");
576        }
577        if let Some(covering_scan) = self.covering_scan {
578            let _ = write!(line, " covering_scan={covering_scan}");
579        }
580        if let Some(rows_expected) = self.rows_expected {
581            let _ = write!(line, " rows_expected={rows_expected}");
582        }
583        if !self.node_properties.is_empty() {
584            let _ = write!(
585                line,
586                " node_properties={}",
587                render_node_properties(&self.node_properties)
588            );
589        }
590
591        lines.push(line);
592
593        for child in &self.children {
594            child.render_text_tree_into(depth.saturating_add(1), lines);
595        }
596    }
597
598    fn render_text_tree_verbose_into(&self, depth: usize, lines: &mut Vec<String>) {
599        // Emit the node heading line first so child metadata stays visually scoped.
600        let node_indent = "  ".repeat(depth);
601        let field_indent = "  ".repeat(depth.saturating_add(1));
602        lines.push(format!(
603            "{}{} execution_mode={}",
604            node_indent,
605            self.node_type.as_str(),
606            execution_mode_label(self.execution_mode)
607        ));
608
609        // Emit all optional node-local fields in a deterministic order.
610        if let Some(access_strategy) = self.access_strategy.as_ref() {
611            lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
612        }
613        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
614            lines.push(format!(
615                "{field_indent}predicate_pushdown={predicate_pushdown}"
616            ));
617        }
618        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
619            lines.push(format!(
620                "{field_indent}residual_predicate={residual_predicate:?}"
621            ));
622        }
623        if let Some(projection) = self.projection.as_ref() {
624            lines.push(format!("{field_indent}projection={projection}"));
625        }
626        if let Some(ordering_source) = self.ordering_source {
627            lines.push(format!(
628                "{}ordering_source={}",
629                field_indent,
630                ordering_source_label(ordering_source)
631            ));
632        }
633        if let Some(limit) = self.limit {
634            lines.push(format!("{field_indent}limit={limit}"));
635        }
636        if let Some(cursor) = self.cursor {
637            lines.push(format!("{field_indent}cursor={cursor}"));
638        }
639        if let Some(covering_scan) = self.covering_scan {
640            lines.push(format!("{field_indent}covering_scan={covering_scan}"));
641        }
642        if let Some(rows_expected) = self.rows_expected {
643            lines.push(format!("{field_indent}rows_expected={rows_expected}"));
644        }
645        if !self.node_properties.is_empty() {
646            lines.push(format!(
647                "{}node_properties={}",
648                field_indent,
649                render_node_properties(&self.node_properties)
650            ));
651        }
652
653        // Recurse in execution order to preserve stable tree topology.
654        for child in &self.children {
655            child.render_text_tree_verbose_into(depth.saturating_add(1), lines);
656        }
657    }
658}
659
660const fn execution_mode_label(mode: ExplainExecutionMode) -> &'static str {
661    match mode {
662        ExplainExecutionMode::Streaming => "Streaming",
663        ExplainExecutionMode::Materialized => "Materialized",
664    }
665}
666
667fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
668    let mut rendered = String::new();
669    let mut first = true;
670    for (key, value) in node_properties {
671        if first {
672            first = false;
673        } else {
674            rendered.push(',');
675        }
676        let _ = write!(rendered, "{key}={value:?}");
677    }
678    rendered
679}
680
681fn write_execution_node_json(node: &ExplainExecutionNodeDescriptor, out: &mut String) {
682    out.push('{');
683
684    write_json_field_name(out, "node_type");
685    write_json_string(out, node.node_type.as_str());
686    out.push(',');
687
688    write_json_field_name(out, "execution_mode");
689    write_json_string(out, execution_mode_label(node.execution_mode));
690    out.push(',');
691
692    write_json_field_name(out, "access_strategy");
693    match node.access_strategy.as_ref() {
694        Some(access) => write_access_json(access, out),
695        None => out.push_str("null"),
696    }
697    out.push(',');
698
699    write_json_field_name(out, "predicate_pushdown");
700    match node.predicate_pushdown.as_ref() {
701        Some(predicate_pushdown) => write_json_string(out, predicate_pushdown),
702        None => out.push_str("null"),
703    }
704    out.push(',');
705
706    write_json_field_name(out, "residual_predicate");
707    match node.residual_predicate.as_ref() {
708        Some(residual_predicate) => write_json_string(out, &format!("{residual_predicate:?}")),
709        None => out.push_str("null"),
710    }
711    out.push(',');
712
713    write_json_field_name(out, "projection");
714    match node.projection.as_ref() {
715        Some(projection) => write_json_string(out, projection),
716        None => out.push_str("null"),
717    }
718    out.push(',');
719
720    write_json_field_name(out, "ordering_source");
721    match node.ordering_source {
722        Some(ordering_source) => write_json_string(out, ordering_source_label(ordering_source)),
723        None => out.push_str("null"),
724    }
725    out.push(',');
726
727    write_json_field_name(out, "limit");
728    match node.limit {
729        Some(limit) => out.push_str(&limit.to_string()),
730        None => out.push_str("null"),
731    }
732    out.push(',');
733
734    write_json_field_name(out, "cursor");
735    match node.cursor {
736        Some(cursor) => out.push_str(if cursor { "true" } else { "false" }),
737        None => out.push_str("null"),
738    }
739    out.push(',');
740
741    write_json_field_name(out, "covering_scan");
742    match node.covering_scan {
743        Some(covering_scan) => out.push_str(if covering_scan { "true" } else { "false" }),
744        None => out.push_str("null"),
745    }
746    out.push(',');
747
748    write_json_field_name(out, "rows_expected");
749    match node.rows_expected {
750        Some(rows_expected) => out.push_str(&rows_expected.to_string()),
751        None => out.push_str("null"),
752    }
753    out.push(',');
754
755    write_json_field_name(out, "children");
756    out.push('[');
757    for (index, child) in node.children.iter().enumerate() {
758        if index > 0 {
759            out.push(',');
760        }
761        write_execution_node_json(child, out);
762    }
763    out.push(']');
764    out.push(',');
765
766    write_json_field_name(out, "node_properties");
767    write_node_properties_json(&node.node_properties, out);
768
769    out.push('}');
770}
771
772///
773/// ExplainJsonVisitor
774///
775/// Visitor that renders one `ExplainAccessPath` subtree into stable JSON.
776///
777
778struct ExplainJsonVisitor<'a> {
779    out: &'a mut String,
780}
781
782impl AccessPathVisitor<()> for ExplainJsonVisitor<'_> {
783    fn visit_by_key(&mut self, key: &Value) {
784        self.out.push('{');
785        write_json_field_name(self.out, "type");
786        write_json_string(self.out, "ByKey");
787        self.out.push(',');
788        write_json_field_name(self.out, "key");
789        write_json_string(self.out, &format!("{key:?}"));
790        self.out.push('}');
791    }
792
793    fn visit_by_keys(&mut self, keys: &[Value]) {
794        self.out.push('{');
795        write_json_field_name(self.out, "type");
796        write_json_string(self.out, "ByKeys");
797        self.out.push(',');
798        write_json_field_name(self.out, "keys");
799        write_value_vec_as_debug_json(keys, self.out);
800        self.out.push('}');
801    }
802
803    fn visit_key_range(&mut self, start: &Value, end: &Value) {
804        self.out.push('{');
805        write_json_field_name(self.out, "type");
806        write_json_string(self.out, "KeyRange");
807        self.out.push(',');
808        write_json_field_name(self.out, "start");
809        write_json_string(self.out, &format!("{start:?}"));
810        self.out.push(',');
811        write_json_field_name(self.out, "end");
812        write_json_string(self.out, &format!("{end:?}"));
813        self.out.push('}');
814    }
815
816    fn visit_index_prefix(
817        &mut self,
818        name: &'static str,
819        fields: &[&'static str],
820        prefix_len: usize,
821        values: &[Value],
822    ) {
823        self.out.push('{');
824        write_json_field_name(self.out, "type");
825        write_json_string(self.out, "IndexPrefix");
826        self.out.push(',');
827        write_json_field_name(self.out, "name");
828        write_json_string(self.out, name);
829        self.out.push(',');
830        write_json_field_name(self.out, "fields");
831        write_str_vec_json(fields, self.out);
832        self.out.push(',');
833        write_json_field_name(self.out, "prefix_len");
834        self.out.push_str(&prefix_len.to_string());
835        self.out.push(',');
836        write_json_field_name(self.out, "values");
837        write_value_vec_as_debug_json(values, self.out);
838        self.out.push('}');
839    }
840
841    fn visit_index_multi_lookup(
842        &mut self,
843        name: &'static str,
844        fields: &[&'static str],
845        values: &[Value],
846    ) {
847        self.out.push('{');
848        write_json_field_name(self.out, "type");
849        write_json_string(self.out, "IndexMultiLookup");
850        self.out.push(',');
851        write_json_field_name(self.out, "name");
852        write_json_string(self.out, name);
853        self.out.push(',');
854        write_json_field_name(self.out, "fields");
855        write_str_vec_json(fields, self.out);
856        self.out.push(',');
857        write_json_field_name(self.out, "values");
858        write_value_vec_as_debug_json(values, self.out);
859        self.out.push('}');
860    }
861
862    fn visit_index_range(
863        &mut self,
864        name: &'static str,
865        fields: &[&'static str],
866        prefix_len: usize,
867        prefix: &[Value],
868        lower: &Bound<Value>,
869        upper: &Bound<Value>,
870    ) {
871        self.out.push('{');
872        write_json_field_name(self.out, "type");
873        write_json_string(self.out, "IndexRange");
874        self.out.push(',');
875        write_json_field_name(self.out, "name");
876        write_json_string(self.out, name);
877        self.out.push(',');
878        write_json_field_name(self.out, "fields");
879        write_str_vec_json(fields, self.out);
880        self.out.push(',');
881        write_json_field_name(self.out, "prefix_len");
882        self.out.push_str(&prefix_len.to_string());
883        self.out.push(',');
884        write_json_field_name(self.out, "prefix");
885        write_value_vec_as_debug_json(prefix, self.out);
886        self.out.push(',');
887        write_json_field_name(self.out, "lower");
888        write_json_string(self.out, &format!("{lower:?}"));
889        self.out.push(',');
890        write_json_field_name(self.out, "upper");
891        write_json_string(self.out, &format!("{upper:?}"));
892        self.out.push('}');
893    }
894
895    fn visit_full_scan(&mut self) {
896        self.out.push('{');
897        write_json_field_name(self.out, "type");
898        write_json_string(self.out, "FullScan");
899        self.out.push('}');
900    }
901
902    fn visit_union(&mut self, children: &[ExplainAccessPath]) {
903        self.out.push('{');
904        write_json_field_name(self.out, "type");
905        write_json_string(self.out, "Union");
906        self.out.push(',');
907        write_json_field_name(self.out, "children");
908        self.out.push('[');
909        for (index, child) in children.iter().enumerate() {
910            if index > 0 {
911                self.out.push(',');
912            }
913            visit_explain_access_path(child, self);
914        }
915        self.out.push(']');
916        self.out.push('}');
917    }
918
919    fn visit_intersection(&mut self, children: &[ExplainAccessPath]) {
920        self.out.push('{');
921        write_json_field_name(self.out, "type");
922        write_json_string(self.out, "Intersection");
923        self.out.push(',');
924        write_json_field_name(self.out, "children");
925        self.out.push('[');
926        for (index, child) in children.iter().enumerate() {
927            if index > 0 {
928                self.out.push(',');
929            }
930            visit_explain_access_path(child, self);
931        }
932        self.out.push(']');
933        self.out.push('}');
934    }
935}
936
937fn write_access_json(access: &ExplainAccessPath, out: &mut String) {
938    let mut visitor = ExplainJsonVisitor { out };
939    visit_explain_access_path(access, &mut visitor);
940}
941
942fn write_node_properties_json(node_properties: &BTreeMap<String, Value>, out: &mut String) {
943    out.push('{');
944    for (index, (key, value)) in node_properties.iter().enumerate() {
945        if index > 0 {
946            out.push(',');
947        }
948        write_json_field_name(out, key);
949        write_json_string(out, &format!("{value:?}"));
950    }
951    out.push('}');
952}
953
954fn write_value_vec_as_debug_json(values: &[Value], out: &mut String) {
955    out.push('[');
956    for (index, value) in values.iter().enumerate() {
957        if index > 0 {
958            out.push(',');
959        }
960        write_json_string(out, &format!("{value:?}"));
961    }
962    out.push(']');
963}
964
965fn write_str_vec_json(values: &[&str], out: &mut String) {
966    out.push('[');
967    for (index, value) in values.iter().enumerate() {
968        if index > 0 {
969            out.push(',');
970        }
971        write_json_string(out, value);
972    }
973    out.push(']');
974}
975
976fn write_json_field_name(out: &mut String, key: &str) {
977    write_json_string(out, key);
978    out.push(':');
979}
980
981fn write_json_string(out: &mut String, value: &str) {
982    out.push('"');
983    for ch in value.chars() {
984        match ch {
985            '"' => out.push_str("\\\""),
986            '\\' => out.push_str("\\\\"),
987            '\n' => out.push_str("\\n"),
988            '\r' => out.push_str("\\r"),
989            '\t' => out.push_str("\\t"),
990            '\u{08}' => out.push_str("\\b"),
991            '\u{0C}' => out.push_str("\\f"),
992            _ => out.push(ch),
993        }
994    }
995    out.push('"');
996}
997
998fn access_strategy_label(access: &ExplainAccessPath) -> String {
999    struct ExplainLabelVisitor;
1000
1001    impl AccessPathVisitor<String> for ExplainLabelVisitor {
1002        fn visit_by_key(&mut self, _key: &Value) -> String {
1003            "ByKey".to_string()
1004        }
1005
1006        fn visit_by_keys(&mut self, _keys: &[Value]) -> String {
1007            "ByKeys".to_string()
1008        }
1009
1010        fn visit_key_range(&mut self, _start: &Value, _end: &Value) -> String {
1011            "KeyRange".to_string()
1012        }
1013
1014        fn visit_index_prefix(
1015            &mut self,
1016            name: &'static str,
1017            _fields: &[&'static str],
1018            _prefix_len: usize,
1019            _values: &[Value],
1020        ) -> String {
1021            format!("IndexPrefix({name})")
1022        }
1023
1024        fn visit_index_multi_lookup(
1025            &mut self,
1026            name: &'static str,
1027            _fields: &[&'static str],
1028            _values: &[Value],
1029        ) -> String {
1030            format!("IndexMultiLookup({name})")
1031        }
1032
1033        fn visit_index_range(
1034            &mut self,
1035            name: &'static str,
1036            _fields: &[&'static str],
1037            _prefix_len: usize,
1038            _prefix: &[Value],
1039            _lower: &Bound<Value>,
1040            _upper: &Bound<Value>,
1041        ) -> String {
1042            format!("IndexRange({name})")
1043        }
1044
1045        fn visit_full_scan(&mut self) -> String {
1046            "FullScan".to_string()
1047        }
1048
1049        fn visit_union(&mut self, children: &[ExplainAccessPath]) -> String {
1050            format!("Union({})", children.len())
1051        }
1052
1053        fn visit_intersection(&mut self, children: &[ExplainAccessPath]) -> String {
1054            format!("Intersection({})", children.len())
1055        }
1056    }
1057
1058    let mut visitor = ExplainLabelVisitor;
1059    visit_explain_access_path(access, &mut visitor)
1060}
1061
1062const fn ordering_source_label(ordering_source: ExplainExecutionOrderingSource) -> &'static str {
1063    match ordering_source {
1064        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
1065        ExplainExecutionOrderingSource::Materialized => "Materialized",
1066        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
1067        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
1068    }
1069}
1070
1071impl ExplainPlan {
1072    /// Return the canonical predicate model used for hashing/fingerprints.
1073    ///
1074    /// The explain projection must remain a faithful rendering of this model.
1075    #[must_use]
1076    pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
1077        if let Some(predicate) = &self.predicate_model {
1078            debug_assert_eq!(
1079                self.predicate,
1080                ExplainPredicate::from_predicate(predicate),
1081                "explain predicate surface drifted from canonical predicate model"
1082            );
1083            Some(predicate)
1084        } else {
1085            debug_assert!(
1086                matches!(self.predicate, ExplainPredicate::None),
1087                "missing canonical predicate model requires ExplainPredicate::None"
1088            );
1089            None
1090        }
1091    }
1092}
1093
1094///
1095/// ExplainGrouping
1096///
1097/// Grouped-shape annotation for deterministic explain/fingerprint surfaces.
1098///
1099
1100#[derive(Clone, Debug, Eq, PartialEq)]
1101pub enum ExplainGrouping {
1102    None,
1103    Grouped {
1104        strategy: ExplainGroupedStrategy,
1105        group_fields: Vec<ExplainGroupField>,
1106        aggregates: Vec<ExplainGroupAggregate>,
1107        having: Option<ExplainGroupHaving>,
1108        max_groups: u64,
1109        max_group_bytes: u64,
1110    },
1111}
1112
1113///
1114/// ExplainGroupedStrategy
1115///
1116/// Deterministic explain projection of grouped strategy selection.
1117///
1118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1119pub enum ExplainGroupedStrategy {
1120    HashGroup,
1121    OrderedGroup,
1122}
1123
1124impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
1125    fn from(value: GroupedPlanStrategyHint) -> Self {
1126        match value {
1127            GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
1128            GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
1129        }
1130    }
1131}
1132
1133///
1134/// ExplainGroupField
1135///
1136/// Stable grouped-key field identity carried by explain/hash surfaces.
1137///
1138
1139#[derive(Clone, Debug, Eq, PartialEq)]
1140pub struct ExplainGroupField {
1141    pub(crate) slot_index: usize,
1142    pub(crate) field: String,
1143}
1144
1145impl ExplainGroupField {
1146    /// Return grouped slot index.
1147    #[must_use]
1148    pub const fn slot_index(&self) -> usize {
1149        self.slot_index
1150    }
1151
1152    /// Borrow grouped field name.
1153    #[must_use]
1154    pub const fn field(&self) -> &str {
1155        self.field.as_str()
1156    }
1157}
1158
1159///
1160/// ExplainGroupAggregate
1161///
1162/// Stable explain-surface projection of one grouped aggregate terminal.
1163///
1164
1165#[derive(Clone, Debug, Eq, PartialEq)]
1166pub struct ExplainGroupAggregate {
1167    pub(crate) kind: AggregateKind,
1168    pub(crate) target_field: Option<String>,
1169    pub(crate) distinct: bool,
1170}
1171
1172impl ExplainGroupAggregate {
1173    /// Return grouped aggregate kind.
1174    #[must_use]
1175    pub const fn kind(&self) -> AggregateKind {
1176        self.kind
1177    }
1178
1179    /// Borrow optional grouped aggregate target field.
1180    #[must_use]
1181    pub fn target_field(&self) -> Option<&str> {
1182        self.target_field.as_deref()
1183    }
1184
1185    /// Return whether grouped aggregate uses DISTINCT input semantics.
1186    #[must_use]
1187    pub const fn distinct(&self) -> bool {
1188        self.distinct
1189    }
1190}
1191
1192///
1193/// ExplainGroupHaving
1194///
1195/// Deterministic explain projection of grouped HAVING clauses.
1196///
1197
1198#[derive(Clone, Debug, Eq, PartialEq)]
1199pub struct ExplainGroupHaving {
1200    pub(crate) clauses: Vec<ExplainGroupHavingClause>,
1201}
1202
1203impl ExplainGroupHaving {
1204    /// Borrow grouped HAVING clauses.
1205    #[must_use]
1206    pub const fn clauses(&self) -> &[ExplainGroupHavingClause] {
1207        self.clauses.as_slice()
1208    }
1209}
1210
1211///
1212/// ExplainGroupHavingClause
1213///
1214/// Stable explain-surface projection for one grouped HAVING clause.
1215///
1216
1217#[derive(Clone, Debug, Eq, PartialEq)]
1218pub struct ExplainGroupHavingClause {
1219    pub(crate) symbol: ExplainGroupHavingSymbol,
1220    pub(crate) op: CompareOp,
1221    pub(crate) value: Value,
1222}
1223
1224impl ExplainGroupHavingClause {
1225    /// Borrow grouped HAVING symbol.
1226    #[must_use]
1227    pub const fn symbol(&self) -> &ExplainGroupHavingSymbol {
1228        &self.symbol
1229    }
1230
1231    /// Return grouped HAVING comparison operator.
1232    #[must_use]
1233    pub const fn op(&self) -> CompareOp {
1234        self.op
1235    }
1236
1237    /// Borrow grouped HAVING literal value.
1238    #[must_use]
1239    pub const fn value(&self) -> &Value {
1240        &self.value
1241    }
1242}
1243
1244///
1245/// ExplainGroupHavingSymbol
1246///
1247/// Stable explain-surface identity for grouped HAVING symbols.
1248///
1249
1250#[derive(Clone, Debug, Eq, PartialEq)]
1251pub enum ExplainGroupHavingSymbol {
1252    GroupField { slot_index: usize, field: String },
1253    AggregateIndex { index: usize },
1254}
1255
1256///
1257/// ExplainOrderPushdown
1258///
1259/// Deterministic ORDER BY pushdown eligibility reported by explain.
1260///
1261
1262#[derive(Clone, Debug, Eq, PartialEq)]
1263pub enum ExplainOrderPushdown {
1264    MissingModelContext,
1265    EligibleSecondaryIndex {
1266        index: &'static str,
1267        prefix_len: usize,
1268    },
1269    Rejected(SecondaryOrderPushdownRejection),
1270}
1271
1272///
1273/// ExplainAccessPath
1274///
1275/// Deterministic projection of logical access path shape for diagnostics.
1276/// Mirrors planner-selected structural paths without runtime cursor state.
1277///
1278#[derive(Clone, Debug, Eq, PartialEq)]
1279pub enum ExplainAccessPath {
1280    ByKey {
1281        key: Value,
1282    },
1283    ByKeys {
1284        keys: Vec<Value>,
1285    },
1286    KeyRange {
1287        start: Value,
1288        end: Value,
1289    },
1290    IndexPrefix {
1291        name: &'static str,
1292        fields: Vec<&'static str>,
1293        prefix_len: usize,
1294        values: Vec<Value>,
1295    },
1296    IndexMultiLookup {
1297        name: &'static str,
1298        fields: Vec<&'static str>,
1299        values: Vec<Value>,
1300    },
1301    IndexRange {
1302        name: &'static str,
1303        fields: Vec<&'static str>,
1304        prefix_len: usize,
1305        prefix: Vec<Value>,
1306        lower: Bound<Value>,
1307        upper: Bound<Value>,
1308    },
1309    FullScan,
1310    Union(Vec<Self>),
1311    Intersection(Vec<Self>),
1312}
1313
1314///
1315/// ExplainPredicate
1316///
1317/// Deterministic projection of canonical predicate structure for explain output.
1318/// This preserves normalized predicate shape used by hashing/fingerprints.
1319///
1320#[derive(Clone, Debug, Eq, PartialEq)]
1321pub enum ExplainPredicate {
1322    None,
1323    True,
1324    False,
1325    And(Vec<Self>),
1326    Or(Vec<Self>),
1327    Not(Box<Self>),
1328    Compare {
1329        field: String,
1330        op: CompareOp,
1331        value: Value,
1332        coercion: CoercionSpec,
1333    },
1334    IsNull {
1335        field: String,
1336    },
1337    IsMissing {
1338        field: String,
1339    },
1340    IsEmpty {
1341        field: String,
1342    },
1343    IsNotEmpty {
1344        field: String,
1345    },
1346    TextContains {
1347        field: String,
1348        value: Value,
1349    },
1350    TextContainsCi {
1351        field: String,
1352        value: Value,
1353    },
1354}
1355
1356///
1357/// ExplainOrderBy
1358///
1359/// Deterministic projection of canonical ORDER BY shape.
1360///
1361#[derive(Clone, Debug, Eq, PartialEq)]
1362pub enum ExplainOrderBy {
1363    None,
1364    Fields(Vec<ExplainOrder>),
1365}
1366
1367///
1368/// ExplainOrder
1369///
1370/// One canonical ORDER BY field + direction pair.
1371///
1372
1373#[derive(Clone, Debug, Eq, PartialEq)]
1374pub struct ExplainOrder {
1375    pub(crate) field: String,
1376    pub(crate) direction: OrderDirection,
1377}
1378
1379impl ExplainOrder {
1380    /// Borrow ORDER BY field name.
1381    #[must_use]
1382    pub const fn field(&self) -> &str {
1383        self.field.as_str()
1384    }
1385
1386    /// Return ORDER BY direction.
1387    #[must_use]
1388    pub const fn direction(&self) -> OrderDirection {
1389        self.direction
1390    }
1391}
1392
1393///
1394/// ExplainPagination
1395///
1396/// Explain-surface projection of pagination window configuration.
1397///
1398
1399#[derive(Clone, Debug, Eq, PartialEq)]
1400pub enum ExplainPagination {
1401    None,
1402    Page { limit: Option<u32>, offset: u32 },
1403}
1404
1405///
1406/// ExplainDeleteLimit
1407///
1408/// Explain-surface projection of delete-limit configuration.
1409///
1410
1411#[derive(Clone, Debug, Eq, PartialEq)]
1412pub enum ExplainDeleteLimit {
1413    None,
1414    Limit { max_rows: u32 },
1415}
1416
1417impl<K> AccessPlannedQuery<K>
1418where
1419    K: FieldValue,
1420{
1421    /// Produce a stable, deterministic explanation of this logical plan.
1422    #[must_use]
1423    pub(crate) fn explain(&self) -> ExplainPlan {
1424        self.explain_inner(None)
1425    }
1426
1427    /// Produce a stable, deterministic explanation of this logical plan
1428    /// with optional model context for query-layer projections.
1429    ///
1430    /// Query explain intentionally does not evaluate executor route pushdown
1431    /// feasibility to keep query-layer dependencies executor-agnostic.
1432    #[must_use]
1433    pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
1434        self.explain_inner(Some(model))
1435    }
1436
1437    fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
1438        // Phase 1: project logical plan variant into scalar core + grouped metadata.
1439        let (logical, grouping) = match &self.logical {
1440            LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
1441            LogicalPlan::Grouped(logical) => (
1442                &logical.scalar,
1443                ExplainGrouping::Grouped {
1444                    strategy: grouped_plan_strategy_hint_for_plan(self)
1445                        .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
1446                    group_fields: logical
1447                        .group
1448                        .group_fields
1449                        .iter()
1450                        .map(|field_slot| ExplainGroupField {
1451                            slot_index: field_slot.index(),
1452                            field: field_slot.field().to_string(),
1453                        })
1454                        .collect(),
1455                    aggregates: logical
1456                        .group
1457                        .aggregates
1458                        .iter()
1459                        .map(|aggregate| ExplainGroupAggregate {
1460                            kind: aggregate.kind,
1461                            target_field: aggregate.target_field.clone(),
1462                            distinct: aggregate.distinct,
1463                        })
1464                        .collect(),
1465                    having: explain_group_having(logical.having.as_ref()),
1466                    max_groups: logical.group.execution.max_groups(),
1467                    max_group_bytes: logical.group.execution.max_group_bytes(),
1468                },
1469            ),
1470        };
1471
1472        // Phase 2: project scalar plan + access path into deterministic explain surface.
1473        explain_scalar_inner(logical, grouping, model, &self.access)
1474    }
1475}
1476
1477fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
1478    let having = having?;
1479
1480    Some(ExplainGroupHaving {
1481        clauses: having
1482            .clauses()
1483            .iter()
1484            .map(explain_group_having_clause)
1485            .collect(),
1486    })
1487}
1488
1489fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
1490    ExplainGroupHavingClause {
1491        symbol: explain_group_having_symbol(clause.symbol()),
1492        op: clause.op(),
1493        value: clause.value().clone(),
1494    }
1495}
1496
1497fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
1498    match symbol {
1499        GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
1500            slot_index: field_slot.index(),
1501            field: field_slot.field().to_string(),
1502        },
1503        GroupHavingSymbol::AggregateIndex(index) => {
1504            ExplainGroupHavingSymbol::AggregateIndex { index: *index }
1505        }
1506    }
1507}
1508
1509fn explain_scalar_inner<K>(
1510    logical: &ScalarPlan,
1511    grouping: ExplainGrouping,
1512    model: Option<&EntityModel>,
1513    access: &AccessPlan<K>,
1514) -> ExplainPlan
1515where
1516    K: FieldValue,
1517{
1518    // Phase 1: derive canonical predicate projection from normalized predicate model.
1519    let predicate_model = logical.predicate.as_ref().map(normalize);
1520    let predicate = match &predicate_model {
1521        Some(predicate) => ExplainPredicate::from_predicate(predicate),
1522        None => ExplainPredicate::None,
1523    };
1524
1525    // Phase 2: project scalar-plan fields into explain-specific enums.
1526    let order_by = explain_order(logical.order.as_ref());
1527    let order_pushdown = explain_order_pushdown(model);
1528    let page = explain_page(logical.page.as_ref());
1529    let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
1530
1531    // Phase 3: assemble one stable explain payload.
1532    ExplainPlan {
1533        mode: logical.mode,
1534        access: ExplainAccessPath::from_access_plan(access),
1535        predicate,
1536        predicate_model,
1537        order_by,
1538        distinct: logical.distinct,
1539        grouping,
1540        order_pushdown,
1541        page,
1542        delete_limit,
1543        consistency: logical.consistency,
1544    }
1545}
1546
1547const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
1548    let _ = model;
1549
1550    // Query explain does not own physical pushdown feasibility routing.
1551    ExplainOrderPushdown::MissingModelContext
1552}
1553
1554impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
1555    fn from(value: SecondaryOrderPushdownEligibility) -> Self {
1556        Self::from(PushdownSurfaceEligibility::from(&value))
1557    }
1558}
1559
1560impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
1561    fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
1562        match value {
1563            PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
1564                Self::EligibleSecondaryIndex { index, prefix_len }
1565            }
1566            PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
1567        }
1568    }
1569}
1570
1571struct ExplainAccessProjection;
1572
1573impl<K> AccessPlanProjection<K> for ExplainAccessProjection
1574where
1575    K: FieldValue,
1576{
1577    type Output = ExplainAccessPath;
1578
1579    fn by_key(&mut self, key: &K) -> Self::Output {
1580        ExplainAccessPath::ByKey {
1581            key: key.to_value(),
1582        }
1583    }
1584
1585    fn by_keys(&mut self, keys: &[K]) -> Self::Output {
1586        ExplainAccessPath::ByKeys {
1587            keys: keys.iter().map(FieldValue::to_value).collect(),
1588        }
1589    }
1590
1591    fn key_range(&mut self, start: &K, end: &K) -> Self::Output {
1592        ExplainAccessPath::KeyRange {
1593            start: start.to_value(),
1594            end: end.to_value(),
1595        }
1596    }
1597
1598    fn index_prefix(
1599        &mut self,
1600        index_name: &'static str,
1601        index_fields: &[&'static str],
1602        prefix_len: usize,
1603        values: &[Value],
1604    ) -> Self::Output {
1605        ExplainAccessPath::IndexPrefix {
1606            name: index_name,
1607            fields: index_fields.to_vec(),
1608            prefix_len,
1609            values: values.to_vec(),
1610        }
1611    }
1612
1613    fn index_multi_lookup(
1614        &mut self,
1615        index_name: &'static str,
1616        index_fields: &[&'static str],
1617        values: &[Value],
1618    ) -> Self::Output {
1619        ExplainAccessPath::IndexMultiLookup {
1620            name: index_name,
1621            fields: index_fields.to_vec(),
1622            values: values.to_vec(),
1623        }
1624    }
1625
1626    fn index_range(
1627        &mut self,
1628        index_name: &'static str,
1629        index_fields: &[&'static str],
1630        prefix_len: usize,
1631        prefix: &[Value],
1632        lower: &Bound<Value>,
1633        upper: &Bound<Value>,
1634    ) -> Self::Output {
1635        ExplainAccessPath::IndexRange {
1636            name: index_name,
1637            fields: index_fields.to_vec(),
1638            prefix_len,
1639            prefix: prefix.to_vec(),
1640            lower: lower.clone(),
1641            upper: upper.clone(),
1642        }
1643    }
1644
1645    fn full_scan(&mut self) -> Self::Output {
1646        ExplainAccessPath::FullScan
1647    }
1648
1649    fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
1650        ExplainAccessPath::Union(children)
1651    }
1652
1653    fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
1654        ExplainAccessPath::Intersection(children)
1655    }
1656}
1657
1658impl ExplainAccessPath {
1659    pub(in crate::db) fn from_access_plan<K>(access: &AccessPlan<K>) -> Self
1660    where
1661        K: FieldValue,
1662    {
1663        let mut projection = ExplainAccessProjection;
1664        project_access_plan(access, &mut projection)
1665    }
1666}
1667
1668impl ExplainPredicate {
1669    fn from_predicate(predicate: &Predicate) -> Self {
1670        match predicate {
1671            Predicate::True => Self::True,
1672            Predicate::False => Self::False,
1673            Predicate::And(children) => {
1674                Self::And(children.iter().map(Self::from_predicate).collect())
1675            }
1676            Predicate::Or(children) => {
1677                Self::Or(children.iter().map(Self::from_predicate).collect())
1678            }
1679            Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
1680            Predicate::Compare(compare) => Self::from_compare(compare),
1681            Predicate::IsNull { field } => Self::IsNull {
1682                field: field.clone(),
1683            },
1684            Predicate::IsMissing { field } => Self::IsMissing {
1685                field: field.clone(),
1686            },
1687            Predicate::IsEmpty { field } => Self::IsEmpty {
1688                field: field.clone(),
1689            },
1690            Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
1691                field: field.clone(),
1692            },
1693            Predicate::TextContains { field, value } => Self::TextContains {
1694                field: field.clone(),
1695                value: value.clone(),
1696            },
1697            Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
1698                field: field.clone(),
1699                value: value.clone(),
1700            },
1701        }
1702    }
1703
1704    fn from_compare(compare: &ComparePredicate) -> Self {
1705        Self::Compare {
1706            field: compare.field.clone(),
1707            op: compare.op,
1708            value: compare.value.clone(),
1709            coercion: compare.coercion.clone(),
1710        }
1711    }
1712}
1713
1714fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
1715    let Some(order) = order else {
1716        return ExplainOrderBy::None;
1717    };
1718
1719    if order.fields.is_empty() {
1720        return ExplainOrderBy::None;
1721    }
1722
1723    ExplainOrderBy::Fields(
1724        order
1725            .fields
1726            .iter()
1727            .map(|(field, direction)| ExplainOrder {
1728                field: field.clone(),
1729                direction: *direction,
1730            })
1731            .collect(),
1732    )
1733}
1734
1735const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1736    match page {
1737        Some(page) => ExplainPagination::Page {
1738            limit: page.limit,
1739            offset: page.offset,
1740        },
1741        None => ExplainPagination::None,
1742    }
1743}
1744
1745const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1746    match limit {
1747        Some(limit) => ExplainDeleteLimit::Limit {
1748            max_rows: limit.max_rows,
1749        },
1750        None => ExplainDeleteLimit::None,
1751    }
1752}
1753
1754///
1755/// TESTS
1756///
1757
1758#[cfg(test)]
1759mod tests;