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 mode: QueryMode,
37    pub access: ExplainAccessPath,
38    pub predicate: ExplainPredicate,
39    predicate_model: Option<Predicate>,
40    pub order_by: ExplainOrderBy,
41    pub distinct: bool,
42    pub grouping: ExplainGrouping,
43    pub order_pushdown: ExplainOrderPushdown,
44    pub page: ExplainPagination,
45    pub delete_limit: ExplainDeleteLimit,
46    pub 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 query: ExplainPlan,
71    pub terminal: AggregateKind,
72    pub route: ExplainAggregateTerminalRoute,
73    pub 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 access_strategy: ExplainAccessPath,
111    pub covering_projection: bool,
112    pub aggregation: AggregateKind,
113    pub execution_mode: ExplainExecutionMode,
114    pub ordering_source: ExplainExecutionOrderingSource,
115    pub limit: Option<u32>,
116    pub cursor: bool,
117    pub 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 node_type: ExplainExecutionNodeType,
171    pub execution_mode: ExplainExecutionMode,
172    pub access_strategy: Option<ExplainAccessPath>,
173    pub predicate_pushdown: Option<String>,
174    pub residual_predicate: Option<ExplainPredicate>,
175    pub projection: Option<String>,
176    pub ordering_source: Option<ExplainExecutionOrderingSource>,
177    pub limit: Option<u32>,
178    pub cursor: Option<bool>,
179    pub covering_scan: Option<bool>,
180    pub rows_expected: Option<u64>,
181    pub children: Vec<Self>,
182    pub node_properties: BTreeMap<String, Value>,
183}
184
185impl ExplainAggregateTerminalPlan {
186    #[must_use]
187    pub(in crate::db) const fn new(
188        query: ExplainPlan,
189        terminal: AggregateKind,
190        execution: ExplainExecutionDescriptor,
191    ) -> Self {
192        let route = execution.route();
193
194        Self {
195            query,
196            terminal,
197            route,
198            execution,
199        }
200    }
201}
202
203impl ExplainExecutionDescriptor {
204    #[must_use]
205    pub(in crate::db) const fn route(&self) -> ExplainAggregateTerminalRoute {
206        match self.ordering_source {
207            ExplainExecutionOrderingSource::IndexSeekFirst { fetch } => {
208                ExplainAggregateTerminalRoute::IndexSeekFirst { fetch }
209            }
210            ExplainExecutionOrderingSource::IndexSeekLast { fetch } => {
211                ExplainAggregateTerminalRoute::IndexSeekLast { fetch }
212            }
213            ExplainExecutionOrderingSource::AccessOrder
214            | ExplainExecutionOrderingSource::Materialized => {
215                ExplainAggregateTerminalRoute::Standard
216            }
217        }
218    }
219}
220
221impl ExplainAggregateTerminalPlan {
222    #[must_use]
223    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
224        ExplainExecutionNodeDescriptor {
225            node_type: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
226            execution_mode: self.execution.execution_mode,
227            access_strategy: Some(self.execution.access_strategy.clone()),
228            predicate_pushdown: None,
229            residual_predicate: None,
230            projection: None,
231            ordering_source: Some(self.execution.ordering_source),
232            limit: self.execution.limit,
233            cursor: Some(self.execution.cursor),
234            covering_scan: Some(self.execution.covering_projection),
235            rows_expected: None,
236            children: Vec::new(),
237            node_properties: self.execution.node_properties.clone(),
238        }
239    }
240}
241
242const fn aggregate_execution_node_type(
243    terminal: AggregateKind,
244    ordering_source: ExplainExecutionOrderingSource,
245) -> ExplainExecutionNodeType {
246    match ordering_source {
247        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
248            ExplainExecutionNodeType::AggregateSeekFirst
249        }
250        ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
251            ExplainExecutionNodeType::AggregateSeekLast
252        }
253        ExplainExecutionOrderingSource::AccessOrder
254        | ExplainExecutionOrderingSource::Materialized => match terminal {
255            AggregateKind::Count => ExplainExecutionNodeType::AggregateCount,
256            AggregateKind::Exists => ExplainExecutionNodeType::AggregateExists,
257            AggregateKind::Min => ExplainExecutionNodeType::AggregateMin,
258            AggregateKind::Max => ExplainExecutionNodeType::AggregateMax,
259            AggregateKind::First => ExplainExecutionNodeType::AggregateFirst,
260            AggregateKind::Last => ExplainExecutionNodeType::AggregateLast,
261            AggregateKind::Sum => ExplainExecutionNodeType::AggregateSum,
262        },
263    }
264}
265
266impl ExplainExecutionNodeType {
267    #[must_use]
268    pub const fn as_str(self) -> &'static str {
269        match self {
270            Self::ByKeyLookup => "ByKeyLookup",
271            Self::ByKeysLookup => "ByKeysLookup",
272            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
273            Self::IndexPrefixScan => "IndexPrefixScan",
274            Self::IndexRangeScan => "IndexRangeScan",
275            Self::IndexMultiLookup => "IndexMultiLookup",
276            Self::FullScan => "FullScan",
277            Self::Union => "Union",
278            Self::Intersection => "Intersection",
279            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
280            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
281            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
282            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
283            Self::DistinctPreOrdered => "DistinctPreOrdered",
284            Self::DistinctMaterialized => "DistinctMaterialized",
285            Self::ProjectionMaterialized => "ProjectionMaterialized",
286            Self::ProjectionIndexOnly => "ProjectionIndexOnly",
287            Self::LimitOffset => "LimitOffset",
288            Self::CursorResume => "CursorResume",
289            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
290            Self::TopNSeek => "TopNSeek",
291            Self::AggregateCount => "AggregateCount",
292            Self::AggregateExists => "AggregateExists",
293            Self::AggregateMin => "AggregateMin",
294            Self::AggregateMax => "AggregateMax",
295            Self::AggregateFirst => "AggregateFirst",
296            Self::AggregateLast => "AggregateLast",
297            Self::AggregateSum => "AggregateSum",
298            Self::AggregateSeekFirst => "AggregateSeekFirst",
299            Self::AggregateSeekLast => "AggregateSeekLast",
300            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
301            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
302            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
303        }
304    }
305}
306
307impl ExplainExecutionNodeDescriptor {
308    #[must_use]
309    pub fn render_text_tree(&self) -> String {
310        let mut lines = Vec::new();
311        self.render_text_tree_into(0, &mut lines);
312        lines.join("\n")
313    }
314
315    #[must_use]
316    pub fn render_json_canonical(&self) -> String {
317        let mut out = String::new();
318        write_execution_node_json(self, &mut out);
319        out
320    }
321
322    #[must_use]
323    pub fn render_text_tree_verbose(&self) -> String {
324        let mut lines = Vec::new();
325        self.render_text_tree_verbose_into(0, &mut lines);
326        lines.join("\n")
327    }
328
329    fn render_text_tree_into(&self, depth: usize, lines: &mut Vec<String>) {
330        let mut line = format!(
331            "{}{} execution_mode={}",
332            "  ".repeat(depth),
333            self.node_type.as_str(),
334            execution_mode_label(self.execution_mode)
335        );
336
337        if let Some(access_strategy) = self.access_strategy.as_ref() {
338            let _ = write!(line, " access={}", access_strategy_label(access_strategy));
339        }
340        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
341            let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
342        }
343        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
344            let _ = write!(line, " residual_predicate={residual_predicate:?}");
345        }
346        if let Some(projection) = self.projection.as_ref() {
347            let _ = write!(line, " projection={projection}");
348        }
349        if let Some(ordering_source) = self.ordering_source {
350            let _ = write!(
351                line,
352                " ordering_source={}",
353                ordering_source_label(ordering_source)
354            );
355        }
356        if let Some(limit) = self.limit {
357            let _ = write!(line, " limit={limit}");
358        }
359        if let Some(cursor) = self.cursor {
360            let _ = write!(line, " cursor={cursor}");
361        }
362        if let Some(covering_scan) = self.covering_scan {
363            let _ = write!(line, " covering_scan={covering_scan}");
364        }
365        if let Some(rows_expected) = self.rows_expected {
366            let _ = write!(line, " rows_expected={rows_expected}");
367        }
368        if !self.node_properties.is_empty() {
369            let _ = write!(
370                line,
371                " node_properties={}",
372                render_node_properties(&self.node_properties)
373            );
374        }
375
376        lines.push(line);
377
378        for child in &self.children {
379            child.render_text_tree_into(depth.saturating_add(1), lines);
380        }
381    }
382
383    fn render_text_tree_verbose_into(&self, depth: usize, lines: &mut Vec<String>) {
384        // Emit the node heading line first so child metadata stays visually scoped.
385        let node_indent = "  ".repeat(depth);
386        let field_indent = "  ".repeat(depth.saturating_add(1));
387        lines.push(format!(
388            "{}{} execution_mode={}",
389            node_indent,
390            self.node_type.as_str(),
391            execution_mode_label(self.execution_mode)
392        ));
393
394        // Emit all optional node-local fields in a deterministic order.
395        if let Some(access_strategy) = self.access_strategy.as_ref() {
396            lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
397        }
398        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
399            lines.push(format!(
400                "{field_indent}predicate_pushdown={predicate_pushdown}"
401            ));
402        }
403        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
404            lines.push(format!(
405                "{field_indent}residual_predicate={residual_predicate:?}"
406            ));
407        }
408        if let Some(projection) = self.projection.as_ref() {
409            lines.push(format!("{field_indent}projection={projection}"));
410        }
411        if let Some(ordering_source) = self.ordering_source {
412            lines.push(format!(
413                "{}ordering_source={}",
414                field_indent,
415                ordering_source_label(ordering_source)
416            ));
417        }
418        if let Some(limit) = self.limit {
419            lines.push(format!("{field_indent}limit={limit}"));
420        }
421        if let Some(cursor) = self.cursor {
422            lines.push(format!("{field_indent}cursor={cursor}"));
423        }
424        if let Some(covering_scan) = self.covering_scan {
425            lines.push(format!("{field_indent}covering_scan={covering_scan}"));
426        }
427        if let Some(rows_expected) = self.rows_expected {
428            lines.push(format!("{field_indent}rows_expected={rows_expected}"));
429        }
430        if !self.node_properties.is_empty() {
431            lines.push(format!(
432                "{}node_properties={}",
433                field_indent,
434                render_node_properties(&self.node_properties)
435            ));
436        }
437
438        // Recurse in execution order to preserve stable tree topology.
439        for child in &self.children {
440            child.render_text_tree_verbose_into(depth.saturating_add(1), lines);
441        }
442    }
443}
444
445const fn execution_mode_label(mode: ExplainExecutionMode) -> &'static str {
446    match mode {
447        ExplainExecutionMode::Streaming => "Streaming",
448        ExplainExecutionMode::Materialized => "Materialized",
449    }
450}
451
452fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
453    let mut rendered = String::new();
454    let mut first = true;
455    for (key, value) in node_properties {
456        if first {
457            first = false;
458        } else {
459            rendered.push(',');
460        }
461        let _ = write!(rendered, "{key}={value:?}");
462    }
463    rendered
464}
465
466fn write_execution_node_json(node: &ExplainExecutionNodeDescriptor, out: &mut String) {
467    out.push('{');
468
469    write_json_field_name(out, "node_type");
470    write_json_string(out, node.node_type.as_str());
471    out.push(',');
472
473    write_json_field_name(out, "execution_mode");
474    write_json_string(out, execution_mode_label(node.execution_mode));
475    out.push(',');
476
477    write_json_field_name(out, "access_strategy");
478    match node.access_strategy.as_ref() {
479        Some(access) => write_access_json(access, out),
480        None => out.push_str("null"),
481    }
482    out.push(',');
483
484    write_json_field_name(out, "predicate_pushdown");
485    match node.predicate_pushdown.as_ref() {
486        Some(predicate_pushdown) => write_json_string(out, predicate_pushdown),
487        None => out.push_str("null"),
488    }
489    out.push(',');
490
491    write_json_field_name(out, "residual_predicate");
492    match node.residual_predicate.as_ref() {
493        Some(residual_predicate) => write_json_string(out, &format!("{residual_predicate:?}")),
494        None => out.push_str("null"),
495    }
496    out.push(',');
497
498    write_json_field_name(out, "projection");
499    match node.projection.as_ref() {
500        Some(projection) => write_json_string(out, projection),
501        None => out.push_str("null"),
502    }
503    out.push(',');
504
505    write_json_field_name(out, "ordering_source");
506    match node.ordering_source {
507        Some(ordering_source) => write_json_string(out, ordering_source_label(ordering_source)),
508        None => out.push_str("null"),
509    }
510    out.push(',');
511
512    write_json_field_name(out, "limit");
513    match node.limit {
514        Some(limit) => out.push_str(&limit.to_string()),
515        None => out.push_str("null"),
516    }
517    out.push(',');
518
519    write_json_field_name(out, "cursor");
520    match node.cursor {
521        Some(cursor) => out.push_str(if cursor { "true" } else { "false" }),
522        None => out.push_str("null"),
523    }
524    out.push(',');
525
526    write_json_field_name(out, "covering_scan");
527    match node.covering_scan {
528        Some(covering_scan) => out.push_str(if covering_scan { "true" } else { "false" }),
529        None => out.push_str("null"),
530    }
531    out.push(',');
532
533    write_json_field_name(out, "rows_expected");
534    match node.rows_expected {
535        Some(rows_expected) => out.push_str(&rows_expected.to_string()),
536        None => out.push_str("null"),
537    }
538    out.push(',');
539
540    write_json_field_name(out, "children");
541    out.push('[');
542    for (index, child) in node.children.iter().enumerate() {
543        if index > 0 {
544            out.push(',');
545        }
546        write_execution_node_json(child, out);
547    }
548    out.push(']');
549    out.push(',');
550
551    write_json_field_name(out, "node_properties");
552    write_node_properties_json(&node.node_properties, out);
553
554    out.push('}');
555}
556
557#[expect(clippy::too_many_lines)]
558fn write_access_json(access: &ExplainAccessPath, out: &mut String) {
559    match access {
560        ExplainAccessPath::ByKey { key } => {
561            out.push('{');
562            write_json_field_name(out, "type");
563            write_json_string(out, "ByKey");
564            out.push(',');
565            write_json_field_name(out, "key");
566            write_json_string(out, &format!("{key:?}"));
567            out.push('}');
568        }
569        ExplainAccessPath::ByKeys { keys } => {
570            out.push('{');
571            write_json_field_name(out, "type");
572            write_json_string(out, "ByKeys");
573            out.push(',');
574            write_json_field_name(out, "keys");
575            write_value_vec_as_debug_json(keys, out);
576            out.push('}');
577        }
578        ExplainAccessPath::KeyRange { start, end } => {
579            out.push('{');
580            write_json_field_name(out, "type");
581            write_json_string(out, "KeyRange");
582            out.push(',');
583            write_json_field_name(out, "start");
584            write_json_string(out, &format!("{start:?}"));
585            out.push(',');
586            write_json_field_name(out, "end");
587            write_json_string(out, &format!("{end:?}"));
588            out.push('}');
589        }
590        ExplainAccessPath::IndexPrefix {
591            name,
592            fields,
593            prefix_len,
594            values,
595        } => {
596            out.push('{');
597            write_json_field_name(out, "type");
598            write_json_string(out, "IndexPrefix");
599            out.push(',');
600            write_json_field_name(out, "name");
601            write_json_string(out, name);
602            out.push(',');
603            write_json_field_name(out, "fields");
604            write_str_vec_json(fields, out);
605            out.push(',');
606            write_json_field_name(out, "prefix_len");
607            out.push_str(&prefix_len.to_string());
608            out.push(',');
609            write_json_field_name(out, "values");
610            write_value_vec_as_debug_json(values, out);
611            out.push('}');
612        }
613        ExplainAccessPath::IndexMultiLookup {
614            name,
615            fields,
616            values,
617        } => {
618            out.push('{');
619            write_json_field_name(out, "type");
620            write_json_string(out, "IndexMultiLookup");
621            out.push(',');
622            write_json_field_name(out, "name");
623            write_json_string(out, name);
624            out.push(',');
625            write_json_field_name(out, "fields");
626            write_str_vec_json(fields, out);
627            out.push(',');
628            write_json_field_name(out, "values");
629            write_value_vec_as_debug_json(values, out);
630            out.push('}');
631        }
632        ExplainAccessPath::IndexRange {
633            name,
634            fields,
635            prefix_len,
636            prefix,
637            lower,
638            upper,
639        } => {
640            out.push('{');
641            write_json_field_name(out, "type");
642            write_json_string(out, "IndexRange");
643            out.push(',');
644            write_json_field_name(out, "name");
645            write_json_string(out, name);
646            out.push(',');
647            write_json_field_name(out, "fields");
648            write_str_vec_json(fields, out);
649            out.push(',');
650            write_json_field_name(out, "prefix_len");
651            out.push_str(&prefix_len.to_string());
652            out.push(',');
653            write_json_field_name(out, "prefix");
654            write_value_vec_as_debug_json(prefix, out);
655            out.push(',');
656            write_json_field_name(out, "lower");
657            write_json_string(out, &format!("{lower:?}"));
658            out.push(',');
659            write_json_field_name(out, "upper");
660            write_json_string(out, &format!("{upper:?}"));
661            out.push('}');
662        }
663        ExplainAccessPath::FullScan => {
664            out.push('{');
665            write_json_field_name(out, "type");
666            write_json_string(out, "FullScan");
667            out.push('}');
668        }
669        ExplainAccessPath::Union(children) => {
670            out.push('{');
671            write_json_field_name(out, "type");
672            write_json_string(out, "Union");
673            out.push(',');
674            write_json_field_name(out, "children");
675            out.push('[');
676            for (index, child) in children.iter().enumerate() {
677                if index > 0 {
678                    out.push(',');
679                }
680                write_access_json(child, out);
681            }
682            out.push(']');
683            out.push('}');
684        }
685        ExplainAccessPath::Intersection(children) => {
686            out.push('{');
687            write_json_field_name(out, "type");
688            write_json_string(out, "Intersection");
689            out.push(',');
690            write_json_field_name(out, "children");
691            out.push('[');
692            for (index, child) in children.iter().enumerate() {
693                if index > 0 {
694                    out.push(',');
695                }
696                write_access_json(child, out);
697            }
698            out.push(']');
699            out.push('}');
700        }
701    }
702}
703
704fn write_node_properties_json(node_properties: &BTreeMap<String, Value>, out: &mut String) {
705    out.push('{');
706    for (index, (key, value)) in node_properties.iter().enumerate() {
707        if index > 0 {
708            out.push(',');
709        }
710        write_json_field_name(out, key);
711        write_json_string(out, &format!("{value:?}"));
712    }
713    out.push('}');
714}
715
716fn write_value_vec_as_debug_json(values: &[Value], out: &mut String) {
717    out.push('[');
718    for (index, value) in values.iter().enumerate() {
719        if index > 0 {
720            out.push(',');
721        }
722        write_json_string(out, &format!("{value:?}"));
723    }
724    out.push(']');
725}
726
727fn write_str_vec_json(values: &[&str], out: &mut String) {
728    out.push('[');
729    for (index, value) in values.iter().enumerate() {
730        if index > 0 {
731            out.push(',');
732        }
733        write_json_string(out, value);
734    }
735    out.push(']');
736}
737
738fn write_json_field_name(out: &mut String, key: &str) {
739    write_json_string(out, key);
740    out.push(':');
741}
742
743fn write_json_string(out: &mut String, value: &str) {
744    out.push('"');
745    for ch in value.chars() {
746        match ch {
747            '"' => out.push_str("\\\""),
748            '\\' => out.push_str("\\\\"),
749            '\n' => out.push_str("\\n"),
750            '\r' => out.push_str("\\r"),
751            '\t' => out.push_str("\\t"),
752            '\u{08}' => out.push_str("\\b"),
753            '\u{0C}' => out.push_str("\\f"),
754            _ => out.push(ch),
755        }
756    }
757    out.push('"');
758}
759
760fn access_strategy_label(access: &ExplainAccessPath) -> String {
761    match access {
762        ExplainAccessPath::ByKey { .. } => "ByKey".to_string(),
763        ExplainAccessPath::ByKeys { .. } => "ByKeys".to_string(),
764        ExplainAccessPath::KeyRange { .. } => "KeyRange".to_string(),
765        ExplainAccessPath::IndexPrefix { name, .. } => format!("IndexPrefix({name})"),
766        ExplainAccessPath::IndexMultiLookup { name, .. } => {
767            format!("IndexMultiLookup({name})")
768        }
769        ExplainAccessPath::IndexRange { name, .. } => format!("IndexRange({name})"),
770        ExplainAccessPath::FullScan => "FullScan".to_string(),
771        ExplainAccessPath::Union(children) => format!("Union({})", children.len()),
772        ExplainAccessPath::Intersection(children) => format!("Intersection({})", children.len()),
773    }
774}
775
776const fn ordering_source_label(ordering_source: ExplainExecutionOrderingSource) -> &'static str {
777    match ordering_source {
778        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
779        ExplainExecutionOrderingSource::Materialized => "Materialized",
780        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
781        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
782    }
783}
784
785impl ExplainPlan {
786    /// Return the canonical predicate model used for hashing/fingerprints.
787    ///
788    /// The explain projection must remain a faithful rendering of this model.
789    #[must_use]
790    pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
791        if let Some(predicate) = &self.predicate_model {
792            debug_assert_eq!(
793                self.predicate,
794                ExplainPredicate::from_predicate(predicate),
795                "explain predicate surface drifted from canonical predicate model"
796            );
797            Some(predicate)
798        } else {
799            debug_assert!(
800                matches!(self.predicate, ExplainPredicate::None),
801                "missing canonical predicate model requires ExplainPredicate::None"
802            );
803            None
804        }
805    }
806}
807
808///
809/// ExplainGrouping
810///
811/// Grouped-shape annotation for deterministic explain/fingerprint surfaces.
812///
813
814#[derive(Clone, Debug, Eq, PartialEq)]
815pub enum ExplainGrouping {
816    None,
817    Grouped {
818        strategy: ExplainGroupedStrategy,
819        group_fields: Vec<ExplainGroupField>,
820        aggregates: Vec<ExplainGroupAggregate>,
821        having: Option<ExplainGroupHaving>,
822        max_groups: u64,
823        max_group_bytes: u64,
824    },
825}
826
827///
828/// ExplainGroupedStrategy
829///
830/// Deterministic explain projection of grouped strategy selection.
831///
832#[derive(Clone, Copy, Debug, Eq, PartialEq)]
833pub enum ExplainGroupedStrategy {
834    HashGroup,
835    OrderedGroup,
836}
837
838impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
839    fn from(value: GroupedPlanStrategyHint) -> Self {
840        match value {
841            GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
842            GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
843        }
844    }
845}
846
847///
848/// ExplainGroupField
849///
850/// Stable grouped-key field identity carried by explain/hash surfaces.
851///
852
853#[derive(Clone, Debug, Eq, PartialEq)]
854pub struct ExplainGroupField {
855    pub slot_index: usize,
856    pub field: String,
857}
858
859///
860/// ExplainGroupAggregate
861///
862/// Stable explain-surface projection of one grouped aggregate terminal.
863///
864
865#[derive(Clone, Debug, Eq, PartialEq)]
866pub struct ExplainGroupAggregate {
867    pub kind: AggregateKind,
868    pub target_field: Option<String>,
869    pub distinct: bool,
870}
871
872///
873/// ExplainGroupHaving
874///
875/// Deterministic explain projection of grouped HAVING clauses.
876///
877
878#[derive(Clone, Debug, Eq, PartialEq)]
879pub struct ExplainGroupHaving {
880    pub clauses: Vec<ExplainGroupHavingClause>,
881}
882
883///
884/// ExplainGroupHavingClause
885///
886/// Stable explain-surface projection for one grouped HAVING clause.
887///
888
889#[derive(Clone, Debug, Eq, PartialEq)]
890pub struct ExplainGroupHavingClause {
891    pub symbol: ExplainGroupHavingSymbol,
892    pub op: CompareOp,
893    pub value: Value,
894}
895
896///
897/// ExplainGroupHavingSymbol
898///
899/// Stable explain-surface identity for grouped HAVING symbols.
900///
901
902#[derive(Clone, Debug, Eq, PartialEq)]
903pub enum ExplainGroupHavingSymbol {
904    GroupField { slot_index: usize, field: String },
905    AggregateIndex { index: usize },
906}
907
908///
909/// ExplainOrderPushdown
910///
911/// Deterministic ORDER BY pushdown eligibility reported by explain.
912///
913
914#[derive(Clone, Debug, Eq, PartialEq)]
915pub enum ExplainOrderPushdown {
916    MissingModelContext,
917    EligibleSecondaryIndex {
918        index: &'static str,
919        prefix_len: usize,
920    },
921    Rejected(SecondaryOrderPushdownRejection),
922}
923
924///
925/// ExplainAccessPath
926///
927/// Deterministic projection of logical access path shape for diagnostics.
928/// Mirrors planner-selected structural paths without runtime cursor state.
929///
930#[derive(Clone, Debug, Eq, PartialEq)]
931pub enum ExplainAccessPath {
932    ByKey {
933        key: Value,
934    },
935    ByKeys {
936        keys: Vec<Value>,
937    },
938    KeyRange {
939        start: Value,
940        end: Value,
941    },
942    IndexPrefix {
943        name: &'static str,
944        fields: Vec<&'static str>,
945        prefix_len: usize,
946        values: Vec<Value>,
947    },
948    IndexMultiLookup {
949        name: &'static str,
950        fields: Vec<&'static str>,
951        values: Vec<Value>,
952    },
953    IndexRange {
954        name: &'static str,
955        fields: Vec<&'static str>,
956        prefix_len: usize,
957        prefix: Vec<Value>,
958        lower: Bound<Value>,
959        upper: Bound<Value>,
960    },
961    FullScan,
962    Union(Vec<Self>),
963    Intersection(Vec<Self>),
964}
965
966///
967/// ExplainPredicate
968///
969/// Deterministic projection of canonical predicate structure for explain output.
970/// This preserves normalized predicate shape used by hashing/fingerprints.
971///
972#[derive(Clone, Debug, Eq, PartialEq)]
973pub enum ExplainPredicate {
974    None,
975    True,
976    False,
977    And(Vec<Self>),
978    Or(Vec<Self>),
979    Not(Box<Self>),
980    Compare {
981        field: String,
982        op: CompareOp,
983        value: Value,
984        coercion: CoercionSpec,
985    },
986    IsNull {
987        field: String,
988    },
989    IsMissing {
990        field: String,
991    },
992    IsEmpty {
993        field: String,
994    },
995    IsNotEmpty {
996        field: String,
997    },
998    TextContains {
999        field: String,
1000        value: Value,
1001    },
1002    TextContainsCi {
1003        field: String,
1004        value: Value,
1005    },
1006}
1007
1008///
1009/// ExplainOrderBy
1010///
1011/// Deterministic projection of canonical ORDER BY shape.
1012///
1013#[derive(Clone, Debug, Eq, PartialEq)]
1014pub enum ExplainOrderBy {
1015    None,
1016    Fields(Vec<ExplainOrder>),
1017}
1018
1019///
1020/// ExplainOrder
1021///
1022/// One canonical ORDER BY field + direction pair.
1023///
1024#[derive(Clone, Debug, Eq, PartialEq)]
1025pub struct ExplainOrder {
1026    pub field: String,
1027    pub direction: OrderDirection,
1028}
1029
1030///
1031/// ExplainPagination
1032///
1033/// Explain-surface projection of pagination window configuration.
1034///
1035#[derive(Clone, Debug, Eq, PartialEq)]
1036pub enum ExplainPagination {
1037    None,
1038    Page { limit: Option<u32>, offset: u32 },
1039}
1040
1041///
1042/// ExplainDeleteLimit
1043///
1044/// Explain-surface projection of delete-limit configuration.
1045///
1046#[derive(Clone, Debug, Eq, PartialEq)]
1047pub enum ExplainDeleteLimit {
1048    None,
1049    Limit { max_rows: u32 },
1050}
1051
1052impl<K> AccessPlannedQuery<K>
1053where
1054    K: FieldValue,
1055{
1056    /// Produce a stable, deterministic explanation of this logical plan.
1057    #[must_use]
1058    pub(crate) fn explain(&self) -> ExplainPlan {
1059        self.explain_inner(None)
1060    }
1061
1062    /// Produce a stable, deterministic explanation of this logical plan
1063    /// with optional model context for query-layer projections.
1064    ///
1065    /// Query explain intentionally does not evaluate executor route pushdown
1066    /// feasibility to keep query-layer dependencies executor-agnostic.
1067    #[must_use]
1068    pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
1069        self.explain_inner(Some(model))
1070    }
1071
1072    fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
1073        // Phase 1: project logical plan variant into scalar core + grouped metadata.
1074        let (logical, grouping) = match &self.logical {
1075            LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
1076            LogicalPlan::Grouped(logical) => (
1077                &logical.scalar,
1078                ExplainGrouping::Grouped {
1079                    strategy: grouped_plan_strategy_hint_for_plan(self)
1080                        .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
1081                    group_fields: logical
1082                        .group
1083                        .group_fields
1084                        .iter()
1085                        .map(|field_slot| ExplainGroupField {
1086                            slot_index: field_slot.index(),
1087                            field: field_slot.field().to_string(),
1088                        })
1089                        .collect(),
1090                    aggregates: logical
1091                        .group
1092                        .aggregates
1093                        .iter()
1094                        .map(|aggregate| ExplainGroupAggregate {
1095                            kind: aggregate.kind,
1096                            target_field: aggregate.target_field.clone(),
1097                            distinct: aggregate.distinct,
1098                        })
1099                        .collect(),
1100                    having: explain_group_having(logical.having.as_ref()),
1101                    max_groups: logical.group.execution.max_groups(),
1102                    max_group_bytes: logical.group.execution.max_group_bytes(),
1103                },
1104            ),
1105        };
1106
1107        // Phase 2: project scalar plan + access path into deterministic explain surface.
1108        explain_scalar_inner(logical, grouping, model, &self.access)
1109    }
1110}
1111
1112fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
1113    let having = having?;
1114
1115    Some(ExplainGroupHaving {
1116        clauses: having
1117            .clauses()
1118            .iter()
1119            .map(explain_group_having_clause)
1120            .collect(),
1121    })
1122}
1123
1124fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
1125    ExplainGroupHavingClause {
1126        symbol: explain_group_having_symbol(clause.symbol()),
1127        op: clause.op(),
1128        value: clause.value().clone(),
1129    }
1130}
1131
1132fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
1133    match symbol {
1134        GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
1135            slot_index: field_slot.index(),
1136            field: field_slot.field().to_string(),
1137        },
1138        GroupHavingSymbol::AggregateIndex(index) => {
1139            ExplainGroupHavingSymbol::AggregateIndex { index: *index }
1140        }
1141    }
1142}
1143
1144fn explain_scalar_inner<K>(
1145    logical: &ScalarPlan,
1146    grouping: ExplainGrouping,
1147    model: Option<&EntityModel>,
1148    access: &AccessPlan<K>,
1149) -> ExplainPlan
1150where
1151    K: FieldValue,
1152{
1153    // Phase 1: derive canonical predicate projection from normalized predicate model.
1154    let predicate_model = logical.predicate.as_ref().map(normalize);
1155    let predicate = match &predicate_model {
1156        Some(predicate) => ExplainPredicate::from_predicate(predicate),
1157        None => ExplainPredicate::None,
1158    };
1159
1160    // Phase 2: project scalar-plan fields into explain-specific enums.
1161    let order_by = explain_order(logical.order.as_ref());
1162    let order_pushdown = explain_order_pushdown(model);
1163    let page = explain_page(logical.page.as_ref());
1164    let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
1165
1166    // Phase 3: assemble one stable explain payload.
1167    ExplainPlan {
1168        mode: logical.mode,
1169        access: ExplainAccessPath::from_access_plan(access),
1170        predicate,
1171        predicate_model,
1172        order_by,
1173        distinct: logical.distinct,
1174        grouping,
1175        order_pushdown,
1176        page,
1177        delete_limit,
1178        consistency: logical.consistency,
1179    }
1180}
1181
1182const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
1183    let _ = model;
1184
1185    // Query explain does not own physical pushdown feasibility routing.
1186    ExplainOrderPushdown::MissingModelContext
1187}
1188
1189impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
1190    fn from(value: SecondaryOrderPushdownEligibility) -> Self {
1191        Self::from(PushdownSurfaceEligibility::from(&value))
1192    }
1193}
1194
1195impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
1196    fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
1197        match value {
1198            PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
1199                Self::EligibleSecondaryIndex { index, prefix_len }
1200            }
1201            PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
1202        }
1203    }
1204}
1205
1206struct ExplainAccessProjection;
1207
1208impl<K> AccessPlanProjection<K> for ExplainAccessProjection
1209where
1210    K: FieldValue,
1211{
1212    type Output = ExplainAccessPath;
1213
1214    fn by_key(&mut self, key: &K) -> Self::Output {
1215        ExplainAccessPath::ByKey {
1216            key: key.to_value(),
1217        }
1218    }
1219
1220    fn by_keys(&mut self, keys: &[K]) -> Self::Output {
1221        ExplainAccessPath::ByKeys {
1222            keys: keys.iter().map(FieldValue::to_value).collect(),
1223        }
1224    }
1225
1226    fn key_range(&mut self, start: &K, end: &K) -> Self::Output {
1227        ExplainAccessPath::KeyRange {
1228            start: start.to_value(),
1229            end: end.to_value(),
1230        }
1231    }
1232
1233    fn index_prefix(
1234        &mut self,
1235        index_name: &'static str,
1236        index_fields: &[&'static str],
1237        prefix_len: usize,
1238        values: &[Value],
1239    ) -> Self::Output {
1240        ExplainAccessPath::IndexPrefix {
1241            name: index_name,
1242            fields: index_fields.to_vec(),
1243            prefix_len,
1244            values: values.to_vec(),
1245        }
1246    }
1247
1248    fn index_multi_lookup(
1249        &mut self,
1250        index_name: &'static str,
1251        index_fields: &[&'static str],
1252        values: &[Value],
1253    ) -> Self::Output {
1254        ExplainAccessPath::IndexMultiLookup {
1255            name: index_name,
1256            fields: index_fields.to_vec(),
1257            values: values.to_vec(),
1258        }
1259    }
1260
1261    fn index_range(
1262        &mut self,
1263        index_name: &'static str,
1264        index_fields: &[&'static str],
1265        prefix_len: usize,
1266        prefix: &[Value],
1267        lower: &Bound<Value>,
1268        upper: &Bound<Value>,
1269    ) -> Self::Output {
1270        ExplainAccessPath::IndexRange {
1271            name: index_name,
1272            fields: index_fields.to_vec(),
1273            prefix_len,
1274            prefix: prefix.to_vec(),
1275            lower: lower.clone(),
1276            upper: upper.clone(),
1277        }
1278    }
1279
1280    fn full_scan(&mut self) -> Self::Output {
1281        ExplainAccessPath::FullScan
1282    }
1283
1284    fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
1285        ExplainAccessPath::Union(children)
1286    }
1287
1288    fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
1289        ExplainAccessPath::Intersection(children)
1290    }
1291}
1292
1293impl ExplainAccessPath {
1294    pub(in crate::db) fn from_access_plan<K>(access: &AccessPlan<K>) -> Self
1295    where
1296        K: FieldValue,
1297    {
1298        let mut projection = ExplainAccessProjection;
1299        project_access_plan(access, &mut projection)
1300    }
1301}
1302
1303impl ExplainPredicate {
1304    fn from_predicate(predicate: &Predicate) -> Self {
1305        match predicate {
1306            Predicate::True => Self::True,
1307            Predicate::False => Self::False,
1308            Predicate::And(children) => {
1309                Self::And(children.iter().map(Self::from_predicate).collect())
1310            }
1311            Predicate::Or(children) => {
1312                Self::Or(children.iter().map(Self::from_predicate).collect())
1313            }
1314            Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
1315            Predicate::Compare(compare) => Self::from_compare(compare),
1316            Predicate::IsNull { field } => Self::IsNull {
1317                field: field.clone(),
1318            },
1319            Predicate::IsMissing { field } => Self::IsMissing {
1320                field: field.clone(),
1321            },
1322            Predicate::IsEmpty { field } => Self::IsEmpty {
1323                field: field.clone(),
1324            },
1325            Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
1326                field: field.clone(),
1327            },
1328            Predicate::TextContains { field, value } => Self::TextContains {
1329                field: field.clone(),
1330                value: value.clone(),
1331            },
1332            Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
1333                field: field.clone(),
1334                value: value.clone(),
1335            },
1336        }
1337    }
1338
1339    fn from_compare(compare: &ComparePredicate) -> Self {
1340        Self::Compare {
1341            field: compare.field.clone(),
1342            op: compare.op,
1343            value: compare.value.clone(),
1344            coercion: compare.coercion.clone(),
1345        }
1346    }
1347}
1348
1349fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
1350    let Some(order) = order else {
1351        return ExplainOrderBy::None;
1352    };
1353
1354    if order.fields.is_empty() {
1355        return ExplainOrderBy::None;
1356    }
1357
1358    ExplainOrderBy::Fields(
1359        order
1360            .fields
1361            .iter()
1362            .map(|(field, direction)| ExplainOrder {
1363                field: field.clone(),
1364                direction: *direction,
1365            })
1366            .collect(),
1367    )
1368}
1369
1370const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1371    match page {
1372        Some(page) => ExplainPagination::Page {
1373            limit: page.limit,
1374            offset: page.offset,
1375        },
1376        None => ExplainPagination::None,
1377    }
1378}
1379
1380const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1381    match limit {
1382        Some(limit) => ExplainDeleteLimit::Limit {
1383            max_rows: limit.max_rows,
1384        },
1385        None => ExplainDeleteLimit::None,
1386    }
1387}
1388
1389///
1390/// TESTS
1391///
1392
1393#[cfg(test)]
1394mod tests;