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