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            explain::{access_projection::write_access_json, writer::JsonWriter},
15            plan::{
16                AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupHavingExpr,
17                GroupHavingSpec, GroupHavingValueExpr, GroupedPlanFallbackReason, LogicalPlan,
18                OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan, grouped_plan_strategy,
19            },
20        },
21    },
22    model::entity::EntityModel,
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) distinct: bool,
233}
234
235impl ExplainGroupAggregate {
236    /// Return grouped aggregate kind.
237    #[must_use]
238    pub const fn kind(&self) -> AggregateKind {
239        self.kind
240    }
241
242    /// Borrow optional grouped aggregate target field.
243    #[must_use]
244    pub fn target_field(&self) -> Option<&str> {
245        self.target_field.as_deref()
246    }
247
248    /// Return whether grouped aggregate uses DISTINCT input semantics.
249    #[must_use]
250    pub const fn distinct(&self) -> bool {
251        self.distinct
252    }
253}
254
255///
256/// ExplainGroupHaving
257///
258/// Deterministic explain projection of grouped HAVING clauses.
259///
260
261#[derive(Clone, Debug, Eq, PartialEq)]
262pub struct ExplainGroupHaving {
263    pub(crate) expr: ExplainGroupHavingExpr,
264}
265
266impl ExplainGroupHaving {
267    /// Borrow widened grouped HAVING expression.
268    #[must_use]
269    pub const fn expr(&self) -> &ExplainGroupHavingExpr {
270        &self.expr
271    }
272}
273
274///
275/// ExplainGroupHavingExpr
276///
277/// Stable explain-surface projection for widened grouped HAVING boolean
278/// expressions.
279///
280
281#[derive(Clone, Debug, Eq, PartialEq)]
282pub enum ExplainGroupHavingExpr {
283    Compare {
284        left: ExplainGroupHavingValueExpr,
285        op: CompareOp,
286        right: ExplainGroupHavingValueExpr,
287    },
288    And(Vec<Self>),
289}
290
291///
292/// ExplainGroupHavingValueExpr
293///
294/// Stable explain-surface projection for grouped HAVING value expressions.
295/// Leaves remain restricted to grouped keys, aggregate outputs, and literals.
296///
297
298#[derive(Clone, Debug, Eq, PartialEq)]
299pub enum ExplainGroupHavingValueExpr {
300    GroupField {
301        slot_index: usize,
302        field: String,
303    },
304    AggregateIndex {
305        index: usize,
306    },
307    Literal(Value),
308    FunctionCall {
309        function: String,
310        args: Vec<Self>,
311    },
312    Binary {
313        op: String,
314        left: Box<Self>,
315        right: Box<Self>,
316    },
317}
318
319///
320/// ExplainOrderPushdown
321///
322/// Deterministic ORDER BY pushdown eligibility reported by explain.
323///
324
325#[derive(Clone, Debug, Eq, PartialEq)]
326pub enum ExplainOrderPushdown {
327    MissingModelContext,
328    EligibleSecondaryIndex {
329        index: &'static str,
330        prefix_len: usize,
331    },
332    Rejected(SecondaryOrderPushdownRejection),
333}
334
335///
336/// ExplainAccessPath
337///
338/// Deterministic projection of logical access path shape for diagnostics.
339/// Mirrors planner-selected structural paths without runtime cursor state.
340///
341
342#[derive(Clone, Debug, Eq, PartialEq)]
343pub enum ExplainAccessPath {
344    ByKey {
345        key: Value,
346    },
347    ByKeys {
348        keys: Vec<Value>,
349    },
350    KeyRange {
351        start: Value,
352        end: Value,
353    },
354    IndexPrefix {
355        name: &'static str,
356        fields: Vec<&'static str>,
357        prefix_len: usize,
358        values: Vec<Value>,
359    },
360    IndexMultiLookup {
361        name: &'static str,
362        fields: Vec<&'static str>,
363        values: Vec<Value>,
364    },
365    IndexRange {
366        name: &'static str,
367        fields: Vec<&'static str>,
368        prefix_len: usize,
369        prefix: Vec<Value>,
370        lower: Bound<Value>,
371        upper: Bound<Value>,
372    },
373    FullScan,
374    Union(Vec<Self>),
375    Intersection(Vec<Self>),
376}
377
378///
379/// ExplainPredicate
380///
381/// Deterministic projection of canonical predicate structure for explain output.
382/// This preserves normalized predicate shape used by hashing/fingerprints.
383///
384
385#[derive(Clone, Debug, Eq, PartialEq)]
386pub enum ExplainPredicate {
387    None,
388    True,
389    False,
390    And(Vec<Self>),
391    Or(Vec<Self>),
392    Not(Box<Self>),
393    Compare {
394        field: String,
395        op: CompareOp,
396        value: Value,
397        coercion: CoercionSpec,
398    },
399    CompareFields {
400        left_field: String,
401        op: CompareOp,
402        right_field: String,
403        coercion: CoercionSpec,
404    },
405    IsNull {
406        field: String,
407    },
408    IsNotNull {
409        field: String,
410    },
411    IsMissing {
412        field: String,
413    },
414    IsEmpty {
415        field: String,
416    },
417    IsNotEmpty {
418        field: String,
419    },
420    TextContains {
421        field: String,
422        value: Value,
423    },
424    TextContainsCi {
425        field: String,
426        value: Value,
427    },
428}
429
430///
431/// ExplainOrderBy
432///
433/// Deterministic projection of canonical ORDER BY shape.
434///
435
436#[derive(Clone, Debug, Eq, PartialEq)]
437pub enum ExplainOrderBy {
438    None,
439    Fields(Vec<ExplainOrder>),
440}
441
442///
443/// ExplainOrder
444///
445/// One canonical ORDER BY field + direction pair.
446///
447
448#[derive(Clone, Debug, Eq, PartialEq)]
449pub struct ExplainOrder {
450    pub(crate) field: String,
451    pub(crate) direction: OrderDirection,
452}
453
454impl ExplainOrder {
455    /// Borrow ORDER BY field name.
456    #[must_use]
457    pub const fn field(&self) -> &str {
458        self.field.as_str()
459    }
460
461    /// Return ORDER BY direction.
462    #[must_use]
463    pub const fn direction(&self) -> OrderDirection {
464        self.direction
465    }
466}
467
468///
469/// ExplainPagination
470///
471/// Explain-surface projection of pagination window configuration.
472///
473
474#[derive(Clone, Debug, Eq, PartialEq)]
475pub enum ExplainPagination {
476    None,
477    Page { limit: Option<u32>, offset: u32 },
478}
479
480///
481/// ExplainDeleteLimit
482///
483/// Explain-surface projection of delete-limit configuration.
484///
485
486#[derive(Clone, Debug, Eq, PartialEq)]
487pub enum ExplainDeleteLimit {
488    None,
489    Limit { max_rows: u32 },
490    Window { limit: Option<u32>, offset: u32 },
491}
492
493impl AccessPlannedQuery {
494    /// Produce a stable, deterministic explanation of this logical plan.
495    #[must_use]
496    #[cfg(test)]
497    pub(crate) fn explain(&self) -> ExplainPlan {
498        self.explain_inner(None)
499    }
500
501    /// Produce a stable, deterministic explanation of this logical plan
502    /// with optional model context for query-layer projections.
503    ///
504    /// Query explain intentionally does not evaluate executor route pushdown
505    /// feasibility to keep query-layer dependencies executor-agnostic.
506    #[must_use]
507    pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
508        self.explain_inner(Some(model))
509    }
510
511    pub(in crate::db::query::explain) fn explain_inner(
512        &self,
513        model: Option<&EntityModel>,
514    ) -> ExplainPlan {
515        // Phase 1: project logical plan variant into scalar core + grouped metadata.
516        let (logical, grouping) = match &self.logical {
517            LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
518            LogicalPlan::Grouped(logical) => {
519                let grouped_strategy = grouped_plan_strategy(self).expect(
520                    "grouped logical explain projection requires planner-owned grouped strategy",
521                );
522
523                (
524                    &logical.scalar,
525                    ExplainGrouping::Grouped {
526                        strategy: grouped_strategy.code(),
527                        fallback_reason: grouped_strategy
528                            .fallback_reason()
529                            .map(GroupedPlanFallbackReason::code),
530                        group_fields: logical
531                            .group
532                            .group_fields
533                            .iter()
534                            .map(|field_slot| ExplainGroupField {
535                                slot_index: field_slot.index(),
536                                field: field_slot.field().to_string(),
537                            })
538                            .collect(),
539                        aggregates: logical
540                            .group
541                            .aggregates
542                            .iter()
543                            .map(|aggregate| ExplainGroupAggregate {
544                                kind: aggregate.kind,
545                                target_field: aggregate.target_field.clone(),
546                                distinct: aggregate.distinct,
547                            })
548                            .collect(),
549                        having: explain_group_having(
550                            logical.having.as_ref(),
551                            logical.having_expr.as_ref(),
552                        ),
553                        max_groups: logical.group.execution.max_groups(),
554                        max_group_bytes: logical.group.execution.max_group_bytes(),
555                    },
556                )
557            }
558        };
559
560        // Phase 2: project scalar plan + access path into deterministic explain surface.
561        explain_scalar_inner(logical, grouping, model, &self.access)
562    }
563}
564
565fn explain_group_having(
566    having: Option<&GroupHavingSpec>,
567    having_expr: Option<&GroupHavingExpr>,
568) -> Option<ExplainGroupHaving> {
569    let expr = match having_expr {
570        Some(expr) => explain_group_having_expr(expr),
571        None => explain_group_having_expr(&GroupHavingExpr::from_legacy_spec(having?)),
572    };
573
574    Some(ExplainGroupHaving { expr })
575}
576
577fn explain_group_having_expr(expr: &GroupHavingExpr) -> ExplainGroupHavingExpr {
578    match expr {
579        GroupHavingExpr::Compare { left, op, right } => ExplainGroupHavingExpr::Compare {
580            left: explain_group_having_value_expr(left),
581            op: *op,
582            right: explain_group_having_value_expr(right),
583        },
584        GroupHavingExpr::And(children) => {
585            ExplainGroupHavingExpr::And(children.iter().map(explain_group_having_expr).collect())
586        }
587    }
588}
589
590fn explain_group_having_value_expr(expr: &GroupHavingValueExpr) -> ExplainGroupHavingValueExpr {
591    match expr {
592        GroupHavingValueExpr::GroupField(field_slot) => ExplainGroupHavingValueExpr::GroupField {
593            slot_index: field_slot.index(),
594            field: field_slot.field().to_string(),
595        },
596        GroupHavingValueExpr::AggregateIndex(index) => {
597            ExplainGroupHavingValueExpr::AggregateIndex { index: *index }
598        }
599        GroupHavingValueExpr::Literal(value) => ExplainGroupHavingValueExpr::Literal(value.clone()),
600        GroupHavingValueExpr::FunctionCall { function, args } => {
601            ExplainGroupHavingValueExpr::FunctionCall {
602                function: function.sql_label().to_string(),
603                args: args.iter().map(explain_group_having_value_expr).collect(),
604            }
605        }
606        GroupHavingValueExpr::Binary { op, left, right } => ExplainGroupHavingValueExpr::Binary {
607            op: explain_group_having_binary_op_label(*op).to_string(),
608            left: Box::new(explain_group_having_value_expr(left)),
609            right: Box::new(explain_group_having_value_expr(right)),
610        },
611    }
612}
613
614const fn explain_group_having_binary_op_label(
615    op: crate::db::query::plan::expr::BinaryOp,
616) -> &'static str {
617    match op {
618        crate::db::query::plan::expr::BinaryOp::Add => "+",
619        crate::db::query::plan::expr::BinaryOp::Sub => "-",
620        crate::db::query::plan::expr::BinaryOp::Mul => "*",
621        crate::db::query::plan::expr::BinaryOp::Div => "/",
622        #[cfg(test)]
623        crate::db::query::plan::expr::BinaryOp::And => "AND",
624        #[cfg(test)]
625        crate::db::query::plan::expr::BinaryOp::Eq => "=",
626    }
627}
628
629fn explain_scalar_inner<K>(
630    logical: &ScalarPlan,
631    grouping: ExplainGrouping,
632    model: Option<&EntityModel>,
633    access: &AccessPlan<K>,
634) -> ExplainPlan
635where
636    K: FieldValue,
637{
638    // Phase 1: consume canonical predicate model from planner-owned scalar semantics.
639    let predicate_model = logical.predicate.clone();
640    let predicate = match &predicate_model {
641        Some(predicate) => ExplainPredicate::from_predicate(predicate),
642        None => ExplainPredicate::None,
643    };
644
645    // Phase 2: project scalar-plan fields into explain-specific enums.
646    let order_by = explain_order(logical.order.as_ref());
647    let order_pushdown = explain_order_pushdown(model);
648    let page = explain_page(logical.page.as_ref());
649    let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
650
651    // Phase 3: assemble one stable explain payload.
652    ExplainPlan {
653        mode: logical.mode,
654        access: ExplainAccessPath::from_access_plan(access),
655        predicate,
656        predicate_model,
657        order_by,
658        distinct: logical.distinct,
659        grouping,
660        order_pushdown,
661        page,
662        delete_limit,
663        consistency: logical.consistency,
664    }
665}
666
667const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
668    let _ = model;
669
670    // Query explain does not own physical pushdown feasibility routing.
671    ExplainOrderPushdown::MissingModelContext
672}
673
674impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
675    fn from(value: SecondaryOrderPushdownEligibility) -> Self {
676        Self::from(PushdownSurfaceEligibility::from(&value))
677    }
678}
679
680impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
681    fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
682        match value {
683            PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
684                Self::EligibleSecondaryIndex { index, prefix_len }
685            }
686            PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
687        }
688    }
689}
690
691impl ExplainPredicate {
692    pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
693        match predicate {
694            Predicate::True => Self::True,
695            Predicate::False => Self::False,
696            Predicate::And(children) => {
697                Self::And(children.iter().map(Self::from_predicate).collect())
698            }
699            Predicate::Or(children) => {
700                Self::Or(children.iter().map(Self::from_predicate).collect())
701            }
702            Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
703            Predicate::Compare(compare) => Self::from_compare(compare),
704            Predicate::CompareFields(compare) => Self::CompareFields {
705                left_field: compare.left_field().to_string(),
706                op: compare.op(),
707                right_field: compare.right_field().to_string(),
708                coercion: compare.coercion().clone(),
709            },
710            Predicate::IsNull { field } => Self::IsNull {
711                field: field.clone(),
712            },
713            Predicate::IsNotNull { field } => Self::IsNotNull {
714                field: field.clone(),
715            },
716            Predicate::IsMissing { field } => Self::IsMissing {
717                field: field.clone(),
718            },
719            Predicate::IsEmpty { field } => Self::IsEmpty {
720                field: field.clone(),
721            },
722            Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
723                field: field.clone(),
724            },
725            Predicate::TextContains { field, value } => Self::TextContains {
726                field: field.clone(),
727                value: value.clone(),
728            },
729            Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
730                field: field.clone(),
731                value: value.clone(),
732            },
733        }
734    }
735
736    fn from_compare(compare: &ComparePredicate) -> Self {
737        Self::Compare {
738            field: compare.field.clone(),
739            op: compare.op,
740            value: compare.value.clone(),
741            coercion: compare.coercion.clone(),
742        }
743    }
744}
745
746fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
747    let Some(order) = order else {
748        return ExplainOrderBy::None;
749    };
750
751    if order.fields.is_empty() {
752        return ExplainOrderBy::None;
753    }
754
755    ExplainOrderBy::Fields(
756        order
757            .fields
758            .iter()
759            .map(|(field, direction)| ExplainOrder {
760                field: field.clone(),
761                direction: *direction,
762            })
763            .collect(),
764    )
765}
766
767const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
768    match page {
769        Some(page) => ExplainPagination::Page {
770            limit: page.limit,
771            offset: page.offset,
772        },
773        None => ExplainPagination::None,
774    }
775}
776
777const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
778    match limit {
779        Some(limit) if limit.offset == 0 => match limit.limit {
780            Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
781            None => ExplainDeleteLimit::Window {
782                limit: None,
783                offset: 0,
784            },
785        },
786        Some(limit) => ExplainDeleteLimit::Window {
787            limit: limit.limit,
788            offset: limit.offset,
789        },
790        None => ExplainDeleteLimit::None,
791    }
792}
793
794fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
795    let mut object = JsonWriter::begin_object(out);
796    object.field_with("mode", |out| write_query_mode_json(explain.mode(), out));
797    object.field_with("access", |out| write_access_json(explain.access(), out));
798    object.field_value_debug("predicate", explain.predicate());
799    object.field_value_debug("order_by", explain.order_by());
800    object.field_bool("distinct", explain.distinct());
801    object.field_value_debug("grouping", explain.grouping());
802    object.field_value_debug("order_pushdown", explain.order_pushdown());
803    object.field_with("page", |out| write_pagination_json(explain.page(), out));
804    object.field_with("delete_limit", |out| {
805        write_delete_limit_json(explain.delete_limit(), out);
806    });
807    object.field_value_debug("consistency", &explain.consistency());
808    object.finish();
809}
810
811fn write_query_mode_json(mode: QueryMode, out: &mut String) {
812    let mut object = JsonWriter::begin_object(out);
813    match mode {
814        QueryMode::Load(spec) => {
815            object.field_str("type", "Load");
816            match spec.limit() {
817                Some(limit) => object.field_u64("limit", u64::from(limit)),
818                None => object.field_null("limit"),
819            }
820            object.field_u64("offset", u64::from(spec.offset()));
821        }
822        QueryMode::Delete(spec) => {
823            object.field_str("type", "Delete");
824            match spec.limit() {
825                Some(limit) => object.field_u64("limit", u64::from(limit)),
826                None => object.field_null("limit"),
827            }
828        }
829    }
830    object.finish();
831}
832
833fn write_pagination_json(page: &ExplainPagination, out: &mut String) {
834    let mut object = JsonWriter::begin_object(out);
835    match page {
836        ExplainPagination::None => {
837            object.field_str("type", "None");
838        }
839        ExplainPagination::Page { limit, offset } => {
840            object.field_str("type", "Page");
841            match limit {
842                Some(limit) => object.field_u64("limit", u64::from(*limit)),
843                None => object.field_null("limit"),
844            }
845            object.field_u64("offset", u64::from(*offset));
846        }
847    }
848    object.finish();
849}
850
851fn write_delete_limit_json(limit: &ExplainDeleteLimit, out: &mut String) {
852    let mut object = JsonWriter::begin_object(out);
853    match limit {
854        ExplainDeleteLimit::None => {
855            object.field_str("type", "None");
856        }
857        ExplainDeleteLimit::Limit { max_rows } => {
858            object.field_str("type", "Limit");
859            object.field_u64("max_rows", u64::from(*max_rows));
860        }
861        ExplainDeleteLimit::Window { limit, offset } => {
862            object.field_str("type", "Window");
863            object.field_with("limit", |out| match limit {
864                Some(limit) => out.push_str(&limit.to_string()),
865                None => out.push_str("null"),
866            });
867            object.field_u64("offset", u64::from(*offset));
868        }
869    }
870    object.finish();
871}