1use crate::{
8 db::{
9 access::{
10 AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
11 SecondaryOrderPushdownRejection,
12 },
13 predicate::{CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate},
14 query::{
15 builder::scalar_projection::render_scalar_projection_expr_sql_label,
16 explain::{
17 access_projection::write_access_json, explain_access_plan, writer::JsonWriter,
18 },
19 plan::{
20 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupedPlanFallbackReason,
21 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
22 expr::Expr, grouped_plan_strategy, render_scalar_filter_expr_sql_label,
23 },
24 },
25 },
26 traits::FieldValue,
27 value::Value,
28};
29use std::ops::Bound;
30
31#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ExplainPlan {
39 pub(crate) mode: QueryMode,
40 pub(crate) access: ExplainAccessPath,
41 pub(crate) filter_expr: Option<String>,
42 filter_expr_model: Option<Expr>,
43 pub(crate) predicate: ExplainPredicate,
44 predicate_model: Option<Predicate>,
45 pub(crate) order_by: ExplainOrderBy,
46 pub(crate) distinct: bool,
47 pub(crate) grouping: ExplainGrouping,
48 pub(crate) order_pushdown: ExplainOrderPushdown,
49 pub(crate) page: ExplainPagination,
50 pub(crate) delete_limit: ExplainDeleteLimit,
51 pub(crate) consistency: MissingRowPolicy,
52}
53
54impl ExplainPlan {
55 #[must_use]
57 pub const fn mode(&self) -> QueryMode {
58 self.mode
59 }
60
61 #[must_use]
63 pub const fn access(&self) -> &ExplainAccessPath {
64 &self.access
65 }
66
67 #[must_use]
69 pub fn filter_expr(&self) -> Option<&str> {
70 self.filter_expr.as_deref()
71 }
72
73 #[must_use]
75 pub(crate) fn filter_expr_model_for_hash(&self) -> Option<&Expr> {
76 if let Some(filter_expr_model) = &self.filter_expr_model {
77 debug_assert_eq!(
78 self.filter_expr(),
79 Some(render_scalar_filter_expr_sql_label(filter_expr_model).as_str()),
80 "explain scalar filter label drifted from canonical filter model"
81 );
82 Some(filter_expr_model)
83 } else {
84 debug_assert!(
85 self.filter_expr.is_none(),
86 "missing canonical filter model requires filter_expr=None"
87 );
88 None
89 }
90 }
91
92 #[must_use]
94 pub const fn predicate(&self) -> &ExplainPredicate {
95 &self.predicate
96 }
97
98 #[must_use]
100 pub const fn order_by(&self) -> &ExplainOrderBy {
101 &self.order_by
102 }
103
104 #[must_use]
106 pub const fn distinct(&self) -> bool {
107 self.distinct
108 }
109
110 #[must_use]
112 pub const fn grouping(&self) -> &ExplainGrouping {
113 &self.grouping
114 }
115
116 #[must_use]
118 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
119 &self.order_pushdown
120 }
121
122 #[must_use]
124 pub const fn page(&self) -> &ExplainPagination {
125 &self.page
126 }
127
128 #[must_use]
130 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
131 &self.delete_limit
132 }
133
134 #[must_use]
136 pub const fn consistency(&self) -> MissingRowPolicy {
137 self.consistency
138 }
139}
140
141impl ExplainPlan {
142 #[must_use]
148 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
149 if let Some(predicate) = &self.predicate_model {
150 debug_assert_eq!(
151 self.predicate,
152 ExplainPredicate::from_predicate(predicate),
153 "explain predicate surface drifted from canonical predicate model"
154 );
155 Some(predicate)
156 } else {
157 debug_assert!(
158 matches!(self.predicate, ExplainPredicate::None),
159 "missing canonical predicate model requires ExplainPredicate::None"
160 );
161 None
162 }
163 }
164
165 #[must_use]
170 pub fn render_text_canonical(&self) -> String {
171 format!(
172 concat!(
173 "mode={:?}\n",
174 "access={:?}\n",
175 "filter_expr={:?}\n",
176 "predicate={:?}\n",
177 "order_by={:?}\n",
178 "distinct={}\n",
179 "grouping={:?}\n",
180 "order_pushdown={:?}\n",
181 "page={:?}\n",
182 "delete_limit={:?}\n",
183 "consistency={:?}",
184 ),
185 self.mode(),
186 self.access(),
187 self.filter_expr(),
188 self.predicate(),
189 self.order_by(),
190 self.distinct(),
191 self.grouping(),
192 self.order_pushdown(),
193 self.page(),
194 self.delete_limit(),
195 self.consistency(),
196 )
197 }
198
199 #[must_use]
201 pub fn render_json_canonical(&self) -> String {
202 let mut out = String::new();
203 write_logical_explain_json(self, &mut out);
204
205 out
206 }
207}
208
209#[derive(Clone, Debug, Eq, PartialEq)]
216pub enum ExplainGrouping {
217 None,
218 Grouped {
219 strategy: &'static str,
220 fallback_reason: Option<&'static str>,
221 group_fields: Vec<ExplainGroupField>,
222 aggregates: Vec<ExplainGroupAggregate>,
223 having: Option<ExplainGroupHaving>,
224 max_groups: u64,
225 max_group_bytes: u64,
226 },
227}
228
229#[derive(Clone, Debug, Eq, PartialEq)]
236pub struct ExplainGroupField {
237 pub(crate) slot_index: usize,
238 pub(crate) field: String,
239}
240
241impl ExplainGroupField {
242 #[must_use]
244 pub const fn slot_index(&self) -> usize {
245 self.slot_index
246 }
247
248 #[must_use]
250 pub const fn field(&self) -> &str {
251 self.field.as_str()
252 }
253}
254
255#[derive(Clone, Debug, Eq, PartialEq)]
262pub struct ExplainGroupAggregate {
263 pub(crate) kind: AggregateKind,
264 pub(crate) target_field: Option<String>,
265 pub(crate) input_expr: Option<String>,
266 pub(crate) filter_expr: Option<String>,
267 pub(crate) distinct: bool,
268}
269
270impl ExplainGroupAggregate {
271 #[must_use]
273 pub const fn kind(&self) -> AggregateKind {
274 self.kind
275 }
276
277 #[must_use]
279 pub fn target_field(&self) -> Option<&str> {
280 self.target_field.as_deref()
281 }
282
283 #[must_use]
285 pub fn input_expr(&self) -> Option<&str> {
286 self.input_expr.as_deref()
287 }
288
289 #[must_use]
291 pub fn filter_expr(&self) -> Option<&str> {
292 self.filter_expr.as_deref()
293 }
294
295 #[must_use]
297 pub const fn distinct(&self) -> bool {
298 self.distinct
299 }
300}
301
302#[derive(Clone, Debug, Eq, PartialEq)]
311pub struct ExplainGroupHaving {
312 pub(crate) expr: Expr,
313}
314
315impl ExplainGroupHaving {
316 #[must_use]
318 pub(in crate::db) const fn expr(&self) -> &Expr {
319 &self.expr
320 }
321}
322
323#[derive(Clone, Debug, Eq, PartialEq)]
330pub enum ExplainOrderPushdown {
331 MissingModelContext,
332 EligibleSecondaryIndex {
333 index: &'static str,
334 prefix_len: usize,
335 },
336 Rejected(SecondaryOrderPushdownRejection),
337}
338
339#[derive(Clone, Debug, Eq, PartialEq)]
347pub enum ExplainAccessPath {
348 ByKey {
349 key: Value,
350 },
351 ByKeys {
352 keys: Vec<Value>,
353 },
354 KeyRange {
355 start: Value,
356 end: Value,
357 },
358 IndexPrefix {
359 name: &'static str,
360 fields: Vec<&'static str>,
361 prefix_len: usize,
362 values: Vec<Value>,
363 },
364 IndexMultiLookup {
365 name: &'static str,
366 fields: Vec<&'static str>,
367 values: Vec<Value>,
368 },
369 IndexRange {
370 name: &'static str,
371 fields: Vec<&'static str>,
372 prefix_len: usize,
373 prefix: Vec<Value>,
374 lower: Bound<Value>,
375 upper: Bound<Value>,
376 },
377 FullScan,
378 Union(Vec<Self>),
379 Intersection(Vec<Self>),
380}
381
382#[derive(Clone, Debug, Eq, PartialEq)]
390pub enum ExplainPredicate {
391 None,
392 True,
393 False,
394 And(Vec<Self>),
395 Or(Vec<Self>),
396 Not(Box<Self>),
397 Compare {
398 field: String,
399 op: CompareOp,
400 value: Value,
401 coercion: CoercionSpec,
402 },
403 CompareFields {
404 left_field: String,
405 op: CompareOp,
406 right_field: String,
407 coercion: CoercionSpec,
408 },
409 IsNull {
410 field: String,
411 },
412 IsNotNull {
413 field: String,
414 },
415 IsMissing {
416 field: String,
417 },
418 IsEmpty {
419 field: String,
420 },
421 IsNotEmpty {
422 field: String,
423 },
424 TextContains {
425 field: String,
426 value: Value,
427 },
428 TextContainsCi {
429 field: String,
430 value: Value,
431 },
432}
433
434#[derive(Clone, Debug, Eq, PartialEq)]
441pub enum ExplainOrderBy {
442 None,
443 Fields(Vec<ExplainOrder>),
444}
445
446#[derive(Clone, Debug, Eq, PartialEq)]
453pub struct ExplainOrder {
454 pub(crate) field: String,
455 pub(crate) direction: OrderDirection,
456}
457
458impl ExplainOrder {
459 #[must_use]
461 pub const fn field(&self) -> &str {
462 self.field.as_str()
463 }
464
465 #[must_use]
467 pub const fn direction(&self) -> OrderDirection {
468 self.direction
469 }
470}
471
472#[derive(Clone, Debug, Eq, PartialEq)]
479pub enum ExplainPagination {
480 None,
481 Page { limit: Option<u32>, offset: u32 },
482}
483
484#[derive(Clone, Debug, Eq, PartialEq)]
491pub enum ExplainDeleteLimit {
492 None,
493 Limit { max_rows: u32 },
494 Window { limit: Option<u32>, offset: u32 },
495}
496
497impl AccessPlannedQuery {
498 #[must_use]
500 pub(crate) fn explain(&self) -> ExplainPlan {
501 self.explain_inner()
502 }
503
504 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
505 let (logical, grouping) = match &self.logical {
507 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
508 LogicalPlan::Grouped(logical) => {
509 let grouped_strategy = grouped_plan_strategy(self).expect(
510 "grouped logical explain projection requires planner-owned grouped strategy",
511 );
512
513 (
514 &logical.scalar,
515 ExplainGrouping::Grouped {
516 strategy: grouped_strategy.code(),
517 fallback_reason: grouped_strategy
518 .fallback_reason()
519 .map(GroupedPlanFallbackReason::code),
520 group_fields: logical
521 .group
522 .group_fields
523 .iter()
524 .map(|field_slot| ExplainGroupField {
525 slot_index: field_slot.index(),
526 field: field_slot.field().to_string(),
527 })
528 .collect(),
529 aggregates: logical
530 .group
531 .aggregates
532 .iter()
533 .map(|aggregate| ExplainGroupAggregate {
534 kind: aggregate.kind,
535 target_field: aggregate.target_field().map(str::to_string),
536 input_expr: aggregate
537 .input_expr()
538 .map(render_scalar_projection_expr_sql_label),
539 filter_expr: aggregate
540 .filter_expr()
541 .map(render_scalar_projection_expr_sql_label),
542 distinct: aggregate.distinct,
543 })
544 .collect(),
545 having: explain_group_having(logical),
546 max_groups: logical.group.execution.max_groups(),
547 max_group_bytes: logical.group.execution.max_group_bytes(),
548 },
549 )
550 }
551 };
552
553 explain_scalar_inner(logical, grouping, &self.access)
555 }
556}
557
558fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
559 let expr = logical.effective_having_expr()?;
560
561 Some(ExplainGroupHaving {
562 expr: expr.into_owned(),
563 })
564}
565
566fn explain_scalar_inner<K>(
567 logical: &ScalarPlan,
568 grouping: ExplainGrouping,
569 access: &AccessPlan<K>,
570) -> ExplainPlan
571where
572 K: FieldValue,
573{
574 let filter_expr = logical
576 .filter_expr
577 .as_ref()
578 .map(render_scalar_filter_expr_sql_label);
579 let filter_expr_model = logical.filter_expr.clone();
580 let predicate_model = logical.predicate.clone();
581 let predicate = match &predicate_model {
582 Some(predicate) => ExplainPredicate::from_predicate(predicate),
583 None => ExplainPredicate::None,
584 };
585
586 let order_by = explain_order(logical.order.as_ref());
588 let order_pushdown = explain_order_pushdown();
589 let page = explain_page(logical.page.as_ref());
590 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
591
592 ExplainPlan {
594 mode: logical.mode,
595 access: explain_access_plan(access),
596 filter_expr,
597 filter_expr_model,
598 predicate,
599 predicate_model,
600 order_by,
601 distinct: logical.distinct,
602 grouping,
603 order_pushdown,
604 page,
605 delete_limit,
606 consistency: logical.consistency,
607 }
608}
609
610const fn explain_order_pushdown() -> ExplainOrderPushdown {
611 ExplainOrderPushdown::MissingModelContext
613}
614
615impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
616 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
617 Self::from(PushdownSurfaceEligibility::from(&value))
618 }
619}
620
621impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
622 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
623 match value {
624 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
625 Self::EligibleSecondaryIndex { index, prefix_len }
626 }
627 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
628 }
629 }
630}
631
632impl ExplainPredicate {
633 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
634 match predicate {
635 Predicate::True => Self::True,
636 Predicate::False => Self::False,
637 Predicate::And(children) => {
638 Self::And(children.iter().map(Self::from_predicate).collect())
639 }
640 Predicate::Or(children) => {
641 Self::Or(children.iter().map(Self::from_predicate).collect())
642 }
643 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
644 Predicate::Compare(compare) => Self::from_compare(compare),
645 Predicate::CompareFields(compare) => Self::CompareFields {
646 left_field: compare.left_field().to_string(),
647 op: compare.op(),
648 right_field: compare.right_field().to_string(),
649 coercion: compare.coercion().clone(),
650 },
651 Predicate::IsNull { field } => Self::IsNull {
652 field: field.clone(),
653 },
654 Predicate::IsNotNull { field } => Self::IsNotNull {
655 field: field.clone(),
656 },
657 Predicate::IsMissing { field } => Self::IsMissing {
658 field: field.clone(),
659 },
660 Predicate::IsEmpty { field } => Self::IsEmpty {
661 field: field.clone(),
662 },
663 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
664 field: field.clone(),
665 },
666 Predicate::TextContains { field, value } => Self::TextContains {
667 field: field.clone(),
668 value: value.clone(),
669 },
670 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
671 field: field.clone(),
672 value: value.clone(),
673 },
674 }
675 }
676
677 fn from_compare(compare: &ComparePredicate) -> Self {
678 Self::Compare {
679 field: compare.field.clone(),
680 op: compare.op,
681 value: compare.value.clone(),
682 coercion: compare.coercion.clone(),
683 }
684 }
685}
686
687fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
688 let Some(order) = order else {
689 return ExplainOrderBy::None;
690 };
691
692 if order.fields.is_empty() {
693 return ExplainOrderBy::None;
694 }
695
696 ExplainOrderBy::Fields(
697 order
698 .fields
699 .iter()
700 .map(|term| ExplainOrder {
701 field: term.rendered_label(),
702 direction: term.direction(),
703 })
704 .collect(),
705 )
706}
707
708const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
709 match page {
710 Some(page) => ExplainPagination::Page {
711 limit: page.limit,
712 offset: page.offset,
713 },
714 None => ExplainPagination::None,
715 }
716}
717
718const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
719 match limit {
720 Some(limit) if limit.offset == 0 => match limit.limit {
721 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
722 None => ExplainDeleteLimit::Window {
723 limit: None,
724 offset: 0,
725 },
726 },
727 Some(limit) => ExplainDeleteLimit::Window {
728 limit: limit.limit,
729 offset: limit.offset,
730 },
731 None => ExplainDeleteLimit::None,
732 }
733}
734
735fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
736 let mut object = JsonWriter::begin_object(out);
737 object.field_with("mode", |out| {
738 let mut object = JsonWriter::begin_object(out);
739 match explain.mode() {
740 QueryMode::Load(spec) => {
741 object.field_str("type", "Load");
742 match spec.limit() {
743 Some(limit) => object.field_u64("limit", u64::from(limit)),
744 None => object.field_null("limit"),
745 }
746 object.field_u64("offset", u64::from(spec.offset()));
747 }
748 QueryMode::Delete(spec) => {
749 object.field_str("type", "Delete");
750 match spec.limit() {
751 Some(limit) => object.field_u64("limit", u64::from(limit)),
752 None => object.field_null("limit"),
753 }
754 }
755 }
756 object.finish();
757 });
758 object.field_with("access", |out| write_access_json(explain.access(), out));
759 match explain.filter_expr() {
760 Some(filter_expr) => object.field_str("filter_expr", filter_expr),
761 None => object.field_null("filter_expr"),
762 }
763 object.field_value_debug("predicate", explain.predicate());
764 object.field_value_debug("order_by", explain.order_by());
765 object.field_bool("distinct", explain.distinct());
766 object.field_value_debug("grouping", explain.grouping());
767 object.field_value_debug("order_pushdown", explain.order_pushdown());
768 object.field_with("page", |out| {
769 let mut object = JsonWriter::begin_object(out);
770 match explain.page() {
771 ExplainPagination::None => {
772 object.field_str("type", "None");
773 }
774 ExplainPagination::Page { limit, offset } => {
775 object.field_str("type", "Page");
776 match limit {
777 Some(limit) => object.field_u64("limit", u64::from(*limit)),
778 None => object.field_null("limit"),
779 }
780 object.field_u64("offset", u64::from(*offset));
781 }
782 }
783 object.finish();
784 });
785 object.field_with("delete_limit", |out| {
786 let mut object = JsonWriter::begin_object(out);
787 match explain.delete_limit() {
788 ExplainDeleteLimit::None => {
789 object.field_str("type", "None");
790 }
791 ExplainDeleteLimit::Limit { max_rows } => {
792 object.field_str("type", "Limit");
793 object.field_u64("max_rows", u64::from(*max_rows));
794 }
795 ExplainDeleteLimit::Window { limit, offset } => {
796 object.field_str("type", "Window");
797 object.field_with("limit", |out| match limit {
798 Some(limit) => out.push_str(&limit.to_string()),
799 None => out.push_str("null"),
800 });
801 object.field_u64("offset", u64::from(*offset));
802 }
803 }
804 object.finish();
805 });
806 object.field_value_debug("consistency", &explain.consistency());
807 object.finish();
808}