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