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 {
330 index: &'static str,
331 prefix_len: usize,
332 },
333 Rejected(SecondaryOrderPushdownRejection),
334}
335
336#[derive(Clone, Debug, Eq, PartialEq)]
344pub enum SecondaryOrderPushdownRejection {
345 NoOrderBy,
346 AccessPathNotSingleIndexPrefix,
347 AccessPathIndexRangeUnsupported {
348 index: &'static str,
349 prefix_len: usize,
350 },
351 InvalidIndexPrefixBounds {
352 prefix_len: usize,
353 index_field_len: usize,
354 },
355 MissingPrimaryKeyTieBreak {
356 field: String,
357 },
358 PrimaryKeyDirectionNotAscending {
359 field: String,
360 },
361 MixedDirectionNotEligible {
362 field: String,
363 },
364 OrderFieldsDoNotMatchIndex {
365 index: &'static str,
366 prefix_len: usize,
367 expected_suffix: Vec<String>,
368 expected_full: Vec<String>,
369 actual: Vec<String>,
370 },
371}
372
373#[derive(Clone, Debug, Eq, PartialEq)]
381pub enum ExplainAccessPath {
382 ByKey {
383 key: Value,
384 },
385 ByKeys {
386 keys: Vec<Value>,
387 },
388 KeyRange {
389 start: Value,
390 end: Value,
391 },
392 IndexPrefix {
393 name: &'static str,
394 fields: Vec<&'static str>,
395 prefix_len: usize,
396 values: Vec<Value>,
397 },
398 IndexMultiLookup {
399 name: &'static str,
400 fields: Vec<&'static str>,
401 values: Vec<Value>,
402 },
403 IndexRange {
404 name: &'static str,
405 fields: Vec<&'static str>,
406 prefix_len: usize,
407 prefix: Vec<Value>,
408 lower: Bound<Value>,
409 upper: Bound<Value>,
410 },
411 FullScan,
412 Union(Vec<Self>),
413 Intersection(Vec<Self>),
414}
415
416#[derive(Clone, Debug, Eq, PartialEq)]
424pub enum ExplainPredicate {
425 None,
426 True,
427 False,
428 And(Vec<Self>),
429 Or(Vec<Self>),
430 Not(Box<Self>),
431 Compare {
432 field: String,
433 op: CompareOp,
434 value: Value,
435 coercion: CoercionSpec,
436 },
437 CompareFields {
438 left_field: String,
439 op: CompareOp,
440 right_field: String,
441 coercion: CoercionSpec,
442 },
443 IsNull {
444 field: String,
445 },
446 IsNotNull {
447 field: String,
448 },
449 IsMissing {
450 field: String,
451 },
452 IsEmpty {
453 field: String,
454 },
455 IsNotEmpty {
456 field: String,
457 },
458 TextContains {
459 field: String,
460 value: Value,
461 },
462 TextContainsCi {
463 field: String,
464 value: Value,
465 },
466}
467
468#[derive(Clone, Debug, Eq, PartialEq)]
475pub enum ExplainOrderBy {
476 None,
477 Fields(Vec<ExplainOrder>),
478}
479
480#[derive(Clone, Debug, Eq, PartialEq)]
487pub struct ExplainOrder {
488 pub(in crate::db) field: String,
489 pub(in crate::db) direction: OrderDirection,
490}
491
492impl ExplainOrder {
493 #[must_use]
495 pub const fn field(&self) -> &str {
496 self.field.as_str()
497 }
498
499 #[must_use]
501 pub const fn direction(&self) -> OrderDirection {
502 self.direction
503 }
504}
505
506#[derive(Clone, Debug, Eq, PartialEq)]
513pub enum ExplainPagination {
514 None,
515 Page { limit: Option<u32>, offset: u32 },
516}
517
518#[derive(Clone, Debug, Eq, PartialEq)]
525pub enum ExplainDeleteLimit {
526 None,
527 Limit { max_rows: u32 },
528 Window { limit: Option<u32>, offset: u32 },
529}
530
531impl AccessPlannedQuery {
532 #[must_use]
534 pub(in crate::db) fn explain(&self) -> ExplainPlan {
535 self.explain_inner()
536 }
537
538 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
539 let (logical, grouping) = match &self.logical {
541 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
542 LogicalPlan::Grouped(logical) => {
543 let grouped_strategy = grouped_plan_strategy(self).expect(
544 "grouped logical explain projection requires planner-owned grouped strategy",
545 );
546
547 (
548 &logical.scalar,
549 ExplainGrouping::Grouped {
550 strategy: grouped_strategy.code(),
551 fallback_reason: grouped_strategy
552 .fallback_reason()
553 .map(GroupedPlanFallbackReason::code),
554 group_fields: logical
555 .group
556 .group_fields
557 .iter()
558 .map(|field_slot| ExplainGroupField {
559 slot_index: field_slot.index(),
560 field: field_slot.field().to_string(),
561 })
562 .collect(),
563 aggregates: logical
564 .group
565 .aggregates
566 .iter()
567 .map(|aggregate| ExplainGroupAggregate {
568 kind: aggregate.kind,
569 target_field: aggregate.target_field().map(str::to_string),
570 input_expr: aggregate
571 .input_expr()
572 .map(render_scalar_projection_expr_plan_label),
573 filter_expr: aggregate
574 .filter_expr()
575 .map(render_scalar_projection_expr_plan_label),
576 distinct: aggregate.distinct,
577 })
578 .collect(),
579 having: explain_group_having(logical),
580 max_groups: logical.group.execution.max_groups(),
581 max_group_bytes: logical.group.execution.max_group_bytes(),
582 },
583 )
584 }
585 };
586
587 explain_scalar_inner(logical, grouping, &self.access)
589 }
590}
591
592fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
593 let expr = logical.effective_having_expr()?;
594
595 Some(ExplainGroupHaving {
596 expr: expr.into_owned(),
597 })
598}
599
600fn explain_scalar_inner<K>(
601 logical: &ScalarPlan,
602 grouping: ExplainGrouping,
603 access: &AccessPlan<K>,
604) -> ExplainPlan
605where
606 K: KeyValueCodec,
607{
608 let filter_expr = logical
610 .filter_expr
611 .as_ref()
612 .map(render_scalar_filter_expr_plan_label);
613 let filter_expr_model = logical.filter_expr.clone();
614 let predicate_model = logical.predicate.clone();
615 let predicate = match &predicate_model {
616 Some(predicate) => ExplainPredicate::from_predicate(predicate),
617 None => ExplainPredicate::None,
618 };
619
620 let order_by = explain_order(logical.order.as_ref());
622 let order_pushdown = explain_order_pushdown();
623 let page = explain_page(logical.page.as_ref());
624 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
625
626 ExplainPlan {
628 mode: logical.mode,
629 access: explain_access_plan(access),
630 filter_expr,
631 filter_expr_model,
632 predicate,
633 predicate_model,
634 order_by,
635 distinct: logical.distinct,
636 grouping,
637 order_pushdown,
638 page,
639 delete_limit,
640 consistency: logical.consistency,
641 }
642}
643
644const fn explain_order_pushdown() -> ExplainOrderPushdown {
645 ExplainOrderPushdown::MissingModelContext
647}
648
649impl ExplainPredicate {
650 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
651 match predicate {
652 Predicate::True => Self::True,
653 Predicate::False => Self::False,
654 Predicate::And(children) => {
655 Self::And(children.iter().map(Self::from_predicate).collect())
656 }
657 Predicate::Or(children) => {
658 Self::Or(children.iter().map(Self::from_predicate).collect())
659 }
660 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
661 Predicate::Compare(compare) => Self::from_compare(compare),
662 Predicate::CompareFields(compare) => Self::CompareFields {
663 left_field: compare.left_field().to_string(),
664 op: compare.op(),
665 right_field: compare.right_field().to_string(),
666 coercion: compare.coercion().clone(),
667 },
668 Predicate::IsNull { field } => Self::IsNull {
669 field: field.clone(),
670 },
671 Predicate::IsNotNull { field } => Self::IsNotNull {
672 field: field.clone(),
673 },
674 Predicate::IsMissing { field } => Self::IsMissing {
675 field: field.clone(),
676 },
677 Predicate::IsEmpty { field } => Self::IsEmpty {
678 field: field.clone(),
679 },
680 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
681 field: field.clone(),
682 },
683 Predicate::TextContains { field, value } => Self::TextContains {
684 field: field.clone(),
685 value: value.clone(),
686 },
687 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
688 field: field.clone(),
689 value: value.clone(),
690 },
691 }
692 }
693
694 fn from_compare(compare: &ComparePredicate) -> Self {
695 Self::Compare {
696 field: compare.field.clone(),
697 op: compare.op,
698 value: compare.value.clone(),
699 coercion: compare.coercion.clone(),
700 }
701 }
702}
703
704fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
705 let Some(order) = order else {
706 return ExplainOrderBy::None;
707 };
708
709 if order.fields.is_empty() {
710 return ExplainOrderBy::None;
711 }
712
713 ExplainOrderBy::Fields(
714 order
715 .fields
716 .iter()
717 .map(|term| ExplainOrder {
718 field: term.rendered_label(),
719 direction: term.direction(),
720 })
721 .collect(),
722 )
723}
724
725const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
726 match page {
727 Some(page) => ExplainPagination::Page {
728 limit: page.limit,
729 offset: page.offset,
730 },
731 None => ExplainPagination::None,
732 }
733}
734
735const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
736 match limit {
737 Some(limit) if limit.offset == 0 => match limit.limit {
738 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
739 None => ExplainDeleteLimit::Window {
740 limit: None,
741 offset: 0,
742 },
743 },
744 Some(limit) => ExplainDeleteLimit::Window {
745 limit: limit.limit,
746 offset: limit.offset,
747 },
748 None => ExplainDeleteLimit::None,
749 }
750}
751
752fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
753 let mut object = JsonWriter::begin_object(out);
754 object.field_with("mode", |out| {
755 let mut object = JsonWriter::begin_object(out);
756 match explain.mode() {
757 QueryMode::Load(spec) => {
758 object.field_str("type", "Load");
759 match spec.limit() {
760 Some(limit) => object.field_u64("limit", u64::from(limit)),
761 None => object.field_null("limit"),
762 }
763 object.field_u64("offset", u64::from(spec.offset()));
764 }
765 QueryMode::Delete(spec) => {
766 object.field_str("type", "Delete");
767 match spec.limit() {
768 Some(limit) => object.field_u64("limit", u64::from(limit)),
769 None => object.field_null("limit"),
770 }
771 }
772 }
773 object.finish();
774 });
775 object.field_with("access", |out| write_access_json(explain.access(), out));
776 match explain.filter_expr() {
777 Some(filter_expr) => object.field_str("filter_expr", filter_expr),
778 None => object.field_null("filter_expr"),
779 }
780 object.field_value_debug("predicate", explain.predicate());
781 object.field_value_debug("order_by", explain.order_by());
782 object.field_bool("distinct", explain.distinct());
783 object.field_value_debug("grouping", explain.grouping());
784 object.field_value_debug("order_pushdown", explain.order_pushdown());
785 object.field_with("page", |out| {
786 let mut object = JsonWriter::begin_object(out);
787 match explain.page() {
788 ExplainPagination::None => {
789 object.field_str("type", "None");
790 }
791 ExplainPagination::Page { limit, offset } => {
792 object.field_str("type", "Page");
793 match limit {
794 Some(limit) => object.field_u64("limit", u64::from(*limit)),
795 None => object.field_null("limit"),
796 }
797 object.field_u64("offset", u64::from(*offset));
798 }
799 }
800 object.finish();
801 });
802 object.field_with("delete_limit", |out| {
803 let mut object = JsonWriter::begin_object(out);
804 match explain.delete_limit() {
805 ExplainDeleteLimit::None => {
806 object.field_str("type", "None");
807 }
808 ExplainDeleteLimit::Limit { max_rows } => {
809 object.field_str("type", "Limit");
810 object.field_u64("max_rows", u64::from(*max_rows));
811 }
812 ExplainDeleteLimit::Window { limit, offset } => {
813 object.field_str("type", "Window");
814 object.field_with("limit", |out| match limit {
815 Some(limit) => out.push_str(&limit.to_string()),
816 None => out.push_str("null"),
817 });
818 object.field_u64("offset", u64::from(*offset));
819 }
820 }
821 object.finish();
822 });
823 object.field_value_debug("consistency", &explain.consistency());
824 object.finish();
825}