1use crate::{
8 db::KeyValueCodec,
9 db::{
10 access::AccessPlan,
11 predicate::{CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate},
12 query::{
13 builder::scalar_projection::render_scalar_projection_expr_plan_label,
14 explain::{
15 access_projection::write_access_json_detailed, explain_access_plan,
16 writer::JsonWriter,
17 },
18 plan::{
19 AccessChoiceCandidateExplainSummary, AccessChoiceExplainSnapshot,
20 AccessChoiceRejectedIndex, AccessChoiceResidualBurden, AccessChoiceSelectedReason,
21 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupedPlanAggregateFamily,
22 GroupedPlanFallbackReason, GroupedPlanStrategy, LogicalPlan, OrderDirection,
23 OrderSpec, PageSpec, QueryMode, ScalarPlan, explain_access_strategy_label,
24 expr::{Expr, PathSpec},
25 grouped_plan_strategy, render_scalar_filter_expr_plan_label,
26 },
27 },
28 },
29 value::Value,
30};
31use std::{fmt, ops::Bound};
32
33#[derive(Clone, Eq, PartialEq)]
40pub struct ExplainPlan {
41 pub(in crate::db) mode: QueryMode,
42 pub(in crate::db) access: ExplainAccessPath,
43 pub(in crate::db) access_decision: ExplainAccessDecision,
44 pub(in crate::db) filter_expr: Option<String>,
45 filter_expr_model: Option<Expr>,
46 pub(in crate::db) predicate: ExplainPredicate,
47 predicate_model: Option<Predicate>,
48 pub(in crate::db) order_by: ExplainOrderBy,
49 pub(in crate::db) distinct: bool,
50 pub(in crate::db) grouping: ExplainGrouping,
51 pub(in crate::db) order_pushdown: ExplainOrderPushdown,
52 pub(in crate::db) page: ExplainPagination,
53 pub(in crate::db) delete_limit: ExplainDeleteLimit,
54 pub(in crate::db) consistency: MissingRowPolicy,
55}
56
57#[expect(clippy::missing_fields_in_debug)]
58impl fmt::Debug for ExplainPlan {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.debug_struct("ExplainPlan")
61 .field("mode", &self.mode)
62 .field("access", &self.access)
63 .field("filter_expr", &self.filter_expr)
64 .field("filter_expr_model", &self.filter_expr_model)
65 .field("predicate", &self.predicate)
66 .field("predicate_model", &self.predicate_model)
67 .field("order_by", &self.order_by)
68 .field("distinct", &self.distinct)
69 .field("grouping", &self.grouping)
70 .field("order_pushdown", &self.order_pushdown)
71 .field("page", &self.page)
72 .field("delete_limit", &self.delete_limit)
73 .field("consistency", &self.consistency)
74 .finish()
75 }
76}
77
78impl ExplainPlan {
79 #[must_use]
81 pub const fn mode(&self) -> QueryMode {
82 self.mode
83 }
84
85 #[must_use]
87 pub const fn access(&self) -> &ExplainAccessPath {
88 &self.access
89 }
90
91 #[must_use]
93 pub const fn access_decision(&self) -> &ExplainAccessDecision {
94 &self.access_decision
95 }
96
97 #[must_use]
99 pub fn filter_expr(&self) -> Option<&str> {
100 self.filter_expr.as_deref()
101 }
102
103 #[must_use]
105 pub(in crate::db::query) fn filter_expr_model_for_hash(&self) -> Option<&Expr> {
106 if let Some(filter_expr_model) = &self.filter_expr_model {
107 debug_assert_eq!(
108 self.filter_expr(),
109 Some(render_scalar_filter_expr_plan_label(filter_expr_model).as_str()),
110 "explain scalar filter label drifted from canonical filter model"
111 );
112 Some(filter_expr_model)
113 } else {
114 debug_assert!(
115 self.filter_expr.is_none(),
116 "missing canonical filter model requires filter_expr=None"
117 );
118 None
119 }
120 }
121
122 #[must_use]
124 pub const fn predicate(&self) -> &ExplainPredicate {
125 &self.predicate
126 }
127
128 #[must_use]
130 pub const fn order_by(&self) -> &ExplainOrderBy {
131 &self.order_by
132 }
133
134 #[must_use]
136 pub const fn distinct(&self) -> bool {
137 self.distinct
138 }
139
140 #[must_use]
142 pub const fn grouping(&self) -> &ExplainGrouping {
143 &self.grouping
144 }
145
146 #[must_use]
148 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
149 &self.order_pushdown
150 }
151
152 #[must_use]
154 pub const fn page(&self) -> &ExplainPagination {
155 &self.page
156 }
157
158 #[must_use]
160 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
161 &self.delete_limit
162 }
163
164 #[must_use]
166 pub const fn consistency(&self) -> MissingRowPolicy {
167 self.consistency
168 }
169}
170
171impl ExplainPlan {
172 #[must_use]
178 pub(in crate::db::query) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
179 if let Some(predicate) = &self.predicate_model {
180 debug_assert_eq!(
181 self.predicate,
182 ExplainPredicate::from_predicate(predicate),
183 "explain predicate surface drifted from canonical predicate model"
184 );
185 Some(predicate)
186 } else {
187 debug_assert!(
188 matches!(self.predicate, ExplainPredicate::None),
189 "missing canonical predicate model requires ExplainPredicate::None"
190 );
191 None
192 }
193 }
194
195 #[must_use]
200 pub fn render_text_canonical(&self) -> String {
201 format!(
202 concat!(
203 "mode={:?}\n",
204 "access={:?}\n",
205 "access_decision={}\n",
206 "filter_expr={:?}\n",
207 "predicate={:?}\n",
208 "order_by={:?}\n",
209 "distinct={}\n",
210 "grouping={:?}\n",
211 "order_pushdown={:?}\n",
212 "page={:?}\n",
213 "delete_limit={:?}\n",
214 "consistency={:?}",
215 ),
216 self.mode(),
217 self.access(),
218 self.access_decision().render_compact_summary(),
219 self.filter_expr(),
220 self.predicate(),
221 self.order_by(),
222 self.distinct(),
223 self.grouping(),
224 self.order_pushdown(),
225 self.page(),
226 self.delete_limit(),
227 self.consistency(),
228 )
229 }
230
231 #[must_use]
233 pub fn render_json_canonical(&self) -> String {
234 let mut out = String::new();
235 write_logical_explain_json(self, &mut out);
236
237 out
238 }
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
248pub enum ExplainGrouping {
249 None,
250 Grouped {
251 strategy: &'static str,
252 fallback_reason: Option<&'static str>,
253 group_fields: Vec<ExplainGroupField>,
254 aggregates: Vec<ExplainGroupAggregate>,
255 having: Option<ExplainGroupHaving>,
256 max_groups: u64,
257 max_group_bytes: u64,
258 },
259}
260
261#[derive(Clone, Debug, Eq, PartialEq)]
268pub struct ExplainGroupField {
269 pub(in crate::db) slot_index: usize,
270 pub(in crate::db) field: String,
271 pub(in crate::db) path: Option<PathSpec>,
272}
273
274impl ExplainGroupField {
275 #[must_use]
277 pub const fn slot_index(&self) -> usize {
278 self.slot_index
279 }
280
281 #[must_use]
283 pub const fn field(&self) -> &str {
284 self.field.as_str()
285 }
286}
287
288#[derive(Clone, Debug, Eq, PartialEq)]
295pub struct ExplainGroupAggregate {
296 pub(in crate::db) kind: AggregateKind,
297 pub(in crate::db) target_field: Option<String>,
298 pub(in crate::db) input_expr: Option<String>,
299 pub(in crate::db) filter_expr: Option<String>,
300 pub(in crate::db) distinct: bool,
301}
302
303impl ExplainGroupAggregate {
304 #[must_use]
306 pub const fn kind(&self) -> AggregateKind {
307 self.kind
308 }
309
310 #[must_use]
312 pub fn target_field(&self) -> Option<&str> {
313 self.target_field.as_deref()
314 }
315
316 #[must_use]
318 pub fn input_expr(&self) -> Option<&str> {
319 self.input_expr.as_deref()
320 }
321
322 #[must_use]
324 pub fn filter_expr(&self) -> Option<&str> {
325 self.filter_expr.as_deref()
326 }
327
328 #[must_use]
330 pub const fn distinct(&self) -> bool {
331 self.distinct
332 }
333}
334
335#[derive(Clone, Debug, Eq, PartialEq)]
344pub struct ExplainGroupHaving {
345 pub(in crate::db) expr: Expr,
346}
347
348impl ExplainGroupHaving {
349 #[must_use]
351 pub(in crate::db) const fn expr(&self) -> &Expr {
352 &self.expr
353 }
354}
355
356#[derive(Clone, Debug, Eq, PartialEq)]
363pub enum ExplainOrderPushdown {
364 MissingModelContext,
365 EligibleSecondaryIndex { index: String, prefix_len: usize },
366 Rejected(SecondaryOrderPushdownRejection),
367}
368
369#[derive(Clone, Debug, Eq, PartialEq)]
377pub enum SecondaryOrderPushdownRejection {
378 NoOrderBy,
379 AccessPathNotSingleIndexPrefix,
380 AccessPathIndexRangeUnsupported {
381 index: String,
382 prefix_len: usize,
383 },
384 InvalidIndexPrefixBounds {
385 prefix_len: usize,
386 index_field_len: usize,
387 },
388 MissingPrimaryKeyTieBreak {
389 field: String,
390 },
391 PrimaryKeyDirectionNotAscending {
392 field: String,
393 },
394 MixedDirectionNotEligible {
395 field: String,
396 },
397 OrderFieldsDoNotMatchIndex {
398 index: String,
399 prefix_len: usize,
400 expected_suffix: Vec<String>,
401 expected_full: Vec<String>,
402 actual: Vec<String>,
403 },
404 VariablePrefixSuffixOrderUnsupported {
405 index: String,
406 prefix_len: usize,
407 expected_full: Vec<String>,
408 actual: Vec<String>,
409 },
410}
411
412#[derive(Clone, Debug, Eq, PartialEq)]
420pub enum ExplainAccessPath {
421 ByKey {
422 key: Value,
423 },
424 ByKeys {
425 keys: Vec<Value>,
426 },
427 KeyRange {
428 start: Value,
429 end: Value,
430 },
431 IndexPrefix {
432 name: String,
433 fields: Vec<String>,
434 prefix_len: usize,
435 values: Vec<Value>,
436 },
437 IndexMultiLookup {
438 name: String,
439 fields: Vec<String>,
440 values: Vec<Value>,
441 },
442 IndexBranchSet {
443 name: String,
444 fields: Vec<String>,
445 fixed_values: Vec<Value>,
446 branch_values: Vec<Value>,
447 branch_field: Option<String>,
448 },
449 IndexRange {
450 name: String,
451 fields: Vec<String>,
452 prefix_len: usize,
453 prefix: Vec<Value>,
454 lower: Bound<Value>,
455 upper: Bound<Value>,
456 },
457 FullScan,
458 Union(Vec<Self>),
459 Intersection(Vec<Self>),
460}
461
462#[derive(Clone, Debug, Eq, PartialEq)]
468pub struct ExplainAccessDecision {
469 pub selected: ExplainSelectedAccess,
471 pub candidates: Vec<ExplainAccessCandidate>,
473 pub alternatives: Vec<ExplainEligibleAlternative>,
475 pub rejections: Vec<ExplainRejectedIndex>,
477 pub residual: ExplainResidualSummary,
479 pub cardinality_evidence_state: &'static str,
481}
482
483impl ExplainAccessDecision {
484 fn from_snapshot(
485 selected_access: &ExplainAccessPath,
486 snapshot: &AccessChoiceExplainSnapshot,
487 ) -> Self {
488 let selected_label = explain_access_strategy_label(selected_access);
489 let selected_candidate =
490 selected_candidate_summary(selected_index_name(selected_access), &snapshot.candidates);
491
492 Self {
493 selected: ExplainSelectedAccess {
494 kind: ExplainAccessDecisionKind::from_access_path(selected_access),
495 index_name: selected_index_name(selected_access).map(ToOwned::to_owned),
496 label: selected_label,
497 reason: snapshot.chosen_reason().code(),
498 },
499 candidates: snapshot
500 .candidates
501 .iter()
502 .map(ExplainAccessCandidate::from_candidate)
503 .collect(),
504 alternatives: snapshot
505 .alternatives
506 .iter()
507 .map(|index_name| ExplainEligibleAlternative {
508 index_name: index_name.clone(),
509 })
510 .collect(),
511 rejections: snapshot
512 .rejected
513 .iter()
514 .map(ExplainRejectedIndex::from_rejection)
515 .collect(),
516 residual: ExplainResidualSummary::from_selected_access_and_candidate(
517 selected_access,
518 selected_candidate,
519 snapshot.chosen_reason(),
520 ),
521 cardinality_evidence_state: snapshot.cardinality_evidence_state,
522 }
523 }
524
525 fn render_compact_summary(&self) -> String {
526 let index = self
527 .selected
528 .index_name
529 .as_deref()
530 .map_or("none", |index| index);
531
532 format!(
533 "kind={} index={} reason={} residual={} cardinality_evidence={} candidates={} alternatives={} rejections={}",
534 self.selected.kind.code(),
535 index,
536 self.selected.reason,
537 self.residual.burden_class,
538 self.cardinality_evidence_state,
539 self.candidates.len(),
540 self.alternatives.len(),
541 self.rejections.len(),
542 )
543 }
544}
545
546#[derive(Clone, Debug, Eq, PartialEq)]
548pub struct ExplainSelectedAccess {
549 pub kind: ExplainAccessDecisionKind,
551 pub index_name: Option<String>,
553 pub label: String,
555 pub reason: &'static str,
557}
558
559#[derive(Clone, Copy, Debug, Eq, PartialEq)]
561pub enum ExplainAccessDecisionKind {
562 ByKey,
564 ByKeys,
566 KeyRange,
568 IndexPrefix,
570 IndexMultiLookup,
572 IndexBranchSet,
574 IndexRange,
576 FullScan,
578 Union,
580 Intersection,
582}
583
584impl ExplainAccessDecisionKind {
585 const fn from_access_path(access: &ExplainAccessPath) -> Self {
586 match access {
587 ExplainAccessPath::ByKey { .. } => Self::ByKey,
588 ExplainAccessPath::ByKeys { .. } => Self::ByKeys,
589 ExplainAccessPath::KeyRange { .. } => Self::KeyRange,
590 ExplainAccessPath::IndexPrefix { .. } => Self::IndexPrefix,
591 ExplainAccessPath::IndexMultiLookup { .. } => Self::IndexMultiLookup,
592 ExplainAccessPath::IndexBranchSet { .. } => Self::IndexBranchSet,
593 ExplainAccessPath::IndexRange { .. } => Self::IndexRange,
594 ExplainAccessPath::FullScan => Self::FullScan,
595 ExplainAccessPath::Union(_) => Self::Union,
596 ExplainAccessPath::Intersection(_) => Self::Intersection,
597 }
598 }
599
600 const fn code(self) -> &'static str {
601 match self {
602 Self::ByKey => "ByKey",
603 Self::ByKeys => "ByKeys",
604 Self::KeyRange => "KeyRange",
605 Self::IndexPrefix => "IndexPrefix",
606 Self::IndexMultiLookup => "IndexMultiLookup",
607 Self::IndexBranchSet => "IndexBranchSet",
608 Self::IndexRange => "IndexRange",
609 Self::FullScan => "FullScan",
610 Self::Union => "Union",
611 Self::Intersection => "Intersection",
612 }
613 }
614}
615
616#[derive(Clone, Debug, Eq, PartialEq)]
618pub struct ExplainAccessCandidate {
619 pub label: String,
621 pub exact: bool,
623 pub filtered: bool,
625 pub range_bound_count: usize,
627 pub order_compatible: bool,
629 pub residual_burden: &'static str,
631 pub residual_predicate_terms: usize,
633 pub exact_prefix_entries: Option<u64>,
635}
636
637impl ExplainAccessCandidate {
638 fn from_candidate(candidate: &AccessChoiceCandidateExplainSummary) -> Self {
639 Self {
640 label: candidate.label(),
641 exact: candidate.exact,
642 filtered: candidate.filtered,
643 range_bound_count: candidate.range_bound_count,
644 order_compatible: candidate.order_compatible,
645 residual_burden: candidate.residual_burden.label(),
646 residual_predicate_terms: candidate.residual_predicate_terms,
647 exact_prefix_entries: candidate.exact_prefix_entries,
648 }
649 }
650}
651
652#[derive(Clone, Debug, Eq, PartialEq)]
654pub struct ExplainEligibleAlternative {
655 pub index_name: String,
657}
658
659#[derive(Clone, Debug, Eq, PartialEq)]
661pub struct ExplainRejectedIndex {
662 pub index_name: Option<String>,
664 pub reason: Option<String>,
666 pub label: String,
668}
669
670impl ExplainRejectedIndex {
671 fn from_rejection(rejection: &AccessChoiceRejectedIndex) -> Self {
672 Self {
673 index_name: Some(rejection.index_name().to_string()),
674 reason: Some(rejection.reason_code().to_string()),
675 label: rejection.label(),
676 }
677 }
678}
679
680#[derive(Clone, Debug, Eq, PartialEq)]
682pub struct ExplainResidualSummary {
683 pub burden_class: &'static str,
685 pub has_residual_filter: bool,
687 pub has_residual_predicate: bool,
689 pub access_bound_predicate_count: usize,
691 pub residual_predicate_count: usize,
693}
694
695impl ExplainResidualSummary {
696 fn from_selected_access_and_candidate(
697 selected_access: &ExplainAccessPath,
698 selected_candidate: Option<&AccessChoiceCandidateExplainSummary>,
699 selected_reason: AccessChoiceSelectedReason,
700 ) -> Self {
701 if let Some(candidate) = selected_candidate {
702 Self {
703 burden_class: candidate.residual_burden.label(),
704 has_residual_filter: matches!(
705 candidate.residual_burden,
706 AccessChoiceResidualBurden::ScalarExpression
707 ),
708 has_residual_predicate: candidate.residual_predicate_terms > 0,
709 access_bound_predicate_count: access_bound_predicate_count(selected_access),
710 residual_predicate_count: candidate.residual_predicate_terms,
711 }
712 } else {
713 let access_bound_predicate_count = access_bound_predicate_count(selected_access);
714 if matches!(
715 selected_reason,
716 AccessChoiceSelectedReason::PlannerExactIndexIntersection
717 ) {
718 Self {
719 burden_class: AccessChoiceResidualBurden::PredicateOnly.label(),
720 has_residual_filter: false,
721 has_residual_predicate: true,
722 access_bound_predicate_count,
723 residual_predicate_count: access_bound_predicate_count,
724 }
725 } else {
726 Self {
727 burden_class: AccessChoiceResidualBurden::None.label(),
728 has_residual_filter: false,
729 has_residual_predicate: false,
730 access_bound_predicate_count,
731 residual_predicate_count: 0,
732 }
733 }
734 }
735 }
736}
737
738#[derive(Clone, Debug, Eq, PartialEq)]
746pub enum ExplainPredicate {
747 None,
748 True,
749 False,
750 And(Vec<Self>),
751 Or(Vec<Self>),
752 Not(Box<Self>),
753 Compare {
754 field: String,
755 op: CompareOp,
756 value: Value,
757 coercion: CoercionSpec,
758 },
759 CompareFields {
760 left_field: String,
761 op: CompareOp,
762 right_field: String,
763 coercion: CoercionSpec,
764 },
765 IsNull {
766 field: String,
767 },
768 IsNotNull {
769 field: String,
770 },
771 IsMissing {
772 field: String,
773 },
774 IsEmpty {
775 field: String,
776 },
777 IsNotEmpty {
778 field: String,
779 },
780 TextContains {
781 field: String,
782 value: Value,
783 },
784 TextContainsCi {
785 field: String,
786 value: Value,
787 },
788}
789
790#[derive(Clone, Debug, Eq, PartialEq)]
797pub enum ExplainOrderBy {
798 None,
799 Fields(Vec<ExplainOrder>),
800}
801
802#[derive(Clone, Debug, Eq, PartialEq)]
809pub struct ExplainOrder {
810 pub(in crate::db) field: String,
811 pub(in crate::db) direction: OrderDirection,
812}
813
814impl ExplainOrder {
815 #[must_use]
817 pub const fn field(&self) -> &str {
818 self.field.as_str()
819 }
820
821 #[must_use]
823 pub const fn direction(&self) -> OrderDirection {
824 self.direction
825 }
826}
827
828#[derive(Clone, Debug, Eq, PartialEq)]
835pub enum ExplainPagination {
836 None,
837 Page { limit: Option<u32>, offset: u32 },
838}
839
840#[derive(Clone, Debug, Eq, PartialEq)]
847pub enum ExplainDeleteLimit {
848 None,
849 Limit { max_rows: u32 },
850 Window { limit: Option<u32>, offset: u32 },
851}
852
853impl AccessPlannedQuery {
854 #[must_use]
856 pub(in crate::db) fn explain(&self) -> ExplainPlan {
857 self.explain_inner()
858 }
859
860 fn explain_inner(&self) -> ExplainPlan {
861 let (logical, grouping) = match &self.logical {
863 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
864 LogicalPlan::Grouped(logical) => {
865 let grouped_strategy = grouped_plan_strategy(self).unwrap_or_else(|| {
866 debug_assert!(
867 grouped_plan_strategy(self).is_some(),
868 "grouped logical explain projection requires planner-owned grouped strategy",
869 );
870 GroupedPlanStrategy::hash_group_with_aggregate_family(
871 GroupedPlanFallbackReason::GroupKeyOrderUnavailable,
872 GroupedPlanAggregateFamily::from_grouped_aggregates(
873 logical.group.aggregates.as_slice(),
874 ),
875 )
876 });
877
878 (
879 &logical.scalar,
880 ExplainGrouping::Grouped {
881 strategy: grouped_strategy.code(),
882 fallback_reason: grouped_strategy
883 .fallback_reason()
884 .map(GroupedPlanFallbackReason::code),
885 group_fields: logical
886 .group
887 .group_fields
888 .iter()
889 .map(|group_field| ExplainGroupField {
890 slot_index: group_field.root_slot(),
891 field: group_field.field().to_string(),
892 path: group_field.as_scalar_path().map(|path| path.path().clone()),
893 })
894 .collect(),
895 aggregates: logical
896 .group
897 .aggregates
898 .iter()
899 .map(|aggregate| ExplainGroupAggregate {
900 kind: aggregate.kind(),
901 target_field: aggregate.target_field().map(str::to_string),
902 input_expr: aggregate
903 .input_expr()
904 .map(render_scalar_projection_expr_plan_label),
905 filter_expr: aggregate
906 .filter_expr()
907 .map(render_scalar_projection_expr_plan_label),
908 distinct: aggregate.raw_distinct(),
909 })
910 .collect(),
911 having: explain_group_having(logical),
912 max_groups: logical.group.execution.max_groups(),
913 max_group_bytes: logical.group.execution.max_group_bytes(),
914 },
915 )
916 }
917 };
918
919 explain_scalar_inner(logical, grouping, &self.access, self.access_choice())
921 }
922}
923
924fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
925 Some(ExplainGroupHaving {
926 expr: logical.having_expr()?.clone(),
927 })
928}
929
930fn explain_scalar_inner<K>(
931 logical: &ScalarPlan,
932 grouping: ExplainGrouping,
933 access: &AccessPlan<K>,
934 access_choice: &AccessChoiceExplainSnapshot,
935) -> ExplainPlan
936where
937 K: KeyValueCodec,
938{
939 let filter_expr = logical
941 .filter_expr
942 .as_ref()
943 .map(render_scalar_filter_expr_plan_label);
944 let filter_expr_model = logical.filter_expr.clone();
945 let predicate_model = logical.predicate.clone();
946 let predicate = match &predicate_model {
947 Some(predicate) => ExplainPredicate::from_predicate(predicate),
948 None => ExplainPredicate::None,
949 };
950
951 let order_by = explain_order(logical.order.as_ref());
953 let order_pushdown = explain_order_pushdown();
954 let page = explain_page(logical.page.as_ref());
955 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
956
957 let access = explain_access_plan(access);
959 let access_decision = ExplainAccessDecision::from_snapshot(&access, access_choice);
960
961 ExplainPlan {
962 mode: logical.mode,
963 access,
964 access_decision,
965 filter_expr,
966 filter_expr_model,
967 predicate,
968 predicate_model,
969 order_by,
970 distinct: logical.distinct,
971 grouping,
972 order_pushdown,
973 page,
974 delete_limit,
975 consistency: logical.consistency,
976 }
977}
978
979fn selected_candidate_summary<'a>(
980 selected_index_name: Option<&str>,
981 candidates: &'a [AccessChoiceCandidateExplainSummary],
982) -> Option<&'a AccessChoiceCandidateExplainSummary> {
983 let selected_index_name = selected_index_name?;
984
985 candidates
986 .iter()
987 .find(|candidate| candidate.index_name() == selected_index_name)
988}
989
990const fn selected_index_name(access: &ExplainAccessPath) -> Option<&str> {
991 match access {
992 ExplainAccessPath::IndexPrefix { name, .. }
993 | ExplainAccessPath::IndexMultiLookup { name, .. }
994 | ExplainAccessPath::IndexBranchSet { name, .. }
995 | ExplainAccessPath::IndexRange { name, .. } => Some(name.as_str()),
996 ExplainAccessPath::ByKey { .. }
997 | ExplainAccessPath::ByKeys { .. }
998 | ExplainAccessPath::KeyRange { .. }
999 | ExplainAccessPath::FullScan
1000 | ExplainAccessPath::Union(_)
1001 | ExplainAccessPath::Intersection(_) => None,
1002 }
1003}
1004
1005fn access_bound_predicate_count(access: &ExplainAccessPath) -> usize {
1006 match access {
1007 ExplainAccessPath::ByKey { .. }
1008 | ExplainAccessPath::ByKeys { .. }
1009 | ExplainAccessPath::IndexMultiLookup { .. } => 1,
1010 ExplainAccessPath::IndexBranchSet {
1011 fixed_values,
1012 branch_values,
1013 ..
1014 } => fixed_values.len() + usize::from(!branch_values.is_empty()),
1015 ExplainAccessPath::KeyRange { .. } => 2,
1016 ExplainAccessPath::IndexPrefix { prefix_len, .. } => *prefix_len,
1017 ExplainAccessPath::IndexRange {
1018 prefix_len,
1019 lower,
1020 upper,
1021 ..
1022 } => *prefix_len + bound_constraint_count(lower) + bound_constraint_count(upper),
1023 ExplainAccessPath::FullScan => 0,
1024 ExplainAccessPath::Union(children) | ExplainAccessPath::Intersection(children) => {
1025 children.iter().map(access_bound_predicate_count).sum()
1026 }
1027 }
1028}
1029
1030const fn bound_constraint_count(bound: &Bound<Value>) -> usize {
1031 match bound {
1032 Bound::Included(_) | Bound::Excluded(_) => 1,
1033 Bound::Unbounded => 0,
1034 }
1035}
1036
1037const fn explain_order_pushdown() -> ExplainOrderPushdown {
1038 ExplainOrderPushdown::MissingModelContext
1040}
1041
1042impl ExplainPredicate {
1043 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
1044 match predicate {
1045 Predicate::True => Self::True,
1046 Predicate::False => Self::False,
1047 Predicate::And(children) => {
1048 Self::And(children.iter().map(Self::from_predicate).collect())
1049 }
1050 Predicate::Or(children) => {
1051 Self::Or(children.iter().map(Self::from_predicate).collect())
1052 }
1053 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
1054 Predicate::Compare(compare) => Self::from_compare(compare),
1055 Predicate::CompareFields(compare) => Self::CompareFields {
1056 left_field: compare.left_field().to_string(),
1057 op: compare.op(),
1058 right_field: compare.right_field().to_string(),
1059 coercion: compare.coercion().clone(),
1060 },
1061 Predicate::IsNull { field } => Self::IsNull {
1062 field: field.clone(),
1063 },
1064 Predicate::IsNotNull { field } => Self::IsNotNull {
1065 field: field.clone(),
1066 },
1067 Predicate::IsMissing { field } => Self::IsMissing {
1068 field: field.clone(),
1069 },
1070 Predicate::IsEmpty { field } => Self::IsEmpty {
1071 field: field.clone(),
1072 },
1073 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
1074 field: field.clone(),
1075 },
1076 Predicate::TextContains { field, value } => Self::TextContains {
1077 field: field.clone(),
1078 value: value.clone(),
1079 },
1080 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
1081 field: field.clone(),
1082 value: value.clone(),
1083 },
1084 }
1085 }
1086
1087 fn from_compare(compare: &ComparePredicate) -> Self {
1088 Self::Compare {
1089 field: compare.field.clone(),
1090 op: compare.op,
1091 value: compare.value.clone(),
1092 coercion: compare.coercion.clone(),
1093 }
1094 }
1095}
1096
1097fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
1098 let Some(order) = order else {
1099 return ExplainOrderBy::None;
1100 };
1101
1102 if order.fields.is_empty() {
1103 return ExplainOrderBy::None;
1104 }
1105
1106 ExplainOrderBy::Fields(
1107 order
1108 .fields
1109 .iter()
1110 .map(|term| ExplainOrder {
1111 field: term.rendered_label(),
1112 direction: term.direction(),
1113 })
1114 .collect(),
1115 )
1116}
1117
1118const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1119 match page {
1120 Some(page) => ExplainPagination::Page {
1121 limit: page.limit,
1122 offset: page.offset,
1123 },
1124 None => ExplainPagination::None,
1125 }
1126}
1127
1128const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1129 match limit {
1130 Some(limit) if limit.offset == 0 => match limit.limit {
1131 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
1132 None => ExplainDeleteLimit::Window {
1133 limit: None,
1134 offset: 0,
1135 },
1136 },
1137 Some(limit) => ExplainDeleteLimit::Window {
1138 limit: limit.limit,
1139 offset: limit.offset,
1140 },
1141 None => ExplainDeleteLimit::None,
1142 }
1143}
1144
1145fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
1146 let mut object = JsonWriter::begin_object(out);
1147 object.field_with("mode", |out| {
1148 let mut object = JsonWriter::begin_object(out);
1149 match explain.mode() {
1150 QueryMode::Load(spec) => {
1151 object.field_str("type", "Load");
1152 match spec.limit() {
1153 Some(limit) => object.field_u64("limit", u64::from(limit)),
1154 None => object.field_null("limit"),
1155 }
1156 object.field_u64("offset", u64::from(spec.offset()));
1157 }
1158 QueryMode::Delete(spec) => {
1159 object.field_str("type", "Delete");
1160 match spec.limit() {
1161 Some(limit) => object.field_u64("limit", u64::from(limit)),
1162 None => object.field_null("limit"),
1163 }
1164 }
1165 }
1166 object.finish();
1167 });
1168 object.field_with("access", |out| {
1169 write_access_json_detailed(explain.access(), out);
1170 });
1171 object.field_with("access_decision", |out| {
1172 write_access_decision_json(explain.access_decision(), out);
1173 });
1174 match explain.filter_expr() {
1175 Some(filter_expr) => object.field_str("filter_expr", filter_expr),
1176 None => object.field_null("filter_expr"),
1177 }
1178 object.field_value_debug("predicate", explain.predicate());
1179 object.field_value_debug("order_by", explain.order_by());
1180 object.field_bool("distinct", explain.distinct());
1181 object.field_value_debug("grouping", explain.grouping());
1182 object.field_value_debug("order_pushdown", explain.order_pushdown());
1183 object.field_with("page", |out| {
1184 let mut object = JsonWriter::begin_object(out);
1185 match explain.page() {
1186 ExplainPagination::None => {
1187 object.field_str("type", "None");
1188 }
1189 ExplainPagination::Page { limit, offset } => {
1190 object.field_str("type", "Page");
1191 match limit {
1192 Some(limit) => object.field_u64("limit", u64::from(*limit)),
1193 None => object.field_null("limit"),
1194 }
1195 object.field_u64("offset", u64::from(*offset));
1196 }
1197 }
1198 object.finish();
1199 });
1200 object.field_with("delete_limit", |out| {
1201 let mut object = JsonWriter::begin_object(out);
1202 match explain.delete_limit() {
1203 ExplainDeleteLimit::None => {
1204 object.field_str("type", "None");
1205 }
1206 ExplainDeleteLimit::Limit { max_rows } => {
1207 object.field_str("type", "Limit");
1208 object.field_u64("max_rows", u64::from(*max_rows));
1209 }
1210 ExplainDeleteLimit::Window { limit, offset } => {
1211 object.field_str("type", "Window");
1212 object.field_with("limit", |out| match limit {
1213 Some(limit) => out.push_str(&limit.to_string()),
1214 None => out.push_str("null"),
1215 });
1216 object.field_u64("offset", u64::from(*offset));
1217 }
1218 }
1219 object.finish();
1220 });
1221 object.field_value_debug("consistency", &explain.consistency());
1222 object.finish();
1223}
1224
1225fn write_access_decision_json(decision: &ExplainAccessDecision, out: &mut String) {
1226 let mut object = JsonWriter::begin_object(out);
1227 object.field_with("selected", |out| {
1228 let mut selected = JsonWriter::begin_object(out);
1229 selected.field_str("kind", decision.selected.kind.code());
1230 match decision.selected.index_name.as_deref() {
1231 Some(index_name) => selected.field_str("index_name", index_name),
1232 None => selected.field_null("index_name"),
1233 }
1234 selected.field_str("label", decision.selected.label.as_str());
1235 selected.field_str("reason", decision.selected.reason);
1236 selected.finish();
1237 });
1238 object.field_with("candidates", |out| {
1239 out.push('[');
1240 for (index, candidate) in decision.candidates.iter().enumerate() {
1241 if index > 0 {
1242 out.push(',');
1243 }
1244 write_access_candidate_json(candidate, out);
1245 }
1246 out.push(']');
1247 });
1248 object.field_with("alternatives", |out| {
1249 out.push('[');
1250 for (index, alternative) in decision.alternatives.iter().enumerate() {
1251 if index > 0 {
1252 out.push(',');
1253 }
1254 let mut object = JsonWriter::begin_object(out);
1255 object.field_str("index_name", alternative.index_name.as_str());
1256 object.finish();
1257 }
1258 out.push(']');
1259 });
1260 object.field_with("rejections", |out| {
1261 out.push('[');
1262 for (index, rejection) in decision.rejections.iter().enumerate() {
1263 if index > 0 {
1264 out.push(',');
1265 }
1266 let mut object = JsonWriter::begin_object(out);
1267 match rejection.index_name.as_deref() {
1268 Some(index_name) => object.field_str("index_name", index_name),
1269 None => object.field_null("index_name"),
1270 }
1271 match rejection.reason.as_deref() {
1272 Some(reason) => object.field_str("reason", reason),
1273 None => object.field_null("reason"),
1274 }
1275 object.field_str("label", rejection.label.as_str());
1276 object.finish();
1277 }
1278 out.push(']');
1279 });
1280 object.field_with("residual", |out| {
1281 let mut residual = JsonWriter::begin_object(out);
1282 residual.field_str("burden_class", decision.residual.burden_class);
1283 residual.field_bool("has_residual_filter", decision.residual.has_residual_filter);
1284 residual.field_bool(
1285 "has_residual_predicate",
1286 decision.residual.has_residual_predicate,
1287 );
1288 residual.field_u64(
1289 "access_bound_predicate_count",
1290 decision.residual.access_bound_predicate_count as u64,
1291 );
1292 residual.field_u64(
1293 "residual_predicate_count",
1294 decision.residual.residual_predicate_count as u64,
1295 );
1296 residual.finish();
1297 });
1298 object.field_str(
1299 "cardinality_evidence_state",
1300 decision.cardinality_evidence_state,
1301 );
1302 object.finish();
1303}
1304
1305fn write_access_candidate_json(candidate: &ExplainAccessCandidate, out: &mut String) {
1306 let mut object = JsonWriter::begin_object(out);
1307 object.field_str("label", candidate.label.as_str());
1308 object.field_bool("exact", candidate.exact);
1309 object.field_bool("filtered", candidate.filtered);
1310 object.field_u64("range_bound_count", candidate.range_bound_count as u64);
1311 object.field_bool("order_compatible", candidate.order_compatible);
1312 object.field_str("residual_burden", candidate.residual_burden);
1313 object.field_u64(
1314 "residual_predicate_terms",
1315 candidate.residual_predicate_terms as u64,
1316 );
1317 if let Some(entries) = candidate.exact_prefix_entries {
1318 object.field_u64("exact_prefix_entries", entries);
1319 } else {
1320 object.field_null("exact_prefix_entries");
1321 }
1322 object.finish();
1323}