1use crate::{
7 db::{
8 access::{
9 AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
10 SecondaryOrderPushdownRejection,
11 },
12 predicate::{
13 CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate, normalize,
14 },
15 query::plan::{
16 AccessPlanProjection, AccessPlannedQuery, AggregateKind, DeleteLimitSpec,
17 GroupHavingClause, GroupHavingSpec, GroupHavingSymbol, GroupedPlanStrategyHint,
18 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
19 grouped_plan_strategy_hint_for_plan, project_access_plan,
20 },
21 },
22 model::entity::EntityModel,
23 traits::FieldValue,
24 value::Value,
25};
26use std::{collections::BTreeMap, fmt::Write, 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
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum ExplainAggregateTerminalRoute {
57 Standard,
58 IndexSeekFirst { fetch: usize },
59 IndexSeekLast { fetch: usize },
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct ExplainAggregateTerminalPlan {
70 pub(crate) query: ExplainPlan,
71 pub(crate) terminal: AggregateKind,
72 pub(crate) route: ExplainAggregateTerminalRoute,
73 pub(crate) execution: ExplainExecutionDescriptor,
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum ExplainExecutionOrderingSource {
84 AccessOrder,
85 Materialized,
86 IndexSeekFirst { fetch: usize },
87 IndexSeekLast { fetch: usize },
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum ExplainExecutionMode {
97 Streaming,
98 Materialized,
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
109pub struct ExplainExecutionDescriptor {
110 pub(crate) access_strategy: ExplainAccessPath,
111 pub(crate) covering_projection: bool,
112 pub(crate) aggregation: AggregateKind,
113 pub(crate) execution_mode: ExplainExecutionMode,
114 pub(crate) ordering_source: ExplainExecutionOrderingSource,
115 pub(crate) limit: Option<u32>,
116 pub(crate) cursor: bool,
117 pub(crate) node_properties: BTreeMap<String, Value>,
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub enum ExplainExecutionNodeType {
127 ByKeyLookup,
128 ByKeysLookup,
129 PrimaryKeyRangeScan,
130 IndexPrefixScan,
131 IndexRangeScan,
132 IndexMultiLookup,
133 FullScan,
134 Union,
135 Intersection,
136 IndexPredicatePrefilter,
137 ResidualPredicateFilter,
138 OrderByAccessSatisfied,
139 OrderByMaterializedSort,
140 DistinctPreOrdered,
141 DistinctMaterialized,
142 ProjectionMaterialized,
143 ProjectionIndexOnly,
144 LimitOffset,
145 CursorResume,
146 IndexRangeLimitPushdown,
147 TopNSeek,
148 AggregateCount,
149 AggregateExists,
150 AggregateMin,
151 AggregateMax,
152 AggregateFirst,
153 AggregateLast,
154 AggregateSum,
155 AggregateSeekFirst,
156 AggregateSeekLast,
157 GroupedAggregateHashMaterialized,
158 GroupedAggregateOrderedMaterialized,
159 SecondaryOrderPushdown,
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct ExplainExecutionNodeDescriptor {
170 pub(crate) node_type: ExplainExecutionNodeType,
171 pub(crate) execution_mode: ExplainExecutionMode,
172 pub(crate) access_strategy: Option<ExplainAccessPath>,
173 pub(crate) predicate_pushdown: Option<String>,
174 pub(crate) residual_predicate: Option<ExplainPredicate>,
175 pub(crate) projection: Option<String>,
176 pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
177 pub(crate) limit: Option<u32>,
178 pub(crate) cursor: Option<bool>,
179 pub(crate) covering_scan: Option<bool>,
180 pub(crate) rows_expected: Option<u64>,
181 pub(crate) children: Vec<Self>,
182 pub(crate) node_properties: BTreeMap<String, Value>,
183}
184
185impl ExplainPlan {
186 #[must_use]
188 pub const fn mode(&self) -> QueryMode {
189 self.mode
190 }
191
192 #[must_use]
194 pub const fn access(&self) -> &ExplainAccessPath {
195 &self.access
196 }
197
198 #[must_use]
200 pub const fn predicate(&self) -> &ExplainPredicate {
201 &self.predicate
202 }
203
204 #[must_use]
206 pub const fn order_by(&self) -> &ExplainOrderBy {
207 &self.order_by
208 }
209
210 #[must_use]
212 pub const fn distinct(&self) -> bool {
213 self.distinct
214 }
215
216 #[must_use]
218 pub const fn grouping(&self) -> &ExplainGrouping {
219 &self.grouping
220 }
221
222 #[must_use]
224 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
225 &self.order_pushdown
226 }
227
228 #[must_use]
230 pub const fn page(&self) -> &ExplainPagination {
231 &self.page
232 }
233
234 #[must_use]
236 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
237 &self.delete_limit
238 }
239
240 #[must_use]
242 pub const fn consistency(&self) -> MissingRowPolicy {
243 self.consistency
244 }
245}
246
247impl ExplainAggregateTerminalPlan {
248 #[must_use]
250 pub const fn query(&self) -> &ExplainPlan {
251 &self.query
252 }
253
254 #[must_use]
256 pub const fn terminal(&self) -> AggregateKind {
257 self.terminal
258 }
259
260 #[must_use]
262 pub const fn route(&self) -> ExplainAggregateTerminalRoute {
263 self.route
264 }
265
266 #[must_use]
268 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
269 &self.execution
270 }
271
272 #[must_use]
273 pub(in crate::db) const fn new(
274 query: ExplainPlan,
275 terminal: AggregateKind,
276 execution: ExplainExecutionDescriptor,
277 ) -> Self {
278 let route = execution.route();
279
280 Self {
281 query,
282 terminal,
283 route,
284 execution,
285 }
286 }
287}
288
289impl ExplainExecutionDescriptor {
290 #[must_use]
292 pub const fn access_strategy(&self) -> &ExplainAccessPath {
293 &self.access_strategy
294 }
295
296 #[must_use]
298 pub const fn covering_projection(&self) -> bool {
299 self.covering_projection
300 }
301
302 #[must_use]
304 pub const fn aggregation(&self) -> AggregateKind {
305 self.aggregation
306 }
307
308 #[must_use]
310 pub const fn execution_mode(&self) -> ExplainExecutionMode {
311 self.execution_mode
312 }
313
314 #[must_use]
316 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
317 self.ordering_source
318 }
319
320 #[must_use]
322 pub const fn limit(&self) -> Option<u32> {
323 self.limit
324 }
325
326 #[must_use]
328 pub const fn cursor(&self) -> bool {
329 self.cursor
330 }
331
332 #[must_use]
334 pub const fn node_properties(&self) -> &BTreeMap<String, Value> {
335 &self.node_properties
336 }
337
338 #[must_use]
339 pub(in crate::db) const fn route(&self) -> ExplainAggregateTerminalRoute {
340 match self.ordering_source {
341 ExplainExecutionOrderingSource::IndexSeekFirst { fetch } => {
342 ExplainAggregateTerminalRoute::IndexSeekFirst { fetch }
343 }
344 ExplainExecutionOrderingSource::IndexSeekLast { fetch } => {
345 ExplainAggregateTerminalRoute::IndexSeekLast { fetch }
346 }
347 ExplainExecutionOrderingSource::AccessOrder
348 | ExplainExecutionOrderingSource::Materialized => {
349 ExplainAggregateTerminalRoute::Standard
350 }
351 }
352 }
353}
354
355impl ExplainAggregateTerminalPlan {
356 #[must_use]
357 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
358 ExplainExecutionNodeDescriptor {
359 node_type: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
360 execution_mode: self.execution.execution_mode,
361 access_strategy: Some(self.execution.access_strategy.clone()),
362 predicate_pushdown: None,
363 residual_predicate: None,
364 projection: None,
365 ordering_source: Some(self.execution.ordering_source),
366 limit: self.execution.limit,
367 cursor: Some(self.execution.cursor),
368 covering_scan: Some(self.execution.covering_projection),
369 rows_expected: None,
370 children: Vec::new(),
371 node_properties: self.execution.node_properties.clone(),
372 }
373 }
374}
375
376const fn aggregate_execution_node_type(
377 terminal: AggregateKind,
378 ordering_source: ExplainExecutionOrderingSource,
379) -> ExplainExecutionNodeType {
380 match ordering_source {
381 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
382 ExplainExecutionNodeType::AggregateSeekFirst
383 }
384 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
385 ExplainExecutionNodeType::AggregateSeekLast
386 }
387 ExplainExecutionOrderingSource::AccessOrder
388 | ExplainExecutionOrderingSource::Materialized => match terminal {
389 AggregateKind::Count => ExplainExecutionNodeType::AggregateCount,
390 AggregateKind::Exists => ExplainExecutionNodeType::AggregateExists,
391 AggregateKind::Min => ExplainExecutionNodeType::AggregateMin,
392 AggregateKind::Max => ExplainExecutionNodeType::AggregateMax,
393 AggregateKind::First => ExplainExecutionNodeType::AggregateFirst,
394 AggregateKind::Last => ExplainExecutionNodeType::AggregateLast,
395 AggregateKind::Sum => ExplainExecutionNodeType::AggregateSum,
396 },
397 }
398}
399
400impl ExplainExecutionNodeType {
401 #[must_use]
402 pub const fn as_str(self) -> &'static str {
403 match self {
404 Self::ByKeyLookup => "ByKeyLookup",
405 Self::ByKeysLookup => "ByKeysLookup",
406 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
407 Self::IndexPrefixScan => "IndexPrefixScan",
408 Self::IndexRangeScan => "IndexRangeScan",
409 Self::IndexMultiLookup => "IndexMultiLookup",
410 Self::FullScan => "FullScan",
411 Self::Union => "Union",
412 Self::Intersection => "Intersection",
413 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
414 Self::ResidualPredicateFilter => "ResidualPredicateFilter",
415 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
416 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
417 Self::DistinctPreOrdered => "DistinctPreOrdered",
418 Self::DistinctMaterialized => "DistinctMaterialized",
419 Self::ProjectionMaterialized => "ProjectionMaterialized",
420 Self::ProjectionIndexOnly => "ProjectionIndexOnly",
421 Self::LimitOffset => "LimitOffset",
422 Self::CursorResume => "CursorResume",
423 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
424 Self::TopNSeek => "TopNSeek",
425 Self::AggregateCount => "AggregateCount",
426 Self::AggregateExists => "AggregateExists",
427 Self::AggregateMin => "AggregateMin",
428 Self::AggregateMax => "AggregateMax",
429 Self::AggregateFirst => "AggregateFirst",
430 Self::AggregateLast => "AggregateLast",
431 Self::AggregateSum => "AggregateSum",
432 Self::AggregateSeekFirst => "AggregateSeekFirst",
433 Self::AggregateSeekLast => "AggregateSeekLast",
434 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
435 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
436 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
437 }
438 }
439}
440
441impl ExplainExecutionNodeDescriptor {
442 #[must_use]
444 pub const fn node_type(&self) -> ExplainExecutionNodeType {
445 self.node_type
446 }
447
448 #[must_use]
450 pub const fn execution_mode(&self) -> ExplainExecutionMode {
451 self.execution_mode
452 }
453
454 #[must_use]
456 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
457 self.access_strategy.as_ref()
458 }
459
460 #[must_use]
462 pub fn predicate_pushdown(&self) -> Option<&str> {
463 self.predicate_pushdown.as_deref()
464 }
465
466 #[must_use]
468 pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
469 self.residual_predicate.as_ref()
470 }
471
472 #[must_use]
474 pub fn projection(&self) -> Option<&str> {
475 self.projection.as_deref()
476 }
477
478 #[must_use]
480 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
481 self.ordering_source
482 }
483
484 #[must_use]
486 pub const fn limit(&self) -> Option<u32> {
487 self.limit
488 }
489
490 #[must_use]
492 pub const fn cursor(&self) -> Option<bool> {
493 self.cursor
494 }
495
496 #[must_use]
498 pub const fn covering_scan(&self) -> Option<bool> {
499 self.covering_scan
500 }
501
502 #[must_use]
504 pub const fn rows_expected(&self) -> Option<u64> {
505 self.rows_expected
506 }
507
508 #[must_use]
510 pub const fn children(&self) -> &[Self] {
511 self.children.as_slice()
512 }
513
514 #[must_use]
516 pub const fn node_properties(&self) -> &BTreeMap<String, Value> {
517 &self.node_properties
518 }
519
520 #[must_use]
521 pub fn render_text_tree(&self) -> String {
522 let mut lines = Vec::new();
523 self.render_text_tree_into(0, &mut lines);
524 lines.join("\n")
525 }
526
527 #[must_use]
528 pub fn render_json_canonical(&self) -> String {
529 let mut out = String::new();
530 write_execution_node_json(self, &mut out);
531 out
532 }
533
534 #[must_use]
535 pub fn render_text_tree_verbose(&self) -> String {
536 let mut lines = Vec::new();
537 self.render_text_tree_verbose_into(0, &mut lines);
538 lines.join("\n")
539 }
540
541 fn render_text_tree_into(&self, depth: usize, lines: &mut Vec<String>) {
542 let mut line = format!(
543 "{}{} execution_mode={}",
544 " ".repeat(depth),
545 self.node_type.as_str(),
546 execution_mode_label(self.execution_mode)
547 );
548
549 if let Some(access_strategy) = self.access_strategy.as_ref() {
550 let _ = write!(line, " access={}", access_strategy_label(access_strategy));
551 }
552 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
553 let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
554 }
555 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
556 let _ = write!(line, " residual_predicate={residual_predicate:?}");
557 }
558 if let Some(projection) = self.projection.as_ref() {
559 let _ = write!(line, " projection={projection}");
560 }
561 if let Some(ordering_source) = self.ordering_source {
562 let _ = write!(
563 line,
564 " ordering_source={}",
565 ordering_source_label(ordering_source)
566 );
567 }
568 if let Some(limit) = self.limit {
569 let _ = write!(line, " limit={limit}");
570 }
571 if let Some(cursor) = self.cursor {
572 let _ = write!(line, " cursor={cursor}");
573 }
574 if let Some(covering_scan) = self.covering_scan {
575 let _ = write!(line, " covering_scan={covering_scan}");
576 }
577 if let Some(rows_expected) = self.rows_expected {
578 let _ = write!(line, " rows_expected={rows_expected}");
579 }
580 if !self.node_properties.is_empty() {
581 let _ = write!(
582 line,
583 " node_properties={}",
584 render_node_properties(&self.node_properties)
585 );
586 }
587
588 lines.push(line);
589
590 for child in &self.children {
591 child.render_text_tree_into(depth.saturating_add(1), lines);
592 }
593 }
594
595 fn render_text_tree_verbose_into(&self, depth: usize, lines: &mut Vec<String>) {
596 let node_indent = " ".repeat(depth);
598 let field_indent = " ".repeat(depth.saturating_add(1));
599 lines.push(format!(
600 "{}{} execution_mode={}",
601 node_indent,
602 self.node_type.as_str(),
603 execution_mode_label(self.execution_mode)
604 ));
605
606 if let Some(access_strategy) = self.access_strategy.as_ref() {
608 lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
609 }
610 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
611 lines.push(format!(
612 "{field_indent}predicate_pushdown={predicate_pushdown}"
613 ));
614 }
615 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
616 lines.push(format!(
617 "{field_indent}residual_predicate={residual_predicate:?}"
618 ));
619 }
620 if let Some(projection) = self.projection.as_ref() {
621 lines.push(format!("{field_indent}projection={projection}"));
622 }
623 if let Some(ordering_source) = self.ordering_source {
624 lines.push(format!(
625 "{}ordering_source={}",
626 field_indent,
627 ordering_source_label(ordering_source)
628 ));
629 }
630 if let Some(limit) = self.limit {
631 lines.push(format!("{field_indent}limit={limit}"));
632 }
633 if let Some(cursor) = self.cursor {
634 lines.push(format!("{field_indent}cursor={cursor}"));
635 }
636 if let Some(covering_scan) = self.covering_scan {
637 lines.push(format!("{field_indent}covering_scan={covering_scan}"));
638 }
639 if let Some(rows_expected) = self.rows_expected {
640 lines.push(format!("{field_indent}rows_expected={rows_expected}"));
641 }
642 if !self.node_properties.is_empty() {
643 lines.push(format!(
644 "{}node_properties={}",
645 field_indent,
646 render_node_properties(&self.node_properties)
647 ));
648 }
649
650 for child in &self.children {
652 child.render_text_tree_verbose_into(depth.saturating_add(1), lines);
653 }
654 }
655}
656
657const fn execution_mode_label(mode: ExplainExecutionMode) -> &'static str {
658 match mode {
659 ExplainExecutionMode::Streaming => "Streaming",
660 ExplainExecutionMode::Materialized => "Materialized",
661 }
662}
663
664fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
665 let mut rendered = String::new();
666 let mut first = true;
667 for (key, value) in node_properties {
668 if first {
669 first = false;
670 } else {
671 rendered.push(',');
672 }
673 let _ = write!(rendered, "{key}={value:?}");
674 }
675 rendered
676}
677
678fn write_execution_node_json(node: &ExplainExecutionNodeDescriptor, out: &mut String) {
679 out.push('{');
680
681 write_json_field_name(out, "node_type");
682 write_json_string(out, node.node_type.as_str());
683 out.push(',');
684
685 write_json_field_name(out, "execution_mode");
686 write_json_string(out, execution_mode_label(node.execution_mode));
687 out.push(',');
688
689 write_json_field_name(out, "access_strategy");
690 match node.access_strategy.as_ref() {
691 Some(access) => write_access_json(access, out),
692 None => out.push_str("null"),
693 }
694 out.push(',');
695
696 write_json_field_name(out, "predicate_pushdown");
697 match node.predicate_pushdown.as_ref() {
698 Some(predicate_pushdown) => write_json_string(out, predicate_pushdown),
699 None => out.push_str("null"),
700 }
701 out.push(',');
702
703 write_json_field_name(out, "residual_predicate");
704 match node.residual_predicate.as_ref() {
705 Some(residual_predicate) => write_json_string(out, &format!("{residual_predicate:?}")),
706 None => out.push_str("null"),
707 }
708 out.push(',');
709
710 write_json_field_name(out, "projection");
711 match node.projection.as_ref() {
712 Some(projection) => write_json_string(out, projection),
713 None => out.push_str("null"),
714 }
715 out.push(',');
716
717 write_json_field_name(out, "ordering_source");
718 match node.ordering_source {
719 Some(ordering_source) => write_json_string(out, ordering_source_label(ordering_source)),
720 None => out.push_str("null"),
721 }
722 out.push(',');
723
724 write_json_field_name(out, "limit");
725 match node.limit {
726 Some(limit) => out.push_str(&limit.to_string()),
727 None => out.push_str("null"),
728 }
729 out.push(',');
730
731 write_json_field_name(out, "cursor");
732 match node.cursor {
733 Some(cursor) => out.push_str(if cursor { "true" } else { "false" }),
734 None => out.push_str("null"),
735 }
736 out.push(',');
737
738 write_json_field_name(out, "covering_scan");
739 match node.covering_scan {
740 Some(covering_scan) => out.push_str(if covering_scan { "true" } else { "false" }),
741 None => out.push_str("null"),
742 }
743 out.push(',');
744
745 write_json_field_name(out, "rows_expected");
746 match node.rows_expected {
747 Some(rows_expected) => out.push_str(&rows_expected.to_string()),
748 None => out.push_str("null"),
749 }
750 out.push(',');
751
752 write_json_field_name(out, "children");
753 out.push('[');
754 for (index, child) in node.children.iter().enumerate() {
755 if index > 0 {
756 out.push(',');
757 }
758 write_execution_node_json(child, out);
759 }
760 out.push(']');
761 out.push(',');
762
763 write_json_field_name(out, "node_properties");
764 write_node_properties_json(&node.node_properties, out);
765
766 out.push('}');
767}
768
769#[expect(clippy::too_many_lines)]
770fn write_access_json(access: &ExplainAccessPath, out: &mut String) {
771 match access {
772 ExplainAccessPath::ByKey { key } => {
773 out.push('{');
774 write_json_field_name(out, "type");
775 write_json_string(out, "ByKey");
776 out.push(',');
777 write_json_field_name(out, "key");
778 write_json_string(out, &format!("{key:?}"));
779 out.push('}');
780 }
781 ExplainAccessPath::ByKeys { keys } => {
782 out.push('{');
783 write_json_field_name(out, "type");
784 write_json_string(out, "ByKeys");
785 out.push(',');
786 write_json_field_name(out, "keys");
787 write_value_vec_as_debug_json(keys, out);
788 out.push('}');
789 }
790 ExplainAccessPath::KeyRange { start, end } => {
791 out.push('{');
792 write_json_field_name(out, "type");
793 write_json_string(out, "KeyRange");
794 out.push(',');
795 write_json_field_name(out, "start");
796 write_json_string(out, &format!("{start:?}"));
797 out.push(',');
798 write_json_field_name(out, "end");
799 write_json_string(out, &format!("{end:?}"));
800 out.push('}');
801 }
802 ExplainAccessPath::IndexPrefix {
803 name,
804 fields,
805 prefix_len,
806 values,
807 } => {
808 out.push('{');
809 write_json_field_name(out, "type");
810 write_json_string(out, "IndexPrefix");
811 out.push(',');
812 write_json_field_name(out, "name");
813 write_json_string(out, name);
814 out.push(',');
815 write_json_field_name(out, "fields");
816 write_str_vec_json(fields, out);
817 out.push(',');
818 write_json_field_name(out, "prefix_len");
819 out.push_str(&prefix_len.to_string());
820 out.push(',');
821 write_json_field_name(out, "values");
822 write_value_vec_as_debug_json(values, out);
823 out.push('}');
824 }
825 ExplainAccessPath::IndexMultiLookup {
826 name,
827 fields,
828 values,
829 } => {
830 out.push('{');
831 write_json_field_name(out, "type");
832 write_json_string(out, "IndexMultiLookup");
833 out.push(',');
834 write_json_field_name(out, "name");
835 write_json_string(out, name);
836 out.push(',');
837 write_json_field_name(out, "fields");
838 write_str_vec_json(fields, out);
839 out.push(',');
840 write_json_field_name(out, "values");
841 write_value_vec_as_debug_json(values, out);
842 out.push('}');
843 }
844 ExplainAccessPath::IndexRange {
845 name,
846 fields,
847 prefix_len,
848 prefix,
849 lower,
850 upper,
851 } => {
852 out.push('{');
853 write_json_field_name(out, "type");
854 write_json_string(out, "IndexRange");
855 out.push(',');
856 write_json_field_name(out, "name");
857 write_json_string(out, name);
858 out.push(',');
859 write_json_field_name(out, "fields");
860 write_str_vec_json(fields, out);
861 out.push(',');
862 write_json_field_name(out, "prefix_len");
863 out.push_str(&prefix_len.to_string());
864 out.push(',');
865 write_json_field_name(out, "prefix");
866 write_value_vec_as_debug_json(prefix, out);
867 out.push(',');
868 write_json_field_name(out, "lower");
869 write_json_string(out, &format!("{lower:?}"));
870 out.push(',');
871 write_json_field_name(out, "upper");
872 write_json_string(out, &format!("{upper:?}"));
873 out.push('}');
874 }
875 ExplainAccessPath::FullScan => {
876 out.push('{');
877 write_json_field_name(out, "type");
878 write_json_string(out, "FullScan");
879 out.push('}');
880 }
881 ExplainAccessPath::Union(children) => {
882 out.push('{');
883 write_json_field_name(out, "type");
884 write_json_string(out, "Union");
885 out.push(',');
886 write_json_field_name(out, "children");
887 out.push('[');
888 for (index, child) in children.iter().enumerate() {
889 if index > 0 {
890 out.push(',');
891 }
892 write_access_json(child, out);
893 }
894 out.push(']');
895 out.push('}');
896 }
897 ExplainAccessPath::Intersection(children) => {
898 out.push('{');
899 write_json_field_name(out, "type");
900 write_json_string(out, "Intersection");
901 out.push(',');
902 write_json_field_name(out, "children");
903 out.push('[');
904 for (index, child) in children.iter().enumerate() {
905 if index > 0 {
906 out.push(',');
907 }
908 write_access_json(child, out);
909 }
910 out.push(']');
911 out.push('}');
912 }
913 }
914}
915
916fn write_node_properties_json(node_properties: &BTreeMap<String, Value>, out: &mut String) {
917 out.push('{');
918 for (index, (key, value)) in node_properties.iter().enumerate() {
919 if index > 0 {
920 out.push(',');
921 }
922 write_json_field_name(out, key);
923 write_json_string(out, &format!("{value:?}"));
924 }
925 out.push('}');
926}
927
928fn write_value_vec_as_debug_json(values: &[Value], out: &mut String) {
929 out.push('[');
930 for (index, value) in values.iter().enumerate() {
931 if index > 0 {
932 out.push(',');
933 }
934 write_json_string(out, &format!("{value:?}"));
935 }
936 out.push(']');
937}
938
939fn write_str_vec_json(values: &[&str], out: &mut String) {
940 out.push('[');
941 for (index, value) in values.iter().enumerate() {
942 if index > 0 {
943 out.push(',');
944 }
945 write_json_string(out, value);
946 }
947 out.push(']');
948}
949
950fn write_json_field_name(out: &mut String, key: &str) {
951 write_json_string(out, key);
952 out.push(':');
953}
954
955fn write_json_string(out: &mut String, value: &str) {
956 out.push('"');
957 for ch in value.chars() {
958 match ch {
959 '"' => out.push_str("\\\""),
960 '\\' => out.push_str("\\\\"),
961 '\n' => out.push_str("\\n"),
962 '\r' => out.push_str("\\r"),
963 '\t' => out.push_str("\\t"),
964 '\u{08}' => out.push_str("\\b"),
965 '\u{0C}' => out.push_str("\\f"),
966 _ => out.push(ch),
967 }
968 }
969 out.push('"');
970}
971
972fn access_strategy_label(access: &ExplainAccessPath) -> String {
973 match access {
974 ExplainAccessPath::ByKey { .. } => "ByKey".to_string(),
975 ExplainAccessPath::ByKeys { .. } => "ByKeys".to_string(),
976 ExplainAccessPath::KeyRange { .. } => "KeyRange".to_string(),
977 ExplainAccessPath::IndexPrefix { name, .. } => format!("IndexPrefix({name})"),
978 ExplainAccessPath::IndexMultiLookup { name, .. } => {
979 format!("IndexMultiLookup({name})")
980 }
981 ExplainAccessPath::IndexRange { name, .. } => format!("IndexRange({name})"),
982 ExplainAccessPath::FullScan => "FullScan".to_string(),
983 ExplainAccessPath::Union(children) => format!("Union({})", children.len()),
984 ExplainAccessPath::Intersection(children) => format!("Intersection({})", children.len()),
985 }
986}
987
988const fn ordering_source_label(ordering_source: ExplainExecutionOrderingSource) -> &'static str {
989 match ordering_source {
990 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
991 ExplainExecutionOrderingSource::Materialized => "Materialized",
992 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
993 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
994 }
995}
996
997impl ExplainPlan {
998 #[must_use]
1002 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
1003 if let Some(predicate) = &self.predicate_model {
1004 debug_assert_eq!(
1005 self.predicate,
1006 ExplainPredicate::from_predicate(predicate),
1007 "explain predicate surface drifted from canonical predicate model"
1008 );
1009 Some(predicate)
1010 } else {
1011 debug_assert!(
1012 matches!(self.predicate, ExplainPredicate::None),
1013 "missing canonical predicate model requires ExplainPredicate::None"
1014 );
1015 None
1016 }
1017 }
1018}
1019
1020#[derive(Clone, Debug, Eq, PartialEq)]
1027pub enum ExplainGrouping {
1028 None,
1029 Grouped {
1030 strategy: ExplainGroupedStrategy,
1031 group_fields: Vec<ExplainGroupField>,
1032 aggregates: Vec<ExplainGroupAggregate>,
1033 having: Option<ExplainGroupHaving>,
1034 max_groups: u64,
1035 max_group_bytes: u64,
1036 },
1037}
1038
1039#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1045pub enum ExplainGroupedStrategy {
1046 HashGroup,
1047 OrderedGroup,
1048}
1049
1050impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
1051 fn from(value: GroupedPlanStrategyHint) -> Self {
1052 match value {
1053 GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
1054 GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
1055 }
1056 }
1057}
1058
1059#[derive(Clone, Debug, Eq, PartialEq)]
1066pub struct ExplainGroupField {
1067 pub(crate) slot_index: usize,
1068 pub(crate) field: String,
1069}
1070
1071impl ExplainGroupField {
1072 #[must_use]
1074 pub const fn slot_index(&self) -> usize {
1075 self.slot_index
1076 }
1077
1078 #[must_use]
1080 pub const fn field(&self) -> &str {
1081 self.field.as_str()
1082 }
1083}
1084
1085#[derive(Clone, Debug, Eq, PartialEq)]
1092pub struct ExplainGroupAggregate {
1093 pub(crate) kind: AggregateKind,
1094 pub(crate) target_field: Option<String>,
1095 pub(crate) distinct: bool,
1096}
1097
1098impl ExplainGroupAggregate {
1099 #[must_use]
1101 pub const fn kind(&self) -> AggregateKind {
1102 self.kind
1103 }
1104
1105 #[must_use]
1107 pub fn target_field(&self) -> Option<&str> {
1108 self.target_field.as_deref()
1109 }
1110
1111 #[must_use]
1113 pub const fn distinct(&self) -> bool {
1114 self.distinct
1115 }
1116}
1117
1118#[derive(Clone, Debug, Eq, PartialEq)]
1125pub struct ExplainGroupHaving {
1126 pub(crate) clauses: Vec<ExplainGroupHavingClause>,
1127}
1128
1129impl ExplainGroupHaving {
1130 #[must_use]
1132 pub const fn clauses(&self) -> &[ExplainGroupHavingClause] {
1133 self.clauses.as_slice()
1134 }
1135}
1136
1137#[derive(Clone, Debug, Eq, PartialEq)]
1144pub struct ExplainGroupHavingClause {
1145 pub(crate) symbol: ExplainGroupHavingSymbol,
1146 pub(crate) op: CompareOp,
1147 pub(crate) value: Value,
1148}
1149
1150impl ExplainGroupHavingClause {
1151 #[must_use]
1153 pub const fn symbol(&self) -> &ExplainGroupHavingSymbol {
1154 &self.symbol
1155 }
1156
1157 #[must_use]
1159 pub const fn op(&self) -> CompareOp {
1160 self.op
1161 }
1162
1163 #[must_use]
1165 pub const fn value(&self) -> &Value {
1166 &self.value
1167 }
1168}
1169
1170#[derive(Clone, Debug, Eq, PartialEq)]
1177pub enum ExplainGroupHavingSymbol {
1178 GroupField { slot_index: usize, field: String },
1179 AggregateIndex { index: usize },
1180}
1181
1182#[derive(Clone, Debug, Eq, PartialEq)]
1189pub enum ExplainOrderPushdown {
1190 MissingModelContext,
1191 EligibleSecondaryIndex {
1192 index: &'static str,
1193 prefix_len: usize,
1194 },
1195 Rejected(SecondaryOrderPushdownRejection),
1196}
1197
1198#[derive(Clone, Debug, Eq, PartialEq)]
1205pub enum ExplainAccessPath {
1206 ByKey {
1207 key: Value,
1208 },
1209 ByKeys {
1210 keys: Vec<Value>,
1211 },
1212 KeyRange {
1213 start: Value,
1214 end: Value,
1215 },
1216 IndexPrefix {
1217 name: &'static str,
1218 fields: Vec<&'static str>,
1219 prefix_len: usize,
1220 values: Vec<Value>,
1221 },
1222 IndexMultiLookup {
1223 name: &'static str,
1224 fields: Vec<&'static str>,
1225 values: Vec<Value>,
1226 },
1227 IndexRange {
1228 name: &'static str,
1229 fields: Vec<&'static str>,
1230 prefix_len: usize,
1231 prefix: Vec<Value>,
1232 lower: Bound<Value>,
1233 upper: Bound<Value>,
1234 },
1235 FullScan,
1236 Union(Vec<Self>),
1237 Intersection(Vec<Self>),
1238}
1239
1240#[derive(Clone, Debug, Eq, PartialEq)]
1247pub enum ExplainPredicate {
1248 None,
1249 True,
1250 False,
1251 And(Vec<Self>),
1252 Or(Vec<Self>),
1253 Not(Box<Self>),
1254 Compare {
1255 field: String,
1256 op: CompareOp,
1257 value: Value,
1258 coercion: CoercionSpec,
1259 },
1260 IsNull {
1261 field: String,
1262 },
1263 IsMissing {
1264 field: String,
1265 },
1266 IsEmpty {
1267 field: String,
1268 },
1269 IsNotEmpty {
1270 field: String,
1271 },
1272 TextContains {
1273 field: String,
1274 value: Value,
1275 },
1276 TextContainsCi {
1277 field: String,
1278 value: Value,
1279 },
1280}
1281
1282#[derive(Clone, Debug, Eq, PartialEq)]
1288pub enum ExplainOrderBy {
1289 None,
1290 Fields(Vec<ExplainOrder>),
1291}
1292
1293#[derive(Clone, Debug, Eq, PartialEq)]
1300pub struct ExplainOrder {
1301 pub(crate) field: String,
1302 pub(crate) direction: OrderDirection,
1303}
1304
1305impl ExplainOrder {
1306 #[must_use]
1308 pub const fn field(&self) -> &str {
1309 self.field.as_str()
1310 }
1311
1312 #[must_use]
1314 pub const fn direction(&self) -> OrderDirection {
1315 self.direction
1316 }
1317}
1318
1319#[derive(Clone, Debug, Eq, PartialEq)]
1326pub enum ExplainPagination {
1327 None,
1328 Page { limit: Option<u32>, offset: u32 },
1329}
1330
1331#[derive(Clone, Debug, Eq, PartialEq)]
1338pub enum ExplainDeleteLimit {
1339 None,
1340 Limit { max_rows: u32 },
1341}
1342
1343impl<K> AccessPlannedQuery<K>
1344where
1345 K: FieldValue,
1346{
1347 #[must_use]
1349 pub(crate) fn explain(&self) -> ExplainPlan {
1350 self.explain_inner(None)
1351 }
1352
1353 #[must_use]
1359 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
1360 self.explain_inner(Some(model))
1361 }
1362
1363 fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
1364 let (logical, grouping) = match &self.logical {
1366 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
1367 LogicalPlan::Grouped(logical) => (
1368 &logical.scalar,
1369 ExplainGrouping::Grouped {
1370 strategy: grouped_plan_strategy_hint_for_plan(self)
1371 .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
1372 group_fields: logical
1373 .group
1374 .group_fields
1375 .iter()
1376 .map(|field_slot| ExplainGroupField {
1377 slot_index: field_slot.index(),
1378 field: field_slot.field().to_string(),
1379 })
1380 .collect(),
1381 aggregates: logical
1382 .group
1383 .aggregates
1384 .iter()
1385 .map(|aggregate| ExplainGroupAggregate {
1386 kind: aggregate.kind,
1387 target_field: aggregate.target_field.clone(),
1388 distinct: aggregate.distinct,
1389 })
1390 .collect(),
1391 having: explain_group_having(logical.having.as_ref()),
1392 max_groups: logical.group.execution.max_groups(),
1393 max_group_bytes: logical.group.execution.max_group_bytes(),
1394 },
1395 ),
1396 };
1397
1398 explain_scalar_inner(logical, grouping, model, &self.access)
1400 }
1401}
1402
1403fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
1404 let having = having?;
1405
1406 Some(ExplainGroupHaving {
1407 clauses: having
1408 .clauses()
1409 .iter()
1410 .map(explain_group_having_clause)
1411 .collect(),
1412 })
1413}
1414
1415fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
1416 ExplainGroupHavingClause {
1417 symbol: explain_group_having_symbol(clause.symbol()),
1418 op: clause.op(),
1419 value: clause.value().clone(),
1420 }
1421}
1422
1423fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
1424 match symbol {
1425 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
1426 slot_index: field_slot.index(),
1427 field: field_slot.field().to_string(),
1428 },
1429 GroupHavingSymbol::AggregateIndex(index) => {
1430 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
1431 }
1432 }
1433}
1434
1435fn explain_scalar_inner<K>(
1436 logical: &ScalarPlan,
1437 grouping: ExplainGrouping,
1438 model: Option<&EntityModel>,
1439 access: &AccessPlan<K>,
1440) -> ExplainPlan
1441where
1442 K: FieldValue,
1443{
1444 let predicate_model = logical.predicate.as_ref().map(normalize);
1446 let predicate = match &predicate_model {
1447 Some(predicate) => ExplainPredicate::from_predicate(predicate),
1448 None => ExplainPredicate::None,
1449 };
1450
1451 let order_by = explain_order(logical.order.as_ref());
1453 let order_pushdown = explain_order_pushdown(model);
1454 let page = explain_page(logical.page.as_ref());
1455 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
1456
1457 ExplainPlan {
1459 mode: logical.mode,
1460 access: ExplainAccessPath::from_access_plan(access),
1461 predicate,
1462 predicate_model,
1463 order_by,
1464 distinct: logical.distinct,
1465 grouping,
1466 order_pushdown,
1467 page,
1468 delete_limit,
1469 consistency: logical.consistency,
1470 }
1471}
1472
1473const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
1474 let _ = model;
1475
1476 ExplainOrderPushdown::MissingModelContext
1478}
1479
1480impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
1481 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
1482 Self::from(PushdownSurfaceEligibility::from(&value))
1483 }
1484}
1485
1486impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
1487 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
1488 match value {
1489 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
1490 Self::EligibleSecondaryIndex { index, prefix_len }
1491 }
1492 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
1493 }
1494 }
1495}
1496
1497struct ExplainAccessProjection;
1498
1499impl<K> AccessPlanProjection<K> for ExplainAccessProjection
1500where
1501 K: FieldValue,
1502{
1503 type Output = ExplainAccessPath;
1504
1505 fn by_key(&mut self, key: &K) -> Self::Output {
1506 ExplainAccessPath::ByKey {
1507 key: key.to_value(),
1508 }
1509 }
1510
1511 fn by_keys(&mut self, keys: &[K]) -> Self::Output {
1512 ExplainAccessPath::ByKeys {
1513 keys: keys.iter().map(FieldValue::to_value).collect(),
1514 }
1515 }
1516
1517 fn key_range(&mut self, start: &K, end: &K) -> Self::Output {
1518 ExplainAccessPath::KeyRange {
1519 start: start.to_value(),
1520 end: end.to_value(),
1521 }
1522 }
1523
1524 fn index_prefix(
1525 &mut self,
1526 index_name: &'static str,
1527 index_fields: &[&'static str],
1528 prefix_len: usize,
1529 values: &[Value],
1530 ) -> Self::Output {
1531 ExplainAccessPath::IndexPrefix {
1532 name: index_name,
1533 fields: index_fields.to_vec(),
1534 prefix_len,
1535 values: values.to_vec(),
1536 }
1537 }
1538
1539 fn index_multi_lookup(
1540 &mut self,
1541 index_name: &'static str,
1542 index_fields: &[&'static str],
1543 values: &[Value],
1544 ) -> Self::Output {
1545 ExplainAccessPath::IndexMultiLookup {
1546 name: index_name,
1547 fields: index_fields.to_vec(),
1548 values: values.to_vec(),
1549 }
1550 }
1551
1552 fn index_range(
1553 &mut self,
1554 index_name: &'static str,
1555 index_fields: &[&'static str],
1556 prefix_len: usize,
1557 prefix: &[Value],
1558 lower: &Bound<Value>,
1559 upper: &Bound<Value>,
1560 ) -> Self::Output {
1561 ExplainAccessPath::IndexRange {
1562 name: index_name,
1563 fields: index_fields.to_vec(),
1564 prefix_len,
1565 prefix: prefix.to_vec(),
1566 lower: lower.clone(),
1567 upper: upper.clone(),
1568 }
1569 }
1570
1571 fn full_scan(&mut self) -> Self::Output {
1572 ExplainAccessPath::FullScan
1573 }
1574
1575 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
1576 ExplainAccessPath::Union(children)
1577 }
1578
1579 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
1580 ExplainAccessPath::Intersection(children)
1581 }
1582}
1583
1584impl ExplainAccessPath {
1585 pub(in crate::db) fn from_access_plan<K>(access: &AccessPlan<K>) -> Self
1586 where
1587 K: FieldValue,
1588 {
1589 let mut projection = ExplainAccessProjection;
1590 project_access_plan(access, &mut projection)
1591 }
1592}
1593
1594impl ExplainPredicate {
1595 fn from_predicate(predicate: &Predicate) -> Self {
1596 match predicate {
1597 Predicate::True => Self::True,
1598 Predicate::False => Self::False,
1599 Predicate::And(children) => {
1600 Self::And(children.iter().map(Self::from_predicate).collect())
1601 }
1602 Predicate::Or(children) => {
1603 Self::Or(children.iter().map(Self::from_predicate).collect())
1604 }
1605 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
1606 Predicate::Compare(compare) => Self::from_compare(compare),
1607 Predicate::IsNull { field } => Self::IsNull {
1608 field: field.clone(),
1609 },
1610 Predicate::IsMissing { field } => Self::IsMissing {
1611 field: field.clone(),
1612 },
1613 Predicate::IsEmpty { field } => Self::IsEmpty {
1614 field: field.clone(),
1615 },
1616 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
1617 field: field.clone(),
1618 },
1619 Predicate::TextContains { field, value } => Self::TextContains {
1620 field: field.clone(),
1621 value: value.clone(),
1622 },
1623 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
1624 field: field.clone(),
1625 value: value.clone(),
1626 },
1627 }
1628 }
1629
1630 fn from_compare(compare: &ComparePredicate) -> Self {
1631 Self::Compare {
1632 field: compare.field.clone(),
1633 op: compare.op,
1634 value: compare.value.clone(),
1635 coercion: compare.coercion.clone(),
1636 }
1637 }
1638}
1639
1640fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
1641 let Some(order) = order else {
1642 return ExplainOrderBy::None;
1643 };
1644
1645 if order.fields.is_empty() {
1646 return ExplainOrderBy::None;
1647 }
1648
1649 ExplainOrderBy::Fields(
1650 order
1651 .fields
1652 .iter()
1653 .map(|(field, direction)| ExplainOrder {
1654 field: field.clone(),
1655 direction: *direction,
1656 })
1657 .collect(),
1658 )
1659}
1660
1661const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1662 match page {
1663 Some(page) => ExplainPagination::Page {
1664 limit: page.limit,
1665 offset: page.offset,
1666 },
1667 None => ExplainPagination::None,
1668 }
1669}
1670
1671const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1672 match limit {
1673 Some(limit) => ExplainDeleteLimit::Limit {
1674 max_rows: limit.max_rows,
1675 },
1676 None => ExplainDeleteLimit::None,
1677 }
1678}
1679
1680#[cfg(test)]
1685mod tests;