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