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) filter_expr: Option<String>,
235 pub(crate) distinct: bool,
236}
237
238impl ExplainGroupAggregate {
239 #[must_use]
241 pub const fn kind(&self) -> AggregateKind {
242 self.kind
243 }
244
245 #[must_use]
247 pub fn target_field(&self) -> Option<&str> {
248 self.target_field.as_deref()
249 }
250
251 #[must_use]
253 pub fn input_expr(&self) -> Option<&str> {
254 self.input_expr.as_deref()
255 }
256
257 #[must_use]
259 pub fn filter_expr(&self) -> Option<&str> {
260 self.filter_expr.as_deref()
261 }
262
263 #[must_use]
265 pub const fn distinct(&self) -> bool {
266 self.distinct
267 }
268}
269
270#[derive(Clone, Debug, Eq, PartialEq)]
277pub struct ExplainGroupHaving {
278 pub(crate) expr: GroupHavingExpr,
279}
280
281impl ExplainGroupHaving {
282 #[must_use]
284 pub const fn expr(&self) -> &GroupHavingExpr {
285 &self.expr
286 }
287}
288
289#[derive(Clone, Debug, Eq, PartialEq)]
297pub enum GroupHavingExpr {
298 Compare {
299 left: GroupHavingValueExpr,
300 op: CompareOp,
301 right: GroupHavingValueExpr,
302 },
303 And(Vec<Self>),
304 Value(GroupHavingValueExpr),
305}
306
307#[derive(Clone, Debug, Eq, PartialEq)]
316pub enum GroupHavingValueExpr {
317 GroupField {
318 slot_index: usize,
319 field: String,
320 },
321 AggregateIndex {
322 index: usize,
323 },
324 Literal(Value),
325 FunctionCall {
326 function: String,
327 args: Vec<Self>,
328 },
329 Unary {
330 op: String,
331 expr: Box<Self>,
332 },
333 Case {
334 when_then_arms: Vec<GroupHavingCaseArm>,
335 else_expr: Box<Self>,
336 },
337 Binary {
338 op: String,
339 left: Box<Self>,
340 right: Box<Self>,
341 },
342}
343
344#[derive(Clone, Debug, Eq, PartialEq)]
354pub struct GroupHavingCaseArm {
355 pub(crate) condition: GroupHavingValueExpr,
356 pub(crate) result: GroupHavingValueExpr,
357}
358
359impl GroupHavingExpr {
360 #[must_use]
363 pub(in crate::db) fn from_plan(
364 expr: &Expr,
365 group_fields: &[crate::db::query::plan::FieldSlot],
366 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
367 ) -> Self {
368 match expr {
372 Expr::Binary { op, left, right } => match op {
373 BinaryOp::Eq => Self::Compare {
374 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
375 op: CompareOp::Eq,
376 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
377 },
378 BinaryOp::Ne => Self::Compare {
379 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
380 op: CompareOp::Ne,
381 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
382 },
383 BinaryOp::Lt => Self::Compare {
384 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
385 op: CompareOp::Lt,
386 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
387 },
388 BinaryOp::Lte => Self::Compare {
389 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
390 op: CompareOp::Lte,
391 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
392 },
393 BinaryOp::Gt => Self::Compare {
394 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
395 op: CompareOp::Gt,
396 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
397 },
398 BinaryOp::Gte => Self::Compare {
399 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
400 op: CompareOp::Gte,
401 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
402 },
403 BinaryOp::And => Self::And(vec![
404 Self::from_plan(left, group_fields, aggregates),
405 Self::from_plan(right, group_fields, aggregates),
406 ]),
407 BinaryOp::Or | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div => {
408 Self::Value(GroupHavingValueExpr::from_plan(
409 expr,
410 group_fields,
411 aggregates,
412 ))
413 }
414 },
415 _ => Self::Value(GroupHavingValueExpr::from_plan(
418 expr,
419 group_fields,
420 aggregates,
421 )),
422 }
423 }
424
425 #[must_use]
428 pub(in crate::db) fn from_group_plan(
429 logical: &crate::db::query::plan::GroupPlan,
430 expr: &Expr,
431 ) -> Self {
432 Self::from_plan(
433 expr,
434 logical.group.group_fields.as_slice(),
435 logical.group.aggregates.as_slice(),
436 )
437 }
438}
439
440impl GroupHavingValueExpr {
441 #[must_use]
444 pub(in crate::db) fn from_plan(
445 expr: &Expr,
446 group_fields: &[crate::db::query::plan::FieldSlot],
447 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
448 ) -> Self {
449 match expr {
452 Expr::Field(field_id) => {
453 let field_name = field_id.as_str();
454 let field_slot = group_fields
455 .iter()
456 .find(|field| field.field() == field_name)
457 .expect("grouped explain requires HAVING fields to match grouped key fields");
458
459 Self::GroupField {
460 slot_index: field_slot.index(),
461 field: field_slot.field().to_string(),
462 }
463 }
464 Expr::Aggregate(aggregate_expr) => {
465 let index = aggregates
466 .iter()
467 .position(|aggregate| {
468 let distinct_matches = aggregate.distinct() == aggregate_expr.is_distinct();
469
470 aggregate.kind() == aggregate_expr.kind()
471 && aggregate.target_field() == aggregate_expr.target_field()
472 && aggregate.semantic_input_expr_owned().as_ref()
473 == aggregate_expr.input_expr()
474 && aggregate.filter_expr() == aggregate_expr.filter_expr()
475 && distinct_matches
476 })
477 .expect(
478 "grouped explain requires HAVING aggregates to match grouped aggregates",
479 );
480
481 Self::AggregateIndex { index }
482 }
483 Expr::Literal(value) => Self::Literal(value.clone()),
487 Expr::FunctionCall { function, args } => Self::FunctionCall {
488 function: function.sql_label().to_string(),
489 args: args
490 .iter()
491 .map(|arg| Self::from_plan(arg, group_fields, aggregates))
492 .collect(),
493 },
494 Expr::Unary { op, expr } => Self::Unary {
495 op: group_having_unary_op_label(*op).to_string(),
496 expr: Box::new(Self::from_plan(expr, group_fields, aggregates)),
497 },
498 Expr::Case {
499 when_then_arms,
500 else_expr,
501 } => Self::Case {
502 when_then_arms: when_then_arms
503 .iter()
504 .map(|arm| {
505 GroupHavingCaseArm::from_plan(
506 arm.condition(),
507 arm.result(),
508 group_fields,
509 aggregates,
510 )
511 })
512 .collect(),
513 else_expr: Box::new(Self::from_plan(else_expr, group_fields, aggregates)),
514 },
515 Expr::Binary { op, left, right } => Self::Binary {
516 op: group_having_binary_op_label(*op).to_string(),
517 left: Box::new(Self::from_plan(left, group_fields, aggregates)),
518 right: Box::new(Self::from_plan(right, group_fields, aggregates)),
519 },
520 #[cfg(test)]
521 Expr::Alias { expr, name: _ } => Self::from_plan(expr, group_fields, aggregates),
522 }
523 }
524}
525
526impl GroupHavingCaseArm {
527 #[must_use]
530 pub(in crate::db) fn from_plan(
531 condition: &Expr,
532 result: &Expr,
533 group_fields: &[crate::db::query::plan::FieldSlot],
534 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
535 ) -> Self {
536 Self {
537 condition: GroupHavingValueExpr::from_plan(condition, group_fields, aggregates),
538 result: GroupHavingValueExpr::from_plan(result, group_fields, aggregates),
539 }
540 }
541}
542
543#[derive(Clone, Debug, Eq, PartialEq)]
550pub enum ExplainOrderPushdown {
551 MissingModelContext,
552 EligibleSecondaryIndex {
553 index: &'static str,
554 prefix_len: usize,
555 },
556 Rejected(SecondaryOrderPushdownRejection),
557}
558
559#[derive(Clone, Debug, Eq, PartialEq)]
567pub enum ExplainAccessPath {
568 ByKey {
569 key: Value,
570 },
571 ByKeys {
572 keys: Vec<Value>,
573 },
574 KeyRange {
575 start: Value,
576 end: Value,
577 },
578 IndexPrefix {
579 name: &'static str,
580 fields: Vec<&'static str>,
581 prefix_len: usize,
582 values: Vec<Value>,
583 },
584 IndexMultiLookup {
585 name: &'static str,
586 fields: Vec<&'static str>,
587 values: Vec<Value>,
588 },
589 IndexRange {
590 name: &'static str,
591 fields: Vec<&'static str>,
592 prefix_len: usize,
593 prefix: Vec<Value>,
594 lower: Bound<Value>,
595 upper: Bound<Value>,
596 },
597 FullScan,
598 Union(Vec<Self>),
599 Intersection(Vec<Self>),
600}
601
602#[derive(Clone, Debug, Eq, PartialEq)]
610pub enum ExplainPredicate {
611 None,
612 True,
613 False,
614 And(Vec<Self>),
615 Or(Vec<Self>),
616 Not(Box<Self>),
617 Compare {
618 field: String,
619 op: CompareOp,
620 value: Value,
621 coercion: CoercionSpec,
622 },
623 CompareFields {
624 left_field: String,
625 op: CompareOp,
626 right_field: String,
627 coercion: CoercionSpec,
628 },
629 IsNull {
630 field: String,
631 },
632 IsNotNull {
633 field: String,
634 },
635 IsMissing {
636 field: String,
637 },
638 IsEmpty {
639 field: String,
640 },
641 IsNotEmpty {
642 field: String,
643 },
644 TextContains {
645 field: String,
646 value: Value,
647 },
648 TextContainsCi {
649 field: String,
650 value: Value,
651 },
652}
653
654#[derive(Clone, Debug, Eq, PartialEq)]
661pub enum ExplainOrderBy {
662 None,
663 Fields(Vec<ExplainOrder>),
664}
665
666#[derive(Clone, Debug, Eq, PartialEq)]
673pub struct ExplainOrder {
674 pub(crate) field: String,
675 pub(crate) direction: OrderDirection,
676}
677
678impl ExplainOrder {
679 #[must_use]
681 pub const fn field(&self) -> &str {
682 self.field.as_str()
683 }
684
685 #[must_use]
687 pub const fn direction(&self) -> OrderDirection {
688 self.direction
689 }
690}
691
692#[derive(Clone, Debug, Eq, PartialEq)]
699pub enum ExplainPagination {
700 None,
701 Page { limit: Option<u32>, offset: u32 },
702}
703
704#[derive(Clone, Debug, Eq, PartialEq)]
711pub enum ExplainDeleteLimit {
712 None,
713 Limit { max_rows: u32 },
714 Window { limit: Option<u32>, offset: u32 },
715}
716
717impl AccessPlannedQuery {
718 #[must_use]
720 pub(crate) fn explain(&self) -> ExplainPlan {
721 self.explain_inner()
722 }
723
724 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
725 let (logical, grouping) = match &self.logical {
727 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
728 LogicalPlan::Grouped(logical) => {
729 let grouped_strategy = grouped_plan_strategy(self).expect(
730 "grouped logical explain projection requires planner-owned grouped strategy",
731 );
732
733 (
734 &logical.scalar,
735 ExplainGrouping::Grouped {
736 strategy: grouped_strategy.code(),
737 fallback_reason: grouped_strategy
738 .fallback_reason()
739 .map(GroupedPlanFallbackReason::code),
740 group_fields: logical
741 .group
742 .group_fields
743 .iter()
744 .map(|field_slot| ExplainGroupField {
745 slot_index: field_slot.index(),
746 field: field_slot.field().to_string(),
747 })
748 .collect(),
749 aggregates: logical
750 .group
751 .aggregates
752 .iter()
753 .map(|aggregate| ExplainGroupAggregate {
754 kind: aggregate.kind,
755 target_field: aggregate.target_field().map(str::to_string),
756 input_expr: aggregate
757 .input_expr()
758 .map(render_scalar_projection_expr_sql_label),
759 filter_expr: aggregate
760 .filter_expr()
761 .map(render_scalar_projection_expr_sql_label),
762 distinct: aggregate.distinct,
763 })
764 .collect(),
765 having: explain_group_having(logical),
766 max_groups: logical.group.execution.max_groups(),
767 max_group_bytes: logical.group.execution.max_group_bytes(),
768 },
769 )
770 }
771 };
772
773 explain_scalar_inner(logical, grouping, &self.access)
775 }
776}
777
778fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
779 let expr = logical.effective_having_expr()?;
780
781 Some(ExplainGroupHaving {
782 expr: GroupHavingExpr::from_group_plan(logical, expr.as_ref()),
783 })
784}
785
786const fn group_having_unary_op_label(op: UnaryOp) -> &'static str {
787 match op {
788 UnaryOp::Not => "NOT",
789 }
790}
791
792const fn group_having_binary_op_label(op: BinaryOp) -> &'static str {
793 match op {
794 BinaryOp::Or => "OR",
795 BinaryOp::And => "AND",
796 BinaryOp::Eq => "=",
797 BinaryOp::Ne => "!=",
798 BinaryOp::Lt => "<",
799 BinaryOp::Lte => "<=",
800 BinaryOp::Gt => ">",
801 BinaryOp::Gte => ">=",
802 BinaryOp::Add => "+",
803 BinaryOp::Sub => "-",
804 BinaryOp::Mul => "*",
805 BinaryOp::Div => "/",
806 }
807}
808
809fn explain_scalar_inner<K>(
810 logical: &ScalarPlan,
811 grouping: ExplainGrouping,
812 access: &AccessPlan<K>,
813) -> ExplainPlan
814where
815 K: FieldValue,
816{
817 let predicate_model = logical.predicate.clone();
819 let predicate = match &predicate_model {
820 Some(predicate) => ExplainPredicate::from_predicate(predicate),
821 None => ExplainPredicate::None,
822 };
823
824 let order_by = explain_order(logical.order.as_ref());
826 let order_pushdown = explain_order_pushdown();
827 let page = explain_page(logical.page.as_ref());
828 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
829
830 ExplainPlan {
832 mode: logical.mode,
833 access: ExplainAccessPath::from_access_plan(access),
834 predicate,
835 predicate_model,
836 order_by,
837 distinct: logical.distinct,
838 grouping,
839 order_pushdown,
840 page,
841 delete_limit,
842 consistency: logical.consistency,
843 }
844}
845
846const fn explain_order_pushdown() -> ExplainOrderPushdown {
847 ExplainOrderPushdown::MissingModelContext
849}
850
851impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
852 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
853 Self::from(PushdownSurfaceEligibility::from(&value))
854 }
855}
856
857impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
858 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
859 match value {
860 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
861 Self::EligibleSecondaryIndex { index, prefix_len }
862 }
863 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
864 }
865 }
866}
867
868impl ExplainPredicate {
869 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
870 match predicate {
871 Predicate::True => Self::True,
872 Predicate::False => Self::False,
873 Predicate::And(children) => {
874 Self::And(children.iter().map(Self::from_predicate).collect())
875 }
876 Predicate::Or(children) => {
877 Self::Or(children.iter().map(Self::from_predicate).collect())
878 }
879 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
880 Predicate::Compare(compare) => Self::from_compare(compare),
881 Predicate::CompareFields(compare) => Self::CompareFields {
882 left_field: compare.left_field().to_string(),
883 op: compare.op(),
884 right_field: compare.right_field().to_string(),
885 coercion: compare.coercion().clone(),
886 },
887 Predicate::IsNull { field } => Self::IsNull {
888 field: field.clone(),
889 },
890 Predicate::IsNotNull { field } => Self::IsNotNull {
891 field: field.clone(),
892 },
893 Predicate::IsMissing { field } => Self::IsMissing {
894 field: field.clone(),
895 },
896 Predicate::IsEmpty { field } => Self::IsEmpty {
897 field: field.clone(),
898 },
899 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
900 field: field.clone(),
901 },
902 Predicate::TextContains { field, value } => Self::TextContains {
903 field: field.clone(),
904 value: value.clone(),
905 },
906 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
907 field: field.clone(),
908 value: value.clone(),
909 },
910 }
911 }
912
913 fn from_compare(compare: &ComparePredicate) -> Self {
914 Self::Compare {
915 field: compare.field.clone(),
916 op: compare.op,
917 value: compare.value.clone(),
918 coercion: compare.coercion.clone(),
919 }
920 }
921}
922
923fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
924 let Some(order) = order else {
925 return ExplainOrderBy::None;
926 };
927
928 if order.fields.is_empty() {
929 return ExplainOrderBy::None;
930 }
931
932 ExplainOrderBy::Fields(
933 order
934 .fields
935 .iter()
936 .map(|term| ExplainOrder {
937 field: term.label().to_owned(),
938 direction: term.direction(),
939 })
940 .collect(),
941 )
942}
943
944const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
945 match page {
946 Some(page) => ExplainPagination::Page {
947 limit: page.limit,
948 offset: page.offset,
949 },
950 None => ExplainPagination::None,
951 }
952}
953
954const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
955 match limit {
956 Some(limit) if limit.offset == 0 => match limit.limit {
957 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
958 None => ExplainDeleteLimit::Window {
959 limit: None,
960 offset: 0,
961 },
962 },
963 Some(limit) => ExplainDeleteLimit::Window {
964 limit: limit.limit,
965 offset: limit.offset,
966 },
967 None => ExplainDeleteLimit::None,
968 }
969}
970
971fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
972 let mut object = JsonWriter::begin_object(out);
973 object.field_with("mode", |out| {
974 let mut object = JsonWriter::begin_object(out);
975 match explain.mode() {
976 QueryMode::Load(spec) => {
977 object.field_str("type", "Load");
978 match spec.limit() {
979 Some(limit) => object.field_u64("limit", u64::from(limit)),
980 None => object.field_null("limit"),
981 }
982 object.field_u64("offset", u64::from(spec.offset()));
983 }
984 QueryMode::Delete(spec) => {
985 object.field_str("type", "Delete");
986 match spec.limit() {
987 Some(limit) => object.field_u64("limit", u64::from(limit)),
988 None => object.field_null("limit"),
989 }
990 }
991 }
992 object.finish();
993 });
994 object.field_with("access", |out| write_access_json(explain.access(), out));
995 object.field_value_debug("predicate", explain.predicate());
996 object.field_value_debug("order_by", explain.order_by());
997 object.field_bool("distinct", explain.distinct());
998 object.field_value_debug("grouping", explain.grouping());
999 object.field_value_debug("order_pushdown", explain.order_pushdown());
1000 object.field_with("page", |out| {
1001 let mut object = JsonWriter::begin_object(out);
1002 match explain.page() {
1003 ExplainPagination::None => {
1004 object.field_str("type", "None");
1005 }
1006 ExplainPagination::Page { limit, offset } => {
1007 object.field_str("type", "Page");
1008 match limit {
1009 Some(limit) => object.field_u64("limit", u64::from(*limit)),
1010 None => object.field_null("limit"),
1011 }
1012 object.field_u64("offset", u64::from(*offset));
1013 }
1014 }
1015 object.finish();
1016 });
1017 object.field_with("delete_limit", |out| {
1018 let mut object = JsonWriter::begin_object(out);
1019 match explain.delete_limit() {
1020 ExplainDeleteLimit::None => {
1021 object.field_str("type", "None");
1022 }
1023 ExplainDeleteLimit::Limit { max_rows } => {
1024 object.field_str("type", "Limit");
1025 object.field_u64("max_rows", u64::from(*max_rows));
1026 }
1027 ExplainDeleteLimit::Window { limit, offset } => {
1028 object.field_str("type", "Window");
1029 object.field_with("limit", |out| match limit {
1030 Some(limit) => out.push_str(&limit.to_string()),
1031 None => out.push_str("null"),
1032 });
1033 object.field_u64("offset", u64::from(*offset));
1034 }
1035 }
1036 object.finish();
1037 });
1038 object.field_value_debug("consistency", &explain.consistency());
1039 object.finish();
1040}