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,
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 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 const fn predicate(&self) -> &ExplainPredicate {
73 &self.predicate
74 }
75
76 #[must_use]
78 pub const fn order_by(&self) -> &ExplainOrderBy {
79 &self.order_by
80 }
81
82 #[must_use]
84 pub const fn distinct(&self) -> bool {
85 self.distinct
86 }
87
88 #[must_use]
90 pub const fn grouping(&self) -> &ExplainGrouping {
91 &self.grouping
92 }
93
94 #[must_use]
96 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
97 &self.order_pushdown
98 }
99
100 #[must_use]
102 pub const fn page(&self) -> &ExplainPagination {
103 &self.page
104 }
105
106 #[must_use]
108 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
109 &self.delete_limit
110 }
111
112 #[must_use]
114 pub const fn consistency(&self) -> MissingRowPolicy {
115 self.consistency
116 }
117}
118
119impl ExplainPlan {
120 #[must_use]
124 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
125 if let Some(predicate) = &self.predicate_model {
126 debug_assert_eq!(
127 self.predicate,
128 ExplainPredicate::from_predicate(predicate),
129 "explain predicate surface drifted from canonical predicate model"
130 );
131 Some(predicate)
132 } else {
133 debug_assert!(
134 matches!(self.predicate, ExplainPredicate::None),
135 "missing canonical predicate model requires ExplainPredicate::None"
136 );
137 None
138 }
139 }
140
141 #[must_use]
146 pub fn render_text_canonical(&self) -> String {
147 format!(
148 concat!(
149 "mode={:?}\n",
150 "access={:?}\n",
151 "filter_expr={:?}\n",
152 "predicate={:?}\n",
153 "order_by={:?}\n",
154 "distinct={}\n",
155 "grouping={:?}\n",
156 "order_pushdown={:?}\n",
157 "page={:?}\n",
158 "delete_limit={:?}\n",
159 "consistency={:?}",
160 ),
161 self.mode(),
162 self.access(),
163 self.filter_expr(),
164 self.predicate(),
165 self.order_by(),
166 self.distinct(),
167 self.grouping(),
168 self.order_pushdown(),
169 self.page(),
170 self.delete_limit(),
171 self.consistency(),
172 )
173 }
174
175 #[must_use]
177 pub fn render_json_canonical(&self) -> String {
178 let mut out = String::new();
179 write_logical_explain_json(self, &mut out);
180
181 out
182 }
183}
184
185#[derive(Clone, Debug, Eq, PartialEq)]
192pub enum ExplainGrouping {
193 None,
194 Grouped {
195 strategy: &'static str,
196 fallback_reason: Option<&'static str>,
197 group_fields: Vec<ExplainGroupField>,
198 aggregates: Vec<ExplainGroupAggregate>,
199 having: Option<ExplainGroupHaving>,
200 max_groups: u64,
201 max_group_bytes: u64,
202 },
203}
204
205#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct ExplainGroupField {
213 pub(crate) slot_index: usize,
214 pub(crate) field: String,
215}
216
217impl ExplainGroupField {
218 #[must_use]
220 pub const fn slot_index(&self) -> usize {
221 self.slot_index
222 }
223
224 #[must_use]
226 pub const fn field(&self) -> &str {
227 self.field.as_str()
228 }
229}
230
231#[derive(Clone, Debug, Eq, PartialEq)]
238pub struct ExplainGroupAggregate {
239 pub(crate) kind: AggregateKind,
240 pub(crate) target_field: Option<String>,
241 pub(crate) input_expr: Option<String>,
242 pub(crate) filter_expr: Option<String>,
243 pub(crate) distinct: bool,
244}
245
246impl ExplainGroupAggregate {
247 #[must_use]
249 pub const fn kind(&self) -> AggregateKind {
250 self.kind
251 }
252
253 #[must_use]
255 pub fn target_field(&self) -> Option<&str> {
256 self.target_field.as_deref()
257 }
258
259 #[must_use]
261 pub fn input_expr(&self) -> Option<&str> {
262 self.input_expr.as_deref()
263 }
264
265 #[must_use]
267 pub fn filter_expr(&self) -> Option<&str> {
268 self.filter_expr.as_deref()
269 }
270
271 #[must_use]
273 pub const fn distinct(&self) -> bool {
274 self.distinct
275 }
276}
277
278#[derive(Clone, Debug, Eq, PartialEq)]
287pub struct ExplainGroupHaving {
288 pub(crate) expr: Expr,
289}
290
291impl ExplainGroupHaving {
292 #[must_use]
294 pub(in crate::db) const fn expr(&self) -> &Expr {
295 &self.expr
296 }
297}
298
299#[derive(Clone, Debug, Eq, PartialEq)]
306pub enum ExplainOrderPushdown {
307 MissingModelContext,
308 EligibleSecondaryIndex {
309 index: &'static str,
310 prefix_len: usize,
311 },
312 Rejected(SecondaryOrderPushdownRejection),
313}
314
315#[derive(Clone, Debug, Eq, PartialEq)]
323pub enum ExplainAccessPath {
324 ByKey {
325 key: Value,
326 },
327 ByKeys {
328 keys: Vec<Value>,
329 },
330 KeyRange {
331 start: Value,
332 end: Value,
333 },
334 IndexPrefix {
335 name: &'static str,
336 fields: Vec<&'static str>,
337 prefix_len: usize,
338 values: Vec<Value>,
339 },
340 IndexMultiLookup {
341 name: &'static str,
342 fields: Vec<&'static str>,
343 values: Vec<Value>,
344 },
345 IndexRange {
346 name: &'static str,
347 fields: Vec<&'static str>,
348 prefix_len: usize,
349 prefix: Vec<Value>,
350 lower: Bound<Value>,
351 upper: Bound<Value>,
352 },
353 FullScan,
354 Union(Vec<Self>),
355 Intersection(Vec<Self>),
356}
357
358#[derive(Clone, Debug, Eq, PartialEq)]
366pub enum ExplainPredicate {
367 None,
368 True,
369 False,
370 And(Vec<Self>),
371 Or(Vec<Self>),
372 Not(Box<Self>),
373 Compare {
374 field: String,
375 op: CompareOp,
376 value: Value,
377 coercion: CoercionSpec,
378 },
379 CompareFields {
380 left_field: String,
381 op: CompareOp,
382 right_field: String,
383 coercion: CoercionSpec,
384 },
385 IsNull {
386 field: String,
387 },
388 IsNotNull {
389 field: String,
390 },
391 IsMissing {
392 field: String,
393 },
394 IsEmpty {
395 field: String,
396 },
397 IsNotEmpty {
398 field: String,
399 },
400 TextContains {
401 field: String,
402 value: Value,
403 },
404 TextContainsCi {
405 field: String,
406 value: Value,
407 },
408}
409
410#[derive(Clone, Debug, Eq, PartialEq)]
417pub enum ExplainOrderBy {
418 None,
419 Fields(Vec<ExplainOrder>),
420}
421
422#[derive(Clone, Debug, Eq, PartialEq)]
429pub struct ExplainOrder {
430 pub(crate) field: String,
431 pub(crate) direction: OrderDirection,
432}
433
434impl ExplainOrder {
435 #[must_use]
437 pub const fn field(&self) -> &str {
438 self.field.as_str()
439 }
440
441 #[must_use]
443 pub const fn direction(&self) -> OrderDirection {
444 self.direction
445 }
446}
447
448#[derive(Clone, Debug, Eq, PartialEq)]
455pub enum ExplainPagination {
456 None,
457 Page { limit: Option<u32>, offset: u32 },
458}
459
460#[derive(Clone, Debug, Eq, PartialEq)]
467pub enum ExplainDeleteLimit {
468 None,
469 Limit { max_rows: u32 },
470 Window { limit: Option<u32>, offset: u32 },
471}
472
473impl AccessPlannedQuery {
474 #[must_use]
476 pub(crate) fn explain(&self) -> ExplainPlan {
477 self.explain_inner()
478 }
479
480 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
481 let (logical, grouping) = match &self.logical {
483 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
484 LogicalPlan::Grouped(logical) => {
485 let grouped_strategy = grouped_plan_strategy(self).expect(
486 "grouped logical explain projection requires planner-owned grouped strategy",
487 );
488
489 (
490 &logical.scalar,
491 ExplainGrouping::Grouped {
492 strategy: grouped_strategy.code(),
493 fallback_reason: grouped_strategy
494 .fallback_reason()
495 .map(GroupedPlanFallbackReason::code),
496 group_fields: logical
497 .group
498 .group_fields
499 .iter()
500 .map(|field_slot| ExplainGroupField {
501 slot_index: field_slot.index(),
502 field: field_slot.field().to_string(),
503 })
504 .collect(),
505 aggregates: logical
506 .group
507 .aggregates
508 .iter()
509 .map(|aggregate| ExplainGroupAggregate {
510 kind: aggregate.kind,
511 target_field: aggregate.target_field().map(str::to_string),
512 input_expr: aggregate
513 .input_expr()
514 .map(render_scalar_projection_expr_sql_label),
515 filter_expr: aggregate
516 .filter_expr()
517 .map(render_scalar_projection_expr_sql_label),
518 distinct: aggregate.distinct,
519 })
520 .collect(),
521 having: explain_group_having(logical),
522 max_groups: logical.group.execution.max_groups(),
523 max_group_bytes: logical.group.execution.max_group_bytes(),
524 },
525 )
526 }
527 };
528
529 explain_scalar_inner(logical, grouping, &self.access)
531 }
532}
533
534fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
535 let expr = logical.effective_having_expr()?;
536
537 Some(ExplainGroupHaving {
538 expr: expr.into_owned(),
539 })
540}
541
542fn explain_scalar_inner<K>(
543 logical: &ScalarPlan,
544 grouping: ExplainGrouping,
545 access: &AccessPlan<K>,
546) -> ExplainPlan
547where
548 K: FieldValue,
549{
550 let filter_expr = logical
552 .filter_expr
553 .as_ref()
554 .map(render_scalar_projection_expr_sql_label);
555 let predicate_model = logical.predicate.clone();
556 let predicate = match &predicate_model {
557 Some(predicate) => ExplainPredicate::from_predicate(predicate),
558 None => ExplainPredicate::None,
559 };
560
561 let order_by = explain_order(logical.order.as_ref());
563 let order_pushdown = explain_order_pushdown();
564 let page = explain_page(logical.page.as_ref());
565 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
566
567 ExplainPlan {
569 mode: logical.mode,
570 access: ExplainAccessPath::from_access_plan(access),
571 filter_expr,
572 predicate,
573 predicate_model,
574 order_by,
575 distinct: logical.distinct,
576 grouping,
577 order_pushdown,
578 page,
579 delete_limit,
580 consistency: logical.consistency,
581 }
582}
583
584const fn explain_order_pushdown() -> ExplainOrderPushdown {
585 ExplainOrderPushdown::MissingModelContext
587}
588
589impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
590 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
591 Self::from(PushdownSurfaceEligibility::from(&value))
592 }
593}
594
595impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
596 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
597 match value {
598 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
599 Self::EligibleSecondaryIndex { index, prefix_len }
600 }
601 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
602 }
603 }
604}
605
606impl ExplainPredicate {
607 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
608 match predicate {
609 Predicate::True => Self::True,
610 Predicate::False => Self::False,
611 Predicate::And(children) => {
612 Self::And(children.iter().map(Self::from_predicate).collect())
613 }
614 Predicate::Or(children) => {
615 Self::Or(children.iter().map(Self::from_predicate).collect())
616 }
617 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
618 Predicate::Compare(compare) => Self::from_compare(compare),
619 Predicate::CompareFields(compare) => Self::CompareFields {
620 left_field: compare.left_field().to_string(),
621 op: compare.op(),
622 right_field: compare.right_field().to_string(),
623 coercion: compare.coercion().clone(),
624 },
625 Predicate::IsNull { field } => Self::IsNull {
626 field: field.clone(),
627 },
628 Predicate::IsNotNull { field } => Self::IsNotNull {
629 field: field.clone(),
630 },
631 Predicate::IsMissing { field } => Self::IsMissing {
632 field: field.clone(),
633 },
634 Predicate::IsEmpty { field } => Self::IsEmpty {
635 field: field.clone(),
636 },
637 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
638 field: field.clone(),
639 },
640 Predicate::TextContains { field, value } => Self::TextContains {
641 field: field.clone(),
642 value: value.clone(),
643 },
644 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
645 field: field.clone(),
646 value: value.clone(),
647 },
648 }
649 }
650
651 fn from_compare(compare: &ComparePredicate) -> Self {
652 Self::Compare {
653 field: compare.field.clone(),
654 op: compare.op,
655 value: compare.value.clone(),
656 coercion: compare.coercion.clone(),
657 }
658 }
659}
660
661fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
662 let Some(order) = order else {
663 return ExplainOrderBy::None;
664 };
665
666 if order.fields.is_empty() {
667 return ExplainOrderBy::None;
668 }
669
670 ExplainOrderBy::Fields(
671 order
672 .fields
673 .iter()
674 .map(|term| ExplainOrder {
675 field: term.rendered_label(),
676 direction: term.direction(),
677 })
678 .collect(),
679 )
680}
681
682const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
683 match page {
684 Some(page) => ExplainPagination::Page {
685 limit: page.limit,
686 offset: page.offset,
687 },
688 None => ExplainPagination::None,
689 }
690}
691
692const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
693 match limit {
694 Some(limit) if limit.offset == 0 => match limit.limit {
695 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
696 None => ExplainDeleteLimit::Window {
697 limit: None,
698 offset: 0,
699 },
700 },
701 Some(limit) => ExplainDeleteLimit::Window {
702 limit: limit.limit,
703 offset: limit.offset,
704 },
705 None => ExplainDeleteLimit::None,
706 }
707}
708
709fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
710 let mut object = JsonWriter::begin_object(out);
711 object.field_with("mode", |out| {
712 let mut object = JsonWriter::begin_object(out);
713 match explain.mode() {
714 QueryMode::Load(spec) => {
715 object.field_str("type", "Load");
716 match spec.limit() {
717 Some(limit) => object.field_u64("limit", u64::from(limit)),
718 None => object.field_null("limit"),
719 }
720 object.field_u64("offset", u64::from(spec.offset()));
721 }
722 QueryMode::Delete(spec) => {
723 object.field_str("type", "Delete");
724 match spec.limit() {
725 Some(limit) => object.field_u64("limit", u64::from(limit)),
726 None => object.field_null("limit"),
727 }
728 }
729 }
730 object.finish();
731 });
732 object.field_with("access", |out| write_access_json(explain.access(), out));
733 match explain.filter_expr() {
734 Some(filter_expr) => object.field_str("filter_expr", filter_expr),
735 None => object.field_null("filter_expr"),
736 }
737 object.field_value_debug("predicate", explain.predicate());
738 object.field_value_debug("order_by", explain.order_by());
739 object.field_bool("distinct", explain.distinct());
740 object.field_value_debug("grouping", explain.grouping());
741 object.field_value_debug("order_pushdown", explain.order_pushdown());
742 object.field_with("page", |out| {
743 let mut object = JsonWriter::begin_object(out);
744 match explain.page() {
745 ExplainPagination::None => {
746 object.field_str("type", "None");
747 }
748 ExplainPagination::Page { limit, offset } => {
749 object.field_str("type", "Page");
750 match limit {
751 Some(limit) => object.field_u64("limit", u64::from(*limit)),
752 None => object.field_null("limit"),
753 }
754 object.field_u64("offset", u64::from(*offset));
755 }
756 }
757 object.finish();
758 });
759 object.field_with("delete_limit", |out| {
760 let mut object = JsonWriter::begin_object(out);
761 match explain.delete_limit() {
762 ExplainDeleteLimit::None => {
763 object.field_str("type", "None");
764 }
765 ExplainDeleteLimit::Limit { max_rows } => {
766 object.field_str("type", "Limit");
767 object.field_u64("max_rows", u64::from(*max_rows));
768 }
769 ExplainDeleteLimit::Window { limit, offset } => {
770 object.field_str("type", "Window");
771 object.field_with("limit", |out| match limit {
772 Some(limit) => out.push_str(&limit.to_string()),
773 None => out.push_str("null"),
774 });
775 object.field_u64("offset", u64::from(*offset));
776 }
777 }
778 object.finish();
779 });
780 object.field_value_debug("consistency", &explain.consistency());
781 object.finish();
782}