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 GroupedPlanAggregateFamily, GroupedPlanFallbackReason, GroupedPlanStrategy,
21 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
22 explain_access_strategy_label, expr::Expr, grouped_plan_strategy,
23 render_scalar_filter_expr_plan_label,
24 },
25 },
26 },
27 traits::KeyValueCodec,
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: ExplainAccessDecisionV1,
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) -> &ExplainAccessDecisionV1 {
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}
403
404#[derive(Clone, Debug, Eq, PartialEq)]
412pub enum ExplainAccessPath {
413 ByKey {
414 key: Value,
415 },
416 ByKeys {
417 keys: Vec<Value>,
418 },
419 KeyRange {
420 start: Value,
421 end: Value,
422 },
423 IndexPrefix {
424 name: String,
425 fields: Vec<String>,
426 prefix_len: usize,
427 values: Vec<Value>,
428 },
429 IndexMultiLookup {
430 name: String,
431 fields: Vec<String>,
432 values: Vec<Value>,
433 },
434 IndexBranchSet {
435 name: String,
436 fields: Vec<String>,
437 fixed_values: Vec<Value>,
438 branch_values: Vec<Value>,
439 branch_field: Option<String>,
440 ordered_suffix: String,
441 },
442 IndexRange {
443 name: String,
444 fields: Vec<String>,
445 prefix_len: usize,
446 prefix: Vec<Value>,
447 lower: Bound<Value>,
448 upper: Bound<Value>,
449 },
450 FullScan,
451 Union(Vec<Self>),
452 Intersection(Vec<Self>),
453}
454
455#[derive(Clone, Debug, Eq, PartialEq)]
461pub struct ExplainAccessDecisionV1 {
462 pub schema_version: u32,
464 pub selected: ExplainSelectedAccessV1,
466 pub candidates: Vec<ExplainAccessCandidateV1>,
468 pub alternatives: Vec<ExplainEligibleAlternativeV1>,
470 pub rejections: Vec<ExplainRejectedIndexV1>,
472 pub residual: ExplainResidualSummaryV1,
474}
475
476impl ExplainAccessDecisionV1 {
477 const SCHEMA_VERSION: u32 = 1;
478
479 fn from_snapshot(
480 selected_access: &ExplainAccessPath,
481 snapshot: &AccessChoiceExplainSnapshot,
482 ) -> Self {
483 let selected_label = explain_access_strategy_label(selected_access);
484 let selected_candidate = selected_candidate_summary(&selected_label, &snapshot.candidates);
485
486 Self {
487 schema_version: Self::SCHEMA_VERSION,
488 selected: ExplainSelectedAccessV1 {
489 kind: ExplainAccessDecisionKind::from_access_path(selected_access),
490 index_name: selected_index_name(selected_access).map(ToOwned::to_owned),
491 label: selected_label,
492 reason: snapshot.chosen_reason().code(),
493 },
494 candidates: snapshot
495 .candidates
496 .iter()
497 .map(ExplainAccessCandidateV1::from_candidate)
498 .collect(),
499 alternatives: snapshot
500 .alternatives
501 .iter()
502 .map(|index_name| ExplainEligibleAlternativeV1 {
503 index_name: index_name.clone(),
504 })
505 .collect(),
506 rejections: snapshot
507 .rejected
508 .iter()
509 .map(|rejection| ExplainRejectedIndexV1::from_rejection(rejection))
510 .collect(),
511 residual: ExplainResidualSummaryV1::from_selected_access_and_candidate(
512 selected_access,
513 selected_candidate,
514 ),
515 }
516 }
517
518 fn render_compact_summary(&self) -> String {
519 let index = self
520 .selected
521 .index_name
522 .as_deref()
523 .map_or("none", |index| index);
524
525 format!(
526 "kind={} index={} reason={} residual={} candidates={} alternatives={} rejections={}",
527 self.selected.kind.code(),
528 index,
529 self.selected.reason,
530 self.residual.burden_class,
531 self.candidates.len(),
532 self.alternatives.len(),
533 self.rejections.len(),
534 )
535 }
536}
537
538#[derive(Clone, Debug, Eq, PartialEq)]
540pub struct ExplainSelectedAccessV1 {
541 pub kind: ExplainAccessDecisionKind,
543 pub index_name: Option<String>,
545 pub label: String,
547 pub reason: &'static str,
549}
550
551#[derive(Clone, Copy, Debug, Eq, PartialEq)]
553pub enum ExplainAccessDecisionKind {
554 ByKey,
556 ByKeys,
558 KeyRange,
560 IndexPrefix,
562 IndexMultiLookup,
564 IndexBranchSet,
566 IndexRange,
568 FullScan,
570 Union,
572 Intersection,
574}
575
576impl ExplainAccessDecisionKind {
577 const fn from_access_path(access: &ExplainAccessPath) -> Self {
578 match access {
579 ExplainAccessPath::ByKey { .. } => Self::ByKey,
580 ExplainAccessPath::ByKeys { .. } => Self::ByKeys,
581 ExplainAccessPath::KeyRange { .. } => Self::KeyRange,
582 ExplainAccessPath::IndexPrefix { .. } => Self::IndexPrefix,
583 ExplainAccessPath::IndexMultiLookup { .. } => Self::IndexMultiLookup,
584 ExplainAccessPath::IndexBranchSet { .. } => Self::IndexBranchSet,
585 ExplainAccessPath::IndexRange { .. } => Self::IndexRange,
586 ExplainAccessPath::FullScan => Self::FullScan,
587 ExplainAccessPath::Union(_) => Self::Union,
588 ExplainAccessPath::Intersection(_) => Self::Intersection,
589 }
590 }
591
592 const fn code(self) -> &'static str {
593 match self {
594 Self::ByKey => "ByKey",
595 Self::ByKeys => "ByKeys",
596 Self::KeyRange => "KeyRange",
597 Self::IndexPrefix => "IndexPrefix",
598 Self::IndexMultiLookup => "IndexMultiLookup",
599 Self::IndexBranchSet => "IndexBranchSet",
600 Self::IndexRange => "IndexRange",
601 Self::FullScan => "FullScan",
602 Self::Union => "Union",
603 Self::Intersection => "Intersection",
604 }
605 }
606}
607
608#[derive(Clone, Debug, Eq, PartialEq)]
610pub struct ExplainAccessCandidateV1 {
611 pub label: String,
613 pub exact: bool,
615 pub filtered: bool,
617 pub range_bound_count: usize,
619 pub order_compatible: bool,
621 pub residual_burden: &'static str,
623 pub residual_predicate_terms: usize,
625}
626
627impl ExplainAccessCandidateV1 {
628 fn from_candidate(candidate: &AccessChoiceCandidateExplainSummary) -> Self {
629 Self {
630 label: candidate.label.clone(),
631 exact: candidate.exact,
632 filtered: candidate.filtered,
633 range_bound_count: candidate.range_bound_count,
634 order_compatible: candidate.order_compatible,
635 residual_burden: candidate.residual_burden.label(),
636 residual_predicate_terms: candidate.residual_predicate_terms,
637 }
638 }
639}
640
641#[derive(Clone, Debug, Eq, PartialEq)]
643pub struct ExplainEligibleAlternativeV1 {
644 pub index_name: String,
646}
647
648#[derive(Clone, Debug, Eq, PartialEq)]
650pub struct ExplainRejectedIndexV1 {
651 pub index_name: Option<String>,
653 pub reason: Option<String>,
655 pub label: String,
657}
658
659impl ExplainRejectedIndexV1 {
660 fn from_rejection(rejection: &str) -> Self {
661 let (index_name, reason) = parse_rejected_index_label(rejection);
662
663 Self {
664 index_name,
665 reason,
666 label: rejection.to_string(),
667 }
668 }
669}
670
671#[derive(Clone, Debug, Eq, PartialEq)]
673pub struct ExplainResidualSummaryV1 {
674 pub burden_class: &'static str,
676 pub has_residual_filter: bool,
678 pub has_residual_predicate: bool,
680 pub access_bound_predicate_count: usize,
682 pub residual_predicate_count: usize,
684 pub predicate_terms: usize,
686}
687
688impl ExplainResidualSummaryV1 {
689 fn from_selected_access_and_candidate(
690 selected_access: &ExplainAccessPath,
691 selected_candidate: Option<&AccessChoiceCandidateExplainSummary>,
692 ) -> Self {
693 match selected_candidate {
694 Some(candidate) => Self {
695 burden_class: candidate.residual_burden.label(),
696 has_residual_filter: matches!(
697 candidate.residual_burden,
698 AccessChoiceResidualBurden::ScalarExpression
699 ),
700 has_residual_predicate: candidate.residual_predicate_terms > 0,
701 access_bound_predicate_count: access_bound_predicate_count(selected_access),
702 residual_predicate_count: candidate.residual_predicate_terms,
703 predicate_terms: candidate.residual_predicate_terms,
704 },
705 None => Self {
706 burden_class: AccessChoiceResidualBurden::None.label(),
707 has_residual_filter: false,
708 has_residual_predicate: false,
709 access_bound_predicate_count: access_bound_predicate_count(selected_access),
710 residual_predicate_count: 0,
711 predicate_terms: 0,
712 },
713 }
714 }
715}
716
717#[derive(Clone, Debug, Eq, PartialEq)]
725pub enum ExplainPredicate {
726 None,
727 True,
728 False,
729 And(Vec<Self>),
730 Or(Vec<Self>),
731 Not(Box<Self>),
732 Compare {
733 field: String,
734 op: CompareOp,
735 value: Value,
736 coercion: CoercionSpec,
737 },
738 CompareFields {
739 left_field: String,
740 op: CompareOp,
741 right_field: String,
742 coercion: CoercionSpec,
743 },
744 IsNull {
745 field: String,
746 },
747 IsNotNull {
748 field: String,
749 },
750 IsMissing {
751 field: String,
752 },
753 IsEmpty {
754 field: String,
755 },
756 IsNotEmpty {
757 field: String,
758 },
759 TextContains {
760 field: String,
761 value: Value,
762 },
763 TextContainsCi {
764 field: String,
765 value: Value,
766 },
767}
768
769#[derive(Clone, Debug, Eq, PartialEq)]
776pub enum ExplainOrderBy {
777 None,
778 Fields(Vec<ExplainOrder>),
779}
780
781#[derive(Clone, Debug, Eq, PartialEq)]
788pub struct ExplainOrder {
789 pub(in crate::db) field: String,
790 pub(in crate::db) direction: OrderDirection,
791}
792
793impl ExplainOrder {
794 #[must_use]
796 pub const fn field(&self) -> &str {
797 self.field.as_str()
798 }
799
800 #[must_use]
802 pub const fn direction(&self) -> OrderDirection {
803 self.direction
804 }
805}
806
807#[derive(Clone, Debug, Eq, PartialEq)]
814pub enum ExplainPagination {
815 None,
816 Page { limit: Option<u32>, offset: u32 },
817}
818
819#[derive(Clone, Debug, Eq, PartialEq)]
826pub enum ExplainDeleteLimit {
827 None,
828 Limit { max_rows: u32 },
829 Window { limit: Option<u32>, offset: u32 },
830}
831
832impl AccessPlannedQuery {
833 #[must_use]
835 pub(in crate::db) fn explain(&self) -> ExplainPlan {
836 self.explain_inner()
837 }
838
839 fn explain_inner(&self) -> ExplainPlan {
840 let (logical, grouping) = match &self.logical {
842 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
843 LogicalPlan::Grouped(logical) => {
844 let grouped_strategy = grouped_plan_strategy(self).unwrap_or_else(|| {
845 debug_assert!(
846 grouped_plan_strategy(self).is_some(),
847 "grouped logical explain projection requires planner-owned grouped strategy",
848 );
849 GroupedPlanStrategy::hash_group_with_aggregate_family(
850 GroupedPlanFallbackReason::GroupKeyOrderUnavailable,
851 GroupedPlanAggregateFamily::from_grouped_aggregates(
852 logical.group.aggregates.as_slice(),
853 ),
854 )
855 });
856
857 (
858 &logical.scalar,
859 ExplainGrouping::Grouped {
860 strategy: grouped_strategy.code(),
861 fallback_reason: grouped_strategy
862 .fallback_reason()
863 .map(GroupedPlanFallbackReason::code),
864 group_fields: logical
865 .group
866 .group_fields
867 .iter()
868 .map(|field_slot| ExplainGroupField {
869 slot_index: field_slot.index(),
870 field: field_slot.field().to_string(),
871 })
872 .collect(),
873 aggregates: logical
874 .group
875 .aggregates
876 .iter()
877 .map(|aggregate| ExplainGroupAggregate {
878 kind: aggregate.kind,
879 target_field: aggregate.target_field().map(str::to_string),
880 input_expr: aggregate
881 .input_expr()
882 .map(render_scalar_projection_expr_plan_label),
883 filter_expr: aggregate
884 .filter_expr()
885 .map(render_scalar_projection_expr_plan_label),
886 distinct: aggregate.distinct,
887 })
888 .collect(),
889 having: explain_group_having(logical),
890 max_groups: logical.group.execution.max_groups(),
891 max_group_bytes: logical.group.execution.max_group_bytes(),
892 },
893 )
894 }
895 };
896
897 explain_scalar_inner(logical, grouping, &self.access, self.access_choice())
899 }
900}
901
902fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
903 let expr = logical.effective_having_expr()?;
904
905 Some(ExplainGroupHaving {
906 expr: expr.into_owned(),
907 })
908}
909
910fn explain_scalar_inner<K>(
911 logical: &ScalarPlan,
912 grouping: ExplainGrouping,
913 access: &AccessPlan<K>,
914 access_choice: &AccessChoiceExplainSnapshot,
915) -> ExplainPlan
916where
917 K: KeyValueCodec,
918{
919 let filter_expr = logical
921 .filter_expr
922 .as_ref()
923 .map(render_scalar_filter_expr_plan_label);
924 let filter_expr_model = logical.filter_expr.clone();
925 let predicate_model = logical.predicate.clone();
926 let predicate = match &predicate_model {
927 Some(predicate) => ExplainPredicate::from_predicate(predicate),
928 None => ExplainPredicate::None,
929 };
930
931 let order_by = explain_order(logical.order.as_ref());
933 let order_pushdown = explain_order_pushdown();
934 let page = explain_page(logical.page.as_ref());
935 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
936
937 let access = explain_access_plan(access);
939 let access_decision = ExplainAccessDecisionV1::from_snapshot(&access, access_choice);
940
941 ExplainPlan {
942 mode: logical.mode,
943 access,
944 access_decision,
945 filter_expr,
946 filter_expr_model,
947 predicate,
948 predicate_model,
949 order_by,
950 distinct: logical.distinct,
951 grouping,
952 order_pushdown,
953 page,
954 delete_limit,
955 consistency: logical.consistency,
956 }
957}
958
959fn selected_candidate_summary<'a>(
960 selected_label: &str,
961 candidates: &'a [AccessChoiceCandidateExplainSummary],
962) -> Option<&'a AccessChoiceCandidateExplainSummary> {
963 candidates
964 .iter()
965 .find(|candidate| candidate.label == selected_label)
966 .or_else(|| (candidates.len() == 1).then(|| &candidates[0]))
967}
968
969const fn selected_index_name(access: &ExplainAccessPath) -> Option<&str> {
970 match access {
971 ExplainAccessPath::IndexPrefix { name, .. }
972 | ExplainAccessPath::IndexMultiLookup { name, .. }
973 | ExplainAccessPath::IndexBranchSet { name, .. }
974 | ExplainAccessPath::IndexRange { name, .. } => Some(name.as_str()),
975 ExplainAccessPath::ByKey { .. }
976 | ExplainAccessPath::ByKeys { .. }
977 | ExplainAccessPath::KeyRange { .. }
978 | ExplainAccessPath::FullScan
979 | ExplainAccessPath::Union(_)
980 | ExplainAccessPath::Intersection(_) => None,
981 }
982}
983
984fn access_bound_predicate_count(access: &ExplainAccessPath) -> usize {
985 match access {
986 ExplainAccessPath::ByKey { .. }
987 | ExplainAccessPath::ByKeys { .. }
988 | ExplainAccessPath::IndexMultiLookup { .. } => 1,
989 ExplainAccessPath::IndexBranchSet {
990 fixed_values,
991 branch_values,
992 ..
993 } => fixed_values.len() + usize::from(!branch_values.is_empty()),
994 ExplainAccessPath::KeyRange { .. } => 2,
995 ExplainAccessPath::IndexPrefix { prefix_len, .. } => *prefix_len,
996 ExplainAccessPath::IndexRange {
997 prefix_len,
998 lower,
999 upper,
1000 ..
1001 } => *prefix_len + bound_constraint_count(lower) + bound_constraint_count(upper),
1002 ExplainAccessPath::FullScan => 0,
1003 ExplainAccessPath::Union(children) | ExplainAccessPath::Intersection(children) => {
1004 children.iter().map(access_bound_predicate_count).sum()
1005 }
1006 }
1007}
1008
1009const fn bound_constraint_count(bound: &Bound<Value>) -> usize {
1010 match bound {
1011 Bound::Included(_) | Bound::Excluded(_) => 1,
1012 Bound::Unbounded => 0,
1013 }
1014}
1015
1016fn parse_rejected_index_label(rejection: &str) -> (Option<String>, Option<String>) {
1017 let Some(rest) = rejection.strip_prefix("index:") else {
1018 return (None, None);
1019 };
1020
1021 match rest.split_once('=') {
1022 Some((index_name, reason)) => (Some(index_name.to_string()), Some(reason.to_string())),
1023 None => (Some(rest.to_string()), None),
1024 }
1025}
1026
1027const fn explain_order_pushdown() -> ExplainOrderPushdown {
1028 ExplainOrderPushdown::MissingModelContext
1030}
1031
1032impl ExplainPredicate {
1033 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
1034 match predicate {
1035 Predicate::True => Self::True,
1036 Predicate::False => Self::False,
1037 Predicate::And(children) => {
1038 Self::And(children.iter().map(Self::from_predicate).collect())
1039 }
1040 Predicate::Or(children) => {
1041 Self::Or(children.iter().map(Self::from_predicate).collect())
1042 }
1043 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
1044 Predicate::Compare(compare) => Self::from_compare(compare),
1045 Predicate::CompareFields(compare) => Self::CompareFields {
1046 left_field: compare.left_field().to_string(),
1047 op: compare.op(),
1048 right_field: compare.right_field().to_string(),
1049 coercion: compare.coercion().clone(),
1050 },
1051 Predicate::IsNull { field } => Self::IsNull {
1052 field: field.clone(),
1053 },
1054 Predicate::IsNotNull { field } => Self::IsNotNull {
1055 field: field.clone(),
1056 },
1057 Predicate::IsMissing { field } => Self::IsMissing {
1058 field: field.clone(),
1059 },
1060 Predicate::IsEmpty { field } => Self::IsEmpty {
1061 field: field.clone(),
1062 },
1063 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
1064 field: field.clone(),
1065 },
1066 Predicate::TextContains { field, value } => Self::TextContains {
1067 field: field.clone(),
1068 value: value.clone(),
1069 },
1070 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
1071 field: field.clone(),
1072 value: value.clone(),
1073 },
1074 }
1075 }
1076
1077 fn from_compare(compare: &ComparePredicate) -> Self {
1078 Self::Compare {
1079 field: compare.field.clone(),
1080 op: compare.op,
1081 value: compare.value.clone(),
1082 coercion: compare.coercion.clone(),
1083 }
1084 }
1085}
1086
1087fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
1088 let Some(order) = order else {
1089 return ExplainOrderBy::None;
1090 };
1091
1092 if order.fields.is_empty() {
1093 return ExplainOrderBy::None;
1094 }
1095
1096 ExplainOrderBy::Fields(
1097 order
1098 .fields
1099 .iter()
1100 .map(|term| ExplainOrder {
1101 field: term.rendered_label(),
1102 direction: term.direction(),
1103 })
1104 .collect(),
1105 )
1106}
1107
1108const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1109 match page {
1110 Some(page) => ExplainPagination::Page {
1111 limit: page.limit,
1112 offset: page.offset,
1113 },
1114 None => ExplainPagination::None,
1115 }
1116}
1117
1118const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1119 match limit {
1120 Some(limit) if limit.offset == 0 => match limit.limit {
1121 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
1122 None => ExplainDeleteLimit::Window {
1123 limit: None,
1124 offset: 0,
1125 },
1126 },
1127 Some(limit) => ExplainDeleteLimit::Window {
1128 limit: limit.limit,
1129 offset: limit.offset,
1130 },
1131 None => ExplainDeleteLimit::None,
1132 }
1133}
1134
1135fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
1136 let mut object = JsonWriter::begin_object(out);
1137 object.field_with("mode", |out| {
1138 let mut object = JsonWriter::begin_object(out);
1139 match explain.mode() {
1140 QueryMode::Load(spec) => {
1141 object.field_str("type", "Load");
1142 match spec.limit() {
1143 Some(limit) => object.field_u64("limit", u64::from(limit)),
1144 None => object.field_null("limit"),
1145 }
1146 object.field_u64("offset", u64::from(spec.offset()));
1147 }
1148 QueryMode::Delete(spec) => {
1149 object.field_str("type", "Delete");
1150 match spec.limit() {
1151 Some(limit) => object.field_u64("limit", u64::from(limit)),
1152 None => object.field_null("limit"),
1153 }
1154 }
1155 }
1156 object.finish();
1157 });
1158 object.field_with("access", |out| {
1159 write_access_json_detailed(explain.access(), out);
1160 });
1161 object.field_with("access_decision", |out| {
1162 write_access_decision_json(explain.access_decision(), out);
1163 });
1164 match explain.filter_expr() {
1165 Some(filter_expr) => object.field_str("filter_expr", filter_expr),
1166 None => object.field_null("filter_expr"),
1167 }
1168 object.field_value_debug("predicate", explain.predicate());
1169 object.field_value_debug("order_by", explain.order_by());
1170 object.field_bool("distinct", explain.distinct());
1171 object.field_value_debug("grouping", explain.grouping());
1172 object.field_value_debug("order_pushdown", explain.order_pushdown());
1173 object.field_with("page", |out| {
1174 let mut object = JsonWriter::begin_object(out);
1175 match explain.page() {
1176 ExplainPagination::None => {
1177 object.field_str("type", "None");
1178 }
1179 ExplainPagination::Page { limit, offset } => {
1180 object.field_str("type", "Page");
1181 match limit {
1182 Some(limit) => object.field_u64("limit", u64::from(*limit)),
1183 None => object.field_null("limit"),
1184 }
1185 object.field_u64("offset", u64::from(*offset));
1186 }
1187 }
1188 object.finish();
1189 });
1190 object.field_with("delete_limit", |out| {
1191 let mut object = JsonWriter::begin_object(out);
1192 match explain.delete_limit() {
1193 ExplainDeleteLimit::None => {
1194 object.field_str("type", "None");
1195 }
1196 ExplainDeleteLimit::Limit { max_rows } => {
1197 object.field_str("type", "Limit");
1198 object.field_u64("max_rows", u64::from(*max_rows));
1199 }
1200 ExplainDeleteLimit::Window { limit, offset } => {
1201 object.field_str("type", "Window");
1202 object.field_with("limit", |out| match limit {
1203 Some(limit) => out.push_str(&limit.to_string()),
1204 None => out.push_str("null"),
1205 });
1206 object.field_u64("offset", u64::from(*offset));
1207 }
1208 }
1209 object.finish();
1210 });
1211 object.field_value_debug("consistency", &explain.consistency());
1212 object.finish();
1213}
1214
1215fn write_access_decision_json(decision: &ExplainAccessDecisionV1, out: &mut String) {
1216 let mut object = JsonWriter::begin_object(out);
1217 object.field_u64("schema_version", u64::from(decision.schema_version));
1218 object.field_with("selected", |out| {
1219 let mut selected = JsonWriter::begin_object(out);
1220 selected.field_str("kind", decision.selected.kind.code());
1221 match decision.selected.index_name.as_deref() {
1222 Some(index_name) => selected.field_str("index_name", index_name),
1223 None => selected.field_null("index_name"),
1224 }
1225 selected.field_str("label", decision.selected.label.as_str());
1226 selected.field_str("reason", decision.selected.reason);
1227 selected.finish();
1228 });
1229 object.field_with("candidates", |out| {
1230 out.push('[');
1231 for (index, candidate) in decision.candidates.iter().enumerate() {
1232 if index > 0 {
1233 out.push(',');
1234 }
1235 write_access_candidate_json(candidate, out);
1236 }
1237 out.push(']');
1238 });
1239 object.field_with("alternatives", |out| {
1240 out.push('[');
1241 for (index, alternative) in decision.alternatives.iter().enumerate() {
1242 if index > 0 {
1243 out.push(',');
1244 }
1245 let mut object = JsonWriter::begin_object(out);
1246 object.field_str("index_name", alternative.index_name.as_str());
1247 object.finish();
1248 }
1249 out.push(']');
1250 });
1251 object.field_with("rejections", |out| {
1252 out.push('[');
1253 for (index, rejection) in decision.rejections.iter().enumerate() {
1254 if index > 0 {
1255 out.push(',');
1256 }
1257 let mut object = JsonWriter::begin_object(out);
1258 match rejection.index_name.as_deref() {
1259 Some(index_name) => object.field_str("index_name", index_name),
1260 None => object.field_null("index_name"),
1261 }
1262 match rejection.reason.as_deref() {
1263 Some(reason) => object.field_str("reason", reason),
1264 None => object.field_null("reason"),
1265 }
1266 object.field_str("label", rejection.label.as_str());
1267 object.finish();
1268 }
1269 out.push(']');
1270 });
1271 object.field_with("residual", |out| {
1272 let mut residual = JsonWriter::begin_object(out);
1273 residual.field_str("burden_class", decision.residual.burden_class);
1274 residual.field_bool("has_residual_filter", decision.residual.has_residual_filter);
1275 residual.field_bool(
1276 "has_residual_predicate",
1277 decision.residual.has_residual_predicate,
1278 );
1279 residual.field_u64(
1280 "access_bound_predicate_count",
1281 decision.residual.access_bound_predicate_count as u64,
1282 );
1283 residual.field_u64(
1284 "residual_predicate_count",
1285 decision.residual.residual_predicate_count as u64,
1286 );
1287 residual.field_u64("predicate_terms", decision.residual.predicate_terms as u64);
1288 residual.finish();
1289 });
1290 object.finish();
1291}
1292
1293fn write_access_candidate_json(candidate: &ExplainAccessCandidateV1, out: &mut String) {
1294 let mut object = JsonWriter::begin_object(out);
1295 object.field_str("label", candidate.label.as_str());
1296 object.field_bool("exact", candidate.exact);
1297 object.field_bool("filtered", candidate.filtered);
1298 object.field_u64("range_bound_count", candidate.range_bound_count as u64);
1299 object.field_bool("order_compatible", candidate.order_compatible);
1300 object.field_str("residual_burden", candidate.residual_burden);
1301 object.field_u64(
1302 "residual_predicate_terms",
1303 candidate.residual_predicate_terms as u64,
1304 );
1305 object.finish();
1306}