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