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