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