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