1use crate::{
7 db::{
8 access::{
9 AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
10 SecondaryOrderPushdownRejection,
11 },
12 predicate::{CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate},
13 query::{
14 builder::scalar_projection::render_scalar_projection_expr_sql_label,
15 explain::{access_projection::write_access_json, writer::JsonWriter},
16 plan::{
17 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupHavingExpr,
18 GroupHavingValueExpr, GroupedPlanFallbackReason, LogicalPlan, OrderDirection,
19 OrderSpec, PageSpec, QueryMode, ScalarPlan, grouped_plan_strategy,
20 },
21 },
22 },
23 traits::FieldValue,
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) predicate: ExplainPredicate,
39 predicate_model: Option<Predicate>,
40 pub(crate) order_by: ExplainOrderBy,
41 pub(crate) distinct: bool,
42 pub(crate) grouping: ExplainGrouping,
43 pub(crate) order_pushdown: ExplainOrderPushdown,
44 pub(crate) page: ExplainPagination,
45 pub(crate) delete_limit: ExplainDeleteLimit,
46 pub(crate) consistency: MissingRowPolicy,
47}
48
49impl ExplainPlan {
50 #[must_use]
52 pub const fn mode(&self) -> QueryMode {
53 self.mode
54 }
55
56 #[must_use]
58 pub const fn access(&self) -> &ExplainAccessPath {
59 &self.access
60 }
61
62 #[must_use]
64 pub const fn predicate(&self) -> &ExplainPredicate {
65 &self.predicate
66 }
67
68 #[must_use]
70 pub const fn order_by(&self) -> &ExplainOrderBy {
71 &self.order_by
72 }
73
74 #[must_use]
76 pub const fn distinct(&self) -> bool {
77 self.distinct
78 }
79
80 #[must_use]
82 pub const fn grouping(&self) -> &ExplainGrouping {
83 &self.grouping
84 }
85
86 #[must_use]
88 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
89 &self.order_pushdown
90 }
91
92 #[must_use]
94 pub const fn page(&self) -> &ExplainPagination {
95 &self.page
96 }
97
98 #[must_use]
100 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
101 &self.delete_limit
102 }
103
104 #[must_use]
106 pub const fn consistency(&self) -> MissingRowPolicy {
107 self.consistency
108 }
109}
110
111impl ExplainPlan {
112 #[must_use]
116 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
117 if let Some(predicate) = &self.predicate_model {
118 debug_assert_eq!(
119 self.predicate,
120 ExplainPredicate::from_predicate(predicate),
121 "explain predicate surface drifted from canonical predicate model"
122 );
123 Some(predicate)
124 } else {
125 debug_assert!(
126 matches!(self.predicate, ExplainPredicate::None),
127 "missing canonical predicate model requires ExplainPredicate::None"
128 );
129 None
130 }
131 }
132
133 #[must_use]
138 pub fn render_text_canonical(&self) -> String {
139 format!(
140 concat!(
141 "mode={:?}\n",
142 "access={:?}\n",
143 "predicate={:?}\n",
144 "order_by={:?}\n",
145 "distinct={}\n",
146 "grouping={:?}\n",
147 "order_pushdown={:?}\n",
148 "page={:?}\n",
149 "delete_limit={:?}\n",
150 "consistency={:?}",
151 ),
152 self.mode(),
153 self.access(),
154 self.predicate(),
155 self.order_by(),
156 self.distinct(),
157 self.grouping(),
158 self.order_pushdown(),
159 self.page(),
160 self.delete_limit(),
161 self.consistency(),
162 )
163 }
164
165 #[must_use]
167 pub fn render_json_canonical(&self) -> String {
168 let mut out = String::new();
169 write_logical_explain_json(self, &mut out);
170
171 out
172 }
173}
174
175#[expect(clippy::large_enum_variant)]
182#[derive(Clone, Debug, Eq, PartialEq)]
183pub enum ExplainGrouping {
184 None,
185 Grouped {
186 strategy: &'static str,
187 fallback_reason: Option<&'static str>,
188 group_fields: Vec<ExplainGroupField>,
189 aggregates: Vec<ExplainGroupAggregate>,
190 having: Option<ExplainGroupHaving>,
191 max_groups: u64,
192 max_group_bytes: u64,
193 },
194}
195
196#[derive(Clone, Debug, Eq, PartialEq)]
203pub struct ExplainGroupField {
204 pub(crate) slot_index: usize,
205 pub(crate) field: String,
206}
207
208impl ExplainGroupField {
209 #[must_use]
211 pub const fn slot_index(&self) -> usize {
212 self.slot_index
213 }
214
215 #[must_use]
217 pub const fn field(&self) -> &str {
218 self.field.as_str()
219 }
220}
221
222#[derive(Clone, Debug, Eq, PartialEq)]
229pub struct ExplainGroupAggregate {
230 pub(crate) kind: AggregateKind,
231 pub(crate) target_field: Option<String>,
232 pub(crate) input_expr: Option<String>,
233 pub(crate) distinct: bool,
234}
235
236impl ExplainGroupAggregate {
237 #[must_use]
239 pub const fn kind(&self) -> AggregateKind {
240 self.kind
241 }
242
243 #[must_use]
245 pub fn target_field(&self) -> Option<&str> {
246 self.target_field.as_deref()
247 }
248
249 #[must_use]
251 pub fn input_expr(&self) -> Option<&str> {
252 self.input_expr.as_deref()
253 }
254
255 #[must_use]
257 pub const fn distinct(&self) -> bool {
258 self.distinct
259 }
260}
261
262#[derive(Clone, Debug, Eq, PartialEq)]
269pub struct ExplainGroupHaving {
270 pub(crate) expr: ExplainGroupHavingExpr,
271}
272
273impl ExplainGroupHaving {
274 #[must_use]
276 pub const fn expr(&self) -> &ExplainGroupHavingExpr {
277 &self.expr
278 }
279}
280
281#[derive(Clone, Debug, Eq, PartialEq)]
289pub enum ExplainGroupHavingExpr {
290 Compare {
291 left: ExplainGroupHavingValueExpr,
292 op: CompareOp,
293 right: ExplainGroupHavingValueExpr,
294 },
295 And(Vec<Self>),
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
306pub enum ExplainGroupHavingValueExpr {
307 GroupField {
308 slot_index: usize,
309 field: String,
310 },
311 AggregateIndex {
312 index: usize,
313 },
314 Literal(Value),
315 FunctionCall {
316 function: String,
317 args: Vec<Self>,
318 },
319 Unary {
320 op: String,
321 expr: Box<Self>,
322 },
323 Case {
324 when_then_arms: Vec<ExplainGroupHavingCaseArm>,
325 else_expr: Box<Self>,
326 },
327 Binary {
328 op: String,
329 left: Box<Self>,
330 right: Box<Self>,
331 },
332}
333
334#[derive(Clone, Debug, Eq, PartialEq)]
343pub struct ExplainGroupHavingCaseArm {
344 pub(crate) condition: ExplainGroupHavingValueExpr,
345 pub(crate) result: ExplainGroupHavingValueExpr,
346}
347
348#[derive(Clone, Debug, Eq, PartialEq)]
355pub enum ExplainOrderPushdown {
356 MissingModelContext,
357 EligibleSecondaryIndex {
358 index: &'static str,
359 prefix_len: usize,
360 },
361 Rejected(SecondaryOrderPushdownRejection),
362}
363
364#[derive(Clone, Debug, Eq, PartialEq)]
372pub enum ExplainAccessPath {
373 ByKey {
374 key: Value,
375 },
376 ByKeys {
377 keys: Vec<Value>,
378 },
379 KeyRange {
380 start: Value,
381 end: Value,
382 },
383 IndexPrefix {
384 name: &'static str,
385 fields: Vec<&'static str>,
386 prefix_len: usize,
387 values: Vec<Value>,
388 },
389 IndexMultiLookup {
390 name: &'static str,
391 fields: Vec<&'static str>,
392 values: Vec<Value>,
393 },
394 IndexRange {
395 name: &'static str,
396 fields: Vec<&'static str>,
397 prefix_len: usize,
398 prefix: Vec<Value>,
399 lower: Bound<Value>,
400 upper: Bound<Value>,
401 },
402 FullScan,
403 Union(Vec<Self>),
404 Intersection(Vec<Self>),
405}
406
407#[derive(Clone, Debug, Eq, PartialEq)]
415pub enum ExplainPredicate {
416 None,
417 True,
418 False,
419 And(Vec<Self>),
420 Or(Vec<Self>),
421 Not(Box<Self>),
422 Compare {
423 field: String,
424 op: CompareOp,
425 value: Value,
426 coercion: CoercionSpec,
427 },
428 CompareFields {
429 left_field: String,
430 op: CompareOp,
431 right_field: String,
432 coercion: CoercionSpec,
433 },
434 IsNull {
435 field: String,
436 },
437 IsNotNull {
438 field: String,
439 },
440 IsMissing {
441 field: String,
442 },
443 IsEmpty {
444 field: String,
445 },
446 IsNotEmpty {
447 field: String,
448 },
449 TextContains {
450 field: String,
451 value: Value,
452 },
453 TextContainsCi {
454 field: String,
455 value: Value,
456 },
457}
458
459#[derive(Clone, Debug, Eq, PartialEq)]
466pub enum ExplainOrderBy {
467 None,
468 Fields(Vec<ExplainOrder>),
469}
470
471#[derive(Clone, Debug, Eq, PartialEq)]
478pub struct ExplainOrder {
479 pub(crate) field: String,
480 pub(crate) direction: OrderDirection,
481}
482
483impl ExplainOrder {
484 #[must_use]
486 pub const fn field(&self) -> &str {
487 self.field.as_str()
488 }
489
490 #[must_use]
492 pub const fn direction(&self) -> OrderDirection {
493 self.direction
494 }
495}
496
497#[derive(Clone, Debug, Eq, PartialEq)]
504pub enum ExplainPagination {
505 None,
506 Page { limit: Option<u32>, offset: u32 },
507}
508
509#[derive(Clone, Debug, Eq, PartialEq)]
516pub enum ExplainDeleteLimit {
517 None,
518 Limit { max_rows: u32 },
519 Window { limit: Option<u32>, offset: u32 },
520}
521
522impl AccessPlannedQuery {
523 #[must_use]
525 pub(crate) fn explain(&self) -> ExplainPlan {
526 self.explain_inner()
527 }
528
529 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
530 let (logical, grouping) = match &self.logical {
532 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
533 LogicalPlan::Grouped(logical) => {
534 let grouped_strategy = grouped_plan_strategy(self).expect(
535 "grouped logical explain projection requires planner-owned grouped strategy",
536 );
537
538 (
539 &logical.scalar,
540 ExplainGrouping::Grouped {
541 strategy: grouped_strategy.code(),
542 fallback_reason: grouped_strategy
543 .fallback_reason()
544 .map(GroupedPlanFallbackReason::code),
545 group_fields: logical
546 .group
547 .group_fields
548 .iter()
549 .map(|field_slot| ExplainGroupField {
550 slot_index: field_slot.index(),
551 field: field_slot.field().to_string(),
552 })
553 .collect(),
554 aggregates: logical
555 .group
556 .aggregates
557 .iter()
558 .map(|aggregate| ExplainGroupAggregate {
559 kind: aggregate.kind,
560 target_field: aggregate.target_field.clone(),
561 input_expr: aggregate
562 .input_expr()
563 .map(render_scalar_projection_expr_sql_label),
564 distinct: aggregate.distinct,
565 })
566 .collect(),
567 having: explain_group_having(logical),
568 max_groups: logical.group.execution.max_groups(),
569 max_group_bytes: logical.group.execution.max_group_bytes(),
570 },
571 )
572 }
573 };
574
575 explain_scalar_inner(logical, grouping, &self.access)
577 }
578}
579
580fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
581 let expr = logical.effective_having_expr()?;
582
583 Some(ExplainGroupHaving {
584 expr: explain_group_having_expr(expr.as_ref()),
585 })
586}
587
588fn explain_group_having_expr(expr: &GroupHavingExpr) -> ExplainGroupHavingExpr {
589 match expr {
590 GroupHavingExpr::Compare { left, op, right } => ExplainGroupHavingExpr::Compare {
591 left: explain_group_having_value_expr(left),
592 op: *op,
593 right: explain_group_having_value_expr(right),
594 },
595 GroupHavingExpr::And(children) => {
596 ExplainGroupHavingExpr::And(children.iter().map(explain_group_having_expr).collect())
597 }
598 }
599}
600
601fn explain_group_having_value_expr(expr: &GroupHavingValueExpr) -> ExplainGroupHavingValueExpr {
602 match expr {
603 GroupHavingValueExpr::GroupField(field_slot) => ExplainGroupHavingValueExpr::GroupField {
604 slot_index: field_slot.index(),
605 field: field_slot.field().to_string(),
606 },
607 GroupHavingValueExpr::AggregateIndex(index) => {
608 ExplainGroupHavingValueExpr::AggregateIndex { index: *index }
609 }
610 GroupHavingValueExpr::Literal(value) => ExplainGroupHavingValueExpr::Literal(value.clone()),
611 GroupHavingValueExpr::FunctionCall { function, args } => {
612 ExplainGroupHavingValueExpr::FunctionCall {
613 function: function.sql_label().to_string(),
614 args: args.iter().map(explain_group_having_value_expr).collect(),
615 }
616 }
617 GroupHavingValueExpr::Unary { op, expr } => ExplainGroupHavingValueExpr::Unary {
618 op: explain_group_having_unary_op_label(*op).to_string(),
619 expr: Box::new(explain_group_having_value_expr(expr)),
620 },
621 GroupHavingValueExpr::Case {
622 when_then_arms,
623 else_expr,
624 } => ExplainGroupHavingValueExpr::Case {
625 when_then_arms: when_then_arms
626 .iter()
627 .map(|arm| ExplainGroupHavingCaseArm {
628 condition: explain_group_having_value_expr(arm.condition()),
629 result: explain_group_having_value_expr(arm.result()),
630 })
631 .collect(),
632 else_expr: Box::new(explain_group_having_value_expr(else_expr)),
633 },
634 GroupHavingValueExpr::Binary { op, left, right } => ExplainGroupHavingValueExpr::Binary {
635 op: explain_group_having_binary_op_label(*op).to_string(),
636 left: Box::new(explain_group_having_value_expr(left)),
637 right: Box::new(explain_group_having_value_expr(right)),
638 },
639 }
640}
641
642const fn explain_group_having_unary_op_label(
643 op: crate::db::query::plan::expr::UnaryOp,
644) -> &'static str {
645 match op {
646 crate::db::query::plan::expr::UnaryOp::Not => "NOT",
647 }
648}
649
650const fn explain_group_having_binary_op_label(
651 op: crate::db::query::plan::expr::BinaryOp,
652) -> &'static str {
653 match op {
654 crate::db::query::plan::expr::BinaryOp::Or => "OR",
655 crate::db::query::plan::expr::BinaryOp::And => "AND",
656 crate::db::query::plan::expr::BinaryOp::Eq => "=",
657 crate::db::query::plan::expr::BinaryOp::Ne => "!=",
658 crate::db::query::plan::expr::BinaryOp::Lt => "<",
659 crate::db::query::plan::expr::BinaryOp::Lte => "<=",
660 crate::db::query::plan::expr::BinaryOp::Gt => ">",
661 crate::db::query::plan::expr::BinaryOp::Gte => ">=",
662 crate::db::query::plan::expr::BinaryOp::Add => "+",
663 crate::db::query::plan::expr::BinaryOp::Sub => "-",
664 crate::db::query::plan::expr::BinaryOp::Mul => "*",
665 crate::db::query::plan::expr::BinaryOp::Div => "/",
666 }
667}
668
669fn explain_scalar_inner<K>(
670 logical: &ScalarPlan,
671 grouping: ExplainGrouping,
672 access: &AccessPlan<K>,
673) -> ExplainPlan
674where
675 K: FieldValue,
676{
677 let predicate_model = logical.predicate.clone();
679 let predicate = match &predicate_model {
680 Some(predicate) => ExplainPredicate::from_predicate(predicate),
681 None => ExplainPredicate::None,
682 };
683
684 let order_by = explain_order(logical.order.as_ref());
686 let order_pushdown = explain_order_pushdown();
687 let page = explain_page(logical.page.as_ref());
688 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
689
690 ExplainPlan {
692 mode: logical.mode,
693 access: ExplainAccessPath::from_access_plan(access),
694 predicate,
695 predicate_model,
696 order_by,
697 distinct: logical.distinct,
698 grouping,
699 order_pushdown,
700 page,
701 delete_limit,
702 consistency: logical.consistency,
703 }
704}
705
706const fn explain_order_pushdown() -> ExplainOrderPushdown {
707 ExplainOrderPushdown::MissingModelContext
709}
710
711impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
712 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
713 Self::from(PushdownSurfaceEligibility::from(&value))
714 }
715}
716
717impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
718 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
719 match value {
720 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
721 Self::EligibleSecondaryIndex { index, prefix_len }
722 }
723 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
724 }
725 }
726}
727
728impl ExplainPredicate {
729 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
730 match predicate {
731 Predicate::True => Self::True,
732 Predicate::False => Self::False,
733 Predicate::And(children) => {
734 Self::And(children.iter().map(Self::from_predicate).collect())
735 }
736 Predicate::Or(children) => {
737 Self::Or(children.iter().map(Self::from_predicate).collect())
738 }
739 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
740 Predicate::Compare(compare) => Self::from_compare(compare),
741 Predicate::CompareFields(compare) => Self::CompareFields {
742 left_field: compare.left_field().to_string(),
743 op: compare.op(),
744 right_field: compare.right_field().to_string(),
745 coercion: compare.coercion().clone(),
746 },
747 Predicate::IsNull { field } => Self::IsNull {
748 field: field.clone(),
749 },
750 Predicate::IsNotNull { field } => Self::IsNotNull {
751 field: field.clone(),
752 },
753 Predicate::IsMissing { field } => Self::IsMissing {
754 field: field.clone(),
755 },
756 Predicate::IsEmpty { field } => Self::IsEmpty {
757 field: field.clone(),
758 },
759 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
760 field: field.clone(),
761 },
762 Predicate::TextContains { field, value } => Self::TextContains {
763 field: field.clone(),
764 value: value.clone(),
765 },
766 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
767 field: field.clone(),
768 value: value.clone(),
769 },
770 }
771 }
772
773 fn from_compare(compare: &ComparePredicate) -> Self {
774 Self::Compare {
775 field: compare.field.clone(),
776 op: compare.op,
777 value: compare.value.clone(),
778 coercion: compare.coercion.clone(),
779 }
780 }
781}
782
783fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
784 let Some(order) = order else {
785 return ExplainOrderBy::None;
786 };
787
788 if order.fields.is_empty() {
789 return ExplainOrderBy::None;
790 }
791
792 ExplainOrderBy::Fields(
793 order
794 .fields
795 .iter()
796 .map(|(field, direction)| ExplainOrder {
797 field: field.clone(),
798 direction: *direction,
799 })
800 .collect(),
801 )
802}
803
804const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
805 match page {
806 Some(page) => ExplainPagination::Page {
807 limit: page.limit,
808 offset: page.offset,
809 },
810 None => ExplainPagination::None,
811 }
812}
813
814const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
815 match limit {
816 Some(limit) if limit.offset == 0 => match limit.limit {
817 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
818 None => ExplainDeleteLimit::Window {
819 limit: None,
820 offset: 0,
821 },
822 },
823 Some(limit) => ExplainDeleteLimit::Window {
824 limit: limit.limit,
825 offset: limit.offset,
826 },
827 None => ExplainDeleteLimit::None,
828 }
829}
830
831fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
832 let mut object = JsonWriter::begin_object(out);
833 object.field_with("mode", |out| {
834 let mut object = JsonWriter::begin_object(out);
835 match explain.mode() {
836 QueryMode::Load(spec) => {
837 object.field_str("type", "Load");
838 match spec.limit() {
839 Some(limit) => object.field_u64("limit", u64::from(limit)),
840 None => object.field_null("limit"),
841 }
842 object.field_u64("offset", u64::from(spec.offset()));
843 }
844 QueryMode::Delete(spec) => {
845 object.field_str("type", "Delete");
846 match spec.limit() {
847 Some(limit) => object.field_u64("limit", u64::from(limit)),
848 None => object.field_null("limit"),
849 }
850 }
851 }
852 object.finish();
853 });
854 object.field_with("access", |out| write_access_json(explain.access(), out));
855 object.field_value_debug("predicate", explain.predicate());
856 object.field_value_debug("order_by", explain.order_by());
857 object.field_bool("distinct", explain.distinct());
858 object.field_value_debug("grouping", explain.grouping());
859 object.field_value_debug("order_pushdown", explain.order_pushdown());
860 object.field_with("page", |out| {
861 let mut object = JsonWriter::begin_object(out);
862 match explain.page() {
863 ExplainPagination::None => {
864 object.field_str("type", "None");
865 }
866 ExplainPagination::Page { limit, offset } => {
867 object.field_str("type", "Page");
868 match limit {
869 Some(limit) => object.field_u64("limit", u64::from(*limit)),
870 None => object.field_null("limit"),
871 }
872 object.field_u64("offset", u64::from(*offset));
873 }
874 }
875 object.finish();
876 });
877 object.field_with("delete_limit", |out| {
878 let mut object = JsonWriter::begin_object(out);
879 match explain.delete_limit() {
880 ExplainDeleteLimit::None => {
881 object.field_str("type", "None");
882 }
883 ExplainDeleteLimit::Limit { max_rows } => {
884 object.field_str("type", "Limit");
885 object.field_u64("max_rows", u64::from(*max_rows));
886 }
887 ExplainDeleteLimit::Window { limit, offset } => {
888 object.field_str("type", "Window");
889 object.field_with("limit", |out| match limit {
890 Some(limit) => out.push_str(&limit.to_string()),
891 None => out.push_str("null"),
892 });
893 object.field_u64("offset", u64::from(*offset));
894 }
895 }
896 object.finish();
897 });
898 object.field_value_debug("consistency", &explain.consistency());
899 object.finish();
900}