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, explain_access_plan, writer::JsonWriter,
15 },
16 plan::{
17 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupedPlanFallbackReason,
18 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
19 expr::Expr, grouped_plan_strategy, render_scalar_filter_expr_plan_label,
20 },
21 },
22 },
23 traits::KeyValueCodec,
24 value::Value,
25};
26use std::ops::Bound;
27
28#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct ExplainPlan {
36 pub(in crate::db) mode: QueryMode,
37 pub(in crate::db) access: ExplainAccessPath,
38 pub(in crate::db) filter_expr: Option<String>,
39 filter_expr_model: Option<Expr>,
40 pub(in crate::db) predicate: ExplainPredicate,
41 predicate_model: Option<Predicate>,
42 pub(in crate::db) order_by: ExplainOrderBy,
43 pub(in crate::db) distinct: bool,
44 pub(in crate::db) grouping: ExplainGrouping,
45 pub(in crate::db) order_pushdown: ExplainOrderPushdown,
46 pub(in crate::db) page: ExplainPagination,
47 pub(in crate::db) delete_limit: ExplainDeleteLimit,
48 pub(in crate::db) consistency: MissingRowPolicy,
49}
50
51impl ExplainPlan {
52 #[must_use]
54 pub const fn mode(&self) -> QueryMode {
55 self.mode
56 }
57
58 #[must_use]
60 pub const fn access(&self) -> &ExplainAccessPath {
61 &self.access
62 }
63
64 #[must_use]
66 pub fn filter_expr(&self) -> Option<&str> {
67 self.filter_expr.as_deref()
68 }
69
70 #[must_use]
72 pub(in crate::db::query) fn filter_expr_model_for_hash(&self) -> Option<&Expr> {
73 if let Some(filter_expr_model) = &self.filter_expr_model {
74 debug_assert_eq!(
75 self.filter_expr(),
76 Some(render_scalar_filter_expr_plan_label(filter_expr_model).as_str()),
77 "explain scalar filter label drifted from canonical filter model"
78 );
79 Some(filter_expr_model)
80 } else {
81 debug_assert!(
82 self.filter_expr.is_none(),
83 "missing canonical filter model requires filter_expr=None"
84 );
85 None
86 }
87 }
88
89 #[must_use]
91 pub const fn predicate(&self) -> &ExplainPredicate {
92 &self.predicate
93 }
94
95 #[must_use]
97 pub const fn order_by(&self) -> &ExplainOrderBy {
98 &self.order_by
99 }
100
101 #[must_use]
103 pub const fn distinct(&self) -> bool {
104 self.distinct
105 }
106
107 #[must_use]
109 pub const fn grouping(&self) -> &ExplainGrouping {
110 &self.grouping
111 }
112
113 #[must_use]
115 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
116 &self.order_pushdown
117 }
118
119 #[must_use]
121 pub const fn page(&self) -> &ExplainPagination {
122 &self.page
123 }
124
125 #[must_use]
127 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
128 &self.delete_limit
129 }
130
131 #[must_use]
133 pub const fn consistency(&self) -> MissingRowPolicy {
134 self.consistency
135 }
136}
137
138impl ExplainPlan {
139 #[must_use]
145 pub(in crate::db::query) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
146 if let Some(predicate) = &self.predicate_model {
147 debug_assert_eq!(
148 self.predicate,
149 ExplainPredicate::from_predicate(predicate),
150 "explain predicate surface drifted from canonical predicate model"
151 );
152 Some(predicate)
153 } else {
154 debug_assert!(
155 matches!(self.predicate, ExplainPredicate::None),
156 "missing canonical predicate model requires ExplainPredicate::None"
157 );
158 None
159 }
160 }
161
162 #[must_use]
167 pub fn render_text_canonical(&self) -> String {
168 format!(
169 concat!(
170 "mode={:?}\n",
171 "access={:?}\n",
172 "filter_expr={:?}\n",
173 "predicate={:?}\n",
174 "order_by={:?}\n",
175 "distinct={}\n",
176 "grouping={:?}\n",
177 "order_pushdown={:?}\n",
178 "page={:?}\n",
179 "delete_limit={:?}\n",
180 "consistency={:?}",
181 ),
182 self.mode(),
183 self.access(),
184 self.filter_expr(),
185 self.predicate(),
186 self.order_by(),
187 self.distinct(),
188 self.grouping(),
189 self.order_pushdown(),
190 self.page(),
191 self.delete_limit(),
192 self.consistency(),
193 )
194 }
195
196 #[must_use]
198 pub fn render_json_canonical(&self) -> String {
199 let mut out = String::new();
200 write_logical_explain_json(self, &mut out);
201
202 out
203 }
204}
205
206#[derive(Clone, Debug, Eq, PartialEq)]
213pub enum ExplainGrouping {
214 None,
215 Grouped {
216 strategy: &'static str,
217 fallback_reason: Option<&'static str>,
218 group_fields: Vec<ExplainGroupField>,
219 aggregates: Vec<ExplainGroupAggregate>,
220 having: Option<ExplainGroupHaving>,
221 max_groups: u64,
222 max_group_bytes: u64,
223 },
224}
225
226#[derive(Clone, Debug, Eq, PartialEq)]
233pub struct ExplainGroupField {
234 pub(in crate::db) slot_index: usize,
235 pub(in crate::db) field: String,
236}
237
238impl ExplainGroupField {
239 #[must_use]
241 pub const fn slot_index(&self) -> usize {
242 self.slot_index
243 }
244
245 #[must_use]
247 pub const fn field(&self) -> &str {
248 self.field.as_str()
249 }
250}
251
252#[derive(Clone, Debug, Eq, PartialEq)]
259pub struct ExplainGroupAggregate {
260 pub(in crate::db) kind: AggregateKind,
261 pub(in crate::db) target_field: Option<String>,
262 pub(in crate::db) input_expr: Option<String>,
263 pub(in crate::db) filter_expr: Option<String>,
264 pub(in crate::db) distinct: bool,
265}
266
267impl ExplainGroupAggregate {
268 #[must_use]
270 pub const fn kind(&self) -> AggregateKind {
271 self.kind
272 }
273
274 #[must_use]
276 pub fn target_field(&self) -> Option<&str> {
277 self.target_field.as_deref()
278 }
279
280 #[must_use]
282 pub fn input_expr(&self) -> Option<&str> {
283 self.input_expr.as_deref()
284 }
285
286 #[must_use]
288 pub fn filter_expr(&self) -> Option<&str> {
289 self.filter_expr.as_deref()
290 }
291
292 #[must_use]
294 pub const fn distinct(&self) -> bool {
295 self.distinct
296 }
297}
298
299#[derive(Clone, Debug, Eq, PartialEq)]
308pub struct ExplainGroupHaving {
309 pub(in crate::db) expr: Expr,
310}
311
312impl ExplainGroupHaving {
313 #[must_use]
315 pub(in crate::db) const fn expr(&self) -> &Expr {
316 &self.expr
317 }
318}
319
320#[derive(Clone, Debug, Eq, PartialEq)]
327pub enum ExplainOrderPushdown {
328 MissingModelContext,
329 EligibleSecondaryIndex { index: String, prefix_len: usize },
330 Rejected(SecondaryOrderPushdownRejection),
331}
332
333#[derive(Clone, Debug, Eq, PartialEq)]
341pub enum SecondaryOrderPushdownRejection {
342 NoOrderBy,
343 AccessPathNotSingleIndexPrefix,
344 AccessPathIndexRangeUnsupported {
345 index: String,
346 prefix_len: usize,
347 },
348 InvalidIndexPrefixBounds {
349 prefix_len: usize,
350 index_field_len: usize,
351 },
352 MissingPrimaryKeyTieBreak {
353 field: String,
354 },
355 PrimaryKeyDirectionNotAscending {
356 field: String,
357 },
358 MixedDirectionNotEligible {
359 field: String,
360 },
361 OrderFieldsDoNotMatchIndex {
362 index: String,
363 prefix_len: usize,
364 expected_suffix: Vec<String>,
365 expected_full: Vec<String>,
366 actual: Vec<String>,
367 },
368}
369
370#[derive(Clone, Debug, Eq, PartialEq)]
378pub enum ExplainAccessPath {
379 ByKey {
380 key: Value,
381 },
382 ByKeys {
383 keys: Vec<Value>,
384 },
385 KeyRange {
386 start: Value,
387 end: Value,
388 },
389 IndexPrefix {
390 name: String,
391 fields: Vec<String>,
392 prefix_len: usize,
393 values: Vec<Value>,
394 },
395 IndexMultiLookup {
396 name: String,
397 fields: Vec<String>,
398 values: Vec<Value>,
399 },
400 IndexRange {
401 name: String,
402 fields: Vec<String>,
403 prefix_len: usize,
404 prefix: Vec<Value>,
405 lower: Bound<Value>,
406 upper: Bound<Value>,
407 },
408 FullScan,
409 Union(Vec<Self>),
410 Intersection(Vec<Self>),
411}
412
413#[derive(Clone, Debug, Eq, PartialEq)]
421pub enum ExplainPredicate {
422 None,
423 True,
424 False,
425 And(Vec<Self>),
426 Or(Vec<Self>),
427 Not(Box<Self>),
428 Compare {
429 field: String,
430 op: CompareOp,
431 value: Value,
432 coercion: CoercionSpec,
433 },
434 CompareFields {
435 left_field: String,
436 op: CompareOp,
437 right_field: String,
438 coercion: CoercionSpec,
439 },
440 IsNull {
441 field: String,
442 },
443 IsNotNull {
444 field: String,
445 },
446 IsMissing {
447 field: String,
448 },
449 IsEmpty {
450 field: String,
451 },
452 IsNotEmpty {
453 field: String,
454 },
455 TextContains {
456 field: String,
457 value: Value,
458 },
459 TextContainsCi {
460 field: String,
461 value: Value,
462 },
463}
464
465#[derive(Clone, Debug, Eq, PartialEq)]
472pub enum ExplainOrderBy {
473 None,
474 Fields(Vec<ExplainOrder>),
475}
476
477#[derive(Clone, Debug, Eq, PartialEq)]
484pub struct ExplainOrder {
485 pub(in crate::db) field: String,
486 pub(in crate::db) direction: OrderDirection,
487}
488
489impl ExplainOrder {
490 #[must_use]
492 pub const fn field(&self) -> &str {
493 self.field.as_str()
494 }
495
496 #[must_use]
498 pub const fn direction(&self) -> OrderDirection {
499 self.direction
500 }
501}
502
503#[derive(Clone, Debug, Eq, PartialEq)]
510pub enum ExplainPagination {
511 None,
512 Page { limit: Option<u32>, offset: u32 },
513}
514
515#[derive(Clone, Debug, Eq, PartialEq)]
522pub enum ExplainDeleteLimit {
523 None,
524 Limit { max_rows: u32 },
525 Window { limit: Option<u32>, offset: u32 },
526}
527
528impl AccessPlannedQuery {
529 #[must_use]
531 pub(in crate::db) fn explain(&self) -> ExplainPlan {
532 self.explain_inner()
533 }
534
535 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
536 let (logical, grouping) = match &self.logical {
538 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
539 LogicalPlan::Grouped(logical) => {
540 let grouped_strategy = grouped_plan_strategy(self).expect(
541 "grouped logical explain projection requires planner-owned grouped strategy",
542 );
543
544 (
545 &logical.scalar,
546 ExplainGrouping::Grouped {
547 strategy: grouped_strategy.code(),
548 fallback_reason: grouped_strategy
549 .fallback_reason()
550 .map(GroupedPlanFallbackReason::code),
551 group_fields: logical
552 .group
553 .group_fields
554 .iter()
555 .map(|field_slot| ExplainGroupField {
556 slot_index: field_slot.index(),
557 field: field_slot.field().to_string(),
558 })
559 .collect(),
560 aggregates: logical
561 .group
562 .aggregates
563 .iter()
564 .map(|aggregate| ExplainGroupAggregate {
565 kind: aggregate.kind,
566 target_field: aggregate.target_field().map(str::to_string),
567 input_expr: aggregate
568 .input_expr()
569 .map(render_scalar_projection_expr_plan_label),
570 filter_expr: aggregate
571 .filter_expr()
572 .map(render_scalar_projection_expr_plan_label),
573 distinct: aggregate.distinct,
574 })
575 .collect(),
576 having: explain_group_having(logical),
577 max_groups: logical.group.execution.max_groups(),
578 max_group_bytes: logical.group.execution.max_group_bytes(),
579 },
580 )
581 }
582 };
583
584 explain_scalar_inner(logical, grouping, &self.access)
586 }
587}
588
589fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
590 let expr = logical.effective_having_expr()?;
591
592 Some(ExplainGroupHaving {
593 expr: expr.into_owned(),
594 })
595}
596
597fn explain_scalar_inner<K>(
598 logical: &ScalarPlan,
599 grouping: ExplainGrouping,
600 access: &AccessPlan<K>,
601) -> ExplainPlan
602where
603 K: KeyValueCodec,
604{
605 let filter_expr = logical
607 .filter_expr
608 .as_ref()
609 .map(render_scalar_filter_expr_plan_label);
610 let filter_expr_model = logical.filter_expr.clone();
611 let predicate_model = logical.predicate.clone();
612 let predicate = match &predicate_model {
613 Some(predicate) => ExplainPredicate::from_predicate(predicate),
614 None => ExplainPredicate::None,
615 };
616
617 let order_by = explain_order(logical.order.as_ref());
619 let order_pushdown = explain_order_pushdown();
620 let page = explain_page(logical.page.as_ref());
621 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
622
623 ExplainPlan {
625 mode: logical.mode,
626 access: explain_access_plan(access),
627 filter_expr,
628 filter_expr_model,
629 predicate,
630 predicate_model,
631 order_by,
632 distinct: logical.distinct,
633 grouping,
634 order_pushdown,
635 page,
636 delete_limit,
637 consistency: logical.consistency,
638 }
639}
640
641const fn explain_order_pushdown() -> ExplainOrderPushdown {
642 ExplainOrderPushdown::MissingModelContext
644}
645
646impl ExplainPredicate {
647 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
648 match predicate {
649 Predicate::True => Self::True,
650 Predicate::False => Self::False,
651 Predicate::And(children) => {
652 Self::And(children.iter().map(Self::from_predicate).collect())
653 }
654 Predicate::Or(children) => {
655 Self::Or(children.iter().map(Self::from_predicate).collect())
656 }
657 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
658 Predicate::Compare(compare) => Self::from_compare(compare),
659 Predicate::CompareFields(compare) => Self::CompareFields {
660 left_field: compare.left_field().to_string(),
661 op: compare.op(),
662 right_field: compare.right_field().to_string(),
663 coercion: compare.coercion().clone(),
664 },
665 Predicate::IsNull { field } => Self::IsNull {
666 field: field.clone(),
667 },
668 Predicate::IsNotNull { field } => Self::IsNotNull {
669 field: field.clone(),
670 },
671 Predicate::IsMissing { field } => Self::IsMissing {
672 field: field.clone(),
673 },
674 Predicate::IsEmpty { field } => Self::IsEmpty {
675 field: field.clone(),
676 },
677 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
678 field: field.clone(),
679 },
680 Predicate::TextContains { field, value } => Self::TextContains {
681 field: field.clone(),
682 value: value.clone(),
683 },
684 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
685 field: field.clone(),
686 value: value.clone(),
687 },
688 }
689 }
690
691 fn from_compare(compare: &ComparePredicate) -> Self {
692 Self::Compare {
693 field: compare.field.clone(),
694 op: compare.op,
695 value: compare.value.clone(),
696 coercion: compare.coercion.clone(),
697 }
698 }
699}
700
701fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
702 let Some(order) = order else {
703 return ExplainOrderBy::None;
704 };
705
706 if order.fields.is_empty() {
707 return ExplainOrderBy::None;
708 }
709
710 ExplainOrderBy::Fields(
711 order
712 .fields
713 .iter()
714 .map(|term| ExplainOrder {
715 field: term.rendered_label(),
716 direction: term.direction(),
717 })
718 .collect(),
719 )
720}
721
722const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
723 match page {
724 Some(page) => ExplainPagination::Page {
725 limit: page.limit,
726 offset: page.offset,
727 },
728 None => ExplainPagination::None,
729 }
730}
731
732const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
733 match limit {
734 Some(limit) if limit.offset == 0 => match limit.limit {
735 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
736 None => ExplainDeleteLimit::Window {
737 limit: None,
738 offset: 0,
739 },
740 },
741 Some(limit) => ExplainDeleteLimit::Window {
742 limit: limit.limit,
743 offset: limit.offset,
744 },
745 None => ExplainDeleteLimit::None,
746 }
747}
748
749fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
750 let mut object = JsonWriter::begin_object(out);
751 object.field_with("mode", |out| {
752 let mut object = JsonWriter::begin_object(out);
753 match explain.mode() {
754 QueryMode::Load(spec) => {
755 object.field_str("type", "Load");
756 match spec.limit() {
757 Some(limit) => object.field_u64("limit", u64::from(limit)),
758 None => object.field_null("limit"),
759 }
760 object.field_u64("offset", u64::from(spec.offset()));
761 }
762 QueryMode::Delete(spec) => {
763 object.field_str("type", "Delete");
764 match spec.limit() {
765 Some(limit) => object.field_u64("limit", u64::from(limit)),
766 None => object.field_null("limit"),
767 }
768 }
769 }
770 object.finish();
771 });
772 object.field_with("access", |out| write_access_json(explain.access(), out));
773 match explain.filter_expr() {
774 Some(filter_expr) => object.field_str("filter_expr", filter_expr),
775 None => object.field_null("filter_expr"),
776 }
777 object.field_value_debug("predicate", explain.predicate());
778 object.field_value_debug("order_by", explain.order_by());
779 object.field_bool("distinct", explain.distinct());
780 object.field_value_debug("grouping", explain.grouping());
781 object.field_value_debug("order_pushdown", explain.order_pushdown());
782 object.field_with("page", |out| {
783 let mut object = JsonWriter::begin_object(out);
784 match explain.page() {
785 ExplainPagination::None => {
786 object.field_str("type", "None");
787 }
788 ExplainPagination::Page { limit, offset } => {
789 object.field_str("type", "Page");
790 match limit {
791 Some(limit) => object.field_u64("limit", u64::from(*limit)),
792 None => object.field_null("limit"),
793 }
794 object.field_u64("offset", u64::from(*offset));
795 }
796 }
797 object.finish();
798 });
799 object.field_with("delete_limit", |out| {
800 let mut object = JsonWriter::begin_object(out);
801 match explain.delete_limit() {
802 ExplainDeleteLimit::None => {
803 object.field_str("type", "None");
804 }
805 ExplainDeleteLimit::Limit { max_rows } => {
806 object.field_str("type", "Limit");
807 object.field_u64("max_rows", u64::from(*max_rows));
808 }
809 ExplainDeleteLimit::Window { limit, offset } => {
810 object.field_str("type", "Window");
811 object.field_with("limit", |out| match limit {
812 Some(limit) => out.push_str(&limit.to_string()),
813 None => out.push_str("null"),
814 });
815 object.field_u64("offset", u64::from(*offset));
816 }
817 }
818 object.finish();
819 });
820 object.field_value_debug("consistency", &explain.consistency());
821 object.finish();
822}