1use crate::{
7 db::{
8 access::{
9 AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
10 SecondaryOrderPushdownRejection,
11 },
12 predicate::{CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate},
13 query::{
14 builder::scalar_projection::render_scalar_projection_expr_sql_label,
15 explain::{access_projection::write_access_json, writer::JsonWriter},
16 plan::{
17 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupedPlanFallbackReason,
18 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
19 expr::{BinaryOp, Expr, UnaryOp},
20 grouped_plan_strategy,
21 },
22 },
23 },
24 traits::FieldValue,
25 value::Value,
26};
27use std::ops::Bound;
28
29#[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 #[must_use]
53 pub const fn mode(&self) -> QueryMode {
54 self.mode
55 }
56
57 #[must_use]
59 pub const fn access(&self) -> &ExplainAccessPath {
60 &self.access
61 }
62
63 #[must_use]
65 pub const fn predicate(&self) -> &ExplainPredicate {
66 &self.predicate
67 }
68
69 #[must_use]
71 pub const fn order_by(&self) -> &ExplainOrderBy {
72 &self.order_by
73 }
74
75 #[must_use]
77 pub const fn distinct(&self) -> bool {
78 self.distinct
79 }
80
81 #[must_use]
83 pub const fn grouping(&self) -> &ExplainGrouping {
84 &self.grouping
85 }
86
87 #[must_use]
89 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
90 &self.order_pushdown
91 }
92
93 #[must_use]
95 pub const fn page(&self) -> &ExplainPagination {
96 &self.page
97 }
98
99 #[must_use]
101 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
102 &self.delete_limit
103 }
104
105 #[must_use]
107 pub const fn consistency(&self) -> MissingRowPolicy {
108 self.consistency
109 }
110}
111
112impl ExplainPlan {
113 #[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 #[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 #[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#[expect(clippy::large_enum_variant)]
183#[derive(Clone, Debug, Eq, PartialEq)]
184pub enum ExplainGrouping {
185 None,
186 Grouped {
187 strategy: &'static str,
188 fallback_reason: Option<&'static str>,
189 group_fields: Vec<ExplainGroupField>,
190 aggregates: Vec<ExplainGroupAggregate>,
191 having: Option<ExplainGroupHaving>,
192 max_groups: u64,
193 max_group_bytes: u64,
194 },
195}
196
197#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct ExplainGroupField {
205 pub(crate) slot_index: usize,
206 pub(crate) field: String,
207}
208
209impl ExplainGroupField {
210 #[must_use]
212 pub const fn slot_index(&self) -> usize {
213 self.slot_index
214 }
215
216 #[must_use]
218 pub const fn field(&self) -> &str {
219 self.field.as_str()
220 }
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct ExplainGroupAggregate {
231 pub(crate) kind: AggregateKind,
232 pub(crate) target_field: Option<String>,
233 pub(crate) input_expr: Option<String>,
234 pub(crate) distinct: bool,
235}
236
237impl ExplainGroupAggregate {
238 #[must_use]
240 pub const fn kind(&self) -> AggregateKind {
241 self.kind
242 }
243
244 #[must_use]
246 pub fn target_field(&self) -> Option<&str> {
247 self.target_field.as_deref()
248 }
249
250 #[must_use]
252 pub fn input_expr(&self) -> Option<&str> {
253 self.input_expr.as_deref()
254 }
255
256 #[must_use]
258 pub const fn distinct(&self) -> bool {
259 self.distinct
260 }
261}
262
263#[derive(Clone, Debug, Eq, PartialEq)]
270pub struct ExplainGroupHaving {
271 pub(crate) expr: GroupHavingExpr,
272}
273
274impl ExplainGroupHaving {
275 #[must_use]
277 pub const fn expr(&self) -> &GroupHavingExpr {
278 &self.expr
279 }
280}
281
282#[derive(Clone, Debug, Eq, PartialEq)]
290pub enum GroupHavingExpr {
291 Compare {
292 left: GroupHavingValueExpr,
293 op: CompareOp,
294 right: GroupHavingValueExpr,
295 },
296 And(Vec<Self>),
297 Value(GroupHavingValueExpr),
298}
299
300#[derive(Clone, Debug, Eq, PartialEq)]
309pub enum GroupHavingValueExpr {
310 GroupField {
311 slot_index: usize,
312 field: String,
313 },
314 AggregateIndex {
315 index: usize,
316 },
317 Literal(Value),
318 FunctionCall {
319 function: String,
320 args: Vec<Self>,
321 },
322 Unary {
323 op: String,
324 expr: Box<Self>,
325 },
326 Case {
327 when_then_arms: Vec<GroupHavingCaseArm>,
328 else_expr: Box<Self>,
329 },
330 Binary {
331 op: String,
332 left: Box<Self>,
333 right: Box<Self>,
334 },
335}
336
337#[derive(Clone, Debug, Eq, PartialEq)]
347pub struct GroupHavingCaseArm {
348 pub(crate) condition: GroupHavingValueExpr,
349 pub(crate) result: GroupHavingValueExpr,
350}
351
352impl GroupHavingExpr {
353 #[must_use]
356 pub(in crate::db) fn from_plan(
357 expr: &Expr,
358 group_fields: &[crate::db::query::plan::FieldSlot],
359 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
360 ) -> Self {
361 match expr {
365 Expr::Binary { op, left, right } => match op {
366 BinaryOp::Eq => Self::Compare {
367 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
368 op: CompareOp::Eq,
369 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
370 },
371 BinaryOp::Ne => Self::Compare {
372 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
373 op: CompareOp::Ne,
374 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
375 },
376 BinaryOp::Lt => Self::Compare {
377 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
378 op: CompareOp::Lt,
379 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
380 },
381 BinaryOp::Lte => Self::Compare {
382 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
383 op: CompareOp::Lte,
384 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
385 },
386 BinaryOp::Gt => Self::Compare {
387 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
388 op: CompareOp::Gt,
389 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
390 },
391 BinaryOp::Gte => Self::Compare {
392 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
393 op: CompareOp::Gte,
394 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
395 },
396 BinaryOp::And => Self::And(vec![
397 Self::from_plan(left, group_fields, aggregates),
398 Self::from_plan(right, group_fields, aggregates),
399 ]),
400 BinaryOp::Or | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div => {
401 Self::Value(GroupHavingValueExpr::from_plan(
402 expr,
403 group_fields,
404 aggregates,
405 ))
406 }
407 },
408 _ => Self::Value(GroupHavingValueExpr::from_plan(
411 expr,
412 group_fields,
413 aggregates,
414 )),
415 }
416 }
417
418 #[must_use]
421 pub(in crate::db) fn from_group_plan(
422 logical: &crate::db::query::plan::GroupPlan,
423 expr: &Expr,
424 ) -> Self {
425 Self::from_plan(
426 expr,
427 logical.group.group_fields.as_slice(),
428 logical.group.aggregates.as_slice(),
429 )
430 }
431}
432
433impl GroupHavingValueExpr {
434 #[must_use]
437 pub(in crate::db) fn from_plan(
438 expr: &Expr,
439 group_fields: &[crate::db::query::plan::FieldSlot],
440 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
441 ) -> Self {
442 match expr {
445 Expr::Field(field_id) => {
446 let field_name = field_id.as_str();
447 let field_slot = group_fields
448 .iter()
449 .find(|field| field.field() == field_name)
450 .expect("grouped explain requires HAVING fields to match grouped key fields");
451
452 Self::GroupField {
453 slot_index: field_slot.index(),
454 field: field_slot.field().to_string(),
455 }
456 }
457 Expr::Aggregate(aggregate_expr) => {
458 let index = aggregates
459 .iter()
460 .position(|aggregate| {
461 let distinct_matches = aggregate.distinct() == aggregate_expr.is_distinct();
462
463 aggregate.kind() == aggregate_expr.kind()
464 && aggregate.target_field() == aggregate_expr.target_field()
465 && aggregate.semantic_input_expr_owned().as_ref()
466 == aggregate_expr.input_expr()
467 && distinct_matches
468 })
469 .expect(
470 "grouped explain requires HAVING aggregates to match grouped aggregates",
471 );
472
473 Self::AggregateIndex { index }
474 }
475 Expr::Literal(value) => Self::Literal(value.clone()),
479 Expr::FunctionCall { function, args } => Self::FunctionCall {
480 function: function.sql_label().to_string(),
481 args: args
482 .iter()
483 .map(|arg| Self::from_plan(arg, group_fields, aggregates))
484 .collect(),
485 },
486 Expr::Unary { op, expr } => Self::Unary {
487 op: group_having_unary_op_label(*op).to_string(),
488 expr: Box::new(Self::from_plan(expr, group_fields, aggregates)),
489 },
490 Expr::Case {
491 when_then_arms,
492 else_expr,
493 } => Self::Case {
494 when_then_arms: when_then_arms
495 .iter()
496 .map(|arm| {
497 GroupHavingCaseArm::from_plan(
498 arm.condition(),
499 arm.result(),
500 group_fields,
501 aggregates,
502 )
503 })
504 .collect(),
505 else_expr: Box::new(Self::from_plan(else_expr, group_fields, aggregates)),
506 },
507 Expr::Binary { op, left, right } => Self::Binary {
508 op: group_having_binary_op_label(*op).to_string(),
509 left: Box::new(Self::from_plan(left, group_fields, aggregates)),
510 right: Box::new(Self::from_plan(right, group_fields, aggregates)),
511 },
512 #[cfg(test)]
513 Expr::Alias { expr, name: _ } => Self::from_plan(expr, group_fields, aggregates),
514 }
515 }
516}
517
518impl GroupHavingCaseArm {
519 #[must_use]
522 pub(in crate::db) fn from_plan(
523 condition: &Expr,
524 result: &Expr,
525 group_fields: &[crate::db::query::plan::FieldSlot],
526 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
527 ) -> Self {
528 Self {
529 condition: GroupHavingValueExpr::from_plan(condition, group_fields, aggregates),
530 result: GroupHavingValueExpr::from_plan(result, group_fields, aggregates),
531 }
532 }
533}
534
535#[derive(Clone, Debug, Eq, PartialEq)]
542pub enum ExplainOrderPushdown {
543 MissingModelContext,
544 EligibleSecondaryIndex {
545 index: &'static str,
546 prefix_len: usize,
547 },
548 Rejected(SecondaryOrderPushdownRejection),
549}
550
551#[derive(Clone, Debug, Eq, PartialEq)]
559pub enum ExplainAccessPath {
560 ByKey {
561 key: Value,
562 },
563 ByKeys {
564 keys: Vec<Value>,
565 },
566 KeyRange {
567 start: Value,
568 end: Value,
569 },
570 IndexPrefix {
571 name: &'static str,
572 fields: Vec<&'static str>,
573 prefix_len: usize,
574 values: Vec<Value>,
575 },
576 IndexMultiLookup {
577 name: &'static str,
578 fields: Vec<&'static str>,
579 values: Vec<Value>,
580 },
581 IndexRange {
582 name: &'static str,
583 fields: Vec<&'static str>,
584 prefix_len: usize,
585 prefix: Vec<Value>,
586 lower: Bound<Value>,
587 upper: Bound<Value>,
588 },
589 FullScan,
590 Union(Vec<Self>),
591 Intersection(Vec<Self>),
592}
593
594#[derive(Clone, Debug, Eq, PartialEq)]
602pub enum ExplainPredicate {
603 None,
604 True,
605 False,
606 And(Vec<Self>),
607 Or(Vec<Self>),
608 Not(Box<Self>),
609 Compare {
610 field: String,
611 op: CompareOp,
612 value: Value,
613 coercion: CoercionSpec,
614 },
615 CompareFields {
616 left_field: String,
617 op: CompareOp,
618 right_field: String,
619 coercion: CoercionSpec,
620 },
621 IsNull {
622 field: String,
623 },
624 IsNotNull {
625 field: String,
626 },
627 IsMissing {
628 field: String,
629 },
630 IsEmpty {
631 field: String,
632 },
633 IsNotEmpty {
634 field: String,
635 },
636 TextContains {
637 field: String,
638 value: Value,
639 },
640 TextContainsCi {
641 field: String,
642 value: Value,
643 },
644}
645
646#[derive(Clone, Debug, Eq, PartialEq)]
653pub enum ExplainOrderBy {
654 None,
655 Fields(Vec<ExplainOrder>),
656}
657
658#[derive(Clone, Debug, Eq, PartialEq)]
665pub struct ExplainOrder {
666 pub(crate) field: String,
667 pub(crate) direction: OrderDirection,
668}
669
670impl ExplainOrder {
671 #[must_use]
673 pub const fn field(&self) -> &str {
674 self.field.as_str()
675 }
676
677 #[must_use]
679 pub const fn direction(&self) -> OrderDirection {
680 self.direction
681 }
682}
683
684#[derive(Clone, Debug, Eq, PartialEq)]
691pub enum ExplainPagination {
692 None,
693 Page { limit: Option<u32>, offset: u32 },
694}
695
696#[derive(Clone, Debug, Eq, PartialEq)]
703pub enum ExplainDeleteLimit {
704 None,
705 Limit { max_rows: u32 },
706 Window { limit: Option<u32>, offset: u32 },
707}
708
709impl AccessPlannedQuery {
710 #[must_use]
712 pub(crate) fn explain(&self) -> ExplainPlan {
713 self.explain_inner()
714 }
715
716 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
717 let (logical, grouping) = match &self.logical {
719 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
720 LogicalPlan::Grouped(logical) => {
721 let grouped_strategy = grouped_plan_strategy(self).expect(
722 "grouped logical explain projection requires planner-owned grouped strategy",
723 );
724
725 (
726 &logical.scalar,
727 ExplainGrouping::Grouped {
728 strategy: grouped_strategy.code(),
729 fallback_reason: grouped_strategy
730 .fallback_reason()
731 .map(GroupedPlanFallbackReason::code),
732 group_fields: logical
733 .group
734 .group_fields
735 .iter()
736 .map(|field_slot| ExplainGroupField {
737 slot_index: field_slot.index(),
738 field: field_slot.field().to_string(),
739 })
740 .collect(),
741 aggregates: logical
742 .group
743 .aggregates
744 .iter()
745 .map(|aggregate| ExplainGroupAggregate {
746 kind: aggregate.kind,
747 target_field: aggregate.target_field().map(str::to_string),
748 input_expr: aggregate
749 .input_expr()
750 .map(render_scalar_projection_expr_sql_label),
751 distinct: aggregate.distinct,
752 })
753 .collect(),
754 having: explain_group_having(logical),
755 max_groups: logical.group.execution.max_groups(),
756 max_group_bytes: logical.group.execution.max_group_bytes(),
757 },
758 )
759 }
760 };
761
762 explain_scalar_inner(logical, grouping, &self.access)
764 }
765}
766
767fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
768 let expr = logical.effective_having_expr()?;
769
770 Some(ExplainGroupHaving {
771 expr: GroupHavingExpr::from_group_plan(logical, expr.as_ref()),
772 })
773}
774
775const fn group_having_unary_op_label(op: UnaryOp) -> &'static str {
776 match op {
777 UnaryOp::Not => "NOT",
778 }
779}
780
781const fn group_having_binary_op_label(op: BinaryOp) -> &'static str {
782 match op {
783 BinaryOp::Or => "OR",
784 BinaryOp::And => "AND",
785 BinaryOp::Eq => "=",
786 BinaryOp::Ne => "!=",
787 BinaryOp::Lt => "<",
788 BinaryOp::Lte => "<=",
789 BinaryOp::Gt => ">",
790 BinaryOp::Gte => ">=",
791 BinaryOp::Add => "+",
792 BinaryOp::Sub => "-",
793 BinaryOp::Mul => "*",
794 BinaryOp::Div => "/",
795 }
796}
797
798fn explain_scalar_inner<K>(
799 logical: &ScalarPlan,
800 grouping: ExplainGrouping,
801 access: &AccessPlan<K>,
802) -> ExplainPlan
803where
804 K: FieldValue,
805{
806 let predicate_model = logical.predicate.clone();
808 let predicate = match &predicate_model {
809 Some(predicate) => ExplainPredicate::from_predicate(predicate),
810 None => ExplainPredicate::None,
811 };
812
813 let order_by = explain_order(logical.order.as_ref());
815 let order_pushdown = explain_order_pushdown();
816 let page = explain_page(logical.page.as_ref());
817 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
818
819 ExplainPlan {
821 mode: logical.mode,
822 access: ExplainAccessPath::from_access_plan(access),
823 predicate,
824 predicate_model,
825 order_by,
826 distinct: logical.distinct,
827 grouping,
828 order_pushdown,
829 page,
830 delete_limit,
831 consistency: logical.consistency,
832 }
833}
834
835const fn explain_order_pushdown() -> ExplainOrderPushdown {
836 ExplainOrderPushdown::MissingModelContext
838}
839
840impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
841 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
842 Self::from(PushdownSurfaceEligibility::from(&value))
843 }
844}
845
846impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
847 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
848 match value {
849 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
850 Self::EligibleSecondaryIndex { index, prefix_len }
851 }
852 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
853 }
854 }
855}
856
857impl ExplainPredicate {
858 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
859 match predicate {
860 Predicate::True => Self::True,
861 Predicate::False => Self::False,
862 Predicate::And(children) => {
863 Self::And(children.iter().map(Self::from_predicate).collect())
864 }
865 Predicate::Or(children) => {
866 Self::Or(children.iter().map(Self::from_predicate).collect())
867 }
868 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
869 Predicate::Compare(compare) => Self::from_compare(compare),
870 Predicate::CompareFields(compare) => Self::CompareFields {
871 left_field: compare.left_field().to_string(),
872 op: compare.op(),
873 right_field: compare.right_field().to_string(),
874 coercion: compare.coercion().clone(),
875 },
876 Predicate::IsNull { field } => Self::IsNull {
877 field: field.clone(),
878 },
879 Predicate::IsNotNull { field } => Self::IsNotNull {
880 field: field.clone(),
881 },
882 Predicate::IsMissing { field } => Self::IsMissing {
883 field: field.clone(),
884 },
885 Predicate::IsEmpty { field } => Self::IsEmpty {
886 field: field.clone(),
887 },
888 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
889 field: field.clone(),
890 },
891 Predicate::TextContains { field, value } => Self::TextContains {
892 field: field.clone(),
893 value: value.clone(),
894 },
895 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
896 field: field.clone(),
897 value: value.clone(),
898 },
899 }
900 }
901
902 fn from_compare(compare: &ComparePredicate) -> Self {
903 Self::Compare {
904 field: compare.field.clone(),
905 op: compare.op,
906 value: compare.value.clone(),
907 coercion: compare.coercion.clone(),
908 }
909 }
910}
911
912fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
913 let Some(order) = order else {
914 return ExplainOrderBy::None;
915 };
916
917 if order.fields.is_empty() {
918 return ExplainOrderBy::None;
919 }
920
921 ExplainOrderBy::Fields(
922 order
923 .fields
924 .iter()
925 .map(|(field, direction)| ExplainOrder {
926 field: field.clone(),
927 direction: *direction,
928 })
929 .collect(),
930 )
931}
932
933const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
934 match page {
935 Some(page) => ExplainPagination::Page {
936 limit: page.limit,
937 offset: page.offset,
938 },
939 None => ExplainPagination::None,
940 }
941}
942
943const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
944 match limit {
945 Some(limit) if limit.offset == 0 => match limit.limit {
946 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
947 None => ExplainDeleteLimit::Window {
948 limit: None,
949 offset: 0,
950 },
951 },
952 Some(limit) => ExplainDeleteLimit::Window {
953 limit: limit.limit,
954 offset: limit.offset,
955 },
956 None => ExplainDeleteLimit::None,
957 }
958}
959
960fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
961 let mut object = JsonWriter::begin_object(out);
962 object.field_with("mode", |out| {
963 let mut object = JsonWriter::begin_object(out);
964 match explain.mode() {
965 QueryMode::Load(spec) => {
966 object.field_str("type", "Load");
967 match spec.limit() {
968 Some(limit) => object.field_u64("limit", u64::from(limit)),
969 None => object.field_null("limit"),
970 }
971 object.field_u64("offset", u64::from(spec.offset()));
972 }
973 QueryMode::Delete(spec) => {
974 object.field_str("type", "Delete");
975 match spec.limit() {
976 Some(limit) => object.field_u64("limit", u64::from(limit)),
977 None => object.field_null("limit"),
978 }
979 }
980 }
981 object.finish();
982 });
983 object.field_with("access", |out| write_access_json(explain.access(), out));
984 object.field_value_debug("predicate", explain.predicate());
985 object.field_value_debug("order_by", explain.order_by());
986 object.field_bool("distinct", explain.distinct());
987 object.field_value_debug("grouping", explain.grouping());
988 object.field_value_debug("order_pushdown", explain.order_pushdown());
989 object.field_with("page", |out| {
990 let mut object = JsonWriter::begin_object(out);
991 match explain.page() {
992 ExplainPagination::None => {
993 object.field_str("type", "None");
994 }
995 ExplainPagination::Page { limit, offset } => {
996 object.field_str("type", "Page");
997 match limit {
998 Some(limit) => object.field_u64("limit", u64::from(*limit)),
999 None => object.field_null("limit"),
1000 }
1001 object.field_u64("offset", u64::from(*offset));
1002 }
1003 }
1004 object.finish();
1005 });
1006 object.field_with("delete_limit", |out| {
1007 let mut object = JsonWriter::begin_object(out);
1008 match explain.delete_limit() {
1009 ExplainDeleteLimit::None => {
1010 object.field_str("type", "None");
1011 }
1012 ExplainDeleteLimit::Limit { max_rows } => {
1013 object.field_str("type", "Limit");
1014 object.field_u64("max_rows", u64::from(*max_rows));
1015 }
1016 ExplainDeleteLimit::Window { limit, offset } => {
1017 object.field_str("type", "Window");
1018 object.field_with("limit", |out| match limit {
1019 Some(limit) => out.push_str(&limit.to_string()),
1020 None => out.push_str("null"),
1021 });
1022 object.field_u64("offset", u64::from(*offset));
1023 }
1024 }
1025 object.finish();
1026 });
1027 object.field_value_debug("consistency", &explain.consistency());
1028 object.finish();
1029}