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 IndexBranchSet,
148 FullScan,
149 Union,
150 Intersection,
151 IndexPredicatePrefilter,
152 ResidualFilter,
153 OrderByAccessSatisfied,
154 OrderByMaterializedSort,
155 DistinctPreOrdered,
156 DistinctMaterialized,
157 ProjectionMaterialized,
158 CoveringRead,
159 LimitOffset,
160 CursorResume,
161 IndexRangeLimitPushdown,
162 TopNSeek,
163 AggregateCount,
164 AggregateExists,
165 AggregateMin,
166 AggregateMax,
167 AggregateFirst,
168 AggregateLast,
169 AggregateSum,
170 AggregateSeekFirst,
171 AggregateSeekLast,
172 GroupedAggregateHashMaterialized,
173 GroupedAggregateOrderedMaterialized,
174 SecondaryOrderPushdown,
175}
176
177#[cfg_attr(
178 doc,
179 doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
180)]
181#[derive(Clone, Debug, Eq, PartialEq)]
182pub struct ExplainExecutionNodeDescriptor {
183 pub(in crate::db) node_type: ExplainExecutionNodeType,
184 pub(in crate::db) execution_mode: ExplainExecutionMode,
185 pub(in crate::db) access_strategy: Option<ExplainAccessPath>,
186 pub(in crate::db) predicate_pushdown: Option<String>,
187 pub(in crate::db) filter_expr: Option<String>,
188 pub(in crate::db) residual_filter_expr: Option<String>,
189 pub(in crate::db) residual_filter_predicate: Option<ExplainPredicate>,
190 pub(in crate::db) projection: Option<String>,
191 pub(in crate::db) ordering_source: Option<ExplainExecutionOrderingSource>,
192 pub(in crate::db) limit: Option<u32>,
193 pub(in crate::db) cursor: Option<bool>,
194 pub(in crate::db) covering_scan: Option<bool>,
195 pub(in crate::db) rows_expected: Option<u64>,
196 pub(in crate::db) children: Vec<Self>,
197 pub(in crate::db) node_properties: ExplainPropertyMap,
198}
199
200#[derive(Clone, Debug, Eq, PartialEq)]
211pub(in crate::db) struct FinalizedQueryDiagnostics {
212 pub(in crate::db) execution: ExplainExecutionNodeDescriptor,
213 pub(in crate::db) route_diagnostics: Vec<String>,
214 pub(in crate::db) logical_diagnostics: Vec<String>,
215 pub(in crate::db) reuse: Option<TraceReuseEvent>,
216}
217
218impl ExplainAggregateTerminalPlan {
219 #[must_use]
221 pub const fn query(&self) -> &ExplainPlan {
222 &self.query
223 }
224
225 #[must_use]
227 pub const fn terminal(&self) -> AggregateKind {
228 self.terminal
229 }
230
231 #[must_use]
233 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
234 &self.execution
235 }
236
237 #[must_use]
238 pub(in crate::db) const fn new(
239 query: ExplainPlan,
240 terminal: AggregateKind,
241 execution: ExplainExecutionDescriptor,
242 ) -> Self {
243 Self {
244 query,
245 terminal,
246 execution,
247 }
248 }
249}
250
251impl ExplainExecutionDescriptor {
252 #[must_use]
254 pub const fn access_strategy(&self) -> &ExplainAccessPath {
255 &self.access_strategy
256 }
257
258 #[must_use]
260 pub const fn covering_projection(&self) -> bool {
261 self.covering_projection
262 }
263
264 #[must_use]
266 pub const fn aggregation(&self) -> AggregateKind {
267 self.aggregation
268 }
269
270 #[must_use]
272 pub const fn execution_mode(&self) -> ExplainExecutionMode {
273 self.execution_mode
274 }
275
276 #[must_use]
278 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
279 self.ordering_source
280 }
281
282 #[must_use]
284 pub const fn limit(&self) -> Option<u32> {
285 self.limit
286 }
287
288 #[must_use]
290 pub const fn cursor(&self) -> bool {
291 self.cursor
292 }
293
294 #[must_use]
296 pub const fn node_properties(&self) -> &ExplainPropertyMap {
297 &self.node_properties
298 }
299}
300
301impl FinalizedQueryDiagnostics {
302 #[must_use]
304 pub(in crate::db) const fn new(
305 execution: ExplainExecutionNodeDescriptor,
306 route_diagnostics: Vec<String>,
307 logical_diagnostics: Vec<String>,
308 reuse: Option<TraceReuseEvent>,
309 ) -> Self {
310 Self {
311 execution,
312 route_diagnostics,
313 logical_diagnostics,
314 reuse,
315 }
316 }
317
318 #[must_use]
320 pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
321 &self.execution
322 }
323}
324
325impl ExplainAggregateTerminalPlan {
326 #[must_use]
328 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
329 ExplainExecutionNodeDescriptor {
330 node_type: match self.execution.ordering_source {
331 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
332 ExplainExecutionNodeType::AggregateSeekFirst
333 }
334 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
335 ExplainExecutionNodeType::AggregateSeekLast
336 }
337 ExplainExecutionOrderingSource::AccessOrder
338 | ExplainExecutionOrderingSource::Materialized => {
339 self.terminal.explain_execution_node_type()
340 }
341 },
342 execution_mode: self.execution.execution_mode,
343 access_strategy: Some(self.execution.access_strategy.clone()),
344 predicate_pushdown: None,
345 filter_expr: None,
346 residual_filter_expr: None,
347 residual_filter_predicate: None,
348 projection: None,
349 ordering_source: Some(self.execution.ordering_source),
350 limit: self.execution.limit,
351 cursor: Some(self.execution.cursor),
352 covering_scan: Some(self.execution.covering_projection),
353 rows_expected: None,
354 children: Vec::new(),
355 node_properties: self.execution.node_properties.clone(),
356 }
357 }
358}
359
360impl AggregateKind {
361 #[must_use]
364 pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
365 match self {
366 Self::Count => ExplainExecutionNodeType::AggregateCount,
367 Self::Exists => ExplainExecutionNodeType::AggregateExists,
368 Self::Min => ExplainExecutionNodeType::AggregateMin,
369 Self::Max => ExplainExecutionNodeType::AggregateMax,
370 Self::First => ExplainExecutionNodeType::AggregateFirst,
371 Self::Last => ExplainExecutionNodeType::AggregateLast,
372 Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
373 }
374 }
375}
376
377impl ExplainExecutionNodeType {
378 #[must_use]
380 pub const fn as_str(self) -> &'static str {
381 match self {
382 Self::ByKeyLookup => "ByKeyLookup",
383 Self::ByKeysLookup => "ByKeysLookup",
384 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
385 Self::IndexPrefixScan => "IndexPrefixScan",
386 Self::IndexRangeScan => "IndexRangeScan",
387 Self::IndexMultiLookup => "IndexMultiLookup",
388 Self::IndexBranchSet => "IndexBranchSet",
389 Self::FullScan => "FullScan",
390 Self::Union => "Union",
391 Self::Intersection => "Intersection",
392 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
393 Self::ResidualFilter => "ResidualFilter",
394 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
395 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
396 Self::DistinctPreOrdered => "DistinctPreOrdered",
397 Self::DistinctMaterialized => "DistinctMaterialized",
398 Self::ProjectionMaterialized => "ProjectionMaterialized",
399 Self::CoveringRead => "CoveringRead",
400 Self::LimitOffset => "LimitOffset",
401 Self::CursorResume => "CursorResume",
402 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
403 Self::TopNSeek => "TopNSeek",
404 Self::AggregateCount => "AggregateCount",
405 Self::AggregateExists => "AggregateExists",
406 Self::AggregateMin => "AggregateMin",
407 Self::AggregateMax => "AggregateMax",
408 Self::AggregateFirst => "AggregateFirst",
409 Self::AggregateLast => "AggregateLast",
410 Self::AggregateSum => "AggregateSum",
411 Self::AggregateSeekFirst => "AggregateSeekFirst",
412 Self::AggregateSeekLast => "AggregateSeekLast",
413 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
414 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
415 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
416 }
417 }
418
419 #[must_use]
421 pub const fn layer_label(self) -> &'static str {
422 crate::db::query::explain::nodes::layer_label(self)
423 }
424}
425
426impl ExplainExecutionNodeDescriptor {
427 pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
429 visit(self);
430
431 for child in self.children() {
432 child.for_each_preorder(visit);
433 }
434 }
435
436 #[must_use]
438 pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
439 let mut found = false;
440 self.for_each_preorder(&mut |node| {
441 if node.node_type() == target {
442 found = true;
443 }
444 });
445
446 found
447 }
448
449 #[must_use]
451 pub const fn node_type(&self) -> ExplainExecutionNodeType {
452 self.node_type
453 }
454
455 #[must_use]
457 pub const fn execution_mode(&self) -> ExplainExecutionMode {
458 self.execution_mode
459 }
460
461 #[must_use]
463 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
464 self.access_strategy.as_ref()
465 }
466
467 #[must_use]
469 pub fn predicate_pushdown(&self) -> Option<&str> {
470 self.predicate_pushdown.as_deref()
471 }
472
473 #[must_use]
475 pub fn filter_expr(&self) -> Option<&str> {
476 self.filter_expr.as_deref()
477 }
478
479 #[must_use]
481 pub fn residual_filter_expr(&self) -> Option<&str> {
482 self.residual_filter_expr.as_deref()
483 }
484
485 #[must_use]
489 pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
490 self.residual_filter_predicate.as_ref()
491 }
492
493 #[must_use]
495 pub fn projection(&self) -> Option<&str> {
496 self.projection.as_deref()
497 }
498
499 #[must_use]
501 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
502 self.ordering_source
503 }
504
505 #[must_use]
507 pub const fn limit(&self) -> Option<u32> {
508 self.limit
509 }
510
511 #[must_use]
513 pub const fn cursor(&self) -> Option<bool> {
514 self.cursor
515 }
516
517 #[must_use]
519 pub const fn covering_scan(&self) -> Option<bool> {
520 self.covering_scan
521 }
522
523 #[must_use]
525 pub const fn rows_expected(&self) -> Option<u64> {
526 self.rows_expected
527 }
528
529 #[must_use]
531 pub const fn children(&self) -> &[Self] {
532 self.children.as_slice()
533 }
534
535 #[must_use]
537 pub const fn node_properties(&self) -> &ExplainPropertyMap {
538 &self.node_properties
539 }
540}
541
542pub(in crate::db::query::explain) const fn execution_mode_label(
543 mode: ExplainExecutionMode,
544) -> &'static str {
545 match mode {
546 ExplainExecutionMode::Streaming => "Streaming",
547 ExplainExecutionMode::Materialized => "Materialized",
548 }
549}
550
551pub(in crate::db::query::explain) const fn ordering_source_label(
552 ordering_source: ExplainExecutionOrderingSource,
553) -> &'static str {
554 match ordering_source {
555 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
556 ExplainExecutionOrderingSource::Materialized => "Materialized",
557 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
558 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
559 }
560}
561
562#[cfg(test)]
567mod tests {
568 use crate::db::query::explain::{
569 ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
570 ExplainPropertyMap,
571 };
572
573 fn node(
574 node_type: ExplainExecutionNodeType,
575 children: Vec<ExplainExecutionNodeDescriptor>,
576 ) -> ExplainExecutionNodeDescriptor {
577 ExplainExecutionNodeDescriptor {
578 node_type,
579 execution_mode: ExplainExecutionMode::Materialized,
580 access_strategy: None,
581 predicate_pushdown: None,
582 filter_expr: None,
583 residual_filter_expr: None,
584 residual_filter_predicate: None,
585 projection: None,
586 ordering_source: None,
587 limit: None,
588 cursor: None,
589 covering_scan: None,
590 rows_expected: None,
591 children,
592 node_properties: ExplainPropertyMap::new(),
593 }
594 }
595
596 #[test]
597 fn execution_node_contains_type_scans_preorder_tree() {
598 let root = node(
599 ExplainExecutionNodeType::Union,
600 vec![
601 node(ExplainExecutionNodeType::FullScan, Vec::new()),
602 node(
603 ExplainExecutionNodeType::Intersection,
604 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
605 ),
606 ],
607 );
608
609 assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
610 assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
611 }
612
613 #[test]
614 fn execution_node_preorder_visits_parent_before_children() {
615 let root = node(
616 ExplainExecutionNodeType::Union,
617 vec![
618 node(ExplainExecutionNodeType::FullScan, Vec::new()),
619 node(
620 ExplainExecutionNodeType::Intersection,
621 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
622 ),
623 ],
624 );
625 let mut visited = Vec::new();
626
627 root.for_each_preorder(&mut |node| visited.push(node.node_type()));
628
629 assert_eq!(
630 visited,
631 vec![
632 ExplainExecutionNodeType::Union,
633 ExplainExecutionNodeType::FullScan,
634 ExplainExecutionNodeType::Intersection,
635 ExplainExecutionNodeType::ResidualFilter,
636 ],
637 );
638 }
639}