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