1use crate::{
7 db::query::{
8 explain::{ExplainAccessPath, ExplainPlan, ExplainPredicate},
9 plan::{AggregateKind, ResidualFilterShape},
10 trace::TraceReuseEvent,
11 },
12 value::Value,
13};
14use std::fmt::{self, Debug};
15
16#[cfg_attr(
17 doc,
18 doc = "ExplainPropertyMap\n\nStable ordered property map for EXPLAIN metadata.\nKeeps deterministic key order without `BTreeMap`."
19)]
20#[derive(Clone, Default, Eq, PartialEq)]
21pub struct ExplainPropertyMap {
22 entries: Vec<(&'static str, Value)>,
23}
24
25impl ExplainPropertyMap {
26 #[must_use]
28 pub const fn new() -> Self {
29 Self {
30 entries: Vec::new(),
31 }
32 }
33
34 pub fn insert(&mut self, key: &'static str, value: Value) -> Option<Value> {
36 match self
37 .entries
38 .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
39 {
40 Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
41 Err(index) => {
42 self.entries.insert(index, (key, value));
43 None
44 }
45 }
46 }
47
48 #[must_use]
50 pub fn get(&self, key: &str) -> Option<&Value> {
51 self.entries
52 .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
53 .ok()
54 .map(|index| &self.entries[index].1)
55 }
56
57 #[must_use]
59 #[cfg(test)]
60 pub fn contains_key(&self, key: &str) -> bool {
61 self.get(key).is_some()
62 }
63
64 #[must_use]
66 pub const fn is_empty(&self) -> bool {
67 self.entries.is_empty()
68 }
69
70 pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Value)> {
72 self.entries.iter().map(|(key, value)| (*key, value))
73 }
74}
75
76impl Debug for ExplainPropertyMap {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 let mut map = f.debug_map();
79 for (key, value) in self.iter() {
80 map.entry(&key, value);
81 }
82 map.finish()
83 }
84}
85
86pub(in crate::db) mod property_keys {
89 pub(in crate::db) const ACCESS_ALTERNATIVES: &str = "acc_alts";
90 pub(in crate::db) const ACCESS_CHOICE: &str = "acc_choice";
91 pub(in crate::db) const ACCESS_REASON: &str = "acc_reason";
92 pub(in crate::db) const ACCESS_REJECTIONS: &str = "acc_reject";
93 pub(in crate::db) const AGGREGATE_CONTRACT: &str = "aggregate_contract";
94 pub(in crate::db) const AGGREGATE_PHYSICAL: &str = "aggregate_physical";
95 pub(in crate::db) const CONTINUATION_MODE: &str = "cont_mode";
96 pub(in crate::db) const COUNT_FOLD: &str = "count_fold";
97 pub(in crate::db) const COVERING_FIELDS: &str = "covering_fields";
98 pub(in crate::db) const COVERING_KIND: &str = "covering_kind";
99 pub(in crate::db) const COVERING_ORDER: &str = "covering_order";
100 pub(in crate::db) const COVERING_READ_KIND: &str = "cov_read_kind";
101 pub(in crate::db) const COVERING_READ_ROUTE: &str = "cov_read_route";
102 pub(in crate::db) const COVERING_SCAN_REASON: &str = "cov_scan_reason";
103 pub(in crate::db) const COVERING_SOURCES: &str = "covering_sources";
104 pub(in crate::db) const EXISTING_ROW_MODE: &str = "existing_row_mode";
105 #[cfg(feature = "sql-explain")]
106 pub(in crate::db) const FILTER_EXPR: &str = "filter_expr";
107 pub(in crate::db) const FAST_PATH: &str = "fast_path";
108 pub(in crate::db) const FAST_REASON: &str = "fast_reason";
109 pub(in crate::db) const FAST_REJECTIONS: &str = "fast_reject";
110 pub(in crate::db) const FETCH: &str = "fetch";
111 pub(in crate::db) const GROUPED_EXECUTION_MODE: &str = "grouped_execution_mode";
112 pub(in crate::db) const GROUPED_PLAN_FALLBACK_REASON: &str = "grouped_plan_fallback_reason";
113 pub(in crate::db) const GROUPED_ROUTE_ELIGIBLE: &str = "grouped_route_eligible";
114 pub(in crate::db) const GROUPED_ROUTE_OUTCOME: &str = "grouped_route_outcome";
115 pub(in crate::db) const GROUPED_ROUTE_REJECTION_REASON: &str = "grouped_route_rejection_reason";
116 #[cfg(feature = "sql-explain")]
117 pub(in crate::db) const AGGREGATE_DIRECT_COUNT_METADATA_ELIGIBLE: &str =
118 "aggregate_direct_count_metadata_eligible";
119 #[cfg(feature = "sql-explain")]
120 pub(in crate::db) const AGGREGATE_DIRECT_COUNT_PREFIXES: &str =
121 "aggregate_direct_count_prefixes";
122 pub(in crate::db) const INDEX: &str = "index";
123 pub(in crate::db) const OFFSET: &str = "offset";
124 pub(in crate::db) const ORDER_BY_INDEX: &str = "order_by_idx";
125 pub(in crate::db) const ORDER_ROUTE_MODE: &str = "ord_route_mode";
126 pub(in crate::db) const ORDER_ROUTE_REASON: &str = "ord_route_reason";
127 pub(in crate::db) const PREDICATE_INDEX_CAPABILITY: &str = "pred_idx_cap";
128 pub(in crate::db) const PREFIX_LEN: &str = "prefix_len";
129 pub(in crate::db) const PREFIX_VALUES: &str = "prefix_values";
130 pub(in crate::db) const PROJECTION_FIELD: &str = "proj_field";
131 pub(in crate::db) const PROJECTION_FIELDS: &str = "proj_fields";
132 #[cfg(feature = "sql-explain")]
133 pub(in crate::db) const PROJECTION_MATERIALIZATION: &str = "proj_materialization";
134 pub(in crate::db) const PROJECTION_MODE: &str = "proj_mode";
135 pub(in crate::db) const PROJECTION_PUSHDOWN: &str = "proj_pushdown";
136 pub(in crate::db) const PUSHDOWN: &str = "pushdown";
137 pub(in crate::db) const RESIDUAL_FILTER_SHAPE: &str = "residual_filter_shape";
138 pub(in crate::db) const RESUME_FROM: &str = "resume_from";
139 pub(in crate::db) const SCAN_DIRECTION: &str = "scan_dir";
140 pub(in crate::db) const TERMINAL: &str = "terminal";
141 pub(in crate::db) const TERMINAL_FIELD: &str = "terminal_field";
142 pub(in crate::db) const TERMINAL_INDEX_ONLY: &str = "terminal_index_only";
143 pub(in crate::db) const TERMINAL_OUTPUT: &str = "terminal_output";
144 pub(in crate::db) const TERMINAL_PROJECTION_MODE: &str = "terminal_projection_mode";
145}
146
147pub(in crate::db) mod property_values {
150 pub(in crate::db) const COVERING_READ: &str = "covering_read";
151 #[cfg(feature = "sql-explain")]
152 pub(in crate::db) const DIRECT_SLOT_ROW: &str = "direct_slot_row";
153 pub(in crate::db) const HYBRID_COVERING: &str = "hybrid_covering";
154 pub(in crate::db) const MATERIALIZED: &str = "materialized";
155 pub(in crate::db) const NONE: &str = "none";
156 pub(in crate::db) const PURE_COVERING: &str = "pure_covering";
157 #[cfg(feature = "sql-explain")]
158 pub(in crate::db) const SCALAR_PROJECTION: &str = "scalar_projection";
159 pub(in crate::db) const STRICT_ALL_OR_NONE: &str = "strict_all_or_none";
160}
161
162#[cfg_attr(
163 doc,
164 doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
165)]
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub struct ExplainAggregateTerminalPlan {
168 pub(in crate::db) query: ExplainPlan,
169 pub(in crate::db) terminal: AggregateKind,
170 pub(in crate::db) execution: ExplainExecutionDescriptor,
171}
172
173#[cfg_attr(
174 doc,
175 doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
176)]
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
178pub enum ExplainExecutionOrderingSource {
179 AccessOrder,
180 Materialized,
181 IndexSeekFirst { fetch: usize },
182 IndexSeekLast { fetch: usize },
183}
184
185#[cfg_attr(
186 doc,
187 doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
188)]
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub enum ExplainExecutionMode {
191 Streaming,
192 Materialized,
193}
194
195#[cfg_attr(
196 doc,
197 doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
198)]
199#[derive(Clone, Debug, Eq, PartialEq)]
200pub struct ExplainExecutionDescriptor {
201 pub(in crate::db) access_strategy: ExplainAccessPath,
202 pub(in crate::db) covering_projection: bool,
203 pub(in crate::db) aggregation: AggregateKind,
204 pub(in crate::db) execution_mode: ExplainExecutionMode,
205 pub(in crate::db) ordering_source: ExplainExecutionOrderingSource,
206 pub(in crate::db) limit: Option<u32>,
207 pub(in crate::db) cursor: bool,
208 pub(in crate::db) node_properties: ExplainPropertyMap,
209}
210
211#[cfg_attr(
212 doc,
213 doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
214)]
215#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216pub enum ExplainExecutionNodeType {
217 ByKeyLookup,
218 ByKeysLookup,
219 PrimaryKeyRangeScan,
220 IndexPrefixScan,
221 IndexRangeScan,
222 IndexMultiLookup,
223 IndexBranchSet,
224 FullScan,
225 Union,
226 Intersection,
227 IndexPredicatePrefilter,
228 ResidualFilter,
229 OrderByAccessSatisfied,
230 OrderByMaterializedSort,
231 DistinctPreOrdered,
232 DistinctMaterialized,
233 ProjectionMaterialized,
234 CoveringRead,
235 LimitOffset,
236 CursorResume,
237 IndexRangeLimitPushdown,
238 TopNSeek,
239 AggregateCount,
240 AggregateExists,
241 AggregateMin,
242 AggregateMax,
243 AggregateFirst,
244 AggregateLast,
245 AggregateSum,
246 AggregateSeekFirst,
247 AggregateSeekLast,
248 GroupedAggregateHashMaterialized,
249 GroupedAggregateOrderedMaterialized,
250 SecondaryOrderPushdown,
251}
252
253#[cfg_attr(
254 doc,
255 doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
256)]
257#[derive(Clone, Debug, Eq, PartialEq)]
258pub struct ExplainExecutionNodeDescriptor {
259 pub(in crate::db) node_type: ExplainExecutionNodeType,
260 pub(in crate::db) execution_mode: ExplainExecutionMode,
261 pub(in crate::db) access_strategy: Option<ExplainAccessPath>,
262 pub(in crate::db) predicate_pushdown: Option<String>,
263 pub(in crate::db) filter_expr: Option<String>,
264 pub(in crate::db) residual_filter_expr: Option<String>,
265 pub(in crate::db) residual_filter_predicate: Option<ExplainPredicate>,
266 pub(in crate::db) projection: Option<String>,
267 pub(in crate::db) ordering_source: Option<ExplainExecutionOrderingSource>,
268 pub(in crate::db) limit: Option<u32>,
269 pub(in crate::db) cursor: Option<bool>,
270 pub(in crate::db) covering_scan: Option<bool>,
271 pub(in crate::db) rows_expected: Option<u64>,
272 pub(in crate::db) children: Vec<Self>,
273 pub(in crate::db) node_properties: ExplainPropertyMap,
274}
275
276#[derive(Clone, Debug, Eq, PartialEq)]
287pub(in crate::db) struct FinalizedQueryDiagnostics {
288 pub(in crate::db) execution: ExplainExecutionNodeDescriptor,
289 pub(in crate::db) route_diagnostics: Vec<String>,
290 pub(in crate::db) logical_diagnostics: Vec<String>,
291 pub(in crate::db) reuse: Option<TraceReuseEvent>,
292}
293
294impl ExplainAggregateTerminalPlan {
295 #[must_use]
297 pub const fn query(&self) -> &ExplainPlan {
298 &self.query
299 }
300
301 #[must_use]
303 pub const fn terminal(&self) -> AggregateKind {
304 self.terminal
305 }
306
307 #[must_use]
309 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
310 &self.execution
311 }
312
313 #[must_use]
314 pub(in crate::db) const fn new(
315 query: ExplainPlan,
316 terminal: AggregateKind,
317 execution: ExplainExecutionDescriptor,
318 ) -> Self {
319 Self {
320 query,
321 terminal,
322 execution,
323 }
324 }
325}
326
327impl ExplainExecutionDescriptor {
328 #[must_use]
330 pub const fn access_strategy(&self) -> &ExplainAccessPath {
331 &self.access_strategy
332 }
333
334 #[must_use]
336 pub const fn covering_projection(&self) -> bool {
337 self.covering_projection
338 }
339
340 #[must_use]
342 pub const fn aggregation(&self) -> AggregateKind {
343 self.aggregation
344 }
345
346 #[must_use]
348 pub const fn execution_mode(&self) -> ExplainExecutionMode {
349 self.execution_mode
350 }
351
352 #[must_use]
354 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
355 self.ordering_source
356 }
357
358 #[must_use]
360 pub const fn limit(&self) -> Option<u32> {
361 self.limit
362 }
363
364 #[must_use]
366 pub const fn cursor(&self) -> bool {
367 self.cursor
368 }
369
370 #[must_use]
372 pub const fn node_properties(&self) -> &ExplainPropertyMap {
373 &self.node_properties
374 }
375}
376
377impl FinalizedQueryDiagnostics {
378 #[must_use]
380 pub(in crate::db) const fn new(
381 execution: ExplainExecutionNodeDescriptor,
382 route_diagnostics: Vec<String>,
383 logical_diagnostics: Vec<String>,
384 reuse: Option<TraceReuseEvent>,
385 ) -> Self {
386 Self {
387 execution,
388 route_diagnostics,
389 logical_diagnostics,
390 reuse,
391 }
392 }
393
394 #[must_use]
396 pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
397 &self.execution
398 }
399}
400
401pub(in crate::db) fn annotate_aggregate_execution_identity_properties(
404 node_properties: &mut ExplainPropertyMap,
405 contract: &'static str,
406 physical: &'static str,
407) {
408 node_properties.insert(property_keys::AGGREGATE_CONTRACT, Value::from(contract));
409 node_properties.insert(property_keys::AGGREGATE_PHYSICAL, Value::from(physical));
410}
411
412impl ExplainAggregateTerminalPlan {
413 #[must_use]
415 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
416 let mut node_properties = self.execution.node_properties.clone();
417 annotate_aggregate_execution_identity_properties(
418 &mut node_properties,
419 "singleton",
420 scalar_aggregate_physical_label(self.execution.ordering_source),
421 );
422
423 ExplainExecutionNodeDescriptor {
424 node_type: match self.execution.ordering_source {
425 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
426 ExplainExecutionNodeType::AggregateSeekFirst
427 }
428 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
429 ExplainExecutionNodeType::AggregateSeekLast
430 }
431 ExplainExecutionOrderingSource::AccessOrder
432 | ExplainExecutionOrderingSource::Materialized => {
433 self.terminal.explain_execution_node_type()
434 }
435 },
436 execution_mode: self.execution.execution_mode,
437 access_strategy: Some(self.execution.access_strategy.clone()),
438 predicate_pushdown: None,
439 filter_expr: None,
440 residual_filter_expr: None,
441 residual_filter_predicate: None,
442 projection: None,
443 ordering_source: Some(self.execution.ordering_source),
444 limit: self.execution.limit,
445 cursor: Some(self.execution.cursor),
446 covering_scan: Some(self.execution.covering_projection),
447 rows_expected: None,
448 children: Vec::new(),
449 node_properties,
450 }
451 }
452}
453
454const fn scalar_aggregate_physical_label(
455 ordering_source: ExplainExecutionOrderingSource,
456) -> &'static str {
457 match ordering_source {
458 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "scalar_seek_first",
459 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "scalar_seek_last",
460 ExplainExecutionOrderingSource::AccessOrder
461 | ExplainExecutionOrderingSource::Materialized => "scalar_terminal",
462 }
463}
464
465impl AggregateKind {
466 #[must_use]
469 pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
470 match self {
471 Self::Count => ExplainExecutionNodeType::AggregateCount,
472 Self::Exists => ExplainExecutionNodeType::AggregateExists,
473 Self::Min => ExplainExecutionNodeType::AggregateMin,
474 Self::Max => ExplainExecutionNodeType::AggregateMax,
475 Self::First => ExplainExecutionNodeType::AggregateFirst,
476 Self::Last => ExplainExecutionNodeType::AggregateLast,
477 Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
478 }
479 }
480}
481
482impl ExplainExecutionNodeType {
483 #[must_use]
485 pub const fn as_str(self) -> &'static str {
486 match self {
487 Self::ByKeyLookup => "ByKeyLookup",
488 Self::ByKeysLookup => "ByKeysLookup",
489 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
490 Self::IndexPrefixScan => "IndexPrefixScan",
491 Self::IndexRangeScan => "IndexRangeScan",
492 Self::IndexMultiLookup => "IndexMultiLookup",
493 Self::IndexBranchSet => "IndexBranchSet",
494 Self::FullScan => "FullScan",
495 Self::Union => "Union",
496 Self::Intersection => "Intersection",
497 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
498 Self::ResidualFilter => "ResidualFilter",
499 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
500 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
501 Self::DistinctPreOrdered => "DistinctPreOrdered",
502 Self::DistinctMaterialized => "DistinctMaterialized",
503 Self::ProjectionMaterialized => "ProjectionMaterialized",
504 Self::CoveringRead => "CoveringRead",
505 Self::LimitOffset => "LimitOffset",
506 Self::CursorResume => "CursorResume",
507 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
508 Self::TopNSeek => "TopNSeek",
509 Self::AggregateCount => "AggregateCount",
510 Self::AggregateExists => "AggregateExists",
511 Self::AggregateMin => "AggregateMin",
512 Self::AggregateMax => "AggregateMax",
513 Self::AggregateFirst => "AggregateFirst",
514 Self::AggregateLast => "AggregateLast",
515 Self::AggregateSum => "AggregateSum",
516 Self::AggregateSeekFirst => "AggregateSeekFirst",
517 Self::AggregateSeekLast => "AggregateSeekLast",
518 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
519 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
520 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
521 }
522 }
523
524 #[must_use]
526 pub const fn layer_label(self) -> &'static str {
527 crate::db::query::explain::nodes::layer_label(self)
528 }
529}
530
531impl ExplainExecutionNodeDescriptor {
532 pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
534 visit(self);
535
536 for child in self.children() {
537 child.for_each_preorder(visit);
538 }
539 }
540
541 #[must_use]
543 #[cfg(test)]
544 pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
545 let mut found = false;
546 self.for_each_preorder(&mut |node| {
547 if node.node_type() == target {
548 found = true;
549 }
550 });
551
552 found
553 }
554
555 #[must_use]
557 pub const fn node_type(&self) -> ExplainExecutionNodeType {
558 self.node_type
559 }
560
561 #[must_use]
563 pub const fn execution_mode(&self) -> ExplainExecutionMode {
564 self.execution_mode
565 }
566
567 #[must_use]
569 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
570 self.access_strategy.as_ref()
571 }
572
573 #[must_use]
575 pub fn predicate_pushdown(&self) -> Option<&str> {
576 self.predicate_pushdown.as_deref()
577 }
578
579 #[must_use]
581 pub fn filter_expr(&self) -> Option<&str> {
582 self.filter_expr.as_deref()
583 }
584
585 #[must_use]
587 pub fn residual_filter_expr(&self) -> Option<&str> {
588 self.residual_filter_expr.as_deref()
589 }
590
591 #[must_use]
595 pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
596 self.residual_filter_predicate.as_ref()
597 }
598
599 #[must_use]
601 pub(in crate::db) const fn residual_filter_shape(&self) -> ResidualFilterShape {
602 ResidualFilterShape::from_presence(
603 self.residual_filter_expr.is_some(),
604 self.residual_filter_predicate.is_some(),
605 )
606 }
607
608 #[must_use]
610 pub const fn has_residual_filter(&self) -> bool {
611 !self.residual_filter_shape().is_absent()
612 }
613
614 #[must_use]
616 pub fn projection(&self) -> Option<&str> {
617 self.projection.as_deref()
618 }
619
620 #[must_use]
622 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
623 self.ordering_source
624 }
625
626 #[must_use]
628 pub const fn limit(&self) -> Option<u32> {
629 self.limit
630 }
631
632 #[must_use]
634 pub const fn cursor(&self) -> Option<bool> {
635 self.cursor
636 }
637
638 #[must_use]
640 pub const fn covering_scan(&self) -> Option<bool> {
641 self.covering_scan
642 }
643
644 #[must_use]
646 pub const fn rows_expected(&self) -> Option<u64> {
647 self.rows_expected
648 }
649
650 #[must_use]
652 pub const fn children(&self) -> &[Self] {
653 self.children.as_slice()
654 }
655
656 #[must_use]
658 pub const fn node_properties(&self) -> &ExplainPropertyMap {
659 &self.node_properties
660 }
661}
662
663pub(in crate::db::query::explain) const fn execution_mode_label(
664 mode: ExplainExecutionMode,
665) -> &'static str {
666 match mode {
667 ExplainExecutionMode::Streaming => "Streaming",
668 ExplainExecutionMode::Materialized => "Materialized",
669 }
670}
671
672pub(in crate::db::query::explain) const fn ordering_source_label(
673 ordering_source: ExplainExecutionOrderingSource,
674) -> &'static str {
675 match ordering_source {
676 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
677 ExplainExecutionOrderingSource::Materialized => "Materialized",
678 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
679 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
680 }
681}
682
683#[cfg(test)]
688mod tests {
689 use crate::db::query::explain::{
690 ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
691 ExplainPropertyMap,
692 };
693
694 fn node(
695 node_type: ExplainExecutionNodeType,
696 children: Vec<ExplainExecutionNodeDescriptor>,
697 ) -> ExplainExecutionNodeDescriptor {
698 ExplainExecutionNodeDescriptor {
699 node_type,
700 execution_mode: ExplainExecutionMode::Materialized,
701 access_strategy: None,
702 predicate_pushdown: None,
703 filter_expr: None,
704 residual_filter_expr: None,
705 residual_filter_predicate: None,
706 projection: None,
707 ordering_source: None,
708 limit: None,
709 cursor: None,
710 covering_scan: None,
711 rows_expected: None,
712 children,
713 node_properties: ExplainPropertyMap::new(),
714 }
715 }
716
717 #[test]
718 fn execution_node_contains_type_scans_preorder_tree() {
719 let root = node(
720 ExplainExecutionNodeType::Union,
721 vec![
722 node(ExplainExecutionNodeType::FullScan, Vec::new()),
723 node(
724 ExplainExecutionNodeType::Intersection,
725 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
726 ),
727 ],
728 );
729
730 assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
731 assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
732 }
733
734 #[test]
735 fn execution_node_preorder_visits_parent_before_children() {
736 let root = node(
737 ExplainExecutionNodeType::Union,
738 vec![
739 node(ExplainExecutionNodeType::FullScan, Vec::new()),
740 node(
741 ExplainExecutionNodeType::Intersection,
742 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
743 ),
744 ],
745 );
746 let mut visited = Vec::new();
747
748 root.for_each_preorder(&mut |node| visited.push(node.node_type()));
749
750 assert_eq!(
751 visited,
752 vec![
753 ExplainExecutionNodeType::Union,
754 ExplainExecutionNodeType::FullScan,
755 ExplainExecutionNodeType::Intersection,
756 ExplainExecutionNodeType::ResidualFilter,
757 ],
758 );
759 }
760}