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