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 mode: QueryMode,
37 pub access: ExplainAccessPath,
38 pub predicate: ExplainPredicate,
39 predicate_model: Option<Predicate>,
40 pub order_by: ExplainOrderBy,
41 pub distinct: bool,
42 pub grouping: ExplainGrouping,
43 pub order_pushdown: ExplainOrderPushdown,
44 pub page: ExplainPagination,
45 pub delete_limit: ExplainDeleteLimit,
46 pub 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 query: ExplainPlan,
71 pub terminal: AggregateKind,
72 pub route: ExplainAggregateTerminalRoute,
73 pub 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 access_strategy: ExplainAccessPath,
111 pub covering_projection: bool,
112 pub aggregation: AggregateKind,
113 pub execution_mode: ExplainExecutionMode,
114 pub ordering_source: ExplainExecutionOrderingSource,
115 pub limit: Option<u32>,
116 pub cursor: bool,
117 pub 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 node_type: ExplainExecutionNodeType,
171 pub execution_mode: ExplainExecutionMode,
172 pub access_strategy: Option<ExplainAccessPath>,
173 pub predicate_pushdown: Option<String>,
174 pub residual_predicate: Option<ExplainPredicate>,
175 pub projection: Option<String>,
176 pub ordering_source: Option<ExplainExecutionOrderingSource>,
177 pub limit: Option<u32>,
178 pub cursor: Option<bool>,
179 pub covering_scan: Option<bool>,
180 pub rows_expected: Option<u64>,
181 pub children: Vec<Self>,
182 pub node_properties: BTreeMap<String, Value>,
183}
184
185impl ExplainAggregateTerminalPlan {
186 #[must_use]
187 pub(in crate::db) const fn new(
188 query: ExplainPlan,
189 terminal: AggregateKind,
190 execution: ExplainExecutionDescriptor,
191 ) -> Self {
192 let route = execution.route();
193
194 Self {
195 query,
196 terminal,
197 route,
198 execution,
199 }
200 }
201}
202
203impl ExplainExecutionDescriptor {
204 #[must_use]
205 pub(in crate::db) const fn route(&self) -> ExplainAggregateTerminalRoute {
206 match self.ordering_source {
207 ExplainExecutionOrderingSource::IndexSeekFirst { fetch } => {
208 ExplainAggregateTerminalRoute::IndexSeekFirst { fetch }
209 }
210 ExplainExecutionOrderingSource::IndexSeekLast { fetch } => {
211 ExplainAggregateTerminalRoute::IndexSeekLast { fetch }
212 }
213 ExplainExecutionOrderingSource::AccessOrder
214 | ExplainExecutionOrderingSource::Materialized => {
215 ExplainAggregateTerminalRoute::Standard
216 }
217 }
218 }
219}
220
221impl ExplainAggregateTerminalPlan {
222 #[must_use]
223 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
224 ExplainExecutionNodeDescriptor {
225 node_type: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
226 execution_mode: self.execution.execution_mode,
227 access_strategy: Some(self.execution.access_strategy.clone()),
228 predicate_pushdown: None,
229 residual_predicate: None,
230 projection: None,
231 ordering_source: Some(self.execution.ordering_source),
232 limit: self.execution.limit,
233 cursor: Some(self.execution.cursor),
234 covering_scan: Some(self.execution.covering_projection),
235 rows_expected: None,
236 children: Vec::new(),
237 node_properties: self.execution.node_properties.clone(),
238 }
239 }
240}
241
242const fn aggregate_execution_node_type(
243 terminal: AggregateKind,
244 ordering_source: ExplainExecutionOrderingSource,
245) -> ExplainExecutionNodeType {
246 match ordering_source {
247 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
248 ExplainExecutionNodeType::AggregateSeekFirst
249 }
250 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
251 ExplainExecutionNodeType::AggregateSeekLast
252 }
253 ExplainExecutionOrderingSource::AccessOrder
254 | ExplainExecutionOrderingSource::Materialized => match terminal {
255 AggregateKind::Count => ExplainExecutionNodeType::AggregateCount,
256 AggregateKind::Exists => ExplainExecutionNodeType::AggregateExists,
257 AggregateKind::Min => ExplainExecutionNodeType::AggregateMin,
258 AggregateKind::Max => ExplainExecutionNodeType::AggregateMax,
259 AggregateKind::First => ExplainExecutionNodeType::AggregateFirst,
260 AggregateKind::Last => ExplainExecutionNodeType::AggregateLast,
261 AggregateKind::Sum => ExplainExecutionNodeType::AggregateSum,
262 },
263 }
264}
265
266impl ExplainExecutionNodeType {
267 #[must_use]
268 pub const fn as_str(self) -> &'static str {
269 match self {
270 Self::ByKeyLookup => "ByKeyLookup",
271 Self::ByKeysLookup => "ByKeysLookup",
272 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
273 Self::IndexPrefixScan => "IndexPrefixScan",
274 Self::IndexRangeScan => "IndexRangeScan",
275 Self::IndexMultiLookup => "IndexMultiLookup",
276 Self::FullScan => "FullScan",
277 Self::Union => "Union",
278 Self::Intersection => "Intersection",
279 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
280 Self::ResidualPredicateFilter => "ResidualPredicateFilter",
281 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
282 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
283 Self::DistinctPreOrdered => "DistinctPreOrdered",
284 Self::DistinctMaterialized => "DistinctMaterialized",
285 Self::ProjectionMaterialized => "ProjectionMaterialized",
286 Self::ProjectionIndexOnly => "ProjectionIndexOnly",
287 Self::LimitOffset => "LimitOffset",
288 Self::CursorResume => "CursorResume",
289 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
290 Self::TopNSeek => "TopNSeek",
291 Self::AggregateCount => "AggregateCount",
292 Self::AggregateExists => "AggregateExists",
293 Self::AggregateMin => "AggregateMin",
294 Self::AggregateMax => "AggregateMax",
295 Self::AggregateFirst => "AggregateFirst",
296 Self::AggregateLast => "AggregateLast",
297 Self::AggregateSum => "AggregateSum",
298 Self::AggregateSeekFirst => "AggregateSeekFirst",
299 Self::AggregateSeekLast => "AggregateSeekLast",
300 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
301 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
302 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
303 }
304 }
305}
306
307impl ExplainExecutionNodeDescriptor {
308 #[must_use]
309 pub fn render_text_tree(&self) -> String {
310 let mut lines = Vec::new();
311 self.render_text_tree_into(0, &mut lines);
312 lines.join("\n")
313 }
314
315 #[must_use]
316 pub fn render_json_canonical(&self) -> String {
317 let mut out = String::new();
318 write_execution_node_json(self, &mut out);
319 out
320 }
321
322 #[must_use]
323 pub fn render_text_tree_verbose(&self) -> String {
324 let mut lines = Vec::new();
325 self.render_text_tree_verbose_into(0, &mut lines);
326 lines.join("\n")
327 }
328
329 fn render_text_tree_into(&self, depth: usize, lines: &mut Vec<String>) {
330 let mut line = format!(
331 "{}{} execution_mode={}",
332 " ".repeat(depth),
333 self.node_type.as_str(),
334 execution_mode_label(self.execution_mode)
335 );
336
337 if let Some(access_strategy) = self.access_strategy.as_ref() {
338 let _ = write!(line, " access={}", access_strategy_label(access_strategy));
339 }
340 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
341 let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
342 }
343 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
344 let _ = write!(line, " residual_predicate={residual_predicate:?}");
345 }
346 if let Some(projection) = self.projection.as_ref() {
347 let _ = write!(line, " projection={projection}");
348 }
349 if let Some(ordering_source) = self.ordering_source {
350 let _ = write!(
351 line,
352 " ordering_source={}",
353 ordering_source_label(ordering_source)
354 );
355 }
356 if let Some(limit) = self.limit {
357 let _ = write!(line, " limit={limit}");
358 }
359 if let Some(cursor) = self.cursor {
360 let _ = write!(line, " cursor={cursor}");
361 }
362 if let Some(covering_scan) = self.covering_scan {
363 let _ = write!(line, " covering_scan={covering_scan}");
364 }
365 if let Some(rows_expected) = self.rows_expected {
366 let _ = write!(line, " rows_expected={rows_expected}");
367 }
368 if !self.node_properties.is_empty() {
369 let _ = write!(
370 line,
371 " node_properties={}",
372 render_node_properties(&self.node_properties)
373 );
374 }
375
376 lines.push(line);
377
378 for child in &self.children {
379 child.render_text_tree_into(depth.saturating_add(1), lines);
380 }
381 }
382
383 fn render_text_tree_verbose_into(&self, depth: usize, lines: &mut Vec<String>) {
384 let node_indent = " ".repeat(depth);
386 let field_indent = " ".repeat(depth.saturating_add(1));
387 lines.push(format!(
388 "{}{} execution_mode={}",
389 node_indent,
390 self.node_type.as_str(),
391 execution_mode_label(self.execution_mode)
392 ));
393
394 if let Some(access_strategy) = self.access_strategy.as_ref() {
396 lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
397 }
398 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
399 lines.push(format!(
400 "{field_indent}predicate_pushdown={predicate_pushdown}"
401 ));
402 }
403 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
404 lines.push(format!(
405 "{field_indent}residual_predicate={residual_predicate:?}"
406 ));
407 }
408 if let Some(projection) = self.projection.as_ref() {
409 lines.push(format!("{field_indent}projection={projection}"));
410 }
411 if let Some(ordering_source) = self.ordering_source {
412 lines.push(format!(
413 "{}ordering_source={}",
414 field_indent,
415 ordering_source_label(ordering_source)
416 ));
417 }
418 if let Some(limit) = self.limit {
419 lines.push(format!("{field_indent}limit={limit}"));
420 }
421 if let Some(cursor) = self.cursor {
422 lines.push(format!("{field_indent}cursor={cursor}"));
423 }
424 if let Some(covering_scan) = self.covering_scan {
425 lines.push(format!("{field_indent}covering_scan={covering_scan}"));
426 }
427 if let Some(rows_expected) = self.rows_expected {
428 lines.push(format!("{field_indent}rows_expected={rows_expected}"));
429 }
430 if !self.node_properties.is_empty() {
431 lines.push(format!(
432 "{}node_properties={}",
433 field_indent,
434 render_node_properties(&self.node_properties)
435 ));
436 }
437
438 for child in &self.children {
440 child.render_text_tree_verbose_into(depth.saturating_add(1), lines);
441 }
442 }
443}
444
445const fn execution_mode_label(mode: ExplainExecutionMode) -> &'static str {
446 match mode {
447 ExplainExecutionMode::Streaming => "Streaming",
448 ExplainExecutionMode::Materialized => "Materialized",
449 }
450}
451
452fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
453 let mut rendered = String::new();
454 let mut first = true;
455 for (key, value) in node_properties {
456 if first {
457 first = false;
458 } else {
459 rendered.push(',');
460 }
461 let _ = write!(rendered, "{key}={value:?}");
462 }
463 rendered
464}
465
466fn write_execution_node_json(node: &ExplainExecutionNodeDescriptor, out: &mut String) {
467 out.push('{');
468
469 write_json_field_name(out, "node_type");
470 write_json_string(out, node.node_type.as_str());
471 out.push(',');
472
473 write_json_field_name(out, "execution_mode");
474 write_json_string(out, execution_mode_label(node.execution_mode));
475 out.push(',');
476
477 write_json_field_name(out, "access_strategy");
478 match node.access_strategy.as_ref() {
479 Some(access) => write_access_json(access, out),
480 None => out.push_str("null"),
481 }
482 out.push(',');
483
484 write_json_field_name(out, "predicate_pushdown");
485 match node.predicate_pushdown.as_ref() {
486 Some(predicate_pushdown) => write_json_string(out, predicate_pushdown),
487 None => out.push_str("null"),
488 }
489 out.push(',');
490
491 write_json_field_name(out, "residual_predicate");
492 match node.residual_predicate.as_ref() {
493 Some(residual_predicate) => write_json_string(out, &format!("{residual_predicate:?}")),
494 None => out.push_str("null"),
495 }
496 out.push(',');
497
498 write_json_field_name(out, "projection");
499 match node.projection.as_ref() {
500 Some(projection) => write_json_string(out, projection),
501 None => out.push_str("null"),
502 }
503 out.push(',');
504
505 write_json_field_name(out, "ordering_source");
506 match node.ordering_source {
507 Some(ordering_source) => write_json_string(out, ordering_source_label(ordering_source)),
508 None => out.push_str("null"),
509 }
510 out.push(',');
511
512 write_json_field_name(out, "limit");
513 match node.limit {
514 Some(limit) => out.push_str(&limit.to_string()),
515 None => out.push_str("null"),
516 }
517 out.push(',');
518
519 write_json_field_name(out, "cursor");
520 match node.cursor {
521 Some(cursor) => out.push_str(if cursor { "true" } else { "false" }),
522 None => out.push_str("null"),
523 }
524 out.push(',');
525
526 write_json_field_name(out, "covering_scan");
527 match node.covering_scan {
528 Some(covering_scan) => out.push_str(if covering_scan { "true" } else { "false" }),
529 None => out.push_str("null"),
530 }
531 out.push(',');
532
533 write_json_field_name(out, "rows_expected");
534 match node.rows_expected {
535 Some(rows_expected) => out.push_str(&rows_expected.to_string()),
536 None => out.push_str("null"),
537 }
538 out.push(',');
539
540 write_json_field_name(out, "children");
541 out.push('[');
542 for (index, child) in node.children.iter().enumerate() {
543 if index > 0 {
544 out.push(',');
545 }
546 write_execution_node_json(child, out);
547 }
548 out.push(']');
549 out.push(',');
550
551 write_json_field_name(out, "node_properties");
552 write_node_properties_json(&node.node_properties, out);
553
554 out.push('}');
555}
556
557#[expect(clippy::too_many_lines)]
558fn write_access_json(access: &ExplainAccessPath, out: &mut String) {
559 match access {
560 ExplainAccessPath::ByKey { key } => {
561 out.push('{');
562 write_json_field_name(out, "type");
563 write_json_string(out, "ByKey");
564 out.push(',');
565 write_json_field_name(out, "key");
566 write_json_string(out, &format!("{key:?}"));
567 out.push('}');
568 }
569 ExplainAccessPath::ByKeys { keys } => {
570 out.push('{');
571 write_json_field_name(out, "type");
572 write_json_string(out, "ByKeys");
573 out.push(',');
574 write_json_field_name(out, "keys");
575 write_value_vec_as_debug_json(keys, out);
576 out.push('}');
577 }
578 ExplainAccessPath::KeyRange { start, end } => {
579 out.push('{');
580 write_json_field_name(out, "type");
581 write_json_string(out, "KeyRange");
582 out.push(',');
583 write_json_field_name(out, "start");
584 write_json_string(out, &format!("{start:?}"));
585 out.push(',');
586 write_json_field_name(out, "end");
587 write_json_string(out, &format!("{end:?}"));
588 out.push('}');
589 }
590 ExplainAccessPath::IndexPrefix {
591 name,
592 fields,
593 prefix_len,
594 values,
595 } => {
596 out.push('{');
597 write_json_field_name(out, "type");
598 write_json_string(out, "IndexPrefix");
599 out.push(',');
600 write_json_field_name(out, "name");
601 write_json_string(out, name);
602 out.push(',');
603 write_json_field_name(out, "fields");
604 write_str_vec_json(fields, out);
605 out.push(',');
606 write_json_field_name(out, "prefix_len");
607 out.push_str(&prefix_len.to_string());
608 out.push(',');
609 write_json_field_name(out, "values");
610 write_value_vec_as_debug_json(values, out);
611 out.push('}');
612 }
613 ExplainAccessPath::IndexMultiLookup {
614 name,
615 fields,
616 values,
617 } => {
618 out.push('{');
619 write_json_field_name(out, "type");
620 write_json_string(out, "IndexMultiLookup");
621 out.push(',');
622 write_json_field_name(out, "name");
623 write_json_string(out, name);
624 out.push(',');
625 write_json_field_name(out, "fields");
626 write_str_vec_json(fields, out);
627 out.push(',');
628 write_json_field_name(out, "values");
629 write_value_vec_as_debug_json(values, out);
630 out.push('}');
631 }
632 ExplainAccessPath::IndexRange {
633 name,
634 fields,
635 prefix_len,
636 prefix,
637 lower,
638 upper,
639 } => {
640 out.push('{');
641 write_json_field_name(out, "type");
642 write_json_string(out, "IndexRange");
643 out.push(',');
644 write_json_field_name(out, "name");
645 write_json_string(out, name);
646 out.push(',');
647 write_json_field_name(out, "fields");
648 write_str_vec_json(fields, out);
649 out.push(',');
650 write_json_field_name(out, "prefix_len");
651 out.push_str(&prefix_len.to_string());
652 out.push(',');
653 write_json_field_name(out, "prefix");
654 write_value_vec_as_debug_json(prefix, out);
655 out.push(',');
656 write_json_field_name(out, "lower");
657 write_json_string(out, &format!("{lower:?}"));
658 out.push(',');
659 write_json_field_name(out, "upper");
660 write_json_string(out, &format!("{upper:?}"));
661 out.push('}');
662 }
663 ExplainAccessPath::FullScan => {
664 out.push('{');
665 write_json_field_name(out, "type");
666 write_json_string(out, "FullScan");
667 out.push('}');
668 }
669 ExplainAccessPath::Union(children) => {
670 out.push('{');
671 write_json_field_name(out, "type");
672 write_json_string(out, "Union");
673 out.push(',');
674 write_json_field_name(out, "children");
675 out.push('[');
676 for (index, child) in children.iter().enumerate() {
677 if index > 0 {
678 out.push(',');
679 }
680 write_access_json(child, out);
681 }
682 out.push(']');
683 out.push('}');
684 }
685 ExplainAccessPath::Intersection(children) => {
686 out.push('{');
687 write_json_field_name(out, "type");
688 write_json_string(out, "Intersection");
689 out.push(',');
690 write_json_field_name(out, "children");
691 out.push('[');
692 for (index, child) in children.iter().enumerate() {
693 if index > 0 {
694 out.push(',');
695 }
696 write_access_json(child, out);
697 }
698 out.push(']');
699 out.push('}');
700 }
701 }
702}
703
704fn write_node_properties_json(node_properties: &BTreeMap<String, Value>, out: &mut String) {
705 out.push('{');
706 for (index, (key, value)) in node_properties.iter().enumerate() {
707 if index > 0 {
708 out.push(',');
709 }
710 write_json_field_name(out, key);
711 write_json_string(out, &format!("{value:?}"));
712 }
713 out.push('}');
714}
715
716fn write_value_vec_as_debug_json(values: &[Value], out: &mut String) {
717 out.push('[');
718 for (index, value) in values.iter().enumerate() {
719 if index > 0 {
720 out.push(',');
721 }
722 write_json_string(out, &format!("{value:?}"));
723 }
724 out.push(']');
725}
726
727fn write_str_vec_json(values: &[&str], out: &mut String) {
728 out.push('[');
729 for (index, value) in values.iter().enumerate() {
730 if index > 0 {
731 out.push(',');
732 }
733 write_json_string(out, value);
734 }
735 out.push(']');
736}
737
738fn write_json_field_name(out: &mut String, key: &str) {
739 write_json_string(out, key);
740 out.push(':');
741}
742
743fn write_json_string(out: &mut String, value: &str) {
744 out.push('"');
745 for ch in value.chars() {
746 match ch {
747 '"' => out.push_str("\\\""),
748 '\\' => out.push_str("\\\\"),
749 '\n' => out.push_str("\\n"),
750 '\r' => out.push_str("\\r"),
751 '\t' => out.push_str("\\t"),
752 '\u{08}' => out.push_str("\\b"),
753 '\u{0C}' => out.push_str("\\f"),
754 _ => out.push(ch),
755 }
756 }
757 out.push('"');
758}
759
760fn access_strategy_label(access: &ExplainAccessPath) -> String {
761 match access {
762 ExplainAccessPath::ByKey { .. } => "ByKey".to_string(),
763 ExplainAccessPath::ByKeys { .. } => "ByKeys".to_string(),
764 ExplainAccessPath::KeyRange { .. } => "KeyRange".to_string(),
765 ExplainAccessPath::IndexPrefix { name, .. } => format!("IndexPrefix({name})"),
766 ExplainAccessPath::IndexMultiLookup { name, .. } => {
767 format!("IndexMultiLookup({name})")
768 }
769 ExplainAccessPath::IndexRange { name, .. } => format!("IndexRange({name})"),
770 ExplainAccessPath::FullScan => "FullScan".to_string(),
771 ExplainAccessPath::Union(children) => format!("Union({})", children.len()),
772 ExplainAccessPath::Intersection(children) => format!("Intersection({})", children.len()),
773 }
774}
775
776const fn ordering_source_label(ordering_source: ExplainExecutionOrderingSource) -> &'static str {
777 match ordering_source {
778 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
779 ExplainExecutionOrderingSource::Materialized => "Materialized",
780 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
781 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
782 }
783}
784
785impl ExplainPlan {
786 #[must_use]
790 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
791 if let Some(predicate) = &self.predicate_model {
792 debug_assert_eq!(
793 self.predicate,
794 ExplainPredicate::from_predicate(predicate),
795 "explain predicate surface drifted from canonical predicate model"
796 );
797 Some(predicate)
798 } else {
799 debug_assert!(
800 matches!(self.predicate, ExplainPredicate::None),
801 "missing canonical predicate model requires ExplainPredicate::None"
802 );
803 None
804 }
805 }
806}
807
808#[derive(Clone, Debug, Eq, PartialEq)]
815pub enum ExplainGrouping {
816 None,
817 Grouped {
818 strategy: ExplainGroupedStrategy,
819 group_fields: Vec<ExplainGroupField>,
820 aggregates: Vec<ExplainGroupAggregate>,
821 having: Option<ExplainGroupHaving>,
822 max_groups: u64,
823 max_group_bytes: u64,
824 },
825}
826
827#[derive(Clone, Copy, Debug, Eq, PartialEq)]
833pub enum ExplainGroupedStrategy {
834 HashGroup,
835 OrderedGroup,
836}
837
838impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
839 fn from(value: GroupedPlanStrategyHint) -> Self {
840 match value {
841 GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
842 GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
843 }
844 }
845}
846
847#[derive(Clone, Debug, Eq, PartialEq)]
854pub struct ExplainGroupField {
855 pub slot_index: usize,
856 pub field: String,
857}
858
859#[derive(Clone, Debug, Eq, PartialEq)]
866pub struct ExplainGroupAggregate {
867 pub kind: AggregateKind,
868 pub target_field: Option<String>,
869 pub distinct: bool,
870}
871
872#[derive(Clone, Debug, Eq, PartialEq)]
879pub struct ExplainGroupHaving {
880 pub clauses: Vec<ExplainGroupHavingClause>,
881}
882
883#[derive(Clone, Debug, Eq, PartialEq)]
890pub struct ExplainGroupHavingClause {
891 pub symbol: ExplainGroupHavingSymbol,
892 pub op: CompareOp,
893 pub value: Value,
894}
895
896#[derive(Clone, Debug, Eq, PartialEq)]
903pub enum ExplainGroupHavingSymbol {
904 GroupField { slot_index: usize, field: String },
905 AggregateIndex { index: usize },
906}
907
908#[derive(Clone, Debug, Eq, PartialEq)]
915pub enum ExplainOrderPushdown {
916 MissingModelContext,
917 EligibleSecondaryIndex {
918 index: &'static str,
919 prefix_len: usize,
920 },
921 Rejected(SecondaryOrderPushdownRejection),
922}
923
924#[derive(Clone, Debug, Eq, PartialEq)]
931pub enum ExplainAccessPath {
932 ByKey {
933 key: Value,
934 },
935 ByKeys {
936 keys: Vec<Value>,
937 },
938 KeyRange {
939 start: Value,
940 end: Value,
941 },
942 IndexPrefix {
943 name: &'static str,
944 fields: Vec<&'static str>,
945 prefix_len: usize,
946 values: Vec<Value>,
947 },
948 IndexMultiLookup {
949 name: &'static str,
950 fields: Vec<&'static str>,
951 values: Vec<Value>,
952 },
953 IndexRange {
954 name: &'static str,
955 fields: Vec<&'static str>,
956 prefix_len: usize,
957 prefix: Vec<Value>,
958 lower: Bound<Value>,
959 upper: Bound<Value>,
960 },
961 FullScan,
962 Union(Vec<Self>),
963 Intersection(Vec<Self>),
964}
965
966#[derive(Clone, Debug, Eq, PartialEq)]
973pub enum ExplainPredicate {
974 None,
975 True,
976 False,
977 And(Vec<Self>),
978 Or(Vec<Self>),
979 Not(Box<Self>),
980 Compare {
981 field: String,
982 op: CompareOp,
983 value: Value,
984 coercion: CoercionSpec,
985 },
986 IsNull {
987 field: String,
988 },
989 IsMissing {
990 field: String,
991 },
992 IsEmpty {
993 field: String,
994 },
995 IsNotEmpty {
996 field: String,
997 },
998 TextContains {
999 field: String,
1000 value: Value,
1001 },
1002 TextContainsCi {
1003 field: String,
1004 value: Value,
1005 },
1006}
1007
1008#[derive(Clone, Debug, Eq, PartialEq)]
1014pub enum ExplainOrderBy {
1015 None,
1016 Fields(Vec<ExplainOrder>),
1017}
1018
1019#[derive(Clone, Debug, Eq, PartialEq)]
1025pub struct ExplainOrder {
1026 pub field: String,
1027 pub direction: OrderDirection,
1028}
1029
1030#[derive(Clone, Debug, Eq, PartialEq)]
1036pub enum ExplainPagination {
1037 None,
1038 Page { limit: Option<u32>, offset: u32 },
1039}
1040
1041#[derive(Clone, Debug, Eq, PartialEq)]
1047pub enum ExplainDeleteLimit {
1048 None,
1049 Limit { max_rows: u32 },
1050}
1051
1052impl<K> AccessPlannedQuery<K>
1053where
1054 K: FieldValue,
1055{
1056 #[must_use]
1058 pub(crate) fn explain(&self) -> ExplainPlan {
1059 self.explain_inner(None)
1060 }
1061
1062 #[must_use]
1068 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
1069 self.explain_inner(Some(model))
1070 }
1071
1072 fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
1073 let (logical, grouping) = match &self.logical {
1075 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
1076 LogicalPlan::Grouped(logical) => (
1077 &logical.scalar,
1078 ExplainGrouping::Grouped {
1079 strategy: grouped_plan_strategy_hint_for_plan(self)
1080 .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
1081 group_fields: logical
1082 .group
1083 .group_fields
1084 .iter()
1085 .map(|field_slot| ExplainGroupField {
1086 slot_index: field_slot.index(),
1087 field: field_slot.field().to_string(),
1088 })
1089 .collect(),
1090 aggregates: logical
1091 .group
1092 .aggregates
1093 .iter()
1094 .map(|aggregate| ExplainGroupAggregate {
1095 kind: aggregate.kind,
1096 target_field: aggregate.target_field.clone(),
1097 distinct: aggregate.distinct,
1098 })
1099 .collect(),
1100 having: explain_group_having(logical.having.as_ref()),
1101 max_groups: logical.group.execution.max_groups(),
1102 max_group_bytes: logical.group.execution.max_group_bytes(),
1103 },
1104 ),
1105 };
1106
1107 explain_scalar_inner(logical, grouping, model, &self.access)
1109 }
1110}
1111
1112fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
1113 let having = having?;
1114
1115 Some(ExplainGroupHaving {
1116 clauses: having
1117 .clauses()
1118 .iter()
1119 .map(explain_group_having_clause)
1120 .collect(),
1121 })
1122}
1123
1124fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
1125 ExplainGroupHavingClause {
1126 symbol: explain_group_having_symbol(clause.symbol()),
1127 op: clause.op(),
1128 value: clause.value().clone(),
1129 }
1130}
1131
1132fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
1133 match symbol {
1134 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
1135 slot_index: field_slot.index(),
1136 field: field_slot.field().to_string(),
1137 },
1138 GroupHavingSymbol::AggregateIndex(index) => {
1139 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
1140 }
1141 }
1142}
1143
1144fn explain_scalar_inner<K>(
1145 logical: &ScalarPlan,
1146 grouping: ExplainGrouping,
1147 model: Option<&EntityModel>,
1148 access: &AccessPlan<K>,
1149) -> ExplainPlan
1150where
1151 K: FieldValue,
1152{
1153 let predicate_model = logical.predicate.as_ref().map(normalize);
1155 let predicate = match &predicate_model {
1156 Some(predicate) => ExplainPredicate::from_predicate(predicate),
1157 None => ExplainPredicate::None,
1158 };
1159
1160 let order_by = explain_order(logical.order.as_ref());
1162 let order_pushdown = explain_order_pushdown(model);
1163 let page = explain_page(logical.page.as_ref());
1164 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
1165
1166 ExplainPlan {
1168 mode: logical.mode,
1169 access: ExplainAccessPath::from_access_plan(access),
1170 predicate,
1171 predicate_model,
1172 order_by,
1173 distinct: logical.distinct,
1174 grouping,
1175 order_pushdown,
1176 page,
1177 delete_limit,
1178 consistency: logical.consistency,
1179 }
1180}
1181
1182const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
1183 let _ = model;
1184
1185 ExplainOrderPushdown::MissingModelContext
1187}
1188
1189impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
1190 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
1191 Self::from(PushdownSurfaceEligibility::from(&value))
1192 }
1193}
1194
1195impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
1196 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
1197 match value {
1198 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
1199 Self::EligibleSecondaryIndex { index, prefix_len }
1200 }
1201 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
1202 }
1203 }
1204}
1205
1206struct ExplainAccessProjection;
1207
1208impl<K> AccessPlanProjection<K> for ExplainAccessProjection
1209where
1210 K: FieldValue,
1211{
1212 type Output = ExplainAccessPath;
1213
1214 fn by_key(&mut self, key: &K) -> Self::Output {
1215 ExplainAccessPath::ByKey {
1216 key: key.to_value(),
1217 }
1218 }
1219
1220 fn by_keys(&mut self, keys: &[K]) -> Self::Output {
1221 ExplainAccessPath::ByKeys {
1222 keys: keys.iter().map(FieldValue::to_value).collect(),
1223 }
1224 }
1225
1226 fn key_range(&mut self, start: &K, end: &K) -> Self::Output {
1227 ExplainAccessPath::KeyRange {
1228 start: start.to_value(),
1229 end: end.to_value(),
1230 }
1231 }
1232
1233 fn index_prefix(
1234 &mut self,
1235 index_name: &'static str,
1236 index_fields: &[&'static str],
1237 prefix_len: usize,
1238 values: &[Value],
1239 ) -> Self::Output {
1240 ExplainAccessPath::IndexPrefix {
1241 name: index_name,
1242 fields: index_fields.to_vec(),
1243 prefix_len,
1244 values: values.to_vec(),
1245 }
1246 }
1247
1248 fn index_multi_lookup(
1249 &mut self,
1250 index_name: &'static str,
1251 index_fields: &[&'static str],
1252 values: &[Value],
1253 ) -> Self::Output {
1254 ExplainAccessPath::IndexMultiLookup {
1255 name: index_name,
1256 fields: index_fields.to_vec(),
1257 values: values.to_vec(),
1258 }
1259 }
1260
1261 fn index_range(
1262 &mut self,
1263 index_name: &'static str,
1264 index_fields: &[&'static str],
1265 prefix_len: usize,
1266 prefix: &[Value],
1267 lower: &Bound<Value>,
1268 upper: &Bound<Value>,
1269 ) -> Self::Output {
1270 ExplainAccessPath::IndexRange {
1271 name: index_name,
1272 fields: index_fields.to_vec(),
1273 prefix_len,
1274 prefix: prefix.to_vec(),
1275 lower: lower.clone(),
1276 upper: upper.clone(),
1277 }
1278 }
1279
1280 fn full_scan(&mut self) -> Self::Output {
1281 ExplainAccessPath::FullScan
1282 }
1283
1284 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
1285 ExplainAccessPath::Union(children)
1286 }
1287
1288 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
1289 ExplainAccessPath::Intersection(children)
1290 }
1291}
1292
1293impl ExplainAccessPath {
1294 pub(in crate::db) fn from_access_plan<K>(access: &AccessPlan<K>) -> Self
1295 where
1296 K: FieldValue,
1297 {
1298 let mut projection = ExplainAccessProjection;
1299 project_access_plan(access, &mut projection)
1300 }
1301}
1302
1303impl ExplainPredicate {
1304 fn from_predicate(predicate: &Predicate) -> Self {
1305 match predicate {
1306 Predicate::True => Self::True,
1307 Predicate::False => Self::False,
1308 Predicate::And(children) => {
1309 Self::And(children.iter().map(Self::from_predicate).collect())
1310 }
1311 Predicate::Or(children) => {
1312 Self::Or(children.iter().map(Self::from_predicate).collect())
1313 }
1314 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
1315 Predicate::Compare(compare) => Self::from_compare(compare),
1316 Predicate::IsNull { field } => Self::IsNull {
1317 field: field.clone(),
1318 },
1319 Predicate::IsMissing { field } => Self::IsMissing {
1320 field: field.clone(),
1321 },
1322 Predicate::IsEmpty { field } => Self::IsEmpty {
1323 field: field.clone(),
1324 },
1325 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
1326 field: field.clone(),
1327 },
1328 Predicate::TextContains { field, value } => Self::TextContains {
1329 field: field.clone(),
1330 value: value.clone(),
1331 },
1332 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
1333 field: field.clone(),
1334 value: value.clone(),
1335 },
1336 }
1337 }
1338
1339 fn from_compare(compare: &ComparePredicate) -> Self {
1340 Self::Compare {
1341 field: compare.field.clone(),
1342 op: compare.op,
1343 value: compare.value.clone(),
1344 coercion: compare.coercion.clone(),
1345 }
1346 }
1347}
1348
1349fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
1350 let Some(order) = order else {
1351 return ExplainOrderBy::None;
1352 };
1353
1354 if order.fields.is_empty() {
1355 return ExplainOrderBy::None;
1356 }
1357
1358 ExplainOrderBy::Fields(
1359 order
1360 .fields
1361 .iter()
1362 .map(|(field, direction)| ExplainOrder {
1363 field: field.clone(),
1364 direction: *direction,
1365 })
1366 .collect(),
1367 )
1368}
1369
1370const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1371 match page {
1372 Some(page) => ExplainPagination::Page {
1373 limit: page.limit,
1374 offset: page.offset,
1375 },
1376 None => ExplainPagination::None,
1377 }
1378}
1379
1380const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1381 match limit {
1382 Some(limit) => ExplainDeleteLimit::Limit {
1383 max_rows: limit.max_rows,
1384 },
1385 None => ExplainDeleteLimit::None,
1386 }
1387}
1388
1389#[cfg(test)]
1394mod tests;