Skip to main content

icydb_core/db/query/explain/
plan.rs

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