1mod predicate;
8
9#[cfg(test)]
10mod tests;
11
12use crate::{
13 db::QueryError,
14 db::{
15 access::AccessPlan,
16 predicate::{CoercionSpec, CompareOp, MissingRowPolicy},
17 query::{
18 builder::scalar_projection::write_scalar_projection_expr_plan_label,
19 explain::{
20 access_projection::write_access_json,
21 explain_access_plan,
22 writer::{JsonWriter, render_logical},
23 },
24 plan::{
25 AccessChoiceCandidateExplainSummary, AccessChoiceExplainSnapshot,
26 AccessChoiceRejectedIndex, AccessChoiceResidualBurden, AccessChoiceSelectedReason,
27 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupedPlanFallbackReason,
28 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
29 expr::{Expr, PathSpec},
30 grouped_plan_strategy_for_explain, write_explain_access_strategy_label,
31 },
32 preparation::PreparationWork,
33 },
34 },
35 value::Value,
36};
37use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
38use std::{fmt, ops::Bound};
39
40#[derive(Clone, Eq, PartialEq)]
47pub struct ExplainPlan {
48 mode: QueryMode,
49 access: ExplainAccessPath,
50 access_decision: ExplainAccessDecision,
51 filter_expr: Option<String>,
52 predicate: ExplainPredicate,
53 order_by: ExplainOrderBy,
54 distinct: bool,
55 grouping: ExplainGrouping,
56 order_pushdown: ExplainOrderPushdown,
57 page: ExplainPagination,
58 delete_limit: ExplainDeleteLimit,
59 consistency: MissingRowPolicy,
60}
61
62#[expect(clippy::missing_fields_in_debug)]
63impl fmt::Debug for ExplainPlan {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.debug_struct("ExplainPlan")
66 .field("mode", &self.mode)
67 .field("access", &self.access)
68 .field("filter_expr", &self.filter_expr)
69 .field("predicate", &self.predicate)
70 .field("order_by", &self.order_by)
71 .field("distinct", &self.distinct)
72 .field("grouping", &self.grouping)
73 .field("order_pushdown", &self.order_pushdown)
74 .field("page", &self.page)
75 .field("delete_limit", &self.delete_limit)
76 .field("consistency", &self.consistency)
77 .finish()
78 }
79}
80
81impl ExplainPlan {
82 #[must_use]
84 pub const fn mode(&self) -> QueryMode {
85 self.mode
86 }
87
88 #[must_use]
90 pub const fn access(&self) -> &ExplainAccessPath {
91 &self.access
92 }
93
94 #[must_use]
96 pub const fn access_decision(&self) -> &ExplainAccessDecision {
97 &self.access_decision
98 }
99
100 #[must_use]
102 pub fn filter_expr(&self) -> Option<&str> {
103 self.filter_expr.as_deref()
104 }
105
106 #[must_use]
108 pub const fn predicate(&self) -> &ExplainPredicate {
109 &self.predicate
110 }
111
112 #[must_use]
114 pub const fn order_by(&self) -> &ExplainOrderBy {
115 &self.order_by
116 }
117
118 #[must_use]
120 pub const fn distinct(&self) -> bool {
121 self.distinct
122 }
123
124 #[must_use]
126 pub const fn grouping(&self) -> &ExplainGrouping {
127 &self.grouping
128 }
129
130 #[must_use]
132 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
133 &self.order_pushdown
134 }
135
136 #[must_use]
138 pub const fn page(&self) -> &ExplainPagination {
139 &self.page
140 }
141
142 #[must_use]
144 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
145 &self.delete_limit
146 }
147
148 #[must_use]
150 pub const fn consistency(&self) -> MissingRowPolicy {
151 self.consistency
152 }
153}
154
155impl ExplainPlan {
156 pub fn render_text_canonical(&self) -> Result<String, QueryError> {
164 render_logical(|out| {
165 write!(out, "mode={:?}\naccess=", self.mode())?;
166 write_access_json(self.access(), out)?;
167 out.write_str("\naccess_decision=")?;
168 write_access_decision_json(self.access_decision(), out)?;
169 write!(
170 out,
171 "\nfilter_expr={:?}\nhas_predicate={}\norder_by={:?}\ndistinct={}\ngrouping=",
172 self.filter_expr(),
173 !matches!(self.predicate(), ExplainPredicate::None),
174 self.order_by(),
175 self.distinct(),
176 )?;
177 write_grouping_json(self.grouping(), out)?;
178 write!(
179 out,
180 "\norder_pushdown={:?}\npage={:?}\ndelete_limit={:?}\nconsistency={:?}",
181 self.order_pushdown(),
182 self.page(),
183 self.delete_limit(),
184 self.consistency(),
185 )
186 })
187 }
188
189 pub fn render_json_canonical(&self) -> Result<String, QueryError> {
197 render_logical(|out| write_logical_explain_json(self, out))
198 }
199}
200
201#[derive(Clone, Debug, Eq, PartialEq)]
208pub enum ExplainGrouping {
209 None,
210 Grouped {
211 strategy: &'static str,
212 fallback_reason: Option<&'static str>,
213 group_fields: Vec<ExplainGroupField>,
214 aggregates: Vec<ExplainGroupAggregate>,
215 having: Option<ExplainGroupHaving>,
216 max_groups: u64,
217 max_group_bytes: u64,
218 },
219}
220
221#[derive(Clone, Debug, Eq, PartialEq)]
228pub struct ExplainGroupField {
229 pub(in crate::db) slot_index: usize,
230 pub(in crate::db) field: String,
231 pub(in crate::db) path: Option<PathSpec>,
232}
233
234impl ExplainGroupField {
235 #[must_use]
237 pub const fn slot_index(&self) -> usize {
238 self.slot_index
239 }
240
241 #[must_use]
243 pub const fn field(&self) -> &str {
244 self.field.as_str()
245 }
246}
247
248#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct ExplainGroupAggregate {
256 pub(in crate::db) kind: AggregateKind,
257 pub(in crate::db) target_field: Option<String>,
258 pub(in crate::db) input_expr: Option<String>,
259 pub(in crate::db) filter_expr: Option<String>,
260 pub(in crate::db) distinct: bool,
261}
262
263impl ExplainGroupAggregate {
264 #[must_use]
266 pub const fn kind(&self) -> AggregateKind {
267 self.kind
268 }
269
270 #[must_use]
272 pub fn target_field(&self) -> Option<&str> {
273 self.target_field.as_deref()
274 }
275
276 #[must_use]
278 pub fn input_expr(&self) -> Option<&str> {
279 self.input_expr.as_deref()
280 }
281
282 #[must_use]
284 pub fn filter_expr(&self) -> Option<&str> {
285 self.filter_expr.as_deref()
286 }
287
288 #[must_use]
290 pub const fn distinct(&self) -> bool {
291 self.distinct
292 }
293}
294
295#[derive(Clone, Debug, Eq, PartialEq)]
304pub struct ExplainGroupHaving {
305 pub(in crate::db) expr: Expr,
306}
307
308#[derive(Clone, Debug, Eq, PartialEq)]
315pub enum ExplainOrderPushdown {
316 MissingModelContext,
317 EligibleSecondaryIndex { index: String, prefix_len: usize },
318 Rejected(SecondaryOrderPushdownRejection),
319}
320
321#[derive(Clone, Debug, Eq, PartialEq)]
329pub enum SecondaryOrderPushdownRejection {
330 NoOrderBy,
331 AccessPathNotSingleIndexPrefix,
332 AccessPathIndexRangeUnsupported {
333 index: String,
334 prefix_len: usize,
335 },
336 InvalidIndexPrefixBounds {
337 prefix_len: usize,
338 index_field_len: usize,
339 },
340 MissingPrimaryKeyTieBreak {
341 field: String,
342 },
343 PrimaryKeyDirectionNotAscending {
344 field: String,
345 },
346 MixedDirectionNotEligible {
347 field: String,
348 },
349 OrderFieldsDoNotMatchIndex {
350 index: String,
351 prefix_len: usize,
352 expected_suffix: Vec<String>,
353 expected_full: Vec<String>,
354 actual: Vec<String>,
355 },
356 VariablePrefixSuffixOrderUnsupported {
357 index: String,
358 prefix_len: usize,
359 expected_full: Vec<String>,
360 actual: Vec<String>,
361 },
362}
363
364#[derive(Clone, Debug, Eq, PartialEq)]
372pub enum ExplainAccessPath {
373 ByKey {
374 key: Value,
375 },
376 ByKeys {
377 keys: Vec<Value>,
378 },
379 KeyRange {
380 start: Option<Value>,
381 end: Option<Value>,
382 },
383 IndexPrefix {
384 name: String,
385 fields: Vec<String>,
386 prefix_len: usize,
387 values: Vec<Value>,
388 },
389 IndexMultiLookup {
390 name: String,
391 fields: Vec<String>,
392 values: Vec<Value>,
393 },
394 IndexBranchSet {
395 name: String,
396 fields: Vec<String>,
397 fixed_values: Vec<Value>,
398 branch_values: Vec<Value>,
399 branch_field: Option<String>,
400 },
401 IndexRange {
402 name: String,
403 fields: Vec<String>,
404 prefix_len: usize,
405 prefix: Vec<Value>,
406 lower: Bound<Value>,
407 upper: Bound<Value>,
408 },
409 FullScan,
410 Union(Vec<Self>),
411 Intersection(Vec<Self>),
412}
413
414#[derive(Clone, Debug, Eq, PartialEq)]
420pub struct ExplainAccessDecision {
421 pub selected: ExplainSelectedAccess,
423 pub candidates: Vec<ExplainAccessCandidate>,
425 pub alternatives: Vec<ExplainEligibleAlternative>,
427 pub rejections: Vec<ExplainRejectedIndex>,
429 pub residual: ExplainResidualSummary,
431 pub cardinality_evidence_state: &'static str,
433}
434
435impl ExplainAccessDecision {
436 fn from_snapshot(
437 selected_access: &ExplainAccessPath,
438 snapshot: &AccessChoiceExplainSnapshot,
439 work: &PreparationWork<'_>,
440 ) -> Result<Self, QueryError> {
441 work.charge(Resource::PredicateExpressionSteps, 1)?;
442 let selected_label =
443 work.render_text(|out| write_explain_access_strategy_label(selected_access, out))?;
444 let index_name = selected_index_name(selected_access)
445 .map(|name| work.copy_text(name))
446 .transpose()?;
447 let mut selected_candidate = None;
450 let mut candidates = work.vec_with_capacity(snapshot.candidates.len())?;
451 for candidate in &snapshot.candidates {
452 let copied = ExplainAccessCandidate::from_candidate(candidate, work)?;
453 if selected_candidate.is_none()
454 && let Some(name) = index_name.as_deref()
455 && name.len() == candidate.index_name().len()
456 {
457 work.charge(Resource::PredicateExpressionSteps, name.len() as u64)?;
458 if name == candidate.index_name() {
459 selected_candidate = Some(candidate);
460 }
461 }
462 candidates.push(copied);
463 }
464
465 Ok(Self {
466 selected: ExplainSelectedAccess {
467 kind: ExplainAccessDecisionKind::from_access_path(selected_access),
468 index_name,
469 label: selected_label,
470 reason: snapshot.chosen_reason().code(),
471 },
472 candidates,
473 alternatives: work.copy_slice(&snapshot.alternatives, |name| {
474 Ok(ExplainEligibleAlternative {
475 index_name: work.copy_text(name)?,
476 })
477 })?,
478 rejections: work.copy_slice(&snapshot.rejected, |rejection| {
479 ExplainRejectedIndex::from_rejection(rejection, work)
480 })?,
481 residual: ExplainResidualSummary::from_selected_access_and_candidate(
482 access_bound_predicate_count(selected_access, work)?,
483 selected_candidate,
484 snapshot.chosen_reason(),
485 ),
486 cardinality_evidence_state: snapshot.cardinality_evidence_state,
487 })
488 }
489}
490
491impl fmt::Display for ExplainAccessDecision {
492 fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
493 let index = self
494 .selected
495 .index_name
496 .as_deref()
497 .map_or("none", |index| index);
498
499 write!(
500 out,
501 "kind={} index={} reason={} residual={} cardinality_evidence={} candidates={} alternatives={} rejections={}",
502 self.selected.kind.code(),
503 index,
504 self.selected.reason,
505 self.residual.burden_class,
506 self.cardinality_evidence_state,
507 self.candidates.len(),
508 self.alternatives.len(),
509 self.rejections.len(),
510 )
511 }
512}
513
514#[derive(Clone, Debug, Eq, PartialEq)]
516pub struct ExplainSelectedAccess {
517 pub kind: ExplainAccessDecisionKind,
519 pub index_name: Option<String>,
521 pub label: String,
523 pub reason: &'static str,
525}
526
527#[derive(Clone, Copy, Debug, Eq, PartialEq)]
529pub enum ExplainAccessDecisionKind {
530 ByKey,
532 ByKeys,
534 KeyRange,
536 IndexPrefix,
538 IndexMultiLookup,
540 IndexBranchSet,
542 IndexRange,
544 FullScan,
546 Union,
548 Intersection,
550}
551
552impl ExplainAccessDecisionKind {
553 const fn from_access_path(access: &ExplainAccessPath) -> Self {
554 match access {
555 ExplainAccessPath::ByKey { .. } => Self::ByKey,
556 ExplainAccessPath::ByKeys { .. } => Self::ByKeys,
557 ExplainAccessPath::KeyRange { .. } => Self::KeyRange,
558 ExplainAccessPath::IndexPrefix { .. } => Self::IndexPrefix,
559 ExplainAccessPath::IndexMultiLookup { .. } => Self::IndexMultiLookup,
560 ExplainAccessPath::IndexBranchSet { .. } => Self::IndexBranchSet,
561 ExplainAccessPath::IndexRange { .. } => Self::IndexRange,
562 ExplainAccessPath::FullScan => Self::FullScan,
563 ExplainAccessPath::Union(_) => Self::Union,
564 ExplainAccessPath::Intersection(_) => Self::Intersection,
565 }
566 }
567
568 const fn code(self) -> &'static str {
569 match self {
570 Self::ByKey => "ByKey",
571 Self::ByKeys => "ByKeys",
572 Self::KeyRange => "KeyRange",
573 Self::IndexPrefix => "IndexPrefix",
574 Self::IndexMultiLookup => "IndexMultiLookup",
575 Self::IndexBranchSet => "IndexBranchSet",
576 Self::IndexRange => "IndexRange",
577 Self::FullScan => "FullScan",
578 Self::Union => "Union",
579 Self::Intersection => "Intersection",
580 }
581 }
582}
583
584#[derive(Clone, Debug, Eq, PartialEq)]
586pub struct ExplainAccessCandidate {
587 pub label: String,
589 pub exact: bool,
591 pub filtered: bool,
593 pub range_bound_count: usize,
595 pub order_compatible: bool,
597 pub residual_burden: &'static str,
599 pub residual_predicate_terms: usize,
601 pub exact_prefix_entries: Option<u64>,
603}
604
605impl ExplainAccessCandidate {
606 fn from_candidate(
607 candidate: &AccessChoiceCandidateExplainSummary,
608 work: &PreparationWork<'_>,
609 ) -> Result<Self, QueryError> {
610 work.charge(Resource::PredicateExpressionSteps, 1)?;
611 Ok(Self {
612 label: work.render_text(|out| write!(out, "{candidate}"))?,
613 exact: candidate.exact,
614 filtered: candidate.filtered,
615 range_bound_count: candidate.range_bound_count,
616 order_compatible: candidate.order_compatible,
617 residual_burden: candidate.residual_burden.label(),
618 residual_predicate_terms: candidate.residual_predicate_terms,
619 exact_prefix_entries: candidate.exact_prefix_entries,
620 })
621 }
622}
623
624#[derive(Clone, Debug, Eq, PartialEq)]
626pub struct ExplainEligibleAlternative {
627 pub index_name: String,
629}
630
631#[derive(Clone, Debug, Eq, PartialEq)]
633pub struct ExplainRejectedIndex {
634 pub index_name: Option<String>,
636 pub reason: Option<String>,
638 pub label: String,
640}
641
642impl ExplainRejectedIndex {
643 fn from_rejection(
644 rejection: &AccessChoiceRejectedIndex,
645 work: &PreparationWork<'_>,
646 ) -> Result<Self, QueryError> {
647 work.charge(Resource::PredicateExpressionSteps, 1)?;
648 Ok(Self {
649 index_name: Some(work.copy_text(rejection.index_name())?),
650 reason: Some(work.copy_text(rejection.reason_code())?),
651 label: work.render_text(|out| write!(out, "{rejection}"))?,
652 })
653 }
654}
655
656#[derive(Clone, Debug, Eq, PartialEq)]
658pub struct ExplainResidualSummary {
659 pub burden_class: &'static str,
661 pub has_residual_filter: bool,
663 pub has_residual_predicate: bool,
665 pub access_bound_predicate_count: usize,
667 pub residual_predicate_count: usize,
669}
670
671impl ExplainResidualSummary {
672 const fn from_selected_access_and_candidate(
673 access_bound_predicate_count: usize,
674 selected_candidate: Option<&AccessChoiceCandidateExplainSummary>,
675 selected_reason: AccessChoiceSelectedReason,
676 ) -> Self {
677 if let Some(candidate) = selected_candidate {
678 Self {
679 burden_class: candidate.residual_burden.label(),
680 has_residual_filter: matches!(
681 candidate.residual_burden,
682 AccessChoiceResidualBurden::ScalarExpression
683 ),
684 has_residual_predicate: candidate.residual_predicate_terms > 0,
685 access_bound_predicate_count,
686 residual_predicate_count: candidate.residual_predicate_terms,
687 }
688 } else if matches!(
689 selected_reason,
690 AccessChoiceSelectedReason::PlannerExactIndexIntersection
691 ) {
692 Self {
693 burden_class: AccessChoiceResidualBurden::PredicateOnly.label(),
694 has_residual_filter: false,
695 has_residual_predicate: true,
696 access_bound_predicate_count,
697 residual_predicate_count: access_bound_predicate_count,
698 }
699 } else {
700 Self {
701 burden_class: AccessChoiceResidualBurden::None.label(),
702 has_residual_filter: false,
703 has_residual_predicate: false,
704 access_bound_predicate_count,
705 residual_predicate_count: 0,
706 }
707 }
708 }
709}
710
711#[derive(Clone, Debug, Eq, PartialEq)]
719pub enum ExplainPredicate {
720 None,
721 True,
722 False,
723 And(Vec<Self>),
724 Or(Vec<Self>),
725 Not(Box<Self>),
726 Compare {
727 field: String,
728 op: CompareOp,
729 value: Value,
730 coercion: CoercionSpec,
731 },
732 CompareFields {
733 left_field: String,
734 op: CompareOp,
735 right_field: String,
736 coercion: CoercionSpec,
737 },
738 IsNull {
739 field: String,
740 },
741 IsNotNull {
742 field: String,
743 },
744 IsMissing {
745 field: String,
746 },
747 IsEmpty {
748 field: String,
749 },
750 IsNotEmpty {
751 field: String,
752 },
753 TextContains {
754 field: String,
755 value: Value,
756 },
757 TextContainsCi {
758 field: String,
759 value: Value,
760 },
761}
762
763#[derive(Clone, Debug, Eq, PartialEq)]
770pub enum ExplainOrderBy {
771 None,
772 Fields(Vec<ExplainOrder>),
773}
774
775#[derive(Clone, Debug, Eq, PartialEq)]
782pub struct ExplainOrder {
783 pub(in crate::db) field: String,
784 pub(in crate::db) direction: OrderDirection,
785}
786
787impl ExplainOrder {
788 #[must_use]
790 pub const fn field(&self) -> &str {
791 self.field.as_str()
792 }
793
794 #[must_use]
796 pub const fn direction(&self) -> OrderDirection {
797 self.direction
798 }
799}
800
801#[derive(Clone, Debug, Eq, PartialEq)]
808pub enum ExplainPagination {
809 None,
810 Page { limit: Option<u32>, offset: u32 },
811}
812
813#[derive(Clone, Debug, Eq, PartialEq)]
820pub enum ExplainDeleteLimit {
821 None,
822 Limit { max_rows: u32 },
823 Window { limit: Option<u32>, offset: u32 },
824}
825
826impl AccessPlannedQuery {
827 pub(in crate::db) fn project_explain(
831 &self,
832 work: &PreparationWork<'_>,
833 ) -> Result<ExplainPlan, QueryError> {
834 work.charge(Resource::PredicateExpressionSteps, 1)?;
835
836 let (logical, grouping) = match &self.logical {
838 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
839 LogicalPlan::Grouped(logical) => {
840 let grouped_strategy = grouped_plan_strategy_for_explain(self, logical, work)?;
841
842 (
843 &logical.scalar,
844 ExplainGrouping::Grouped {
845 strategy: grouped_strategy.code(),
846 fallback_reason: grouped_strategy
847 .fallback_reason()
848 .map(GroupedPlanFallbackReason::code),
849 group_fields: {
850 let mut fields =
851 work.vec_with_capacity(logical.group.group_fields.len())?;
852 for group_field in logical.group.group_fields.iter() {
853 fields.push(ExplainGroupField {
854 slot_index: group_field.root_slot(),
855 field: work.copy_text(group_field.field())?,
856 path: group_field
857 .as_scalar_path()
858 .map(|path| {
859 let path = path.path();
860 Ok::<_, QueryError>(PathSpec::new(
861 work.copy_text(path.root().as_str())?,
862 work.copy_slice(path.segments(), |segment| {
863 work.copy_text(segment)
864 })?,
865 ))
866 })
867 .transpose()?,
868 });
869 }
870 fields
871 },
872 aggregates: work.copy_slice(&logical.group.aggregates, |aggregate| {
873 work.charge(Resource::PredicateExpressionSteps, 1)?;
874 Ok(ExplainGroupAggregate {
875 kind: aggregate.kind(),
876 target_field: aggregate
877 .target_field()
878 .map(|field| work.copy_text(field))
879 .transpose()?,
880 input_expr: aggregate
881 .input_expr()
882 .map(|expr| explain_expr_label(expr, work))
883 .transpose()?,
884 filter_expr: aggregate
885 .filter_expr()
886 .map(|expr| explain_expr_label(expr, work))
887 .transpose()?,
888 distinct: aggregate.raw_distinct(),
889 })
890 })?,
891 having: explain_group_having(logical, work)?,
892 max_groups: logical.group.execution.max_groups(),
893 max_group_bytes: logical.group.execution.max_group_bytes(),
894 },
895 )
896 }
897 };
898
899 explain_scalar_inner(logical, grouping, &self.access, self.access_choice(), work)
901 }
902}
903
904fn explain_group_having(
905 logical: &crate::db::query::plan::GroupPlan,
906 work: &PreparationWork<'_>,
907) -> Result<Option<ExplainGroupHaving>, QueryError> {
908 logical
909 .having_expr()
910 .map(|expr| {
911 Ok(ExplainGroupHaving {
912 expr: work.copy_expr(expr)?,
913 })
914 })
915 .transpose()
916}
917
918fn explain_expr_label(expr: &Expr, work: &PreparationWork<'_>) -> Result<String, QueryError> {
921 work.render_text(|out| write_scalar_projection_expr_plan_label(expr, out))
922}
923
924fn explain_scalar_inner(
925 logical: &ScalarPlan,
926 grouping: ExplainGrouping,
927 access: &AccessPlan<Value>,
928 access_choice: &AccessChoiceExplainSnapshot,
929 work: &PreparationWork<'_>,
930) -> Result<ExplainPlan, QueryError> {
931 let filter_expr = logical
933 .filter_expr
934 .as_ref()
935 .map(|expr| explain_expr_label(expr, work))
936 .transpose()?;
937 let predicate = match &logical.predicate {
938 Some(predicate) => ExplainPredicate::from_predicate(predicate, work)?,
939 None => ExplainPredicate::None,
940 };
941
942 let order_by = explain_order(logical.order.as_ref(), work)?;
944 let order_pushdown = explain_order_pushdown();
945 let page = explain_page(logical.page.as_ref());
946 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
947
948 let access = explain_access_plan(access, work)?;
950 let access_decision = ExplainAccessDecision::from_snapshot(&access, access_choice, work)?;
951
952 Ok(ExplainPlan {
953 mode: logical.mode,
954 access,
955 access_decision,
956 filter_expr,
957 predicate,
958 order_by,
959 distinct: logical.distinct,
960 grouping,
961 order_pushdown,
962 page,
963 delete_limit,
964 consistency: logical.consistency,
965 })
966}
967
968const fn selected_index_name(access: &ExplainAccessPath) -> Option<&str> {
969 match access {
970 ExplainAccessPath::IndexPrefix { name, .. }
971 | ExplainAccessPath::IndexMultiLookup { name, .. }
972 | ExplainAccessPath::IndexBranchSet { name, .. }
973 | ExplainAccessPath::IndexRange { name, .. } => Some(name.as_str()),
974 ExplainAccessPath::ByKey { .. }
975 | ExplainAccessPath::ByKeys { .. }
976 | ExplainAccessPath::KeyRange { .. }
977 | ExplainAccessPath::FullScan
978 | ExplainAccessPath::Union(_)
979 | ExplainAccessPath::Intersection(_) => None,
980 }
981}
982
983fn access_bound_predicate_count(
984 access: &ExplainAccessPath,
985 work: &PreparationWork<'_>,
986) -> Result<usize, QueryError> {
987 work.charge(Resource::PredicateExpressionSteps, 1)?;
988 Ok(match access {
989 ExplainAccessPath::ByKey { .. }
990 | ExplainAccessPath::ByKeys { .. }
991 | ExplainAccessPath::IndexMultiLookup { .. } => 1,
992 ExplainAccessPath::IndexBranchSet {
993 fixed_values,
994 branch_values,
995 ..
996 } => fixed_values.len() + usize::from(!branch_values.is_empty()),
997 ExplainAccessPath::KeyRange { .. } => 2,
998 ExplainAccessPath::IndexPrefix { prefix_len, .. } => *prefix_len,
999 ExplainAccessPath::IndexRange {
1000 prefix_len,
1001 lower,
1002 upper,
1003 ..
1004 } => *prefix_len + bound_constraint_count(lower) + bound_constraint_count(upper),
1005 ExplainAccessPath::FullScan => 0,
1006 ExplainAccessPath::Union(children) | ExplainAccessPath::Intersection(children) => {
1007 let mut count = 0;
1008 for child in children {
1009 count += access_bound_predicate_count(child, work)?;
1010 }
1011 count
1012 }
1013 })
1014}
1015
1016const fn bound_constraint_count(bound: &Bound<Value>) -> usize {
1017 match bound {
1018 Bound::Included(_) | Bound::Excluded(_) => 1,
1019 Bound::Unbounded => 0,
1020 }
1021}
1022
1023const fn explain_order_pushdown() -> ExplainOrderPushdown {
1024 ExplainOrderPushdown::MissingModelContext
1026}
1027
1028fn explain_order(
1029 order: Option<&OrderSpec>,
1030 work: &PreparationWork<'_>,
1031) -> Result<ExplainOrderBy, QueryError> {
1032 let Some(order) = order else {
1033 return Ok(ExplainOrderBy::None);
1034 };
1035
1036 if order.fields.is_empty() {
1037 return Ok(ExplainOrderBy::None);
1038 }
1039
1040 Ok(ExplainOrderBy::Fields(work.copy_slice(
1041 &order.fields,
1042 |term| {
1043 Ok(ExplainOrder {
1044 field: explain_expr_label(term.expr(), work)?,
1045 direction: term.direction(),
1046 })
1047 },
1048 )?))
1049}
1050
1051pub(in crate::db) const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1052 match page {
1053 Some(page) => ExplainPagination::Page {
1054 limit: page.limit,
1055 offset: page.offset,
1056 },
1057 None => ExplainPagination::None,
1058 }
1059}
1060
1061const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1062 match limit {
1063 Some(limit) if limit.offset == 0 => match limit.limit {
1064 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
1065 None => ExplainDeleteLimit::Window {
1066 limit: None,
1067 offset: 0,
1068 },
1069 },
1070 Some(limit) => ExplainDeleteLimit::Window {
1071 limit: limit.limit,
1072 offset: limit.offset,
1073 },
1074 None => ExplainDeleteLimit::None,
1075 }
1076}
1077
1078fn write_logical_explain_json(explain: &ExplainPlan, out: &mut dyn fmt::Write) -> fmt::Result {
1079 let mut object = JsonWriter::begin_object(out)?;
1080 object.field_with("mode", |out| {
1081 let mut object = JsonWriter::begin_object(out)?;
1082 match explain.mode() {
1083 QueryMode::Load(spec) => {
1084 object.field_str("type", "Load")?;
1085 match spec.limit() {
1086 Some(limit) => object.field_u64("limit", u64::from(limit))?,
1087 None => object.field_null("limit")?,
1088 }
1089 object.field_u64("offset", u64::from(spec.offset()))?;
1090 }
1091 QueryMode::Delete(spec) => {
1092 object.field_str("type", "Delete")?;
1093 match spec.limit() {
1094 Some(limit) => object.field_u64("limit", u64::from(limit))?,
1095 None => object.field_null("limit")?,
1096 }
1097 }
1098 }
1099 object.finish()?;
1100 Ok(())
1101 })?;
1102 object.field_with("access", |out| {
1103 write_access_json(explain.access(), out)?;
1104 Ok(())
1105 })?;
1106 object.field_with("access_decision", |out| {
1107 write_access_decision_json(explain.access_decision(), out)?;
1108 Ok(())
1109 })?;
1110 match explain.filter_expr() {
1111 Some(filter_expr) => object.field_str("filter_expr", filter_expr)?,
1112 None => object.field_null("filter_expr")?,
1113 }
1114 object.field_bool(
1115 "has_predicate",
1116 !matches!(explain.predicate(), ExplainPredicate::None),
1117 )?;
1118 object.field_value_debug("order_by", explain.order_by())?;
1119 object.field_bool("distinct", explain.distinct())?;
1120 object.field_with("grouping", |out| {
1121 write_grouping_json(explain.grouping(), out)
1122 })?;
1123 object.field_value_debug("order_pushdown", explain.order_pushdown())?;
1124 object.field_with("page", |out| {
1125 let mut object = JsonWriter::begin_object(out)?;
1126 match explain.page() {
1127 ExplainPagination::None => {
1128 object.field_str("type", "None")?;
1129 }
1130 ExplainPagination::Page { limit, offset } => {
1131 object.field_str("type", "Page")?;
1132 match 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(*offset))?;
1137 }
1138 }
1139 object.finish()?;
1140 Ok(())
1141 })?;
1142 object.field_with("delete_limit", |out| {
1143 let mut object = JsonWriter::begin_object(out)?;
1144 match explain.delete_limit() {
1145 ExplainDeleteLimit::None => {
1146 object.field_str("type", "None")?;
1147 }
1148 ExplainDeleteLimit::Limit { max_rows } => {
1149 object.field_str("type", "Limit")?;
1150 object.field_u64("max_rows", u64::from(*max_rows))?;
1151 }
1152 ExplainDeleteLimit::Window { limit, offset } => {
1153 object.field_str("type", "Window")?;
1154 object.field_with("limit", |out| match limit {
1155 Some(limit) => write!(out, "{limit}"),
1156 None => out.write_str("null"),
1157 })?;
1158 object.field_u64("offset", u64::from(*offset))?;
1159 }
1160 }
1161 object.finish()?;
1162 Ok(())
1163 })?;
1164 object.field_value_debug("consistency", &explain.consistency())?;
1165 object.finish()
1166}
1167
1168fn write_grouping_json(grouping: &ExplainGrouping, out: &mut dyn fmt::Write) -> fmt::Result {
1171 let ExplainGrouping::Grouped {
1172 strategy,
1173 fallback_reason,
1174 group_fields,
1175 aggregates,
1176 having,
1177 max_groups,
1178 max_group_bytes,
1179 } = grouping
1180 else {
1181 return out.write_str("null");
1182 };
1183 let mut object = JsonWriter::begin_object(out)?;
1184 object.field_str("strategy", strategy)?;
1185 match fallback_reason {
1186 Some(reason) => object.field_str("fallback_reason", reason)?,
1187 None => object.field_null("fallback_reason")?,
1188 }
1189 object.field_with("group_fields", |out| {
1190 out.write_char('[')?;
1191 for (index, field) in group_fields.iter().enumerate() {
1192 if index != 0 {
1193 out.write_char(',')?;
1194 }
1195 let mut field_object = JsonWriter::begin_object(out)?;
1196 field_object.field_u64("slot_index", field.slot_index as u64)?;
1197 field_object.field_str("field", &field.field)?;
1198 field_object.finish()?;
1199 }
1200 out.write_char(']')
1201 })?;
1202 object.field_with("aggregates", |out| {
1203 out.write_char('[')?;
1204 for (index, aggregate) in aggregates.iter().enumerate() {
1205 if index != 0 {
1206 out.write_char(',')?;
1207 }
1208 let mut aggregate_object = JsonWriter::begin_object(out)?;
1209 aggregate_object.field_value_debug("kind", &aggregate.kind)?;
1210 for (name, label) in [
1211 ("target_field", aggregate.target_field()),
1212 ("input_expr", aggregate.input_expr()),
1213 ("filter_expr", aggregate.filter_expr()),
1214 ] {
1215 match label {
1216 Some(label) => aggregate_object.field_str(name, label)?,
1217 None => aggregate_object.field_null(name)?,
1218 }
1219 }
1220 aggregate_object.field_bool("distinct", aggregate.distinct)?;
1221 aggregate_object.finish()?;
1222 }
1223 out.write_char(']')
1224 })?;
1225 object.field_bool("has_having", having.is_some())?;
1226 object.field_u64("max_groups", *max_groups)?;
1227 object.field_u64("max_group_bytes", *max_group_bytes)?;
1228 object.finish()
1229}
1230
1231fn write_access_decision_json(
1232 decision: &ExplainAccessDecision,
1233 out: &mut dyn fmt::Write,
1234) -> fmt::Result {
1235 let mut object = JsonWriter::begin_object(out)?;
1236 object.field_with("selected", |out| {
1237 let mut selected = JsonWriter::begin_object(out)?;
1238 selected.field_str("kind", decision.selected.kind.code())?;
1239 match decision.selected.index_name.as_deref() {
1240 Some(index_name) => selected.field_str("index_name", index_name)?,
1241 None => selected.field_null("index_name")?,
1242 }
1243 selected.field_str("label", decision.selected.label.as_str())?;
1244 selected.field_str("reason", decision.selected.reason)?;
1245 selected.finish()?;
1246 Ok(())
1247 })?;
1248 object.field_with("candidates", |out| {
1249 out.write_char('[')?;
1250 for (index, candidate) in decision.candidates.iter().enumerate() {
1251 if index > 0 {
1252 out.write_char(',')?;
1253 }
1254 write_access_candidate_json(candidate, out)?;
1255 }
1256 out.write_char(']')?;
1257 Ok(())
1258 })?;
1259 object.field_with("alternatives", |out| {
1260 out.write_char('[')?;
1261 for (index, alternative) in decision.alternatives.iter().enumerate() {
1262 if index > 0 {
1263 out.write_char(',')?;
1264 }
1265 let mut object = JsonWriter::begin_object(out)?;
1266 object.field_str("index_name", alternative.index_name.as_str())?;
1267 object.finish()?;
1268 }
1269 out.write_char(']')?;
1270 Ok(())
1271 })?;
1272 object.field_with("rejections", |out| {
1273 out.write_char('[')?;
1274 for (index, rejection) in decision.rejections.iter().enumerate() {
1275 if index > 0 {
1276 out.write_char(',')?;
1277 }
1278 let mut object = JsonWriter::begin_object(out)?;
1279 match rejection.index_name.as_deref() {
1280 Some(index_name) => object.field_str("index_name", index_name)?,
1281 None => object.field_null("index_name")?,
1282 }
1283 match rejection.reason.as_deref() {
1284 Some(reason) => object.field_str("reason", reason)?,
1285 None => object.field_null("reason")?,
1286 }
1287 object.field_str("label", rejection.label.as_str())?;
1288 object.finish()?;
1289 }
1290 out.write_char(']')?;
1291 Ok(())
1292 })?;
1293 object.field_with("residual", |out| {
1294 let mut residual = JsonWriter::begin_object(out)?;
1295 residual.field_str("burden_class", decision.residual.burden_class)?;
1296 residual.field_bool("has_residual_filter", decision.residual.has_residual_filter)?;
1297 residual.field_bool(
1298 "has_residual_predicate",
1299 decision.residual.has_residual_predicate,
1300 )?;
1301 residual.field_u64(
1302 "access_bound_predicate_count",
1303 decision.residual.access_bound_predicate_count as u64,
1304 )?;
1305 residual.field_u64(
1306 "residual_predicate_count",
1307 decision.residual.residual_predicate_count as u64,
1308 )?;
1309 residual.finish()?;
1310 Ok(())
1311 })?;
1312 object.field_str(
1313 "cardinality_evidence_state",
1314 decision.cardinality_evidence_state,
1315 )?;
1316 object.finish()
1317}
1318
1319fn write_access_candidate_json(
1320 candidate: &ExplainAccessCandidate,
1321 out: &mut dyn fmt::Write,
1322) -> fmt::Result {
1323 let mut object = JsonWriter::begin_object(out)?;
1324 object.field_str("label", candidate.label.as_str())?;
1325 object.field_bool("exact", candidate.exact)?;
1326 object.field_bool("filtered", candidate.filtered)?;
1327 object.field_u64("range_bound_count", candidate.range_bound_count as u64)?;
1328 object.field_bool("order_compatible", candidate.order_compatible)?;
1329 object.field_str("residual_burden", candidate.residual_burden)?;
1330 object.field_u64(
1331 "residual_predicate_terms",
1332 candidate.residual_predicate_terms as u64,
1333 )?;
1334 if let Some(entries) = candidate.exact_prefix_entries {
1335 object.field_u64("exact_prefix_entries", entries)?;
1336 } else {
1337 object.field_null("exact_prefix_entries")?;
1338 }
1339 object.finish()
1340}