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 ExplainExecutionNodeType::aggregate_terminal(self.terminal)
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 ExplainExecutionNodeType {
359 #[must_use]
361 pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
362 match kind {
363 AggregateKind::Count => Self::AggregateCount,
364 AggregateKind::Exists => Self::AggregateExists,
365 AggregateKind::Min => Self::AggregateMin,
366 AggregateKind::Max => Self::AggregateMax,
367 AggregateKind::First => Self::AggregateFirst,
368 AggregateKind::Last => Self::AggregateLast,
369 AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
370 }
371 }
372
373 #[must_use]
375 pub const fn as_str(self) -> &'static str {
376 match self {
377 Self::ByKeyLookup => "ByKeyLookup",
378 Self::ByKeysLookup => "ByKeysLookup",
379 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
380 Self::IndexPrefixScan => "IndexPrefixScan",
381 Self::IndexRangeScan => "IndexRangeScan",
382 Self::IndexMultiLookup => "IndexMultiLookup",
383 Self::FullScan => "FullScan",
384 Self::Union => "Union",
385 Self::Intersection => "Intersection",
386 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
387 Self::ResidualFilter => "ResidualFilter",
388 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
389 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
390 Self::DistinctPreOrdered => "DistinctPreOrdered",
391 Self::DistinctMaterialized => "DistinctMaterialized",
392 Self::ProjectionMaterialized => "ProjectionMaterialized",
393 Self::CoveringRead => "CoveringRead",
394 Self::LimitOffset => "LimitOffset",
395 Self::CursorResume => "CursorResume",
396 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
397 Self::TopNSeek => "TopNSeek",
398 Self::AggregateCount => "AggregateCount",
399 Self::AggregateExists => "AggregateExists",
400 Self::AggregateMin => "AggregateMin",
401 Self::AggregateMax => "AggregateMax",
402 Self::AggregateFirst => "AggregateFirst",
403 Self::AggregateLast => "AggregateLast",
404 Self::AggregateSum => "AggregateSum",
405 Self::AggregateSeekFirst => "AggregateSeekFirst",
406 Self::AggregateSeekLast => "AggregateSeekLast",
407 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
408 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
409 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
410 }
411 }
412
413 #[must_use]
415 pub const fn layer_label(self) -> &'static str {
416 crate::db::query::explain::nodes::layer_label(self)
417 }
418}
419
420impl ExplainExecutionNodeDescriptor {
421 pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
423 visit(self);
424
425 for child in self.children() {
426 child.for_each_preorder(visit);
427 }
428 }
429
430 #[must_use]
432 pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
433 let mut found = false;
434 self.for_each_preorder(&mut |node| {
435 if node.node_type() == target {
436 found = true;
437 }
438 });
439
440 found
441 }
442
443 #[must_use]
445 pub const fn node_type(&self) -> ExplainExecutionNodeType {
446 self.node_type
447 }
448
449 #[must_use]
451 pub const fn execution_mode(&self) -> ExplainExecutionMode {
452 self.execution_mode
453 }
454
455 #[must_use]
457 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
458 self.access_strategy.as_ref()
459 }
460
461 #[must_use]
463 pub fn predicate_pushdown(&self) -> Option<&str> {
464 self.predicate_pushdown.as_deref()
465 }
466
467 #[must_use]
469 pub fn filter_expr(&self) -> Option<&str> {
470 self.filter_expr.as_deref()
471 }
472
473 #[must_use]
475 pub fn residual_filter_expr(&self) -> Option<&str> {
476 self.residual_filter_expr.as_deref()
477 }
478
479 #[must_use]
483 pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
484 self.residual_filter_predicate.as_ref()
485 }
486
487 #[must_use]
489 pub fn projection(&self) -> Option<&str> {
490 self.projection.as_deref()
491 }
492
493 #[must_use]
495 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
496 self.ordering_source
497 }
498
499 #[must_use]
501 pub const fn limit(&self) -> Option<u32> {
502 self.limit
503 }
504
505 #[must_use]
507 pub const fn cursor(&self) -> Option<bool> {
508 self.cursor
509 }
510
511 #[must_use]
513 pub const fn covering_scan(&self) -> Option<bool> {
514 self.covering_scan
515 }
516
517 #[must_use]
519 pub const fn rows_expected(&self) -> Option<u64> {
520 self.rows_expected
521 }
522
523 #[must_use]
525 pub const fn children(&self) -> &[Self] {
526 self.children.as_slice()
527 }
528
529 #[must_use]
531 pub const fn node_properties(&self) -> &ExplainPropertyMap {
532 &self.node_properties
533 }
534}
535
536pub(in crate::db::query::explain) const fn execution_mode_label(
537 mode: ExplainExecutionMode,
538) -> &'static str {
539 match mode {
540 ExplainExecutionMode::Streaming => "Streaming",
541 ExplainExecutionMode::Materialized => "Materialized",
542 }
543}
544
545pub(in crate::db::query::explain) const fn ordering_source_label(
546 ordering_source: ExplainExecutionOrderingSource,
547) -> &'static str {
548 match ordering_source {
549 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
550 ExplainExecutionOrderingSource::Materialized => "Materialized",
551 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
552 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
553 }
554}
555
556#[cfg(test)]
561mod tests {
562 use crate::db::query::explain::{
563 ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
564 ExplainPropertyMap,
565 };
566
567 fn node(
568 node_type: ExplainExecutionNodeType,
569 children: Vec<ExplainExecutionNodeDescriptor>,
570 ) -> ExplainExecutionNodeDescriptor {
571 ExplainExecutionNodeDescriptor {
572 node_type,
573 execution_mode: ExplainExecutionMode::Materialized,
574 access_strategy: None,
575 predicate_pushdown: None,
576 filter_expr: None,
577 residual_filter_expr: None,
578 residual_filter_predicate: None,
579 projection: None,
580 ordering_source: None,
581 limit: None,
582 cursor: None,
583 covering_scan: None,
584 rows_expected: None,
585 children,
586 node_properties: ExplainPropertyMap::new(),
587 }
588 }
589
590 #[test]
591 fn execution_node_contains_type_scans_preorder_tree() {
592 let root = node(
593 ExplainExecutionNodeType::Union,
594 vec![
595 node(ExplainExecutionNodeType::FullScan, Vec::new()),
596 node(
597 ExplainExecutionNodeType::Intersection,
598 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
599 ),
600 ],
601 );
602
603 assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
604 assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
605 }
606
607 #[test]
608 fn execution_node_preorder_visits_parent_before_children() {
609 let root = node(
610 ExplainExecutionNodeType::Union,
611 vec![
612 node(ExplainExecutionNodeType::FullScan, Vec::new()),
613 node(
614 ExplainExecutionNodeType::Intersection,
615 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
616 ),
617 ],
618 );
619 let mut visited = Vec::new();
620
621 root.for_each_preorder(&mut |node| visited.push(node.node_type()));
622
623 assert_eq!(
624 visited,
625 vec![
626 ExplainExecutionNodeType::Union,
627 ExplainExecutionNodeType::FullScan,
628 ExplainExecutionNodeType::Intersection,
629 ExplainExecutionNodeType::ResidualFilter,
630 ],
631 );
632 }
633}