1use crate::{
7 db::query::{
8 explain::{ExplainAccessPath, ExplainPlan, ExplainPredicate},
9 plan::AggregateKind,
10 },
11 value::Value,
12};
13use std::fmt::{self, Debug};
14
15#[cfg_attr(
16 doc,
17 doc = "ExplainPropertyMap\n\nStable ordered property map for EXPLAIN metadata.\nKeeps deterministic key order without `BTreeMap`."
18)]
19#[derive(Clone, Default, Eq, PartialEq)]
20pub struct ExplainPropertyMap {
21 entries: Vec<(&'static str, Value)>,
22}
23
24impl ExplainPropertyMap {
25 #[must_use]
27 pub const fn new() -> Self {
28 Self {
29 entries: Vec::new(),
30 }
31 }
32
33 pub fn insert(&mut self, key: &'static str, value: Value) -> Option<Value> {
35 match self
36 .entries
37 .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
38 {
39 Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
40 Err(index) => {
41 self.entries.insert(index, (key, value));
42 None
43 }
44 }
45 }
46
47 #[must_use]
49 pub fn get(&self, key: &str) -> Option<&Value> {
50 self.entries
51 .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
52 .ok()
53 .map(|index| &self.entries[index].1)
54 }
55
56 #[must_use]
58 pub fn contains_key(&self, key: &str) -> bool {
59 self.get(key).is_some()
60 }
61
62 #[must_use]
64 pub const fn is_empty(&self) -> bool {
65 self.entries.is_empty()
66 }
67
68 pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Value)> {
70 self.entries.iter().map(|(key, value)| (*key, value))
71 }
72}
73
74impl Debug for ExplainPropertyMap {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 let mut map = f.debug_map();
77 for (key, value) in self.iter() {
78 map.entry(&key, value);
79 }
80 map.finish()
81 }
82}
83
84#[cfg_attr(
85 doc,
86 doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
87)]
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct ExplainAggregateTerminalPlan {
90 pub(crate) query: ExplainPlan,
91 pub(crate) terminal: AggregateKind,
92 pub(crate) execution: ExplainExecutionDescriptor,
93}
94
95#[cfg_attr(
96 doc,
97 doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
98)]
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub enum ExplainExecutionOrderingSource {
101 AccessOrder,
102 Materialized,
103 IndexSeekFirst { fetch: usize },
104 IndexSeekLast { fetch: usize },
105}
106
107#[cfg_attr(
108 doc,
109 doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
110)]
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum ExplainExecutionMode {
113 Streaming,
114 Materialized,
115}
116
117#[cfg_attr(
118 doc,
119 doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
120)]
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct ExplainExecutionDescriptor {
123 pub(crate) access_strategy: ExplainAccessPath,
124 pub(crate) covering_projection: bool,
125 pub(crate) aggregation: AggregateKind,
126 pub(crate) execution_mode: ExplainExecutionMode,
127 pub(crate) ordering_source: ExplainExecutionOrderingSource,
128 pub(crate) limit: Option<u32>,
129 pub(crate) cursor: bool,
130 pub(crate) node_properties: ExplainPropertyMap,
131}
132
133#[cfg_attr(
134 doc,
135 doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
136)]
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum ExplainExecutionNodeType {
139 ByKeyLookup,
140 ByKeysLookup,
141 PrimaryKeyRangeScan,
142 IndexPrefixScan,
143 IndexRangeScan,
144 IndexMultiLookup,
145 FullScan,
146 Union,
147 Intersection,
148 IndexPredicatePrefilter,
149 ResidualPredicateFilter,
150 OrderByAccessSatisfied,
151 OrderByMaterializedSort,
152 DistinctPreOrdered,
153 DistinctMaterialized,
154 ProjectionMaterialized,
155 CoveringRead,
156 LimitOffset,
157 CursorResume,
158 IndexRangeLimitPushdown,
159 TopNSeek,
160 AggregateCount,
161 AggregateExists,
162 AggregateMin,
163 AggregateMax,
164 AggregateFirst,
165 AggregateLast,
166 AggregateSum,
167 AggregateSeekFirst,
168 AggregateSeekLast,
169 GroupedAggregateHashMaterialized,
170 GroupedAggregateOrderedMaterialized,
171 SecondaryOrderPushdown,
172}
173
174#[cfg_attr(
175 doc,
176 doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
177)]
178#[derive(Clone, Debug, Eq, PartialEq)]
179pub struct ExplainExecutionNodeDescriptor {
180 pub(crate) node_type: ExplainExecutionNodeType,
181 pub(crate) execution_mode: ExplainExecutionMode,
182 pub(crate) access_strategy: Option<ExplainAccessPath>,
183 pub(crate) predicate_pushdown: Option<String>,
184 pub(crate) residual_predicate: Option<ExplainPredicate>,
185 pub(crate) projection: Option<String>,
186 pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
187 pub(crate) limit: Option<u32>,
188 pub(crate) cursor: Option<bool>,
189 pub(crate) covering_scan: Option<bool>,
190 pub(crate) rows_expected: Option<u64>,
191 pub(crate) children: Vec<Self>,
192 pub(crate) node_properties: ExplainPropertyMap,
193}
194
195impl ExplainAggregateTerminalPlan {
196 #[must_use]
198 pub const fn query(&self) -> &ExplainPlan {
199 &self.query
200 }
201
202 #[must_use]
204 pub const fn terminal(&self) -> AggregateKind {
205 self.terminal
206 }
207
208 #[must_use]
210 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
211 &self.execution
212 }
213
214 #[must_use]
215 pub(in crate::db) const fn new(
216 query: ExplainPlan,
217 terminal: AggregateKind,
218 execution: ExplainExecutionDescriptor,
219 ) -> Self {
220 Self {
221 query,
222 terminal,
223 execution,
224 }
225 }
226}
227
228impl ExplainExecutionDescriptor {
229 #[must_use]
231 pub const fn access_strategy(&self) -> &ExplainAccessPath {
232 &self.access_strategy
233 }
234
235 #[must_use]
237 pub const fn covering_projection(&self) -> bool {
238 self.covering_projection
239 }
240
241 #[must_use]
243 pub const fn aggregation(&self) -> AggregateKind {
244 self.aggregation
245 }
246
247 #[must_use]
249 pub const fn execution_mode(&self) -> ExplainExecutionMode {
250 self.execution_mode
251 }
252
253 #[must_use]
255 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
256 self.ordering_source
257 }
258
259 #[must_use]
261 pub const fn limit(&self) -> Option<u32> {
262 self.limit
263 }
264
265 #[must_use]
267 pub const fn cursor(&self) -> bool {
268 self.cursor
269 }
270
271 #[must_use]
273 pub const fn node_properties(&self) -> &ExplainPropertyMap {
274 &self.node_properties
275 }
276}
277
278impl ExplainAggregateTerminalPlan {
279 #[must_use]
281 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
282 ExplainExecutionNodeDescriptor {
283 node_type: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
284 execution_mode: self.execution.execution_mode,
285 access_strategy: Some(self.execution.access_strategy.clone()),
286 predicate_pushdown: None,
287 residual_predicate: None,
288 projection: None,
289 ordering_source: Some(self.execution.ordering_source),
290 limit: self.execution.limit,
291 cursor: Some(self.execution.cursor),
292 covering_scan: Some(self.execution.covering_projection),
293 rows_expected: None,
294 children: Vec::new(),
295 node_properties: self.execution.node_properties.clone(),
296 }
297 }
298}
299
300const fn aggregate_execution_node_type(
301 terminal: AggregateKind,
302 ordering_source: ExplainExecutionOrderingSource,
303) -> ExplainExecutionNodeType {
304 match ordering_source {
305 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
306 ExplainExecutionNodeType::AggregateSeekFirst
307 }
308 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
309 ExplainExecutionNodeType::AggregateSeekLast
310 }
311 ExplainExecutionOrderingSource::AccessOrder
312 | ExplainExecutionOrderingSource::Materialized => {
313 ExplainExecutionNodeType::aggregate_terminal(terminal)
314 }
315 }
316}
317
318impl ExplainExecutionNodeType {
319 #[must_use]
321 pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
322 match kind {
323 AggregateKind::Count => Self::AggregateCount,
324 AggregateKind::Exists => Self::AggregateExists,
325 AggregateKind::Min => Self::AggregateMin,
326 AggregateKind::Max => Self::AggregateMax,
327 AggregateKind::First => Self::AggregateFirst,
328 AggregateKind::Last => Self::AggregateLast,
329 AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
330 }
331 }
332
333 #[must_use]
335 pub const fn as_str(self) -> &'static str {
336 match self {
337 Self::ByKeyLookup => "ByKeyLookup",
338 Self::ByKeysLookup => "ByKeysLookup",
339 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
340 Self::IndexPrefixScan => "IndexPrefixScan",
341 Self::IndexRangeScan => "IndexRangeScan",
342 Self::IndexMultiLookup => "IndexMultiLookup",
343 Self::FullScan => "FullScan",
344 Self::Union => "Union",
345 Self::Intersection => "Intersection",
346 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
347 Self::ResidualPredicateFilter => "ResidualPredicateFilter",
348 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
349 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
350 Self::DistinctPreOrdered => "DistinctPreOrdered",
351 Self::DistinctMaterialized => "DistinctMaterialized",
352 Self::ProjectionMaterialized => "ProjectionMaterialized",
353 Self::CoveringRead => "CoveringRead",
354 Self::LimitOffset => "LimitOffset",
355 Self::CursorResume => "CursorResume",
356 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
357 Self::TopNSeek => "TopNSeek",
358 Self::AggregateCount => "AggregateCount",
359 Self::AggregateExists => "AggregateExists",
360 Self::AggregateMin => "AggregateMin",
361 Self::AggregateMax => "AggregateMax",
362 Self::AggregateFirst => "AggregateFirst",
363 Self::AggregateLast => "AggregateLast",
364 Self::AggregateSum => "AggregateSum",
365 Self::AggregateSeekFirst => "AggregateSeekFirst",
366 Self::AggregateSeekLast => "AggregateSeekLast",
367 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
368 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
369 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
370 }
371 }
372
373 #[must_use]
375 pub const fn layer_label(self) -> &'static str {
376 crate::db::query::explain::nodes::layer_label(self)
377 }
378}
379
380impl ExplainExecutionNodeDescriptor {
381 #[must_use]
383 pub const fn node_type(&self) -> ExplainExecutionNodeType {
384 self.node_type
385 }
386
387 #[must_use]
389 pub const fn execution_mode(&self) -> ExplainExecutionMode {
390 self.execution_mode
391 }
392
393 #[must_use]
395 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
396 self.access_strategy.as_ref()
397 }
398
399 #[must_use]
401 pub fn predicate_pushdown(&self) -> Option<&str> {
402 self.predicate_pushdown.as_deref()
403 }
404
405 #[must_use]
407 pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
408 self.residual_predicate.as_ref()
409 }
410
411 #[must_use]
413 pub fn projection(&self) -> Option<&str> {
414 self.projection.as_deref()
415 }
416
417 #[must_use]
419 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
420 self.ordering_source
421 }
422
423 #[must_use]
425 pub const fn limit(&self) -> Option<u32> {
426 self.limit
427 }
428
429 #[must_use]
431 pub const fn cursor(&self) -> Option<bool> {
432 self.cursor
433 }
434
435 #[must_use]
437 pub const fn covering_scan(&self) -> Option<bool> {
438 self.covering_scan
439 }
440
441 #[must_use]
443 pub const fn rows_expected(&self) -> Option<u64> {
444 self.rows_expected
445 }
446
447 #[must_use]
449 pub const fn children(&self) -> &[Self] {
450 self.children.as_slice()
451 }
452
453 #[must_use]
455 pub const fn node_properties(&self) -> &ExplainPropertyMap {
456 &self.node_properties
457 }
458}
459
460pub(in crate::db::query::explain) const fn execution_mode_label(
461 mode: ExplainExecutionMode,
462) -> &'static str {
463 match mode {
464 ExplainExecutionMode::Streaming => "Streaming",
465 ExplainExecutionMode::Materialized => "Materialized",
466 }
467}
468
469pub(in crate::db::query::explain) const fn ordering_source_label(
470 ordering_source: ExplainExecutionOrderingSource,
471) -> &'static str {
472 match ordering_source {
473 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
474 ExplainExecutionOrderingSource::Materialized => "Materialized",
475 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
476 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
477 }
478}