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