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 pub(in crate::db) const GROUPED_ROUTE_ELIGIBLE: &str = "grouped_route_eligible";
116 pub(in crate::db) const GROUPED_ROUTE_OUTCOME: &str = "grouped_route_outcome";
117 pub(in crate::db) const GROUPED_ROUTE_REJECTION_REASON: &str = "grouped_route_rejection_reason";
118 #[cfg(feature = "sql-explain")]
119 pub(in crate::db) const AGGREGATE_DIRECT_COUNT_METADATA_ELIGIBLE: &str =
120 "aggregate_direct_count_metadata_eligible";
121 #[cfg(feature = "sql-explain")]
122 pub(in crate::db) const AGGREGATE_DIRECT_COUNT_PREFIXES: &str =
123 "aggregate_direct_count_prefixes";
124 pub(in crate::db) const INDEX: &str = "index";
125 pub(in crate::db) const LIMIT_STOP_AFTER: &str = "limit_stop_after";
126 pub(in crate::db) const OFFSET: &str = "offset";
127 pub(in crate::db) const ORDER_BY_INDEX: &str = "order_by_idx";
128 pub(in crate::db) const ORDER_BY_INDEX_HINT: &str = "order_by_idx_hint";
129 pub(in crate::db) const ORDER_ROUTE_MODE: &str = "ord_route_mode";
130 pub(in crate::db) const ORDER_ROUTE_REASON: &str = "ord_route_reason";
131 pub(in crate::db) const PREDICATE_INDEX_CAPABILITY: &str = "pred_idx_cap";
132 pub(in crate::db) const PREFIX_LEN: &str = "prefix_len";
133 pub(in crate::db) const PREFIX_VALUES: &str = "prefix_values";
134 pub(in crate::db) const PROJECTION_FIELD: &str = "proj_field";
135 pub(in crate::db) const PROJECTION_FIELDS: &str = "proj_fields";
136 #[cfg(feature = "sql-explain")]
137 pub(in crate::db) const PROJECTION_MATERIALIZATION: &str = "proj_materialization";
138 pub(in crate::db) const PROJECTION_MODE: &str = "proj_mode";
139 pub(in crate::db) const PROJECTION_PUSHDOWN: &str = "proj_pushdown";
140 pub(in crate::db) const PUSHDOWN: &str = "pushdown";
141 pub(in crate::db) const RESIDUAL_FILTER_SHAPE: &str = "residual_filter_shape";
142 pub(in crate::db) const RESUME_FROM: &str = "resume_from";
143 pub(in crate::db) const ROUTE_FAMILY: &str = "route_family";
144 pub(in crate::db) const ROUTE_OUTCOME: &str = "route_outcome";
145 pub(in crate::db) const ROUTE_REASON: &str = "route_reason";
146 pub(in crate::db) const SCAN_DIRECTION: &str = "scan_dir";
147 pub(in crate::db) const TERMINAL: &str = "terminal";
148 pub(in crate::db) const TERMINAL_FIELD: &str = "terminal_field";
149 pub(in crate::db) const TERMINAL_INDEX_ONLY: &str = "terminal_index_only";
150 pub(in crate::db) const TERMINAL_OUTPUT: &str = "terminal_output";
151 pub(in crate::db) const TERMINAL_PROJECTION_MODE: &str = "terminal_projection_mode";
152}
153
154pub(in crate::db) mod property_values {
157 pub(in crate::db) const COVERING_READ: &str = "covering_read";
158 #[cfg(feature = "sql-explain")]
159 pub(in crate::db) const DIRECT_SLOT_ROW: &str = "direct_slot_row";
160 pub(in crate::db) const HYBRID_COVERING: &str = "hybrid_covering";
161 pub(in crate::db) const MATERIALIZED: &str = "materialized";
162 pub(in crate::db) const NONE: &str = "none";
163 pub(in crate::db) const PURE_COVERING: &str = "pure_covering";
164 #[cfg(feature = "sql-explain")]
165 pub(in crate::db) const SCALAR_PROJECTION: &str = "scalar_projection";
166 pub(in crate::db) const STRICT_ALL_OR_NONE: &str = "strict_all_or_none";
167}
168
169#[cfg_attr(
170 doc,
171 doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
172)]
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct ExplainAggregateTerminalPlan {
175 pub(in crate::db) query: ExplainPlan,
176 pub(in crate::db) terminal: AggregateKind,
177 pub(in crate::db) execution: ExplainExecutionDescriptor,
178 pub(in crate::db) read_intent: ReadIntentKind,
179}
180
181#[cfg_attr(
182 doc,
183 doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
184)]
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub enum ExplainExecutionOrderingSource {
187 AccessOrder,
188 Materialized,
189 IndexSeekFirst { fetch: usize },
190 IndexSeekLast { fetch: usize },
191}
192
193#[cfg_attr(
194 doc,
195 doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
196)]
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum ExplainExecutionMode {
199 Streaming,
200 Materialized,
201}
202
203#[cfg_attr(
204 doc,
205 doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
206)]
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub struct ExplainExecutionDescriptor {
209 pub(in crate::db) access_strategy: ExplainAccessPath,
210 pub(in crate::db) covering_projection: bool,
211 pub(in crate::db) aggregation: AggregateKind,
212 pub(in crate::db) execution_mode: ExplainExecutionMode,
213 pub(in crate::db) ordering_source: ExplainExecutionOrderingSource,
214 pub(in crate::db) limit: Option<u32>,
215 pub(in crate::db) cursor: bool,
216 pub(in crate::db) node_properties: ExplainPropertyMap,
217}
218
219#[cfg_attr(
220 doc,
221 doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
222)]
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224pub enum ExplainExecutionNodeType {
225 ByKeyLookup,
226 ByKeysLookup,
227 PrimaryKeyRangeScan,
228 IndexPrefixScan,
229 IndexRangeScan,
230 IndexMultiLookup,
231 IndexBranchSet,
232 FullScan,
233 Union,
234 Intersection,
235 IndexPredicatePrefilter,
236 ResidualFilter,
237 OrderByAccessSatisfied,
238 OrderByMaterializedSort,
239 DistinctPreOrdered,
240 DistinctMaterialized,
241 ProjectionMaterialized,
242 CoveringRead,
243 LimitOffset,
244 CursorResume,
245 IndexRangeLimitPushdown,
246 TopNSeek,
247 AggregateCount,
248 AggregateExists,
249 AggregateMin,
250 AggregateMax,
251 AggregateFirst,
252 AggregateLast,
253 AggregateSum,
254 AggregateSeekFirst,
255 AggregateSeekLast,
256 GroupedAggregateHashMaterialized,
257 GroupedAggregateOrderedMaterialized,
258 SecondaryOrderPushdown,
259}
260
261#[cfg_attr(
262 doc,
263 doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
264)]
265#[derive(Clone, Debug, Eq, PartialEq)]
266pub struct ExplainExecutionNodeDescriptor {
267 pub(in crate::db) node_type: ExplainExecutionNodeType,
268 pub(in crate::db) execution_mode: ExplainExecutionMode,
269 pub(in crate::db) access_strategy: Option<ExplainAccessPath>,
270 pub(in crate::db) predicate_pushdown: Option<String>,
271 pub(in crate::db) filter_expr: Option<String>,
272 pub(in crate::db) residual_filter_expr: Option<String>,
273 pub(in crate::db) residual_filter_predicate: Option<ExplainPredicate>,
274 pub(in crate::db) projection: Option<String>,
275 pub(in crate::db) ordering_source: Option<ExplainExecutionOrderingSource>,
276 pub(in crate::db) limit: Option<u32>,
277 pub(in crate::db) cursor: Option<bool>,
278 pub(in crate::db) covering_scan: Option<bool>,
279 pub(in crate::db) rows_expected: Option<u64>,
280 pub(in crate::db) children: Vec<Self>,
281 pub(in crate::db) node_properties: ExplainPropertyMap,
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
295pub(in crate::db) struct FinalizedQueryDiagnostics {
296 pub(in crate::db) execution: ExplainExecutionNodeDescriptor,
297 pub(in crate::db) admission: Option<QueryAdmissionSummary>,
298 pub(in crate::db) route_diagnostics: Vec<String>,
299 pub(in crate::db) logical_diagnostics: Vec<String>,
300 pub(in crate::db) reuse: Option<TraceReuseEvent>,
301}
302
303impl ExplainAggregateTerminalPlan {
304 #[must_use]
306 pub const fn query(&self) -> &ExplainPlan {
307 &self.query
308 }
309
310 #[must_use]
312 pub const fn terminal(&self) -> AggregateKind {
313 self.terminal
314 }
315
316 #[must_use]
318 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
319 &self.execution
320 }
321
322 #[must_use]
327 pub const fn read_intent(&self) -> ReadIntentKind {
328 self.read_intent
329 }
330
331 #[must_use]
332 pub(in crate::db) const fn new(
333 query: ExplainPlan,
334 terminal: AggregateKind,
335 execution: ExplainExecutionDescriptor,
336 ) -> Self {
337 Self {
338 query,
339 terminal,
340 execution,
341 read_intent: ReadIntentKind::Unspecified,
342 }
343 }
344
345 #[must_use]
346 pub(in crate::db) const fn with_read_intent(mut self, read_intent: ReadIntentKind) -> Self {
347 self.read_intent = read_intent;
348 self
349 }
350}
351
352impl ExplainExecutionDescriptor {
353 #[must_use]
355 pub const fn access_strategy(&self) -> &ExplainAccessPath {
356 &self.access_strategy
357 }
358
359 #[must_use]
361 pub const fn covering_projection(&self) -> bool {
362 self.covering_projection
363 }
364
365 #[must_use]
367 pub const fn aggregation(&self) -> AggregateKind {
368 self.aggregation
369 }
370
371 #[must_use]
373 pub const fn execution_mode(&self) -> ExplainExecutionMode {
374 self.execution_mode
375 }
376
377 #[must_use]
379 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
380 self.ordering_source
381 }
382
383 #[must_use]
385 pub const fn limit(&self) -> Option<u32> {
386 self.limit
387 }
388
389 #[must_use]
391 pub const fn cursor(&self) -> bool {
392 self.cursor
393 }
394
395 #[must_use]
397 pub const fn node_properties(&self) -> &ExplainPropertyMap {
398 &self.node_properties
399 }
400}
401
402impl FinalizedQueryDiagnostics {
403 #[must_use]
405 pub(in crate::db) const fn new(
406 execution: ExplainExecutionNodeDescriptor,
407 route_diagnostics: Vec<String>,
408 logical_diagnostics: Vec<String>,
409 reuse: Option<TraceReuseEvent>,
410 ) -> Self {
411 Self {
412 execution,
413 admission: None,
414 route_diagnostics,
415 logical_diagnostics,
416 reuse,
417 }
418 }
419
420 #[must_use]
422 pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
423 &self.execution
424 }
425
426 #[must_use]
428 pub(in crate::db) fn with_admission(mut self, admission: QueryAdmissionSummary) -> Self {
429 self.admission = Some(admission);
430 self
431 }
432
433 #[must_use]
435 pub(in crate::db) const fn admission(&self) -> Option<&QueryAdmissionSummary> {
436 self.admission.as_ref()
437 }
438}
439
440pub(in crate::db) fn annotate_aggregate_execution_identity_properties(
443 node_properties: &mut ExplainPropertyMap,
444 contract: &'static str,
445 physical: &'static str,
446) {
447 node_properties.insert(property_keys::AGGREGATE_CONTRACT, Value::from(contract));
448 node_properties.insert(property_keys::AGGREGATE_PHYSICAL, Value::from(physical));
449}
450
451impl ExplainAggregateTerminalPlan {
452 #[must_use]
454 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
455 let mut node_properties = self.execution.node_properties.clone();
456 annotate_aggregate_execution_identity_properties(
457 &mut node_properties,
458 "singleton",
459 scalar_aggregate_physical_label(self.execution.ordering_source),
460 );
461
462 ExplainExecutionNodeDescriptor {
463 node_type: match self.execution.ordering_source {
464 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
465 ExplainExecutionNodeType::AggregateSeekFirst
466 }
467 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
468 ExplainExecutionNodeType::AggregateSeekLast
469 }
470 ExplainExecutionOrderingSource::AccessOrder
471 | ExplainExecutionOrderingSource::Materialized => {
472 self.terminal.explain_execution_node_type()
473 }
474 },
475 execution_mode: self.execution.execution_mode,
476 access_strategy: Some(self.execution.access_strategy.clone()),
477 predicate_pushdown: None,
478 filter_expr: None,
479 residual_filter_expr: None,
480 residual_filter_predicate: None,
481 projection: None,
482 ordering_source: Some(self.execution.ordering_source),
483 limit: self.execution.limit,
484 cursor: Some(self.execution.cursor),
485 covering_scan: Some(self.execution.covering_projection),
486 rows_expected: None,
487 children: Vec::new(),
488 node_properties,
489 }
490 }
491}
492
493const fn scalar_aggregate_physical_label(
494 ordering_source: ExplainExecutionOrderingSource,
495) -> &'static str {
496 match ordering_source {
497 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "scalar_seek_first",
498 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "scalar_seek_last",
499 ExplainExecutionOrderingSource::AccessOrder
500 | ExplainExecutionOrderingSource::Materialized => "scalar_terminal",
501 }
502}
503
504impl AggregateKind {
505 #[must_use]
508 pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
509 match self {
510 Self::Count => ExplainExecutionNodeType::AggregateCount,
511 Self::Exists => ExplainExecutionNodeType::AggregateExists,
512 Self::Min => ExplainExecutionNodeType::AggregateMin,
513 Self::Max => ExplainExecutionNodeType::AggregateMax,
514 Self::First => ExplainExecutionNodeType::AggregateFirst,
515 Self::Last => ExplainExecutionNodeType::AggregateLast,
516 Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
517 }
518 }
519}
520
521impl ExplainExecutionNodeType {
522 #[must_use]
524 pub const fn as_str(self) -> &'static str {
525 match self {
526 Self::ByKeyLookup => "ByKeyLookup",
527 Self::ByKeysLookup => "ByKeysLookup",
528 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
529 Self::IndexPrefixScan => "IndexPrefixScan",
530 Self::IndexRangeScan => "IndexRangeScan",
531 Self::IndexMultiLookup => "IndexMultiLookup",
532 Self::IndexBranchSet => "IndexBranchSet",
533 Self::FullScan => "FullScan",
534 Self::Union => "Union",
535 Self::Intersection => "Intersection",
536 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
537 Self::ResidualFilter => "ResidualFilter",
538 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
539 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
540 Self::DistinctPreOrdered => "DistinctPreOrdered",
541 Self::DistinctMaterialized => "DistinctMaterialized",
542 Self::ProjectionMaterialized => "ProjectionMaterialized",
543 Self::CoveringRead => "CoveringRead",
544 Self::LimitOffset => "LimitOffset",
545 Self::CursorResume => "CursorResume",
546 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
547 Self::TopNSeek => "TopNSeek",
548 Self::AggregateCount => "AggregateCount",
549 Self::AggregateExists => "AggregateExists",
550 Self::AggregateMin => "AggregateMin",
551 Self::AggregateMax => "AggregateMax",
552 Self::AggregateFirst => "AggregateFirst",
553 Self::AggregateLast => "AggregateLast",
554 Self::AggregateSum => "AggregateSum",
555 Self::AggregateSeekFirst => "AggregateSeekFirst",
556 Self::AggregateSeekLast => "AggregateSeekLast",
557 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
558 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
559 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
560 }
561 }
562
563 #[must_use]
565 pub const fn layer_label(self) -> &'static str {
566 crate::db::query::explain::nodes::layer_label(self)
567 }
568}
569
570impl ExplainExecutionNodeDescriptor {
571 pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
573 visit(self);
574
575 for child in self.children() {
576 child.for_each_preorder(visit);
577 }
578 }
579
580 #[must_use]
582 #[cfg(test)]
583 pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
584 let mut found = false;
585 self.for_each_preorder(&mut |node| {
586 if node.node_type() == target {
587 found = true;
588 }
589 });
590
591 found
592 }
593
594 #[must_use]
596 pub const fn node_type(&self) -> ExplainExecutionNodeType {
597 self.node_type
598 }
599
600 #[must_use]
602 pub const fn execution_mode(&self) -> ExplainExecutionMode {
603 self.execution_mode
604 }
605
606 #[must_use]
608 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
609 self.access_strategy.as_ref()
610 }
611
612 #[must_use]
614 pub fn predicate_pushdown(&self) -> Option<&str> {
615 self.predicate_pushdown.as_deref()
616 }
617
618 #[must_use]
620 pub fn filter_expr(&self) -> Option<&str> {
621 self.filter_expr.as_deref()
622 }
623
624 #[must_use]
626 pub fn residual_filter_expr(&self) -> Option<&str> {
627 self.residual_filter_expr.as_deref()
628 }
629
630 #[must_use]
634 pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
635 self.residual_filter_predicate.as_ref()
636 }
637
638 #[must_use]
640 pub(in crate::db) const fn residual_filter_shape(&self) -> ResidualFilterShape {
641 ResidualFilterShape::from_presence(
642 self.residual_filter_expr.is_some(),
643 self.residual_filter_predicate.is_some(),
644 )
645 }
646
647 #[must_use]
649 pub const fn has_residual_filter(&self) -> bool {
650 !self.residual_filter_shape().is_absent()
651 }
652
653 #[must_use]
655 pub fn projection(&self) -> Option<&str> {
656 self.projection.as_deref()
657 }
658
659 #[must_use]
661 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
662 self.ordering_source
663 }
664
665 #[must_use]
667 pub const fn limit(&self) -> Option<u32> {
668 self.limit
669 }
670
671 #[must_use]
673 pub const fn cursor(&self) -> Option<bool> {
674 self.cursor
675 }
676
677 #[must_use]
679 pub const fn covering_scan(&self) -> Option<bool> {
680 self.covering_scan
681 }
682
683 #[must_use]
685 pub const fn rows_expected(&self) -> Option<u64> {
686 self.rows_expected
687 }
688
689 #[must_use]
691 pub const fn children(&self) -> &[Self] {
692 self.children.as_slice()
693 }
694
695 #[must_use]
697 pub const fn node_properties(&self) -> &ExplainPropertyMap {
698 &self.node_properties
699 }
700}
701
702pub(in crate::db::query::explain) const fn execution_mode_label(
703 mode: ExplainExecutionMode,
704) -> &'static str {
705 match mode {
706 ExplainExecutionMode::Streaming => "Streaming",
707 ExplainExecutionMode::Materialized => "Materialized",
708 }
709}
710
711pub(in crate::db::query::explain) const fn ordering_source_label(
712 ordering_source: ExplainExecutionOrderingSource,
713) -> &'static str {
714 match ordering_source {
715 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
716 ExplainExecutionOrderingSource::Materialized => "Materialized",
717 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
718 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
719 }
720}
721
722#[cfg(test)]
727mod tests {
728 use crate::db::query::explain::{
729 ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
730 ExplainPropertyMap,
731 };
732
733 fn node(
734 node_type: ExplainExecutionNodeType,
735 children: Vec<ExplainExecutionNodeDescriptor>,
736 ) -> ExplainExecutionNodeDescriptor {
737 ExplainExecutionNodeDescriptor {
738 node_type,
739 execution_mode: ExplainExecutionMode::Materialized,
740 access_strategy: None,
741 predicate_pushdown: None,
742 filter_expr: None,
743 residual_filter_expr: None,
744 residual_filter_predicate: None,
745 projection: None,
746 ordering_source: None,
747 limit: None,
748 cursor: None,
749 covering_scan: None,
750 rows_expected: None,
751 children,
752 node_properties: ExplainPropertyMap::new(),
753 }
754 }
755
756 #[test]
757 fn execution_node_contains_type_scans_preorder_tree() {
758 let root = node(
759 ExplainExecutionNodeType::Union,
760 vec![
761 node(ExplainExecutionNodeType::FullScan, Vec::new()),
762 node(
763 ExplainExecutionNodeType::Intersection,
764 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
765 ),
766 ],
767 );
768
769 assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
770 assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
771 }
772
773 #[test]
774 fn execution_node_preorder_visits_parent_before_children() {
775 let root = node(
776 ExplainExecutionNodeType::Union,
777 vec![
778 node(ExplainExecutionNodeType::FullScan, Vec::new()),
779 node(
780 ExplainExecutionNodeType::Intersection,
781 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
782 ),
783 ],
784 );
785 let mut visited = Vec::new();
786
787 root.for_each_preorder(&mut |node| visited.push(node.node_type()));
788
789 assert_eq!(
790 visited,
791 vec![
792 ExplainExecutionNodeType::Union,
793 ExplainExecutionNodeType::FullScan,
794 ExplainExecutionNodeType::Intersection,
795 ExplainExecutionNodeType::ResidualFilter,
796 ],
797 );
798 }
799}