Skip to main content

icydb_core/db/query/explain/
plan.rs

1//! Module: query::explain::plan
2//! Responsibility: deterministic logical-plan projection for EXPLAIN.
3//! Does not own: execution descriptor rendering or access visitor adapters.
4//! Boundary: logical explain DTOs and plan-side projection logic.
5
6use crate::{
7    db::{
8        access::{
9            AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
10            SecondaryOrderPushdownRejection,
11        },
12        predicate::{CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate},
13        query::{
14            builder::scalar_projection::render_scalar_projection_expr_sql_label,
15            explain::{access_projection::write_access_json, writer::JsonWriter},
16            plan::{
17                AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupHavingExpr,
18                GroupHavingValueExpr, GroupedPlanFallbackReason, LogicalPlan, OrderDirection,
19                OrderSpec, PageSpec, QueryMode, ScalarPlan, grouped_plan_strategy,
20            },
21        },
22    },
23    traits::FieldValue,
24    value::Value,
25};
26use std::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
49impl ExplainPlan {
50    /// Return query mode projected by this explain plan.
51    #[must_use]
52    pub const fn mode(&self) -> QueryMode {
53        self.mode
54    }
55
56    /// Borrow projected access-path shape.
57    #[must_use]
58    pub const fn access(&self) -> &ExplainAccessPath {
59        &self.access
60    }
61
62    /// Borrow projected predicate shape.
63    #[must_use]
64    pub const fn predicate(&self) -> &ExplainPredicate {
65        &self.predicate
66    }
67
68    /// Borrow projected ORDER BY shape.
69    #[must_use]
70    pub const fn order_by(&self) -> &ExplainOrderBy {
71        &self.order_by
72    }
73
74    /// Return whether DISTINCT is enabled.
75    #[must_use]
76    pub const fn distinct(&self) -> bool {
77        self.distinct
78    }
79
80    /// Borrow projected grouped-shape metadata.
81    #[must_use]
82    pub const fn grouping(&self) -> &ExplainGrouping {
83        &self.grouping
84    }
85
86    /// Borrow projected ORDER pushdown status.
87    #[must_use]
88    pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
89        &self.order_pushdown
90    }
91
92    /// Borrow projected pagination status.
93    #[must_use]
94    pub const fn page(&self) -> &ExplainPagination {
95        &self.page
96    }
97
98    /// Borrow projected delete-limit status.
99    #[must_use]
100    pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
101        &self.delete_limit
102    }
103
104    /// Return missing-row consistency policy.
105    #[must_use]
106    pub const fn consistency(&self) -> MissingRowPolicy {
107        self.consistency
108    }
109}
110
111impl ExplainPlan {
112    /// Return the canonical predicate model used for hashing/fingerprints.
113    ///
114    /// The explain projection must remain a faithful rendering of this model.
115    #[must_use]
116    pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
117        if let Some(predicate) = &self.predicate_model {
118            debug_assert_eq!(
119                self.predicate,
120                ExplainPredicate::from_predicate(predicate),
121                "explain predicate surface drifted from canonical predicate model"
122            );
123            Some(predicate)
124        } else {
125            debug_assert!(
126                matches!(self.predicate, ExplainPredicate::None),
127                "missing canonical predicate model requires ExplainPredicate::None"
128            );
129            None
130        }
131    }
132
133    /// Render this logical explain plan as deterministic canonical text.
134    ///
135    /// This surface is frontend-facing and intentionally stable for SQL/CLI
136    /// explain output and snapshot-style diagnostics.
137    #[must_use]
138    pub fn render_text_canonical(&self) -> String {
139        format!(
140            concat!(
141                "mode={:?}\n",
142                "access={:?}\n",
143                "predicate={:?}\n",
144                "order_by={:?}\n",
145                "distinct={}\n",
146                "grouping={:?}\n",
147                "order_pushdown={:?}\n",
148                "page={:?}\n",
149                "delete_limit={:?}\n",
150                "consistency={:?}",
151            ),
152            self.mode(),
153            self.access(),
154            self.predicate(),
155            self.order_by(),
156            self.distinct(),
157            self.grouping(),
158            self.order_pushdown(),
159            self.page(),
160            self.delete_limit(),
161            self.consistency(),
162        )
163    }
164
165    /// Render this logical explain plan as canonical JSON.
166    #[must_use]
167    pub fn render_json_canonical(&self) -> String {
168        let mut out = String::new();
169        write_logical_explain_json(self, &mut out);
170
171        out
172    }
173}
174
175///
176/// ExplainGrouping
177///
178/// Grouped-shape annotation for deterministic explain/fingerprint surfaces.
179///
180
181#[expect(clippy::large_enum_variant)]
182#[derive(Clone, Debug, Eq, PartialEq)]
183pub enum ExplainGrouping {
184    None,
185    Grouped {
186        strategy: &'static str,
187        fallback_reason: Option<&'static str>,
188        group_fields: Vec<ExplainGroupField>,
189        aggregates: Vec<ExplainGroupAggregate>,
190        having: Option<ExplainGroupHaving>,
191        max_groups: u64,
192        max_group_bytes: u64,
193    },
194}
195
196///
197/// ExplainGroupField
198///
199/// Stable grouped-key field identity carried by explain/hash surfaces.
200///
201
202#[derive(Clone, Debug, Eq, PartialEq)]
203pub struct ExplainGroupField {
204    pub(crate) slot_index: usize,
205    pub(crate) field: String,
206}
207
208impl ExplainGroupField {
209    /// Return grouped slot index.
210    #[must_use]
211    pub const fn slot_index(&self) -> usize {
212        self.slot_index
213    }
214
215    /// Borrow grouped field name.
216    #[must_use]
217    pub const fn field(&self) -> &str {
218        self.field.as_str()
219    }
220}
221
222///
223/// ExplainGroupAggregate
224///
225/// Stable explain-surface projection of one grouped aggregate terminal.
226///
227
228#[derive(Clone, Debug, Eq, PartialEq)]
229pub struct ExplainGroupAggregate {
230    pub(crate) kind: AggregateKind,
231    pub(crate) target_field: Option<String>,
232    pub(crate) input_expr: Option<String>,
233    pub(crate) distinct: bool,
234}
235
236impl ExplainGroupAggregate {
237    /// Return grouped aggregate kind.
238    #[must_use]
239    pub const fn kind(&self) -> AggregateKind {
240        self.kind
241    }
242
243    /// Borrow optional grouped aggregate target field.
244    #[must_use]
245    pub fn target_field(&self) -> Option<&str> {
246        self.target_field.as_deref()
247    }
248
249    /// Borrow optional grouped aggregate input expression label.
250    #[must_use]
251    pub fn input_expr(&self) -> Option<&str> {
252        self.input_expr.as_deref()
253    }
254
255    /// Return whether grouped aggregate uses DISTINCT input semantics.
256    #[must_use]
257    pub const fn distinct(&self) -> bool {
258        self.distinct
259    }
260}
261
262///
263/// ExplainGroupHaving
264///
265/// Deterministic explain projection of grouped HAVING clauses.
266///
267
268#[derive(Clone, Debug, Eq, PartialEq)]
269pub struct ExplainGroupHaving {
270    pub(crate) expr: ExplainGroupHavingExpr,
271}
272
273impl ExplainGroupHaving {
274    /// Borrow widened grouped HAVING expression.
275    #[must_use]
276    pub const fn expr(&self) -> &ExplainGroupHavingExpr {
277        &self.expr
278    }
279}
280
281///
282/// ExplainGroupHavingExpr
283///
284/// Stable explain-surface projection for widened grouped HAVING boolean
285/// expressions.
286///
287
288#[derive(Clone, Debug, Eq, PartialEq)]
289pub enum ExplainGroupHavingExpr {
290    Compare {
291        left: ExplainGroupHavingValueExpr,
292        op: CompareOp,
293        right: ExplainGroupHavingValueExpr,
294    },
295    And(Vec<Self>),
296}
297
298///
299/// ExplainGroupHavingValueExpr
300///
301/// Stable explain-surface projection for grouped HAVING value expressions.
302/// Leaves remain restricted to grouped keys, aggregate outputs, and literals.
303///
304
305#[derive(Clone, Debug, Eq, PartialEq)]
306pub enum ExplainGroupHavingValueExpr {
307    GroupField {
308        slot_index: usize,
309        field: String,
310    },
311    AggregateIndex {
312        index: usize,
313    },
314    Literal(Value),
315    FunctionCall {
316        function: String,
317        args: Vec<Self>,
318    },
319    Unary {
320        op: String,
321        expr: Box<Self>,
322    },
323    Case {
324        when_then_arms: Vec<ExplainGroupHavingCaseArm>,
325        else_expr: Box<Self>,
326    },
327    Binary {
328        op: String,
329        left: Box<Self>,
330        right: Box<Self>,
331    },
332}
333
334///
335/// ExplainGroupHavingCaseArm
336///
337/// Stable explain-surface projection for one grouped HAVING searched-CASE arm.
338/// This keeps explain output aligned with the planner-owned grouped HAVING
339/// expression seam when searched CASE support is admitted through SQL.
340///
341
342#[derive(Clone, Debug, Eq, PartialEq)]
343pub struct ExplainGroupHavingCaseArm {
344    pub(crate) condition: ExplainGroupHavingValueExpr,
345    pub(crate) result: ExplainGroupHavingValueExpr,
346}
347
348///
349/// ExplainOrderPushdown
350///
351/// Deterministic ORDER BY pushdown eligibility reported by explain.
352///
353
354#[derive(Clone, Debug, Eq, PartialEq)]
355pub enum ExplainOrderPushdown {
356    MissingModelContext,
357    EligibleSecondaryIndex {
358        index: &'static str,
359        prefix_len: usize,
360    },
361    Rejected(SecondaryOrderPushdownRejection),
362}
363
364///
365/// ExplainAccessPath
366///
367/// Deterministic projection of logical access path shape for diagnostics.
368/// Mirrors planner-selected structural paths without runtime cursor state.
369///
370
371#[derive(Clone, Debug, Eq, PartialEq)]
372pub enum ExplainAccessPath {
373    ByKey {
374        key: Value,
375    },
376    ByKeys {
377        keys: Vec<Value>,
378    },
379    KeyRange {
380        start: Value,
381        end: Value,
382    },
383    IndexPrefix {
384        name: &'static str,
385        fields: Vec<&'static str>,
386        prefix_len: usize,
387        values: Vec<Value>,
388    },
389    IndexMultiLookup {
390        name: &'static str,
391        fields: Vec<&'static str>,
392        values: Vec<Value>,
393    },
394    IndexRange {
395        name: &'static str,
396        fields: Vec<&'static str>,
397        prefix_len: usize,
398        prefix: Vec<Value>,
399        lower: Bound<Value>,
400        upper: Bound<Value>,
401    },
402    FullScan,
403    Union(Vec<Self>),
404    Intersection(Vec<Self>),
405}
406
407///
408/// ExplainPredicate
409///
410/// Deterministic projection of canonical predicate structure for explain output.
411/// This preserves normalized predicate shape used by hashing/fingerprints.
412///
413
414#[derive(Clone, Debug, Eq, PartialEq)]
415pub enum ExplainPredicate {
416    None,
417    True,
418    False,
419    And(Vec<Self>),
420    Or(Vec<Self>),
421    Not(Box<Self>),
422    Compare {
423        field: String,
424        op: CompareOp,
425        value: Value,
426        coercion: CoercionSpec,
427    },
428    CompareFields {
429        left_field: String,
430        op: CompareOp,
431        right_field: String,
432        coercion: CoercionSpec,
433    },
434    IsNull {
435        field: String,
436    },
437    IsNotNull {
438        field: String,
439    },
440    IsMissing {
441        field: String,
442    },
443    IsEmpty {
444        field: String,
445    },
446    IsNotEmpty {
447        field: String,
448    },
449    TextContains {
450        field: String,
451        value: Value,
452    },
453    TextContainsCi {
454        field: String,
455        value: Value,
456    },
457}
458
459///
460/// ExplainOrderBy
461///
462/// Deterministic projection of canonical ORDER BY shape.
463///
464
465#[derive(Clone, Debug, Eq, PartialEq)]
466pub enum ExplainOrderBy {
467    None,
468    Fields(Vec<ExplainOrder>),
469}
470
471///
472/// ExplainOrder
473///
474/// One canonical ORDER BY field + direction pair.
475///
476
477#[derive(Clone, Debug, Eq, PartialEq)]
478pub struct ExplainOrder {
479    pub(crate) field: String,
480    pub(crate) direction: OrderDirection,
481}
482
483impl ExplainOrder {
484    /// Borrow ORDER BY field name.
485    #[must_use]
486    pub const fn field(&self) -> &str {
487        self.field.as_str()
488    }
489
490    /// Return ORDER BY direction.
491    #[must_use]
492    pub const fn direction(&self) -> OrderDirection {
493        self.direction
494    }
495}
496
497///
498/// ExplainPagination
499///
500/// Explain-surface projection of pagination window configuration.
501///
502
503#[derive(Clone, Debug, Eq, PartialEq)]
504pub enum ExplainPagination {
505    None,
506    Page { limit: Option<u32>, offset: u32 },
507}
508
509///
510/// ExplainDeleteLimit
511///
512/// Explain-surface projection of delete-limit configuration.
513///
514
515#[derive(Clone, Debug, Eq, PartialEq)]
516pub enum ExplainDeleteLimit {
517    None,
518    Limit { max_rows: u32 },
519    Window { limit: Option<u32>, offset: u32 },
520}
521
522impl AccessPlannedQuery {
523    /// Produce a stable, deterministic explanation of this logical plan.
524    #[must_use]
525    pub(crate) fn explain(&self) -> ExplainPlan {
526        self.explain_inner()
527    }
528
529    pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
530        // Phase 1: project logical plan variant into scalar core + grouped metadata.
531        let (logical, grouping) = match &self.logical {
532            LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
533            LogicalPlan::Grouped(logical) => {
534                let grouped_strategy = grouped_plan_strategy(self).expect(
535                    "grouped logical explain projection requires planner-owned grouped strategy",
536                );
537
538                (
539                    &logical.scalar,
540                    ExplainGrouping::Grouped {
541                        strategy: grouped_strategy.code(),
542                        fallback_reason: grouped_strategy
543                            .fallback_reason()
544                            .map(GroupedPlanFallbackReason::code),
545                        group_fields: logical
546                            .group
547                            .group_fields
548                            .iter()
549                            .map(|field_slot| ExplainGroupField {
550                                slot_index: field_slot.index(),
551                                field: field_slot.field().to_string(),
552                            })
553                            .collect(),
554                        aggregates: logical
555                            .group
556                            .aggregates
557                            .iter()
558                            .map(|aggregate| ExplainGroupAggregate {
559                                kind: aggregate.kind,
560                                target_field: aggregate.target_field.clone(),
561                                input_expr: aggregate
562                                    .input_expr()
563                                    .map(render_scalar_projection_expr_sql_label),
564                                distinct: aggregate.distinct,
565                            })
566                            .collect(),
567                        having: explain_group_having(logical),
568                        max_groups: logical.group.execution.max_groups(),
569                        max_group_bytes: logical.group.execution.max_group_bytes(),
570                    },
571                )
572            }
573        };
574
575        // Phase 2: project scalar plan + access path into deterministic explain surface.
576        explain_scalar_inner(logical, grouping, &self.access)
577    }
578}
579
580fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
581    let expr = logical.effective_having_expr()?;
582
583    Some(ExplainGroupHaving {
584        expr: explain_group_having_expr(expr.as_ref()),
585    })
586}
587
588fn explain_group_having_expr(expr: &GroupHavingExpr) -> ExplainGroupHavingExpr {
589    match expr {
590        GroupHavingExpr::Compare { left, op, right } => ExplainGroupHavingExpr::Compare {
591            left: explain_group_having_value_expr(left),
592            op: *op,
593            right: explain_group_having_value_expr(right),
594        },
595        GroupHavingExpr::And(children) => {
596            ExplainGroupHavingExpr::And(children.iter().map(explain_group_having_expr).collect())
597        }
598    }
599}
600
601fn explain_group_having_value_expr(expr: &GroupHavingValueExpr) -> ExplainGroupHavingValueExpr {
602    match expr {
603        GroupHavingValueExpr::GroupField(field_slot) => ExplainGroupHavingValueExpr::GroupField {
604            slot_index: field_slot.index(),
605            field: field_slot.field().to_string(),
606        },
607        GroupHavingValueExpr::AggregateIndex(index) => {
608            ExplainGroupHavingValueExpr::AggregateIndex { index: *index }
609        }
610        GroupHavingValueExpr::Literal(value) => ExplainGroupHavingValueExpr::Literal(value.clone()),
611        GroupHavingValueExpr::FunctionCall { function, args } => {
612            ExplainGroupHavingValueExpr::FunctionCall {
613                function: function.sql_label().to_string(),
614                args: args.iter().map(explain_group_having_value_expr).collect(),
615            }
616        }
617        GroupHavingValueExpr::Unary { op, expr } => ExplainGroupHavingValueExpr::Unary {
618            op: explain_group_having_unary_op_label(*op).to_string(),
619            expr: Box::new(explain_group_having_value_expr(expr)),
620        },
621        GroupHavingValueExpr::Case {
622            when_then_arms,
623            else_expr,
624        } => ExplainGroupHavingValueExpr::Case {
625            when_then_arms: when_then_arms
626                .iter()
627                .map(|arm| ExplainGroupHavingCaseArm {
628                    condition: explain_group_having_value_expr(arm.condition()),
629                    result: explain_group_having_value_expr(arm.result()),
630                })
631                .collect(),
632            else_expr: Box::new(explain_group_having_value_expr(else_expr)),
633        },
634        GroupHavingValueExpr::Binary { op, left, right } => ExplainGroupHavingValueExpr::Binary {
635            op: explain_group_having_binary_op_label(*op).to_string(),
636            left: Box::new(explain_group_having_value_expr(left)),
637            right: Box::new(explain_group_having_value_expr(right)),
638        },
639    }
640}
641
642const fn explain_group_having_unary_op_label(
643    op: crate::db::query::plan::expr::UnaryOp,
644) -> &'static str {
645    match op {
646        crate::db::query::plan::expr::UnaryOp::Not => "NOT",
647    }
648}
649
650const fn explain_group_having_binary_op_label(
651    op: crate::db::query::plan::expr::BinaryOp,
652) -> &'static str {
653    match op {
654        crate::db::query::plan::expr::BinaryOp::Or => "OR",
655        crate::db::query::plan::expr::BinaryOp::And => "AND",
656        crate::db::query::plan::expr::BinaryOp::Eq => "=",
657        crate::db::query::plan::expr::BinaryOp::Ne => "!=",
658        crate::db::query::plan::expr::BinaryOp::Lt => "<",
659        crate::db::query::plan::expr::BinaryOp::Lte => "<=",
660        crate::db::query::plan::expr::BinaryOp::Gt => ">",
661        crate::db::query::plan::expr::BinaryOp::Gte => ">=",
662        crate::db::query::plan::expr::BinaryOp::Add => "+",
663        crate::db::query::plan::expr::BinaryOp::Sub => "-",
664        crate::db::query::plan::expr::BinaryOp::Mul => "*",
665        crate::db::query::plan::expr::BinaryOp::Div => "/",
666    }
667}
668
669fn explain_scalar_inner<K>(
670    logical: &ScalarPlan,
671    grouping: ExplainGrouping,
672    access: &AccessPlan<K>,
673) -> ExplainPlan
674where
675    K: FieldValue,
676{
677    // Phase 1: consume canonical predicate model from planner-owned scalar semantics.
678    let predicate_model = logical.predicate.clone();
679    let predicate = match &predicate_model {
680        Some(predicate) => ExplainPredicate::from_predicate(predicate),
681        None => ExplainPredicate::None,
682    };
683
684    // Phase 2: project scalar-plan fields into explain-specific enums.
685    let order_by = explain_order(logical.order.as_ref());
686    let order_pushdown = explain_order_pushdown();
687    let page = explain_page(logical.page.as_ref());
688    let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
689
690    // Phase 3: assemble one stable explain payload.
691    ExplainPlan {
692        mode: logical.mode,
693        access: ExplainAccessPath::from_access_plan(access),
694        predicate,
695        predicate_model,
696        order_by,
697        distinct: logical.distinct,
698        grouping,
699        order_pushdown,
700        page,
701        delete_limit,
702        consistency: logical.consistency,
703    }
704}
705
706const fn explain_order_pushdown() -> ExplainOrderPushdown {
707    // Query explain does not own physical pushdown feasibility routing.
708    ExplainOrderPushdown::MissingModelContext
709}
710
711impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
712    fn from(value: SecondaryOrderPushdownEligibility) -> Self {
713        Self::from(PushdownSurfaceEligibility::from(&value))
714    }
715}
716
717impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
718    fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
719        match value {
720            PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
721                Self::EligibleSecondaryIndex { index, prefix_len }
722            }
723            PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
724        }
725    }
726}
727
728impl ExplainPredicate {
729    pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
730        match predicate {
731            Predicate::True => Self::True,
732            Predicate::False => Self::False,
733            Predicate::And(children) => {
734                Self::And(children.iter().map(Self::from_predicate).collect())
735            }
736            Predicate::Or(children) => {
737                Self::Or(children.iter().map(Self::from_predicate).collect())
738            }
739            Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
740            Predicate::Compare(compare) => Self::from_compare(compare),
741            Predicate::CompareFields(compare) => Self::CompareFields {
742                left_field: compare.left_field().to_string(),
743                op: compare.op(),
744                right_field: compare.right_field().to_string(),
745                coercion: compare.coercion().clone(),
746            },
747            Predicate::IsNull { field } => Self::IsNull {
748                field: field.clone(),
749            },
750            Predicate::IsNotNull { field } => Self::IsNotNull {
751                field: field.clone(),
752            },
753            Predicate::IsMissing { field } => Self::IsMissing {
754                field: field.clone(),
755            },
756            Predicate::IsEmpty { field } => Self::IsEmpty {
757                field: field.clone(),
758            },
759            Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
760                field: field.clone(),
761            },
762            Predicate::TextContains { field, value } => Self::TextContains {
763                field: field.clone(),
764                value: value.clone(),
765            },
766            Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
767                field: field.clone(),
768                value: value.clone(),
769            },
770        }
771    }
772
773    fn from_compare(compare: &ComparePredicate) -> Self {
774        Self::Compare {
775            field: compare.field.clone(),
776            op: compare.op,
777            value: compare.value.clone(),
778            coercion: compare.coercion.clone(),
779        }
780    }
781}
782
783fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
784    let Some(order) = order else {
785        return ExplainOrderBy::None;
786    };
787
788    if order.fields.is_empty() {
789        return ExplainOrderBy::None;
790    }
791
792    ExplainOrderBy::Fields(
793        order
794            .fields
795            .iter()
796            .map(|(field, direction)| ExplainOrder {
797                field: field.clone(),
798                direction: *direction,
799            })
800            .collect(),
801    )
802}
803
804const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
805    match page {
806        Some(page) => ExplainPagination::Page {
807            limit: page.limit,
808            offset: page.offset,
809        },
810        None => ExplainPagination::None,
811    }
812}
813
814const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
815    match limit {
816        Some(limit) if limit.offset == 0 => match limit.limit {
817            Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
818            None => ExplainDeleteLimit::Window {
819                limit: None,
820                offset: 0,
821            },
822        },
823        Some(limit) => ExplainDeleteLimit::Window {
824            limit: limit.limit,
825            offset: limit.offset,
826        },
827        None => ExplainDeleteLimit::None,
828    }
829}
830
831fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
832    let mut object = JsonWriter::begin_object(out);
833    object.field_with("mode", |out| {
834        let mut object = JsonWriter::begin_object(out);
835        match explain.mode() {
836            QueryMode::Load(spec) => {
837                object.field_str("type", "Load");
838                match spec.limit() {
839                    Some(limit) => object.field_u64("limit", u64::from(limit)),
840                    None => object.field_null("limit"),
841                }
842                object.field_u64("offset", u64::from(spec.offset()));
843            }
844            QueryMode::Delete(spec) => {
845                object.field_str("type", "Delete");
846                match spec.limit() {
847                    Some(limit) => object.field_u64("limit", u64::from(limit)),
848                    None => object.field_null("limit"),
849                }
850            }
851        }
852        object.finish();
853    });
854    object.field_with("access", |out| write_access_json(explain.access(), out));
855    object.field_value_debug("predicate", explain.predicate());
856    object.field_value_debug("order_by", explain.order_by());
857    object.field_bool("distinct", explain.distinct());
858    object.field_value_debug("grouping", explain.grouping());
859    object.field_value_debug("order_pushdown", explain.order_pushdown());
860    object.field_with("page", |out| {
861        let mut object = JsonWriter::begin_object(out);
862        match explain.page() {
863            ExplainPagination::None => {
864                object.field_str("type", "None");
865            }
866            ExplainPagination::Page { limit, offset } => {
867                object.field_str("type", "Page");
868                match limit {
869                    Some(limit) => object.field_u64("limit", u64::from(*limit)),
870                    None => object.field_null("limit"),
871                }
872                object.field_u64("offset", u64::from(*offset));
873            }
874        }
875        object.finish();
876    });
877    object.field_with("delete_limit", |out| {
878        let mut object = JsonWriter::begin_object(out);
879        match explain.delete_limit() {
880            ExplainDeleteLimit::None => {
881                object.field_str("type", "None");
882            }
883            ExplainDeleteLimit::Limit { max_rows } => {
884                object.field_str("type", "Limit");
885                object.field_u64("max_rows", u64::from(*max_rows));
886            }
887            ExplainDeleteLimit::Window { limit, offset } => {
888                object.field_str("type", "Window");
889                object.field_with("limit", |out| match limit {
890                    Some(limit) => out.push_str(&limit.to_string()),
891                    None => out.push_str("null"),
892                });
893                object.field_u64("offset", u64::from(*offset));
894            }
895        }
896        object.finish();
897    });
898    object.field_value_debug("consistency", &explain.consistency());
899    object.finish();
900}