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 Binary {
320 op: String,
321 left: Box<Self>,
322 right: Box<Self>,
323 },
324}
325
326#[derive(Clone, Debug, Eq, PartialEq)]
333pub enum ExplainOrderPushdown {
334 MissingModelContext,
335 EligibleSecondaryIndex {
336 index: &'static str,
337 prefix_len: usize,
338 },
339 Rejected(SecondaryOrderPushdownRejection),
340}
341
342#[derive(Clone, Debug, Eq, PartialEq)]
350pub enum ExplainAccessPath {
351 ByKey {
352 key: Value,
353 },
354 ByKeys {
355 keys: Vec<Value>,
356 },
357 KeyRange {
358 start: Value,
359 end: Value,
360 },
361 IndexPrefix {
362 name: &'static str,
363 fields: Vec<&'static str>,
364 prefix_len: usize,
365 values: Vec<Value>,
366 },
367 IndexMultiLookup {
368 name: &'static str,
369 fields: Vec<&'static str>,
370 values: Vec<Value>,
371 },
372 IndexRange {
373 name: &'static str,
374 fields: Vec<&'static str>,
375 prefix_len: usize,
376 prefix: Vec<Value>,
377 lower: Bound<Value>,
378 upper: Bound<Value>,
379 },
380 FullScan,
381 Union(Vec<Self>),
382 Intersection(Vec<Self>),
383}
384
385#[derive(Clone, Debug, Eq, PartialEq)]
393pub enum ExplainPredicate {
394 None,
395 True,
396 False,
397 And(Vec<Self>),
398 Or(Vec<Self>),
399 Not(Box<Self>),
400 Compare {
401 field: String,
402 op: CompareOp,
403 value: Value,
404 coercion: CoercionSpec,
405 },
406 CompareFields {
407 left_field: String,
408 op: CompareOp,
409 right_field: String,
410 coercion: CoercionSpec,
411 },
412 IsNull {
413 field: String,
414 },
415 IsNotNull {
416 field: String,
417 },
418 IsMissing {
419 field: String,
420 },
421 IsEmpty {
422 field: String,
423 },
424 IsNotEmpty {
425 field: String,
426 },
427 TextContains {
428 field: String,
429 value: Value,
430 },
431 TextContainsCi {
432 field: String,
433 value: Value,
434 },
435}
436
437#[derive(Clone, Debug, Eq, PartialEq)]
444pub enum ExplainOrderBy {
445 None,
446 Fields(Vec<ExplainOrder>),
447}
448
449#[derive(Clone, Debug, Eq, PartialEq)]
456pub struct ExplainOrder {
457 pub(crate) field: String,
458 pub(crate) direction: OrderDirection,
459}
460
461impl ExplainOrder {
462 #[must_use]
464 pub const fn field(&self) -> &str {
465 self.field.as_str()
466 }
467
468 #[must_use]
470 pub const fn direction(&self) -> OrderDirection {
471 self.direction
472 }
473}
474
475#[derive(Clone, Debug, Eq, PartialEq)]
482pub enum ExplainPagination {
483 None,
484 Page { limit: Option<u32>, offset: u32 },
485}
486
487#[derive(Clone, Debug, Eq, PartialEq)]
494pub enum ExplainDeleteLimit {
495 None,
496 Limit { max_rows: u32 },
497 Window { limit: Option<u32>, offset: u32 },
498}
499
500impl AccessPlannedQuery {
501 #[must_use]
503 pub(crate) fn explain(&self) -> ExplainPlan {
504 self.explain_inner()
505 }
506
507 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
508 let (logical, grouping) = match &self.logical {
510 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
511 LogicalPlan::Grouped(logical) => {
512 let grouped_strategy = grouped_plan_strategy(self).expect(
513 "grouped logical explain projection requires planner-owned grouped strategy",
514 );
515
516 (
517 &logical.scalar,
518 ExplainGrouping::Grouped {
519 strategy: grouped_strategy.code(),
520 fallback_reason: grouped_strategy
521 .fallback_reason()
522 .map(GroupedPlanFallbackReason::code),
523 group_fields: logical
524 .group
525 .group_fields
526 .iter()
527 .map(|field_slot| ExplainGroupField {
528 slot_index: field_slot.index(),
529 field: field_slot.field().to_string(),
530 })
531 .collect(),
532 aggregates: logical
533 .group
534 .aggregates
535 .iter()
536 .map(|aggregate| ExplainGroupAggregate {
537 kind: aggregate.kind,
538 target_field: aggregate.target_field.clone(),
539 input_expr: aggregate
540 .input_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: explain_group_having_expr(expr.as_ref()),
563 })
564}
565
566fn explain_group_having_expr(expr: &GroupHavingExpr) -> ExplainGroupHavingExpr {
567 match expr {
568 GroupHavingExpr::Compare { left, op, right } => ExplainGroupHavingExpr::Compare {
569 left: explain_group_having_value_expr(left),
570 op: *op,
571 right: explain_group_having_value_expr(right),
572 },
573 GroupHavingExpr::And(children) => {
574 ExplainGroupHavingExpr::And(children.iter().map(explain_group_having_expr).collect())
575 }
576 }
577}
578
579fn explain_group_having_value_expr(expr: &GroupHavingValueExpr) -> ExplainGroupHavingValueExpr {
580 match expr {
581 GroupHavingValueExpr::GroupField(field_slot) => ExplainGroupHavingValueExpr::GroupField {
582 slot_index: field_slot.index(),
583 field: field_slot.field().to_string(),
584 },
585 GroupHavingValueExpr::AggregateIndex(index) => {
586 ExplainGroupHavingValueExpr::AggregateIndex { index: *index }
587 }
588 GroupHavingValueExpr::Literal(value) => ExplainGroupHavingValueExpr::Literal(value.clone()),
589 GroupHavingValueExpr::FunctionCall { function, args } => {
590 ExplainGroupHavingValueExpr::FunctionCall {
591 function: function.sql_label().to_string(),
592 args: args.iter().map(explain_group_having_value_expr).collect(),
593 }
594 }
595 GroupHavingValueExpr::Binary { op, left, right } => ExplainGroupHavingValueExpr::Binary {
596 op: explain_group_having_binary_op_label(*op).to_string(),
597 left: Box::new(explain_group_having_value_expr(left)),
598 right: Box::new(explain_group_having_value_expr(right)),
599 },
600 }
601}
602
603const fn explain_group_having_binary_op_label(
604 op: crate::db::query::plan::expr::BinaryOp,
605) -> &'static str {
606 match op {
607 crate::db::query::plan::expr::BinaryOp::Add => "+",
608 crate::db::query::plan::expr::BinaryOp::Sub => "-",
609 crate::db::query::plan::expr::BinaryOp::Mul => "*",
610 crate::db::query::plan::expr::BinaryOp::Div => "/",
611 #[cfg(test)]
612 crate::db::query::plan::expr::BinaryOp::And => "AND",
613 #[cfg(test)]
614 crate::db::query::plan::expr::BinaryOp::Eq => "=",
615 }
616}
617
618fn explain_scalar_inner<K>(
619 logical: &ScalarPlan,
620 grouping: ExplainGrouping,
621 access: &AccessPlan<K>,
622) -> ExplainPlan
623where
624 K: FieldValue,
625{
626 let predicate_model = logical.predicate.clone();
628 let predicate = match &predicate_model {
629 Some(predicate) => ExplainPredicate::from_predicate(predicate),
630 None => ExplainPredicate::None,
631 };
632
633 let order_by = explain_order(logical.order.as_ref());
635 let order_pushdown = explain_order_pushdown();
636 let page = explain_page(logical.page.as_ref());
637 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
638
639 ExplainPlan {
641 mode: logical.mode,
642 access: ExplainAccessPath::from_access_plan(access),
643 predicate,
644 predicate_model,
645 order_by,
646 distinct: logical.distinct,
647 grouping,
648 order_pushdown,
649 page,
650 delete_limit,
651 consistency: logical.consistency,
652 }
653}
654
655const fn explain_order_pushdown() -> ExplainOrderPushdown {
656 ExplainOrderPushdown::MissingModelContext
658}
659
660impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
661 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
662 Self::from(PushdownSurfaceEligibility::from(&value))
663 }
664}
665
666impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
667 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
668 match value {
669 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
670 Self::EligibleSecondaryIndex { index, prefix_len }
671 }
672 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
673 }
674 }
675}
676
677impl ExplainPredicate {
678 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
679 match predicate {
680 Predicate::True => Self::True,
681 Predicate::False => Self::False,
682 Predicate::And(children) => {
683 Self::And(children.iter().map(Self::from_predicate).collect())
684 }
685 Predicate::Or(children) => {
686 Self::Or(children.iter().map(Self::from_predicate).collect())
687 }
688 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
689 Predicate::Compare(compare) => Self::from_compare(compare),
690 Predicate::CompareFields(compare) => Self::CompareFields {
691 left_field: compare.left_field().to_string(),
692 op: compare.op(),
693 right_field: compare.right_field().to_string(),
694 coercion: compare.coercion().clone(),
695 },
696 Predicate::IsNull { field } => Self::IsNull {
697 field: field.clone(),
698 },
699 Predicate::IsNotNull { field } => Self::IsNotNull {
700 field: field.clone(),
701 },
702 Predicate::IsMissing { field } => Self::IsMissing {
703 field: field.clone(),
704 },
705 Predicate::IsEmpty { field } => Self::IsEmpty {
706 field: field.clone(),
707 },
708 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
709 field: field.clone(),
710 },
711 Predicate::TextContains { field, value } => Self::TextContains {
712 field: field.clone(),
713 value: value.clone(),
714 },
715 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
716 field: field.clone(),
717 value: value.clone(),
718 },
719 }
720 }
721
722 fn from_compare(compare: &ComparePredicate) -> Self {
723 Self::Compare {
724 field: compare.field.clone(),
725 op: compare.op,
726 value: compare.value.clone(),
727 coercion: compare.coercion.clone(),
728 }
729 }
730}
731
732fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
733 let Some(order) = order else {
734 return ExplainOrderBy::None;
735 };
736
737 if order.fields.is_empty() {
738 return ExplainOrderBy::None;
739 }
740
741 ExplainOrderBy::Fields(
742 order
743 .fields
744 .iter()
745 .map(|(field, direction)| ExplainOrder {
746 field: field.clone(),
747 direction: *direction,
748 })
749 .collect(),
750 )
751}
752
753const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
754 match page {
755 Some(page) => ExplainPagination::Page {
756 limit: page.limit,
757 offset: page.offset,
758 },
759 None => ExplainPagination::None,
760 }
761}
762
763const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
764 match limit {
765 Some(limit) if limit.offset == 0 => match limit.limit {
766 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
767 None => ExplainDeleteLimit::Window {
768 limit: None,
769 offset: 0,
770 },
771 },
772 Some(limit) => ExplainDeleteLimit::Window {
773 limit: limit.limit,
774 offset: limit.offset,
775 },
776 None => ExplainDeleteLimit::None,
777 }
778}
779
780fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
781 let mut object = JsonWriter::begin_object(out);
782 object.field_with("mode", |out| write_query_mode_json(explain.mode(), out));
783 object.field_with("access", |out| write_access_json(explain.access(), out));
784 object.field_value_debug("predicate", explain.predicate());
785 object.field_value_debug("order_by", explain.order_by());
786 object.field_bool("distinct", explain.distinct());
787 object.field_value_debug("grouping", explain.grouping());
788 object.field_value_debug("order_pushdown", explain.order_pushdown());
789 object.field_with("page", |out| write_pagination_json(explain.page(), out));
790 object.field_with("delete_limit", |out| {
791 write_delete_limit_json(explain.delete_limit(), out);
792 });
793 object.field_value_debug("consistency", &explain.consistency());
794 object.finish();
795}
796
797fn write_query_mode_json(mode: QueryMode, out: &mut String) {
798 let mut object = JsonWriter::begin_object(out);
799 match mode {
800 QueryMode::Load(spec) => {
801 object.field_str("type", "Load");
802 match spec.limit() {
803 Some(limit) => object.field_u64("limit", u64::from(limit)),
804 None => object.field_null("limit"),
805 }
806 object.field_u64("offset", u64::from(spec.offset()));
807 }
808 QueryMode::Delete(spec) => {
809 object.field_str("type", "Delete");
810 match spec.limit() {
811 Some(limit) => object.field_u64("limit", u64::from(limit)),
812 None => object.field_null("limit"),
813 }
814 }
815 }
816 object.finish();
817}
818
819fn write_pagination_json(page: &ExplainPagination, out: &mut String) {
820 let mut object = JsonWriter::begin_object(out);
821 match page {
822 ExplainPagination::None => {
823 object.field_str("type", "None");
824 }
825 ExplainPagination::Page { limit, offset } => {
826 object.field_str("type", "Page");
827 match limit {
828 Some(limit) => object.field_u64("limit", u64::from(*limit)),
829 None => object.field_null("limit"),
830 }
831 object.field_u64("offset", u64::from(*offset));
832 }
833 }
834 object.finish();
835}
836
837fn write_delete_limit_json(limit: &ExplainDeleteLimit, out: &mut String) {
838 let mut object = JsonWriter::begin_object(out);
839 match limit {
840 ExplainDeleteLimit::None => {
841 object.field_str("type", "None");
842 }
843 ExplainDeleteLimit::Limit { max_rows } => {
844 object.field_str("type", "Limit");
845 object.field_u64("max_rows", u64::from(*max_rows));
846 }
847 ExplainDeleteLimit::Window { limit, offset } => {
848 object.field_str("type", "Window");
849 object.field_with("limit", |out| match limit {
850 Some(limit) => out.push_str(&limit.to_string()),
851 None => out.push_str("null"),
852 });
853 object.field_u64("offset", u64::from(*offset));
854 }
855 }
856 object.finish();
857}