1use crate::{
7 db::{
8 executor::RouteExecutionMode,
9 query::{
10 explain::{ExplainAccessPath, ExplainPlan, ExplainPredicate},
11 plan::AggregateKind,
12 trace::TraceReuseEvent,
13 },
14 },
15 value::Value,
16};
17use std::fmt::{self, Debug};
18
19#[cfg_attr(
20 doc,
21 doc = "ExplainPropertyMap\n\nStable ordered property map for EXPLAIN metadata.\nKeeps deterministic key order without `BTreeMap`."
22)]
23#[derive(Clone, Default, Eq, PartialEq)]
24pub struct ExplainPropertyMap {
25 entries: Vec<(&'static str, Value)>,
26}
27
28impl ExplainPropertyMap {
29 #[must_use]
31 pub const fn new() -> Self {
32 Self {
33 entries: Vec::new(),
34 }
35 }
36
37 pub fn insert(&mut self, key: &'static str, value: Value) -> Option<Value> {
39 match self
40 .entries
41 .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
42 {
43 Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
44 Err(index) => {
45 self.entries.insert(index, (key, value));
46 None
47 }
48 }
49 }
50
51 #[must_use]
53 pub fn get(&self, key: &str) -> Option<&Value> {
54 self.entries
55 .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
56 .ok()
57 .map(|index| &self.entries[index].1)
58 }
59
60 #[must_use]
62 #[cfg(test)]
63 pub fn contains_key(&self, key: &str) -> bool {
64 self.get(key).is_some()
65 }
66
67 #[must_use]
69 pub const fn is_empty(&self) -> bool {
70 self.entries.is_empty()
71 }
72
73 pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Value)> {
75 self.entries.iter().map(|(key, value)| (*key, value))
76 }
77}
78
79impl Debug for ExplainPropertyMap {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 let mut map = f.debug_map();
82 for (key, value) in self.iter() {
83 map.entry(&key, value);
84 }
85 map.finish()
86 }
87}
88
89#[cfg_attr(
90 doc,
91 doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
92)]
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct ExplainAggregateTerminalPlan {
95 pub(crate) query: ExplainPlan,
96 pub(crate) terminal: AggregateKind,
97 pub(crate) execution: ExplainExecutionDescriptor,
98}
99
100#[cfg_attr(
101 doc,
102 doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
103)]
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub enum ExplainExecutionOrderingSource {
106 AccessOrder,
107 Materialized,
108 IndexSeekFirst { fetch: usize },
109 IndexSeekLast { fetch: usize },
110}
111
112#[cfg_attr(
113 doc,
114 doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
115)]
116pub type ExplainExecutionMode = RouteExecutionMode;
117
118#[cfg_attr(
119 doc,
120 doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
121)]
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct ExplainExecutionDescriptor {
124 pub(crate) access_strategy: ExplainAccessPath,
125 pub(crate) covering_projection: bool,
126 pub(crate) aggregation: AggregateKind,
127 pub(crate) execution_mode: ExplainExecutionMode,
128 pub(crate) ordering_source: ExplainExecutionOrderingSource,
129 pub(crate) limit: Option<u32>,
130 pub(crate) cursor: bool,
131 pub(crate) node_properties: ExplainPropertyMap,
132}
133
134#[cfg_attr(
135 doc,
136 doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
137)]
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub enum ExplainExecutionNodeType {
140 ByKeyLookup,
141 ByKeysLookup,
142 PrimaryKeyRangeScan,
143 IndexPrefixScan,
144 IndexRangeScan,
145 IndexMultiLookup,
146 FullScan,
147 Union,
148 Intersection,
149 IndexPredicatePrefilter,
150 ResidualFilter,
151 OrderByAccessSatisfied,
152 OrderByMaterializedSort,
153 DistinctPreOrdered,
154 DistinctMaterialized,
155 ProjectionMaterialized,
156 CoveringRead,
157 LimitOffset,
158 CursorResume,
159 IndexRangeLimitPushdown,
160 TopNSeek,
161 AggregateCount,
162 AggregateExists,
163 AggregateMin,
164 AggregateMax,
165 AggregateFirst,
166 AggregateLast,
167 AggregateSum,
168 AggregateSeekFirst,
169 AggregateSeekLast,
170 GroupedAggregateHashMaterialized,
171 GroupedAggregateOrderedMaterialized,
172 SecondaryOrderPushdown,
173}
174
175#[cfg_attr(
176 doc,
177 doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
178)]
179#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct ExplainExecutionNodeDescriptor {
181 pub(crate) node_type: ExplainExecutionNodeType,
182 pub(crate) execution_mode: ExplainExecutionMode,
183 pub(crate) access_strategy: Option<ExplainAccessPath>,
184 pub(crate) predicate_pushdown: Option<String>,
185 pub(crate) filter_expr: Option<String>,
186 pub(crate) residual_filter_expr: Option<String>,
187 pub(crate) residual_filter_predicate: Option<ExplainPredicate>,
188 pub(crate) projection: Option<String>,
189 pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
190 pub(crate) limit: Option<u32>,
191 pub(crate) cursor: Option<bool>,
192 pub(crate) covering_scan: Option<bool>,
193 pub(crate) rows_expected: Option<u64>,
194 pub(crate) children: Vec<Self>,
195 pub(crate) node_properties: ExplainPropertyMap,
196}
197
198#[derive(Clone, Debug, Eq, PartialEq)]
209pub(in crate::db) struct FinalizedQueryDiagnostics {
210 pub(crate) execution: ExplainExecutionNodeDescriptor,
211 pub(crate) route_diagnostics: Vec<String>,
212 pub(crate) logical_diagnostics: Vec<String>,
213 pub(crate) reuse: Option<TraceReuseEvent>,
214}
215
216impl ExplainAggregateTerminalPlan {
217 #[must_use]
219 pub const fn query(&self) -> &ExplainPlan {
220 &self.query
221 }
222
223 #[must_use]
225 pub const fn terminal(&self) -> AggregateKind {
226 self.terminal
227 }
228
229 #[must_use]
231 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
232 &self.execution
233 }
234
235 #[must_use]
236 pub(in crate::db) const fn new(
237 query: ExplainPlan,
238 terminal: AggregateKind,
239 execution: ExplainExecutionDescriptor,
240 ) -> Self {
241 Self {
242 query,
243 terminal,
244 execution,
245 }
246 }
247}
248
249impl ExplainExecutionDescriptor {
250 #[must_use]
252 pub const fn access_strategy(&self) -> &ExplainAccessPath {
253 &self.access_strategy
254 }
255
256 #[must_use]
258 pub const fn covering_projection(&self) -> bool {
259 self.covering_projection
260 }
261
262 #[must_use]
264 pub const fn aggregation(&self) -> AggregateKind {
265 self.aggregation
266 }
267
268 #[must_use]
270 pub const fn execution_mode(&self) -> ExplainExecutionMode {
271 self.execution_mode
272 }
273
274 #[must_use]
276 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
277 self.ordering_source
278 }
279
280 #[must_use]
282 pub const fn limit(&self) -> Option<u32> {
283 self.limit
284 }
285
286 #[must_use]
288 pub const fn cursor(&self) -> bool {
289 self.cursor
290 }
291
292 #[must_use]
294 pub const fn node_properties(&self) -> &ExplainPropertyMap {
295 &self.node_properties
296 }
297}
298
299impl FinalizedQueryDiagnostics {
300 #[must_use]
302 pub(in crate::db) const fn new(
303 execution: ExplainExecutionNodeDescriptor,
304 route_diagnostics: Vec<String>,
305 logical_diagnostics: Vec<String>,
306 reuse: Option<TraceReuseEvent>,
307 ) -> Self {
308 Self {
309 execution,
310 route_diagnostics,
311 logical_diagnostics,
312 reuse,
313 }
314 }
315
316 #[must_use]
318 pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
319 &self.execution
320 }
321}
322
323impl ExplainAggregateTerminalPlan {
324 #[must_use]
326 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
327 ExplainExecutionNodeDescriptor {
328 node_type: match self.execution.ordering_source {
329 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
330 ExplainExecutionNodeType::AggregateSeekFirst
331 }
332 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
333 ExplainExecutionNodeType::AggregateSeekLast
334 }
335 ExplainExecutionOrderingSource::AccessOrder
336 | ExplainExecutionOrderingSource::Materialized => {
337 self.terminal.explain_execution_node_type()
338 }
339 },
340 execution_mode: self.execution.execution_mode,
341 access_strategy: Some(self.execution.access_strategy.clone()),
342 predicate_pushdown: None,
343 filter_expr: None,
344 residual_filter_expr: None,
345 residual_filter_predicate: None,
346 projection: None,
347 ordering_source: Some(self.execution.ordering_source),
348 limit: self.execution.limit,
349 cursor: Some(self.execution.cursor),
350 covering_scan: Some(self.execution.covering_projection),
351 rows_expected: None,
352 children: Vec::new(),
353 node_properties: self.execution.node_properties.clone(),
354 }
355 }
356}
357
358impl AggregateKind {
359 #[must_use]
362 pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
363 match self {
364 Self::Count => ExplainExecutionNodeType::AggregateCount,
365 Self::Exists => ExplainExecutionNodeType::AggregateExists,
366 Self::Min => ExplainExecutionNodeType::AggregateMin,
367 Self::Max => ExplainExecutionNodeType::AggregateMax,
368 Self::First => ExplainExecutionNodeType::AggregateFirst,
369 Self::Last => ExplainExecutionNodeType::AggregateLast,
370 Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
371 }
372 }
373}
374
375impl ExplainExecutionNodeType {
376 #[must_use]
378 pub const fn as_str(self) -> &'static str {
379 match self {
380 Self::ByKeyLookup => "ByKeyLookup",
381 Self::ByKeysLookup => "ByKeysLookup",
382 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
383 Self::IndexPrefixScan => "IndexPrefixScan",
384 Self::IndexRangeScan => "IndexRangeScan",
385 Self::IndexMultiLookup => "IndexMultiLookup",
386 Self::FullScan => "FullScan",
387 Self::Union => "Union",
388 Self::Intersection => "Intersection",
389 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
390 Self::ResidualFilter => "ResidualFilter",
391 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
392 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
393 Self::DistinctPreOrdered => "DistinctPreOrdered",
394 Self::DistinctMaterialized => "DistinctMaterialized",
395 Self::ProjectionMaterialized => "ProjectionMaterialized",
396 Self::CoveringRead => "CoveringRead",
397 Self::LimitOffset => "LimitOffset",
398 Self::CursorResume => "CursorResume",
399 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
400 Self::TopNSeek => "TopNSeek",
401 Self::AggregateCount => "AggregateCount",
402 Self::AggregateExists => "AggregateExists",
403 Self::AggregateMin => "AggregateMin",
404 Self::AggregateMax => "AggregateMax",
405 Self::AggregateFirst => "AggregateFirst",
406 Self::AggregateLast => "AggregateLast",
407 Self::AggregateSum => "AggregateSum",
408 Self::AggregateSeekFirst => "AggregateSeekFirst",
409 Self::AggregateSeekLast => "AggregateSeekLast",
410 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
411 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
412 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
413 }
414 }
415
416 #[must_use]
418 pub const fn layer_label(self) -> &'static str {
419 crate::db::query::explain::nodes::layer_label(self)
420 }
421}
422
423impl ExplainExecutionNodeDescriptor {
424 pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
426 visit(self);
427
428 for child in self.children() {
429 child.for_each_preorder(visit);
430 }
431 }
432
433 #[must_use]
435 pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
436 let mut found = false;
437 self.for_each_preorder(&mut |node| {
438 if node.node_type() == target {
439 found = true;
440 }
441 });
442
443 found
444 }
445
446 #[must_use]
448 pub const fn node_type(&self) -> ExplainExecutionNodeType {
449 self.node_type
450 }
451
452 #[must_use]
454 pub const fn execution_mode(&self) -> ExplainExecutionMode {
455 self.execution_mode
456 }
457
458 #[must_use]
460 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
461 self.access_strategy.as_ref()
462 }
463
464 #[must_use]
466 pub fn predicate_pushdown(&self) -> Option<&str> {
467 self.predicate_pushdown.as_deref()
468 }
469
470 #[must_use]
472 pub fn filter_expr(&self) -> Option<&str> {
473 self.filter_expr.as_deref()
474 }
475
476 #[must_use]
478 pub fn residual_filter_expr(&self) -> Option<&str> {
479 self.residual_filter_expr.as_deref()
480 }
481
482 #[must_use]
486 pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
487 self.residual_filter_predicate.as_ref()
488 }
489
490 #[must_use]
492 pub fn projection(&self) -> Option<&str> {
493 self.projection.as_deref()
494 }
495
496 #[must_use]
498 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
499 self.ordering_source
500 }
501
502 #[must_use]
504 pub const fn limit(&self) -> Option<u32> {
505 self.limit
506 }
507
508 #[must_use]
510 pub const fn cursor(&self) -> Option<bool> {
511 self.cursor
512 }
513
514 #[must_use]
516 pub const fn covering_scan(&self) -> Option<bool> {
517 self.covering_scan
518 }
519
520 #[must_use]
522 pub const fn rows_expected(&self) -> Option<u64> {
523 self.rows_expected
524 }
525
526 #[must_use]
528 pub const fn children(&self) -> &[Self] {
529 self.children.as_slice()
530 }
531
532 #[must_use]
534 pub const fn node_properties(&self) -> &ExplainPropertyMap {
535 &self.node_properties
536 }
537}
538
539pub(in crate::db::query::explain) const fn execution_mode_label(
540 mode: ExplainExecutionMode,
541) -> &'static str {
542 match mode {
543 ExplainExecutionMode::Streaming => "Streaming",
544 ExplainExecutionMode::Materialized => "Materialized",
545 }
546}
547
548pub(in crate::db::query::explain) const fn ordering_source_label(
549 ordering_source: ExplainExecutionOrderingSource,
550) -> &'static str {
551 match ordering_source {
552 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
553 ExplainExecutionOrderingSource::Materialized => "Materialized",
554 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
555 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
556 }
557}
558
559#[cfg(test)]
564mod tests {
565 use crate::db::query::explain::{
566 ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
567 ExplainPropertyMap,
568 };
569
570 fn node(
571 node_type: ExplainExecutionNodeType,
572 children: Vec<ExplainExecutionNodeDescriptor>,
573 ) -> ExplainExecutionNodeDescriptor {
574 ExplainExecutionNodeDescriptor {
575 node_type,
576 execution_mode: ExplainExecutionMode::Materialized,
577 access_strategy: None,
578 predicate_pushdown: None,
579 filter_expr: None,
580 residual_filter_expr: None,
581 residual_filter_predicate: None,
582 projection: None,
583 ordering_source: None,
584 limit: None,
585 cursor: None,
586 covering_scan: None,
587 rows_expected: None,
588 children,
589 node_properties: ExplainPropertyMap::new(),
590 }
591 }
592
593 #[test]
594 fn execution_node_contains_type_scans_preorder_tree() {
595 let root = node(
596 ExplainExecutionNodeType::Union,
597 vec![
598 node(ExplainExecutionNodeType::FullScan, Vec::new()),
599 node(
600 ExplainExecutionNodeType::Intersection,
601 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
602 ),
603 ],
604 );
605
606 assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
607 assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
608 }
609
610 #[test]
611 fn execution_node_preorder_visits_parent_before_children() {
612 let root = node(
613 ExplainExecutionNodeType::Union,
614 vec![
615 node(ExplainExecutionNodeType::FullScan, Vec::new()),
616 node(
617 ExplainExecutionNodeType::Intersection,
618 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
619 ),
620 ],
621 );
622 let mut visited = Vec::new();
623
624 root.for_each_preorder(&mut |node| visited.push(node.node_type()));
625
626 assert_eq!(
627 visited,
628 vec![
629 ExplainExecutionNodeType::Union,
630 ExplainExecutionNodeType::FullScan,
631 ExplainExecutionNodeType::Intersection,
632 ExplainExecutionNodeType::ResidualFilter,
633 ],
634 );
635 }
636}