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