1use 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::{BinaryOp, Expr, UnaryOp},
21 grouped_plan_strategy,
22 },
23 },
24 },
25 traits::FieldValue,
26 value::Value,
27};
28use std::ops::Bound;
29
30#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct ExplainPlan {
38 pub(crate) mode: QueryMode,
39 pub(crate) access: ExplainAccessPath,
40 pub(crate) predicate: ExplainPredicate,
41 predicate_model: Option<Predicate>,
42 pub(crate) order_by: ExplainOrderBy,
43 pub(crate) distinct: bool,
44 pub(crate) grouping: ExplainGrouping,
45 pub(crate) order_pushdown: ExplainOrderPushdown,
46 pub(crate) page: ExplainPagination,
47 pub(crate) delete_limit: ExplainDeleteLimit,
48 pub(crate) consistency: MissingRowPolicy,
49}
50
51impl ExplainPlan {
52 #[must_use]
54 pub const fn mode(&self) -> QueryMode {
55 self.mode
56 }
57
58 #[must_use]
60 pub const fn access(&self) -> &ExplainAccessPath {
61 &self.access
62 }
63
64 #[must_use]
66 pub const fn predicate(&self) -> &ExplainPredicate {
67 &self.predicate
68 }
69
70 #[must_use]
72 pub const fn order_by(&self) -> &ExplainOrderBy {
73 &self.order_by
74 }
75
76 #[must_use]
78 pub const fn distinct(&self) -> bool {
79 self.distinct
80 }
81
82 #[must_use]
84 pub const fn grouping(&self) -> &ExplainGrouping {
85 &self.grouping
86 }
87
88 #[must_use]
90 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
91 &self.order_pushdown
92 }
93
94 #[must_use]
96 pub const fn page(&self) -> &ExplainPagination {
97 &self.page
98 }
99
100 #[must_use]
102 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
103 &self.delete_limit
104 }
105
106 #[must_use]
108 pub const fn consistency(&self) -> MissingRowPolicy {
109 self.consistency
110 }
111}
112
113impl ExplainPlan {
114 #[must_use]
118 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
119 if let Some(predicate) = &self.predicate_model {
120 debug_assert_eq!(
121 self.predicate,
122 ExplainPredicate::from_predicate(predicate),
123 "explain predicate surface drifted from canonical predicate model"
124 );
125 Some(predicate)
126 } else {
127 debug_assert!(
128 matches!(self.predicate, ExplainPredicate::None),
129 "missing canonical predicate model requires ExplainPredicate::None"
130 );
131 None
132 }
133 }
134
135 #[must_use]
140 pub fn render_text_canonical(&self) -> String {
141 format!(
142 concat!(
143 "mode={:?}\n",
144 "access={:?}\n",
145 "predicate={:?}\n",
146 "order_by={:?}\n",
147 "distinct={}\n",
148 "grouping={:?}\n",
149 "order_pushdown={:?}\n",
150 "page={:?}\n",
151 "delete_limit={:?}\n",
152 "consistency={:?}",
153 ),
154 self.mode(),
155 self.access(),
156 self.predicate(),
157 self.order_by(),
158 self.distinct(),
159 self.grouping(),
160 self.order_pushdown(),
161 self.page(),
162 self.delete_limit(),
163 self.consistency(),
164 )
165 }
166
167 #[must_use]
169 pub fn render_json_canonical(&self) -> String {
170 let mut out = String::new();
171 write_logical_explain_json(self, &mut out);
172
173 out
174 }
175}
176
177#[expect(clippy::large_enum_variant)]
184#[derive(Clone, Debug, Eq, PartialEq)]
185pub enum ExplainGrouping {
186 None,
187 Grouped {
188 strategy: &'static str,
189 fallback_reason: Option<&'static str>,
190 group_fields: Vec<ExplainGroupField>,
191 aggregates: Vec<ExplainGroupAggregate>,
192 having: Option<ExplainGroupHaving>,
193 max_groups: u64,
194 max_group_bytes: u64,
195 },
196}
197
198#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct ExplainGroupField {
206 pub(crate) slot_index: usize,
207 pub(crate) field: String,
208}
209
210impl ExplainGroupField {
211 #[must_use]
213 pub const fn slot_index(&self) -> usize {
214 self.slot_index
215 }
216
217 #[must_use]
219 pub const fn field(&self) -> &str {
220 self.field.as_str()
221 }
222}
223
224#[derive(Clone, Debug, Eq, PartialEq)]
231pub struct ExplainGroupAggregate {
232 pub(crate) kind: AggregateKind,
233 pub(crate) target_field: Option<String>,
234 pub(crate) input_expr: Option<String>,
235 pub(crate) filter_expr: Option<String>,
236 pub(crate) distinct: bool,
237}
238
239impl ExplainGroupAggregate {
240 #[must_use]
242 pub const fn kind(&self) -> AggregateKind {
243 self.kind
244 }
245
246 #[must_use]
248 pub fn target_field(&self) -> Option<&str> {
249 self.target_field.as_deref()
250 }
251
252 #[must_use]
254 pub fn input_expr(&self) -> Option<&str> {
255 self.input_expr.as_deref()
256 }
257
258 #[must_use]
260 pub fn filter_expr(&self) -> Option<&str> {
261 self.filter_expr.as_deref()
262 }
263
264 #[must_use]
266 pub const fn distinct(&self) -> bool {
267 self.distinct
268 }
269}
270
271#[derive(Clone, Debug, Eq, PartialEq)]
278pub struct ExplainGroupHaving {
279 pub(crate) expr: GroupHavingExpr,
280}
281
282impl ExplainGroupHaving {
283 #[must_use]
285 pub const fn expr(&self) -> &GroupHavingExpr {
286 &self.expr
287 }
288}
289
290#[derive(Clone, Debug, Eq, PartialEq)]
298pub enum GroupHavingExpr {
299 Compare {
300 left: GroupHavingValueExpr,
301 op: CompareOp,
302 right: GroupHavingValueExpr,
303 },
304 And(Vec<Self>),
305 Value(GroupHavingValueExpr),
306}
307
308#[derive(Clone, Debug, Eq, PartialEq)]
317pub enum GroupHavingValueExpr {
318 GroupField {
319 slot_index: usize,
320 field: String,
321 },
322 AggregateIndex {
323 index: usize,
324 },
325 Literal(Value),
326 FunctionCall {
327 function: String,
328 args: Vec<Self>,
329 },
330 Unary {
331 op: String,
332 expr: Box<Self>,
333 },
334 Case {
335 when_then_arms: Vec<GroupHavingCaseArm>,
336 else_expr: Box<Self>,
337 },
338 Binary {
339 op: String,
340 left: Box<Self>,
341 right: Box<Self>,
342 },
343}
344
345#[derive(Clone, Debug, Eq, PartialEq)]
355pub struct GroupHavingCaseArm {
356 pub(crate) condition: GroupHavingValueExpr,
357 pub(crate) result: GroupHavingValueExpr,
358}
359
360impl GroupHavingExpr {
361 #[must_use]
364 pub(in crate::db) fn from_plan(
365 expr: &Expr,
366 group_fields: &[crate::db::query::plan::FieldSlot],
367 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
368 ) -> Self {
369 match expr {
373 Expr::Binary { op, left, right } => match op {
374 BinaryOp::Eq => Self::Compare {
375 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
376 op: CompareOp::Eq,
377 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
378 },
379 BinaryOp::Ne => Self::Compare {
380 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
381 op: CompareOp::Ne,
382 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
383 },
384 BinaryOp::Lt => Self::Compare {
385 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
386 op: CompareOp::Lt,
387 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
388 },
389 BinaryOp::Lte => Self::Compare {
390 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
391 op: CompareOp::Lte,
392 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
393 },
394 BinaryOp::Gt => Self::Compare {
395 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
396 op: CompareOp::Gt,
397 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
398 },
399 BinaryOp::Gte => Self::Compare {
400 left: GroupHavingValueExpr::from_plan(left, group_fields, aggregates),
401 op: CompareOp::Gte,
402 right: GroupHavingValueExpr::from_plan(right, group_fields, aggregates),
403 },
404 BinaryOp::And => Self::And(vec![
405 Self::from_plan(left, group_fields, aggregates),
406 Self::from_plan(right, group_fields, aggregates),
407 ]),
408 BinaryOp::Or | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div => {
409 Self::Value(GroupHavingValueExpr::from_plan(
410 expr,
411 group_fields,
412 aggregates,
413 ))
414 }
415 },
416 _ => Self::Value(GroupHavingValueExpr::from_plan(
419 expr,
420 group_fields,
421 aggregates,
422 )),
423 }
424 }
425
426 #[must_use]
429 pub(in crate::db) fn from_group_plan(
430 logical: &crate::db::query::plan::GroupPlan,
431 expr: &Expr,
432 ) -> Self {
433 Self::from_plan(
434 expr,
435 logical.group.group_fields.as_slice(),
436 logical.group.aggregates.as_slice(),
437 )
438 }
439}
440
441impl GroupHavingValueExpr {
442 #[must_use]
445 pub(in crate::db) fn from_plan(
446 expr: &Expr,
447 group_fields: &[crate::db::query::plan::FieldSlot],
448 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
449 ) -> Self {
450 match expr {
453 Expr::Field(field_id) => {
454 let field_name = field_id.as_str();
455 let field_slot = group_fields
456 .iter()
457 .find(|field| field.field() == field_name)
458 .expect("grouped explain requires HAVING fields to match grouped key fields");
459
460 Self::GroupField {
461 slot_index: field_slot.index(),
462 field: field_slot.field().to_string(),
463 }
464 }
465 Expr::Aggregate(aggregate_expr) => {
466 let index = aggregates
467 .iter()
468 .position(|aggregate| {
469 let distinct_matches = aggregate.distinct() == aggregate_expr.is_distinct();
470
471 aggregate.kind() == aggregate_expr.kind()
472 && aggregate.target_field() == aggregate_expr.target_field()
473 && aggregate.semantic_input_expr_owned().as_ref()
474 == aggregate_expr.input_expr()
475 && aggregate.filter_expr() == aggregate_expr.filter_expr()
476 && distinct_matches
477 })
478 .expect(
479 "grouped explain requires HAVING aggregates to match grouped aggregates",
480 );
481
482 Self::AggregateIndex { index }
483 }
484 Expr::Literal(value) => Self::Literal(value.clone()),
488 Expr::FunctionCall { function, args } => Self::FunctionCall {
489 function: function.sql_label().to_string(),
490 args: args
491 .iter()
492 .map(|arg| Self::from_plan(arg, group_fields, aggregates))
493 .collect(),
494 },
495 Expr::Unary { op, expr } => Self::Unary {
496 op: group_having_unary_op_label(*op).to_string(),
497 expr: Box::new(Self::from_plan(expr, group_fields, aggregates)),
498 },
499 Expr::Case {
500 when_then_arms,
501 else_expr,
502 } => Self::Case {
503 when_then_arms: when_then_arms
504 .iter()
505 .map(|arm| {
506 GroupHavingCaseArm::from_plan(
507 arm.condition(),
508 arm.result(),
509 group_fields,
510 aggregates,
511 )
512 })
513 .collect(),
514 else_expr: Box::new(Self::from_plan(else_expr, group_fields, aggregates)),
515 },
516 Expr::Binary { op, left, right } => Self::Binary {
517 op: group_having_binary_op_label(*op).to_string(),
518 left: Box::new(Self::from_plan(left, group_fields, aggregates)),
519 right: Box::new(Self::from_plan(right, group_fields, aggregates)),
520 },
521 #[cfg(test)]
522 Expr::Alias { expr, name: _ } => Self::from_plan(expr, group_fields, aggregates),
523 }
524 }
525}
526
527impl GroupHavingCaseArm {
528 #[must_use]
531 pub(in crate::db) fn from_plan(
532 condition: &Expr,
533 result: &Expr,
534 group_fields: &[crate::db::query::plan::FieldSlot],
535 aggregates: &[crate::db::query::plan::GroupAggregateSpec],
536 ) -> Self {
537 Self {
538 condition: GroupHavingValueExpr::from_plan(condition, group_fields, aggregates),
539 result: GroupHavingValueExpr::from_plan(result, group_fields, aggregates),
540 }
541 }
542}
543
544#[derive(Clone, Debug, Eq, PartialEq)]
551pub enum ExplainOrderPushdown {
552 MissingModelContext,
553 EligibleSecondaryIndex {
554 index: &'static str,
555 prefix_len: usize,
556 },
557 Rejected(SecondaryOrderPushdownRejection),
558}
559
560#[derive(Clone, Debug, Eq, PartialEq)]
568pub enum ExplainAccessPath {
569 ByKey {
570 key: Value,
571 },
572 ByKeys {
573 keys: Vec<Value>,
574 },
575 KeyRange {
576 start: Value,
577 end: Value,
578 },
579 IndexPrefix {
580 name: &'static str,
581 fields: Vec<&'static str>,
582 prefix_len: usize,
583 values: Vec<Value>,
584 },
585 IndexMultiLookup {
586 name: &'static str,
587 fields: Vec<&'static str>,
588 values: Vec<Value>,
589 },
590 IndexRange {
591 name: &'static str,
592 fields: Vec<&'static str>,
593 prefix_len: usize,
594 prefix: Vec<Value>,
595 lower: Bound<Value>,
596 upper: Bound<Value>,
597 },
598 FullScan,
599 Union(Vec<Self>),
600 Intersection(Vec<Self>),
601}
602
603#[derive(Clone, Debug, Eq, PartialEq)]
611pub enum ExplainPredicate {
612 None,
613 True,
614 False,
615 And(Vec<Self>),
616 Or(Vec<Self>),
617 Not(Box<Self>),
618 Compare {
619 field: String,
620 op: CompareOp,
621 value: Value,
622 coercion: CoercionSpec,
623 },
624 CompareFields {
625 left_field: String,
626 op: CompareOp,
627 right_field: String,
628 coercion: CoercionSpec,
629 },
630 IsNull {
631 field: String,
632 },
633 IsNotNull {
634 field: String,
635 },
636 IsMissing {
637 field: String,
638 },
639 IsEmpty {
640 field: String,
641 },
642 IsNotEmpty {
643 field: String,
644 },
645 TextContains {
646 field: String,
647 value: Value,
648 },
649 TextContainsCi {
650 field: String,
651 value: Value,
652 },
653}
654
655#[derive(Clone, Debug, Eq, PartialEq)]
662pub enum ExplainOrderBy {
663 None,
664 Fields(Vec<ExplainOrder>),
665}
666
667#[derive(Clone, Debug, Eq, PartialEq)]
674pub struct ExplainOrder {
675 pub(crate) field: String,
676 pub(crate) direction: OrderDirection,
677}
678
679impl ExplainOrder {
680 #[must_use]
682 pub const fn field(&self) -> &str {
683 self.field.as_str()
684 }
685
686 #[must_use]
688 pub const fn direction(&self) -> OrderDirection {
689 self.direction
690 }
691}
692
693#[derive(Clone, Debug, Eq, PartialEq)]
700pub enum ExplainPagination {
701 None,
702 Page { limit: Option<u32>, offset: u32 },
703}
704
705#[derive(Clone, Debug, Eq, PartialEq)]
712pub enum ExplainDeleteLimit {
713 None,
714 Limit { max_rows: u32 },
715 Window { limit: Option<u32>, offset: u32 },
716}
717
718impl AccessPlannedQuery {
719 #[must_use]
721 pub(crate) fn explain(&self) -> ExplainPlan {
722 self.explain_inner()
723 }
724
725 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
726 let (logical, grouping) = match &self.logical {
728 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
729 LogicalPlan::Grouped(logical) => {
730 let grouped_strategy = grouped_plan_strategy(self).expect(
731 "grouped logical explain projection requires planner-owned grouped strategy",
732 );
733
734 (
735 &logical.scalar,
736 ExplainGrouping::Grouped {
737 strategy: grouped_strategy.code(),
738 fallback_reason: grouped_strategy
739 .fallback_reason()
740 .map(GroupedPlanFallbackReason::code),
741 group_fields: logical
742 .group
743 .group_fields
744 .iter()
745 .map(|field_slot| ExplainGroupField {
746 slot_index: field_slot.index(),
747 field: field_slot.field().to_string(),
748 })
749 .collect(),
750 aggregates: logical
751 .group
752 .aggregates
753 .iter()
754 .map(|aggregate| ExplainGroupAggregate {
755 kind: aggregate.kind,
756 target_field: aggregate.target_field().map(str::to_string),
757 input_expr: aggregate
758 .input_expr()
759 .map(render_scalar_projection_expr_sql_label),
760 filter_expr: aggregate
761 .filter_expr()
762 .map(render_scalar_projection_expr_sql_label),
763 distinct: aggregate.distinct,
764 })
765 .collect(),
766 having: explain_group_having(logical),
767 max_groups: logical.group.execution.max_groups(),
768 max_group_bytes: logical.group.execution.max_group_bytes(),
769 },
770 )
771 }
772 };
773
774 explain_scalar_inner(logical, grouping, &self.access)
776 }
777}
778
779fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
780 let expr = logical.effective_having_expr()?;
781
782 Some(ExplainGroupHaving {
783 expr: GroupHavingExpr::from_group_plan(logical, expr.as_ref()),
784 })
785}
786
787const fn group_having_unary_op_label(op: UnaryOp) -> &'static str {
788 match op {
789 UnaryOp::Not => "NOT",
790 }
791}
792
793const fn group_having_binary_op_label(op: BinaryOp) -> &'static str {
794 match op {
795 BinaryOp::Or => "OR",
796 BinaryOp::And => "AND",
797 BinaryOp::Eq => "=",
798 BinaryOp::Ne => "!=",
799 BinaryOp::Lt => "<",
800 BinaryOp::Lte => "<=",
801 BinaryOp::Gt => ">",
802 BinaryOp::Gte => ">=",
803 BinaryOp::Add => "+",
804 BinaryOp::Sub => "-",
805 BinaryOp::Mul => "*",
806 BinaryOp::Div => "/",
807 }
808}
809
810fn explain_scalar_inner<K>(
811 logical: &ScalarPlan,
812 grouping: ExplainGrouping,
813 access: &AccessPlan<K>,
814) -> ExplainPlan
815where
816 K: FieldValue,
817{
818 let predicate_model = logical.predicate.clone();
820 let predicate = match &predicate_model {
821 Some(predicate) => ExplainPredicate::from_predicate(predicate),
822 None => ExplainPredicate::None,
823 };
824
825 let order_by = explain_order(logical.order.as_ref());
827 let order_pushdown = explain_order_pushdown();
828 let page = explain_page(logical.page.as_ref());
829 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
830
831 ExplainPlan {
833 mode: logical.mode,
834 access: ExplainAccessPath::from_access_plan(access),
835 predicate,
836 predicate_model,
837 order_by,
838 distinct: logical.distinct,
839 grouping,
840 order_pushdown,
841 page,
842 delete_limit,
843 consistency: logical.consistency,
844 }
845}
846
847const fn explain_order_pushdown() -> ExplainOrderPushdown {
848 ExplainOrderPushdown::MissingModelContext
850}
851
852impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
853 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
854 Self::from(PushdownSurfaceEligibility::from(&value))
855 }
856}
857
858impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
859 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
860 match value {
861 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
862 Self::EligibleSecondaryIndex { index, prefix_len }
863 }
864 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
865 }
866 }
867}
868
869impl ExplainPredicate {
870 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
871 match predicate {
872 Predicate::True => Self::True,
873 Predicate::False => Self::False,
874 Predicate::And(children) => {
875 Self::And(children.iter().map(Self::from_predicate).collect())
876 }
877 Predicate::Or(children) => {
878 Self::Or(children.iter().map(Self::from_predicate).collect())
879 }
880 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
881 Predicate::Compare(compare) => Self::from_compare(compare),
882 Predicate::CompareFields(compare) => Self::CompareFields {
883 left_field: compare.left_field().to_string(),
884 op: compare.op(),
885 right_field: compare.right_field().to_string(),
886 coercion: compare.coercion().clone(),
887 },
888 Predicate::IsNull { field } => Self::IsNull {
889 field: field.clone(),
890 },
891 Predicate::IsNotNull { field } => Self::IsNotNull {
892 field: field.clone(),
893 },
894 Predicate::IsMissing { field } => Self::IsMissing {
895 field: field.clone(),
896 },
897 Predicate::IsEmpty { field } => Self::IsEmpty {
898 field: field.clone(),
899 },
900 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
901 field: field.clone(),
902 },
903 Predicate::TextContains { field, value } => Self::TextContains {
904 field: field.clone(),
905 value: value.clone(),
906 },
907 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
908 field: field.clone(),
909 value: value.clone(),
910 },
911 }
912 }
913
914 fn from_compare(compare: &ComparePredicate) -> Self {
915 Self::Compare {
916 field: compare.field.clone(),
917 op: compare.op,
918 value: compare.value.clone(),
919 coercion: compare.coercion.clone(),
920 }
921 }
922}
923
924fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
925 let Some(order) = order else {
926 return ExplainOrderBy::None;
927 };
928
929 if order.fields.is_empty() {
930 return ExplainOrderBy::None;
931 }
932
933 ExplainOrderBy::Fields(
934 order
935 .fields
936 .iter()
937 .map(|term| ExplainOrder {
938 field: term.rendered_label(),
939 direction: term.direction(),
940 })
941 .collect(),
942 )
943}
944
945const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
946 match page {
947 Some(page) => ExplainPagination::Page {
948 limit: page.limit,
949 offset: page.offset,
950 },
951 None => ExplainPagination::None,
952 }
953}
954
955const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
956 match limit {
957 Some(limit) if limit.offset == 0 => match limit.limit {
958 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
959 None => ExplainDeleteLimit::Window {
960 limit: None,
961 offset: 0,
962 },
963 },
964 Some(limit) => ExplainDeleteLimit::Window {
965 limit: limit.limit,
966 offset: limit.offset,
967 },
968 None => ExplainDeleteLimit::None,
969 }
970}
971
972fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
973 let mut object = JsonWriter::begin_object(out);
974 object.field_with("mode", |out| {
975 let mut object = JsonWriter::begin_object(out);
976 match explain.mode() {
977 QueryMode::Load(spec) => {
978 object.field_str("type", "Load");
979 match spec.limit() {
980 Some(limit) => object.field_u64("limit", u64::from(limit)),
981 None => object.field_null("limit"),
982 }
983 object.field_u64("offset", u64::from(spec.offset()));
984 }
985 QueryMode::Delete(spec) => {
986 object.field_str("type", "Delete");
987 match spec.limit() {
988 Some(limit) => object.field_u64("limit", u64::from(limit)),
989 None => object.field_null("limit"),
990 }
991 }
992 }
993 object.finish();
994 });
995 object.field_with("access", |out| write_access_json(explain.access(), out));
996 object.field_value_debug("predicate", explain.predicate());
997 object.field_value_debug("order_by", explain.order_by());
998 object.field_bool("distinct", explain.distinct());
999 object.field_value_debug("grouping", explain.grouping());
1000 object.field_value_debug("order_pushdown", explain.order_pushdown());
1001 object.field_with("page", |out| {
1002 let mut object = JsonWriter::begin_object(out);
1003 match explain.page() {
1004 ExplainPagination::None => {
1005 object.field_str("type", "None");
1006 }
1007 ExplainPagination::Page { limit, offset } => {
1008 object.field_str("type", "Page");
1009 match limit {
1010 Some(limit) => object.field_u64("limit", u64::from(*limit)),
1011 None => object.field_null("limit"),
1012 }
1013 object.field_u64("offset", u64::from(*offset));
1014 }
1015 }
1016 object.finish();
1017 });
1018 object.field_with("delete_limit", |out| {
1019 let mut object = JsonWriter::begin_object(out);
1020 match explain.delete_limit() {
1021 ExplainDeleteLimit::None => {
1022 object.field_str("type", "None");
1023 }
1024 ExplainDeleteLimit::Limit { max_rows } => {
1025 object.field_str("type", "Limit");
1026 object.field_u64("max_rows", u64::from(*max_rows));
1027 }
1028 ExplainDeleteLimit::Window { limit, offset } => {
1029 object.field_str("type", "Window");
1030 object.field_with("limit", |out| match limit {
1031 Some(limit) => out.push_str(&limit.to_string()),
1032 None => out.push_str("null"),
1033 });
1034 object.field_u64("offset", u64::from(*offset));
1035 }
1036 }
1037 object.finish();
1038 });
1039 object.field_value_debug("consistency", &explain.consistency());
1040 object.finish();
1041}