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