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