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