1use crate::{
7 db::query::{
8 explain::{ExplainAccessPath, ExplainPlan, ExplainPredicate},
9 plan::AggregateKind,
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
86#[cfg_attr(
87 doc,
88 doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
89)]
90#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct ExplainAggregateTerminalPlan {
92 pub(in crate::db) query: ExplainPlan,
93 pub(in crate::db) terminal: AggregateKind,
94 pub(in crate::db) execution: ExplainExecutionDescriptor,
95}
96
97#[cfg_attr(
98 doc,
99 doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
100)]
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum ExplainExecutionOrderingSource {
103 AccessOrder,
104 Materialized,
105 IndexSeekFirst { fetch: usize },
106 IndexSeekLast { fetch: usize },
107}
108
109#[cfg_attr(
110 doc,
111 doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
112)]
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum ExplainExecutionMode {
115 Streaming,
116 Materialized,
117}
118
119#[cfg_attr(
120 doc,
121 doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
122)]
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct ExplainExecutionDescriptor {
125 pub(in crate::db) access_strategy: ExplainAccessPath,
126 pub(in crate::db) covering_projection: bool,
127 pub(in crate::db) aggregation: AggregateKind,
128 pub(in crate::db) execution_mode: ExplainExecutionMode,
129 pub(in crate::db) ordering_source: ExplainExecutionOrderingSource,
130 pub(in crate::db) limit: Option<u32>,
131 pub(in crate::db) cursor: bool,
132 pub(in crate::db) node_properties: ExplainPropertyMap,
133}
134
135#[cfg_attr(
136 doc,
137 doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
138)]
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub enum ExplainExecutionNodeType {
141 ByKeyLookup,
142 ByKeysLookup,
143 PrimaryKeyRangeScan,
144 IndexPrefixScan,
145 IndexRangeScan,
146 IndexMultiLookup,
147 FullScan,
148 Union,
149 Intersection,
150 IndexPredicatePrefilter,
151 ResidualFilter,
152 OrderByAccessSatisfied,
153 OrderByMaterializedSort,
154 DistinctPreOrdered,
155 DistinctMaterialized,
156 ProjectionMaterialized,
157 CoveringRead,
158 LimitOffset,
159 CursorResume,
160 IndexRangeLimitPushdown,
161 TopNSeek,
162 AggregateCount,
163 AggregateExists,
164 AggregateMin,
165 AggregateMax,
166 AggregateFirst,
167 AggregateLast,
168 AggregateSum,
169 AggregateSeekFirst,
170 AggregateSeekLast,
171 GroupedAggregateHashMaterialized,
172 GroupedAggregateOrderedMaterialized,
173 SecondaryOrderPushdown,
174}
175
176#[cfg_attr(
177 doc,
178 doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
179)]
180#[derive(Clone, Debug, Eq, PartialEq)]
181pub struct ExplainExecutionNodeDescriptor {
182 pub(in crate::db) node_type: ExplainExecutionNodeType,
183 pub(in crate::db) execution_mode: ExplainExecutionMode,
184 pub(in crate::db) access_strategy: Option<ExplainAccessPath>,
185 pub(in crate::db) predicate_pushdown: Option<String>,
186 pub(in crate::db) filter_expr: Option<String>,
187 pub(in crate::db) residual_filter_expr: Option<String>,
188 pub(in crate::db) residual_filter_predicate: Option<ExplainPredicate>,
189 pub(in crate::db) projection: Option<String>,
190 pub(in crate::db) ordering_source: Option<ExplainExecutionOrderingSource>,
191 pub(in crate::db) limit: Option<u32>,
192 pub(in crate::db) cursor: Option<bool>,
193 pub(in crate::db) covering_scan: Option<bool>,
194 pub(in crate::db) rows_expected: Option<u64>,
195 pub(in crate::db) children: Vec<Self>,
196 pub(in crate::db) node_properties: ExplainPropertyMap,
197}
198
199#[derive(Clone, Debug, Eq, PartialEq)]
210pub(in crate::db) struct FinalizedQueryDiagnostics {
211 pub(in crate::db) execution: ExplainExecutionNodeDescriptor,
212 pub(in crate::db) route_diagnostics: Vec<String>,
213 pub(in crate::db) logical_diagnostics: Vec<String>,
214 pub(in crate::db) reuse: Option<TraceReuseEvent>,
215}
216
217impl ExplainAggregateTerminalPlan {
218 #[must_use]
220 pub const fn query(&self) -> &ExplainPlan {
221 &self.query
222 }
223
224 #[must_use]
226 pub const fn terminal(&self) -> AggregateKind {
227 self.terminal
228 }
229
230 #[must_use]
232 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
233 &self.execution
234 }
235
236 #[must_use]
237 pub(in crate::db) const fn new(
238 query: ExplainPlan,
239 terminal: AggregateKind,
240 execution: ExplainExecutionDescriptor,
241 ) -> Self {
242 Self {
243 query,
244 terminal,
245 execution,
246 }
247 }
248}
249
250impl ExplainExecutionDescriptor {
251 #[must_use]
253 pub const fn access_strategy(&self) -> &ExplainAccessPath {
254 &self.access_strategy
255 }
256
257 #[must_use]
259 pub const fn covering_projection(&self) -> bool {
260 self.covering_projection
261 }
262
263 #[must_use]
265 pub const fn aggregation(&self) -> AggregateKind {
266 self.aggregation
267 }
268
269 #[must_use]
271 pub const fn execution_mode(&self) -> ExplainExecutionMode {
272 self.execution_mode
273 }
274
275 #[must_use]
277 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
278 self.ordering_source
279 }
280
281 #[must_use]
283 pub const fn limit(&self) -> Option<u32> {
284 self.limit
285 }
286
287 #[must_use]
289 pub const fn cursor(&self) -> bool {
290 self.cursor
291 }
292
293 #[must_use]
295 pub const fn node_properties(&self) -> &ExplainPropertyMap {
296 &self.node_properties
297 }
298}
299
300impl FinalizedQueryDiagnostics {
301 #[must_use]
303 pub(in crate::db) const fn new(
304 execution: ExplainExecutionNodeDescriptor,
305 route_diagnostics: Vec<String>,
306 logical_diagnostics: Vec<String>,
307 reuse: Option<TraceReuseEvent>,
308 ) -> Self {
309 Self {
310 execution,
311 route_diagnostics,
312 logical_diagnostics,
313 reuse,
314 }
315 }
316
317 #[must_use]
319 pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
320 &self.execution
321 }
322}
323
324impl ExplainAggregateTerminalPlan {
325 #[must_use]
327 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
328 ExplainExecutionNodeDescriptor {
329 node_type: match self.execution.ordering_source {
330 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
331 ExplainExecutionNodeType::AggregateSeekFirst
332 }
333 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
334 ExplainExecutionNodeType::AggregateSeekLast
335 }
336 ExplainExecutionOrderingSource::AccessOrder
337 | ExplainExecutionOrderingSource::Materialized => {
338 self.terminal.explain_execution_node_type()
339 }
340 },
341 execution_mode: self.execution.execution_mode,
342 access_strategy: Some(self.execution.access_strategy.clone()),
343 predicate_pushdown: None,
344 filter_expr: None,
345 residual_filter_expr: None,
346 residual_filter_predicate: None,
347 projection: None,
348 ordering_source: Some(self.execution.ordering_source),
349 limit: self.execution.limit,
350 cursor: Some(self.execution.cursor),
351 covering_scan: Some(self.execution.covering_projection),
352 rows_expected: None,
353 children: Vec::new(),
354 node_properties: self.execution.node_properties.clone(),
355 }
356 }
357}
358
359impl AggregateKind {
360 #[must_use]
363 pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
364 match self {
365 Self::Count => ExplainExecutionNodeType::AggregateCount,
366 Self::Exists => ExplainExecutionNodeType::AggregateExists,
367 Self::Min => ExplainExecutionNodeType::AggregateMin,
368 Self::Max => ExplainExecutionNodeType::AggregateMax,
369 Self::First => ExplainExecutionNodeType::AggregateFirst,
370 Self::Last => ExplainExecutionNodeType::AggregateLast,
371 Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
372 }
373 }
374}
375
376impl ExplainExecutionNodeType {
377 #[must_use]
379 pub const fn as_str(self) -> &'static str {
380 match self {
381 Self::ByKeyLookup => "ByKeyLookup",
382 Self::ByKeysLookup => "ByKeysLookup",
383 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
384 Self::IndexPrefixScan => "IndexPrefixScan",
385 Self::IndexRangeScan => "IndexRangeScan",
386 Self::IndexMultiLookup => "IndexMultiLookup",
387 Self::FullScan => "FullScan",
388 Self::Union => "Union",
389 Self::Intersection => "Intersection",
390 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
391 Self::ResidualFilter => "ResidualFilter",
392 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
393 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
394 Self::DistinctPreOrdered => "DistinctPreOrdered",
395 Self::DistinctMaterialized => "DistinctMaterialized",
396 Self::ProjectionMaterialized => "ProjectionMaterialized",
397 Self::CoveringRead => "CoveringRead",
398 Self::LimitOffset => "LimitOffset",
399 Self::CursorResume => "CursorResume",
400 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
401 Self::TopNSeek => "TopNSeek",
402 Self::AggregateCount => "AggregateCount",
403 Self::AggregateExists => "AggregateExists",
404 Self::AggregateMin => "AggregateMin",
405 Self::AggregateMax => "AggregateMax",
406 Self::AggregateFirst => "AggregateFirst",
407 Self::AggregateLast => "AggregateLast",
408 Self::AggregateSum => "AggregateSum",
409 Self::AggregateSeekFirst => "AggregateSeekFirst",
410 Self::AggregateSeekLast => "AggregateSeekLast",
411 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
412 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
413 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
414 }
415 }
416
417 #[must_use]
419 pub const fn layer_label(self) -> &'static str {
420 crate::db::query::explain::nodes::layer_label(self)
421 }
422}
423
424impl ExplainExecutionNodeDescriptor {
425 pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
427 visit(self);
428
429 for child in self.children() {
430 child.for_each_preorder(visit);
431 }
432 }
433
434 #[must_use]
436 pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
437 let mut found = false;
438 self.for_each_preorder(&mut |node| {
439 if node.node_type() == target {
440 found = true;
441 }
442 });
443
444 found
445 }
446
447 #[must_use]
449 pub const fn node_type(&self) -> ExplainExecutionNodeType {
450 self.node_type
451 }
452
453 #[must_use]
455 pub const fn execution_mode(&self) -> ExplainExecutionMode {
456 self.execution_mode
457 }
458
459 #[must_use]
461 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
462 self.access_strategy.as_ref()
463 }
464
465 #[must_use]
467 pub fn predicate_pushdown(&self) -> Option<&str> {
468 self.predicate_pushdown.as_deref()
469 }
470
471 #[must_use]
473 pub fn filter_expr(&self) -> Option<&str> {
474 self.filter_expr.as_deref()
475 }
476
477 #[must_use]
479 pub fn residual_filter_expr(&self) -> Option<&str> {
480 self.residual_filter_expr.as_deref()
481 }
482
483 #[must_use]
487 pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
488 self.residual_filter_predicate.as_ref()
489 }
490
491 #[must_use]
493 pub fn projection(&self) -> Option<&str> {
494 self.projection.as_deref()
495 }
496
497 #[must_use]
499 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
500 self.ordering_source
501 }
502
503 #[must_use]
505 pub const fn limit(&self) -> Option<u32> {
506 self.limit
507 }
508
509 #[must_use]
511 pub const fn cursor(&self) -> Option<bool> {
512 self.cursor
513 }
514
515 #[must_use]
517 pub const fn covering_scan(&self) -> Option<bool> {
518 self.covering_scan
519 }
520
521 #[must_use]
523 pub const fn rows_expected(&self) -> Option<u64> {
524 self.rows_expected
525 }
526
527 #[must_use]
529 pub const fn children(&self) -> &[Self] {
530 self.children.as_slice()
531 }
532
533 #[must_use]
535 pub const fn node_properties(&self) -> &ExplainPropertyMap {
536 &self.node_properties
537 }
538}
539
540pub(in crate::db::query::explain) const fn execution_mode_label(
541 mode: ExplainExecutionMode,
542) -> &'static str {
543 match mode {
544 ExplainExecutionMode::Streaming => "Streaming",
545 ExplainExecutionMode::Materialized => "Materialized",
546 }
547}
548
549pub(in crate::db::query::explain) const fn ordering_source_label(
550 ordering_source: ExplainExecutionOrderingSource,
551) -> &'static str {
552 match ordering_source {
553 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
554 ExplainExecutionOrderingSource::Materialized => "Materialized",
555 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
556 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
557 }
558}
559
560#[cfg(test)]
565mod tests {
566 use crate::db::query::explain::{
567 ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
568 ExplainPropertyMap,
569 };
570
571 fn node(
572 node_type: ExplainExecutionNodeType,
573 children: Vec<ExplainExecutionNodeDescriptor>,
574 ) -> ExplainExecutionNodeDescriptor {
575 ExplainExecutionNodeDescriptor {
576 node_type,
577 execution_mode: ExplainExecutionMode::Materialized,
578 access_strategy: None,
579 predicate_pushdown: None,
580 filter_expr: None,
581 residual_filter_expr: None,
582 residual_filter_predicate: None,
583 projection: None,
584 ordering_source: None,
585 limit: None,
586 cursor: None,
587 covering_scan: None,
588 rows_expected: None,
589 children,
590 node_properties: ExplainPropertyMap::new(),
591 }
592 }
593
594 #[test]
595 fn execution_node_contains_type_scans_preorder_tree() {
596 let root = node(
597 ExplainExecutionNodeType::Union,
598 vec![
599 node(ExplainExecutionNodeType::FullScan, Vec::new()),
600 node(
601 ExplainExecutionNodeType::Intersection,
602 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
603 ),
604 ],
605 );
606
607 assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
608 assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
609 }
610
611 #[test]
612 fn execution_node_preorder_visits_parent_before_children() {
613 let root = node(
614 ExplainExecutionNodeType::Union,
615 vec![
616 node(ExplainExecutionNodeType::FullScan, Vec::new()),
617 node(
618 ExplainExecutionNodeType::Intersection,
619 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
620 ),
621 ],
622 );
623 let mut visited = Vec::new();
624
625 root.for_each_preorder(&mut |node| visited.push(node.node_type()));
626
627 assert_eq!(
628 visited,
629 vec![
630 ExplainExecutionNodeType::Union,
631 ExplainExecutionNodeType::FullScan,
632 ExplainExecutionNodeType::Intersection,
633 ExplainExecutionNodeType::ResidualFilter,
634 ],
635 );
636 }
637}