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 let mut node_properties = self.execution.node_properties.clone();
330 node_properties.insert("aggregate_contract", Value::from("singleton"));
331 node_properties.insert(
332 "aggregate_physical",
333 Value::from(scalar_aggregate_physical_label(
334 self.execution.ordering_source,
335 )),
336 );
337
338 ExplainExecutionNodeDescriptor {
339 node_type: match self.execution.ordering_source {
340 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
341 ExplainExecutionNodeType::AggregateSeekFirst
342 }
343 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
344 ExplainExecutionNodeType::AggregateSeekLast
345 }
346 ExplainExecutionOrderingSource::AccessOrder
347 | ExplainExecutionOrderingSource::Materialized => {
348 self.terminal.explain_execution_node_type()
349 }
350 },
351 execution_mode: self.execution.execution_mode,
352 access_strategy: Some(self.execution.access_strategy.clone()),
353 predicate_pushdown: None,
354 filter_expr: None,
355 residual_filter_expr: None,
356 residual_filter_predicate: None,
357 projection: None,
358 ordering_source: Some(self.execution.ordering_source),
359 limit: self.execution.limit,
360 cursor: Some(self.execution.cursor),
361 covering_scan: Some(self.execution.covering_projection),
362 rows_expected: None,
363 children: Vec::new(),
364 node_properties,
365 }
366 }
367}
368
369const fn scalar_aggregate_physical_label(
370 ordering_source: ExplainExecutionOrderingSource,
371) -> &'static str {
372 match ordering_source {
373 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "scalar_seek_first",
374 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "scalar_seek_last",
375 ExplainExecutionOrderingSource::AccessOrder
376 | ExplainExecutionOrderingSource::Materialized => "scalar_terminal",
377 }
378}
379
380impl AggregateKind {
381 #[must_use]
384 pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
385 match self {
386 Self::Count => ExplainExecutionNodeType::AggregateCount,
387 Self::Exists => ExplainExecutionNodeType::AggregateExists,
388 Self::Min => ExplainExecutionNodeType::AggregateMin,
389 Self::Max => ExplainExecutionNodeType::AggregateMax,
390 Self::First => ExplainExecutionNodeType::AggregateFirst,
391 Self::Last => ExplainExecutionNodeType::AggregateLast,
392 Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
393 }
394 }
395}
396
397impl ExplainExecutionNodeType {
398 #[must_use]
400 pub const fn as_str(self) -> &'static str {
401 match self {
402 Self::ByKeyLookup => "ByKeyLookup",
403 Self::ByKeysLookup => "ByKeysLookup",
404 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
405 Self::IndexPrefixScan => "IndexPrefixScan",
406 Self::IndexRangeScan => "IndexRangeScan",
407 Self::IndexMultiLookup => "IndexMultiLookup",
408 Self::IndexBranchSet => "IndexBranchSet",
409 Self::FullScan => "FullScan",
410 Self::Union => "Union",
411 Self::Intersection => "Intersection",
412 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
413 Self::ResidualFilter => "ResidualFilter",
414 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
415 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
416 Self::DistinctPreOrdered => "DistinctPreOrdered",
417 Self::DistinctMaterialized => "DistinctMaterialized",
418 Self::ProjectionMaterialized => "ProjectionMaterialized",
419 Self::CoveringRead => "CoveringRead",
420 Self::LimitOffset => "LimitOffset",
421 Self::CursorResume => "CursorResume",
422 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
423 Self::TopNSeek => "TopNSeek",
424 Self::AggregateCount => "AggregateCount",
425 Self::AggregateExists => "AggregateExists",
426 Self::AggregateMin => "AggregateMin",
427 Self::AggregateMax => "AggregateMax",
428 Self::AggregateFirst => "AggregateFirst",
429 Self::AggregateLast => "AggregateLast",
430 Self::AggregateSum => "AggregateSum",
431 Self::AggregateSeekFirst => "AggregateSeekFirst",
432 Self::AggregateSeekLast => "AggregateSeekLast",
433 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
434 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
435 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
436 }
437 }
438
439 #[must_use]
441 pub const fn layer_label(self) -> &'static str {
442 crate::db::query::explain::nodes::layer_label(self)
443 }
444}
445
446impl ExplainExecutionNodeDescriptor {
447 pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
449 visit(self);
450
451 for child in self.children() {
452 child.for_each_preorder(visit);
453 }
454 }
455
456 #[must_use]
458 pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
459 let mut found = false;
460 self.for_each_preorder(&mut |node| {
461 if node.node_type() == target {
462 found = true;
463 }
464 });
465
466 found
467 }
468
469 #[must_use]
471 pub const fn node_type(&self) -> ExplainExecutionNodeType {
472 self.node_type
473 }
474
475 #[must_use]
477 pub const fn execution_mode(&self) -> ExplainExecutionMode {
478 self.execution_mode
479 }
480
481 #[must_use]
483 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
484 self.access_strategy.as_ref()
485 }
486
487 #[must_use]
489 pub fn predicate_pushdown(&self) -> Option<&str> {
490 self.predicate_pushdown.as_deref()
491 }
492
493 #[must_use]
495 pub fn filter_expr(&self) -> Option<&str> {
496 self.filter_expr.as_deref()
497 }
498
499 #[must_use]
501 pub fn residual_filter_expr(&self) -> Option<&str> {
502 self.residual_filter_expr.as_deref()
503 }
504
505 #[must_use]
509 pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
510 self.residual_filter_predicate.as_ref()
511 }
512
513 #[must_use]
515 pub fn projection(&self) -> Option<&str> {
516 self.projection.as_deref()
517 }
518
519 #[must_use]
521 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
522 self.ordering_source
523 }
524
525 #[must_use]
527 pub const fn limit(&self) -> Option<u32> {
528 self.limit
529 }
530
531 #[must_use]
533 pub const fn cursor(&self) -> Option<bool> {
534 self.cursor
535 }
536
537 #[must_use]
539 pub const fn covering_scan(&self) -> Option<bool> {
540 self.covering_scan
541 }
542
543 #[must_use]
545 pub const fn rows_expected(&self) -> Option<u64> {
546 self.rows_expected
547 }
548
549 #[must_use]
551 pub const fn children(&self) -> &[Self] {
552 self.children.as_slice()
553 }
554
555 #[must_use]
557 pub const fn node_properties(&self) -> &ExplainPropertyMap {
558 &self.node_properties
559 }
560}
561
562pub(in crate::db::query::explain) const fn execution_mode_label(
563 mode: ExplainExecutionMode,
564) -> &'static str {
565 match mode {
566 ExplainExecutionMode::Streaming => "Streaming",
567 ExplainExecutionMode::Materialized => "Materialized",
568 }
569}
570
571pub(in crate::db::query::explain) const fn ordering_source_label(
572 ordering_source: ExplainExecutionOrderingSource,
573) -> &'static str {
574 match ordering_source {
575 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
576 ExplainExecutionOrderingSource::Materialized => "Materialized",
577 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
578 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
579 }
580}
581
582#[cfg(test)]
587mod tests {
588 use crate::db::query::explain::{
589 ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
590 ExplainPropertyMap,
591 };
592
593 fn node(
594 node_type: ExplainExecutionNodeType,
595 children: Vec<ExplainExecutionNodeDescriptor>,
596 ) -> ExplainExecutionNodeDescriptor {
597 ExplainExecutionNodeDescriptor {
598 node_type,
599 execution_mode: ExplainExecutionMode::Materialized,
600 access_strategy: None,
601 predicate_pushdown: None,
602 filter_expr: None,
603 residual_filter_expr: None,
604 residual_filter_predicate: None,
605 projection: None,
606 ordering_source: None,
607 limit: None,
608 cursor: None,
609 covering_scan: None,
610 rows_expected: None,
611 children,
612 node_properties: ExplainPropertyMap::new(),
613 }
614 }
615
616 #[test]
617 fn execution_node_contains_type_scans_preorder_tree() {
618 let root = node(
619 ExplainExecutionNodeType::Union,
620 vec![
621 node(ExplainExecutionNodeType::FullScan, Vec::new()),
622 node(
623 ExplainExecutionNodeType::Intersection,
624 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
625 ),
626 ],
627 );
628
629 assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
630 assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
631 }
632
633 #[test]
634 fn execution_node_preorder_visits_parent_before_children() {
635 let root = node(
636 ExplainExecutionNodeType::Union,
637 vec![
638 node(ExplainExecutionNodeType::FullScan, Vec::new()),
639 node(
640 ExplainExecutionNodeType::Intersection,
641 vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
642 ),
643 ],
644 );
645 let mut visited = Vec::new();
646
647 root.for_each_preorder(&mut |node| visited.push(node.node_type()));
648
649 assert_eq!(
650 visited,
651 vec![
652 ExplainExecutionNodeType::Union,
653 ExplainExecutionNodeType::FullScan,
654 ExplainExecutionNodeType::Intersection,
655 ExplainExecutionNodeType::ResidualFilter,
656 ],
657 );
658 }
659}