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