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) predicate: ExplainPredicate,
40 predicate_model: Option<Predicate>,
41 pub(crate) order_by: ExplainOrderBy,
42 pub(crate) distinct: bool,
43 pub(crate) grouping: ExplainGrouping,
44 pub(crate) order_pushdown: ExplainOrderPushdown,
45 pub(crate) page: ExplainPagination,
46 pub(crate) delete_limit: ExplainDeleteLimit,
47 pub(crate) consistency: MissingRowPolicy,
48}
49
50impl ExplainPlan {
51 #[must_use]
53 pub const fn mode(&self) -> QueryMode {
54 self.mode
55 }
56
57 #[must_use]
59 pub const fn access(&self) -> &ExplainAccessPath {
60 &self.access
61 }
62
63 #[must_use]
65 pub const fn predicate(&self) -> &ExplainPredicate {
66 &self.predicate
67 }
68
69 #[must_use]
71 pub const fn order_by(&self) -> &ExplainOrderBy {
72 &self.order_by
73 }
74
75 #[must_use]
77 pub const fn distinct(&self) -> bool {
78 self.distinct
79 }
80
81 #[must_use]
83 pub const fn grouping(&self) -> &ExplainGrouping {
84 &self.grouping
85 }
86
87 #[must_use]
89 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
90 &self.order_pushdown
91 }
92
93 #[must_use]
95 pub const fn page(&self) -> &ExplainPagination {
96 &self.page
97 }
98
99 #[must_use]
101 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
102 &self.delete_limit
103 }
104
105 #[must_use]
107 pub const fn consistency(&self) -> MissingRowPolicy {
108 self.consistency
109 }
110}
111
112impl ExplainPlan {
113 #[must_use]
117 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
118 if let Some(predicate) = &self.predicate_model {
119 debug_assert_eq!(
120 self.predicate,
121 ExplainPredicate::from_predicate(predicate),
122 "explain predicate surface drifted from canonical predicate model"
123 );
124 Some(predicate)
125 } else {
126 debug_assert!(
127 matches!(self.predicate, ExplainPredicate::None),
128 "missing canonical predicate model requires ExplainPredicate::None"
129 );
130 None
131 }
132 }
133
134 #[must_use]
139 pub fn render_text_canonical(&self) -> String {
140 format!(
141 concat!(
142 "mode={:?}\n",
143 "access={:?}\n",
144 "predicate={:?}\n",
145 "order_by={:?}\n",
146 "distinct={}\n",
147 "grouping={:?}\n",
148 "order_pushdown={:?}\n",
149 "page={:?}\n",
150 "delete_limit={:?}\n",
151 "consistency={:?}",
152 ),
153 self.mode(),
154 self.access(),
155 self.predicate(),
156 self.order_by(),
157 self.distinct(),
158 self.grouping(),
159 self.order_pushdown(),
160 self.page(),
161 self.delete_limit(),
162 self.consistency(),
163 )
164 }
165
166 #[must_use]
168 pub fn render_json_canonical(&self) -> String {
169 let mut out = String::new();
170 write_logical_explain_json(self, &mut out);
171
172 out
173 }
174}
175
176#[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) filter_expr: Option<String>,
234 pub(crate) distinct: bool,
235}
236
237impl ExplainGroupAggregate {
238 #[must_use]
240 pub const fn kind(&self) -> AggregateKind {
241 self.kind
242 }
243
244 #[must_use]
246 pub fn target_field(&self) -> Option<&str> {
247 self.target_field.as_deref()
248 }
249
250 #[must_use]
252 pub fn input_expr(&self) -> Option<&str> {
253 self.input_expr.as_deref()
254 }
255
256 #[must_use]
258 pub fn filter_expr(&self) -> Option<&str> {
259 self.filter_expr.as_deref()
260 }
261
262 #[must_use]
264 pub const fn distinct(&self) -> bool {
265 self.distinct
266 }
267}
268
269#[derive(Clone, Debug, Eq, PartialEq)]
278pub struct ExplainGroupHaving {
279 pub(crate) expr: Expr,
280}
281
282impl ExplainGroupHaving {
283 #[must_use]
285 pub(in crate::db) const fn expr(&self) -> &Expr {
286 &self.expr
287 }
288}
289
290#[derive(Clone, Debug, Eq, PartialEq)]
297pub enum ExplainOrderPushdown {
298 MissingModelContext,
299 EligibleSecondaryIndex {
300 index: &'static str,
301 prefix_len: usize,
302 },
303 Rejected(SecondaryOrderPushdownRejection),
304}
305
306#[derive(Clone, Debug, Eq, PartialEq)]
314pub enum ExplainAccessPath {
315 ByKey {
316 key: Value,
317 },
318 ByKeys {
319 keys: Vec<Value>,
320 },
321 KeyRange {
322 start: Value,
323 end: Value,
324 },
325 IndexPrefix {
326 name: &'static str,
327 fields: Vec<&'static str>,
328 prefix_len: usize,
329 values: Vec<Value>,
330 },
331 IndexMultiLookup {
332 name: &'static str,
333 fields: Vec<&'static str>,
334 values: Vec<Value>,
335 },
336 IndexRange {
337 name: &'static str,
338 fields: Vec<&'static str>,
339 prefix_len: usize,
340 prefix: Vec<Value>,
341 lower: Bound<Value>,
342 upper: Bound<Value>,
343 },
344 FullScan,
345 Union(Vec<Self>),
346 Intersection(Vec<Self>),
347}
348
349#[derive(Clone, Debug, Eq, PartialEq)]
357pub enum ExplainPredicate {
358 None,
359 True,
360 False,
361 And(Vec<Self>),
362 Or(Vec<Self>),
363 Not(Box<Self>),
364 Compare {
365 field: String,
366 op: CompareOp,
367 value: Value,
368 coercion: CoercionSpec,
369 },
370 CompareFields {
371 left_field: String,
372 op: CompareOp,
373 right_field: String,
374 coercion: CoercionSpec,
375 },
376 IsNull {
377 field: String,
378 },
379 IsNotNull {
380 field: String,
381 },
382 IsMissing {
383 field: String,
384 },
385 IsEmpty {
386 field: String,
387 },
388 IsNotEmpty {
389 field: String,
390 },
391 TextContains {
392 field: String,
393 value: Value,
394 },
395 TextContainsCi {
396 field: String,
397 value: Value,
398 },
399}
400
401#[derive(Clone, Debug, Eq, PartialEq)]
408pub enum ExplainOrderBy {
409 None,
410 Fields(Vec<ExplainOrder>),
411}
412
413#[derive(Clone, Debug, Eq, PartialEq)]
420pub struct ExplainOrder {
421 pub(crate) field: String,
422 pub(crate) direction: OrderDirection,
423}
424
425impl ExplainOrder {
426 #[must_use]
428 pub const fn field(&self) -> &str {
429 self.field.as_str()
430 }
431
432 #[must_use]
434 pub const fn direction(&self) -> OrderDirection {
435 self.direction
436 }
437}
438
439#[derive(Clone, Debug, Eq, PartialEq)]
446pub enum ExplainPagination {
447 None,
448 Page { limit: Option<u32>, offset: u32 },
449}
450
451#[derive(Clone, Debug, Eq, PartialEq)]
458pub enum ExplainDeleteLimit {
459 None,
460 Limit { max_rows: u32 },
461 Window { limit: Option<u32>, offset: u32 },
462}
463
464impl AccessPlannedQuery {
465 #[must_use]
467 pub(crate) fn explain(&self) -> ExplainPlan {
468 self.explain_inner()
469 }
470
471 pub(in crate::db::query::explain) fn explain_inner(&self) -> ExplainPlan {
472 let (logical, grouping) = match &self.logical {
474 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
475 LogicalPlan::Grouped(logical) => {
476 let grouped_strategy = grouped_plan_strategy(self).expect(
477 "grouped logical explain projection requires planner-owned grouped strategy",
478 );
479
480 (
481 &logical.scalar,
482 ExplainGrouping::Grouped {
483 strategy: grouped_strategy.code(),
484 fallback_reason: grouped_strategy
485 .fallback_reason()
486 .map(GroupedPlanFallbackReason::code),
487 group_fields: logical
488 .group
489 .group_fields
490 .iter()
491 .map(|field_slot| ExplainGroupField {
492 slot_index: field_slot.index(),
493 field: field_slot.field().to_string(),
494 })
495 .collect(),
496 aggregates: logical
497 .group
498 .aggregates
499 .iter()
500 .map(|aggregate| ExplainGroupAggregate {
501 kind: aggregate.kind,
502 target_field: aggregate.target_field().map(str::to_string),
503 input_expr: aggregate
504 .input_expr()
505 .map(render_scalar_projection_expr_sql_label),
506 filter_expr: aggregate
507 .filter_expr()
508 .map(render_scalar_projection_expr_sql_label),
509 distinct: aggregate.distinct,
510 })
511 .collect(),
512 having: explain_group_having(logical),
513 max_groups: logical.group.execution.max_groups(),
514 max_group_bytes: logical.group.execution.max_group_bytes(),
515 },
516 )
517 }
518 };
519
520 explain_scalar_inner(logical, grouping, &self.access)
522 }
523}
524
525fn explain_group_having(logical: &crate::db::query::plan::GroupPlan) -> Option<ExplainGroupHaving> {
526 let expr = logical.effective_having_expr()?;
527
528 Some(ExplainGroupHaving {
529 expr: expr.into_owned(),
530 })
531}
532
533fn explain_scalar_inner<K>(
534 logical: &ScalarPlan,
535 grouping: ExplainGrouping,
536 access: &AccessPlan<K>,
537) -> ExplainPlan
538where
539 K: FieldValue,
540{
541 let predicate_model = logical.predicate.clone();
543 let predicate = match &predicate_model {
544 Some(predicate) => ExplainPredicate::from_predicate(predicate),
545 None => ExplainPredicate::None,
546 };
547
548 let order_by = explain_order(logical.order.as_ref());
550 let order_pushdown = explain_order_pushdown();
551 let page = explain_page(logical.page.as_ref());
552 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
553
554 ExplainPlan {
556 mode: logical.mode,
557 access: ExplainAccessPath::from_access_plan(access),
558 predicate,
559 predicate_model,
560 order_by,
561 distinct: logical.distinct,
562 grouping,
563 order_pushdown,
564 page,
565 delete_limit,
566 consistency: logical.consistency,
567 }
568}
569
570const fn explain_order_pushdown() -> ExplainOrderPushdown {
571 ExplainOrderPushdown::MissingModelContext
573}
574
575impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
576 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
577 Self::from(PushdownSurfaceEligibility::from(&value))
578 }
579}
580
581impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
582 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
583 match value {
584 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
585 Self::EligibleSecondaryIndex { index, prefix_len }
586 }
587 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
588 }
589 }
590}
591
592impl ExplainPredicate {
593 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
594 match predicate {
595 Predicate::True => Self::True,
596 Predicate::False => Self::False,
597 Predicate::And(children) => {
598 Self::And(children.iter().map(Self::from_predicate).collect())
599 }
600 Predicate::Or(children) => {
601 Self::Or(children.iter().map(Self::from_predicate).collect())
602 }
603 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
604 Predicate::Compare(compare) => Self::from_compare(compare),
605 Predicate::CompareFields(compare) => Self::CompareFields {
606 left_field: compare.left_field().to_string(),
607 op: compare.op(),
608 right_field: compare.right_field().to_string(),
609 coercion: compare.coercion().clone(),
610 },
611 Predicate::IsNull { field } => Self::IsNull {
612 field: field.clone(),
613 },
614 Predicate::IsNotNull { field } => Self::IsNotNull {
615 field: field.clone(),
616 },
617 Predicate::IsMissing { field } => Self::IsMissing {
618 field: field.clone(),
619 },
620 Predicate::IsEmpty { field } => Self::IsEmpty {
621 field: field.clone(),
622 },
623 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
624 field: field.clone(),
625 },
626 Predicate::TextContains { field, value } => Self::TextContains {
627 field: field.clone(),
628 value: value.clone(),
629 },
630 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
631 field: field.clone(),
632 value: value.clone(),
633 },
634 }
635 }
636
637 fn from_compare(compare: &ComparePredicate) -> Self {
638 Self::Compare {
639 field: compare.field.clone(),
640 op: compare.op,
641 value: compare.value.clone(),
642 coercion: compare.coercion.clone(),
643 }
644 }
645}
646
647fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
648 let Some(order) = order else {
649 return ExplainOrderBy::None;
650 };
651
652 if order.fields.is_empty() {
653 return ExplainOrderBy::None;
654 }
655
656 ExplainOrderBy::Fields(
657 order
658 .fields
659 .iter()
660 .map(|term| ExplainOrder {
661 field: term.rendered_label(),
662 direction: term.direction(),
663 })
664 .collect(),
665 )
666}
667
668const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
669 match page {
670 Some(page) => ExplainPagination::Page {
671 limit: page.limit,
672 offset: page.offset,
673 },
674 None => ExplainPagination::None,
675 }
676}
677
678const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
679 match limit {
680 Some(limit) if limit.offset == 0 => match limit.limit {
681 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
682 None => ExplainDeleteLimit::Window {
683 limit: None,
684 offset: 0,
685 },
686 },
687 Some(limit) => ExplainDeleteLimit::Window {
688 limit: limit.limit,
689 offset: limit.offset,
690 },
691 None => ExplainDeleteLimit::None,
692 }
693}
694
695fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
696 let mut object = JsonWriter::begin_object(out);
697 object.field_with("mode", |out| {
698 let mut object = JsonWriter::begin_object(out);
699 match explain.mode() {
700 QueryMode::Load(spec) => {
701 object.field_str("type", "Load");
702 match spec.limit() {
703 Some(limit) => object.field_u64("limit", u64::from(limit)),
704 None => object.field_null("limit"),
705 }
706 object.field_u64("offset", u64::from(spec.offset()));
707 }
708 QueryMode::Delete(spec) => {
709 object.field_str("type", "Delete");
710 match spec.limit() {
711 Some(limit) => object.field_u64("limit", u64::from(limit)),
712 None => object.field_null("limit"),
713 }
714 }
715 }
716 object.finish();
717 });
718 object.field_with("access", |out| write_access_json(explain.access(), out));
719 object.field_value_debug("predicate", explain.predicate());
720 object.field_value_debug("order_by", explain.order_by());
721 object.field_bool("distinct", explain.distinct());
722 object.field_value_debug("grouping", explain.grouping());
723 object.field_value_debug("order_pushdown", explain.order_pushdown());
724 object.field_with("page", |out| {
725 let mut object = JsonWriter::begin_object(out);
726 match explain.page() {
727 ExplainPagination::None => {
728 object.field_str("type", "None");
729 }
730 ExplainPagination::Page { limit, offset } => {
731 object.field_str("type", "Page");
732 match limit {
733 Some(limit) => object.field_u64("limit", u64::from(*limit)),
734 None => object.field_null("limit"),
735 }
736 object.field_u64("offset", u64::from(*offset));
737 }
738 }
739 object.finish();
740 });
741 object.field_with("delete_limit", |out| {
742 let mut object = JsonWriter::begin_object(out);
743 match explain.delete_limit() {
744 ExplainDeleteLimit::None => {
745 object.field_str("type", "None");
746 }
747 ExplainDeleteLimit::Limit { max_rows } => {
748 object.field_str("type", "Limit");
749 object.field_u64("max_rows", u64::from(*max_rows));
750 }
751 ExplainDeleteLimit::Window { limit, offset } => {
752 object.field_str("type", "Window");
753 object.field_with("limit", |out| match limit {
754 Some(limit) => out.push_str(&limit.to_string()),
755 None => out.push_str("null"),
756 });
757 object.field_u64("offset", u64::from(*offset));
758 }
759 }
760 object.finish();
761 });
762 object.field_value_debug("consistency", &explain.consistency());
763 object.finish();
764}