Skip to main content

icydb_core/db/query/explain/
render.rs

1//! Module: query::explain::render
2//! Responsibility: text/json rendering for explain execution descriptors.
3//! Does not own: planner or executor decision derivation.
4//! Boundary: consumes explain projection types and emits deterministic render output.
5
6use crate::{
7    db::query::explain::{
8        ExplainExecutionNodeDescriptor,
9        access_projection::access_strategy_label,
10        execution::{execution_mode_label, ordering_source_label},
11        nodes::{
12            execution_mode_detail_label, fast_path_reason, fast_path_selected,
13            predicate_pushdown_mode,
14        },
15    },
16    value::Value,
17};
18use std::{collections::BTreeMap, fmt::Write};
19
20impl ExplainExecutionNodeDescriptor {
21    /// Render this execution subtree as a compact text tree.
22    #[must_use]
23    pub fn render_text_tree(&self) -> String {
24        let mut lines = Vec::new();
25        let mut node_id_counter = 0_u64;
26        self.render_text_tree_into(0, &mut node_id_counter, &mut lines);
27        lines.join("\n")
28    }
29
30    /// Render this execution subtree as a verbose text tree with properties.
31    #[must_use]
32    pub fn render_text_tree_verbose(&self) -> String {
33        let mut lines = Vec::new();
34        let mut node_id_counter = 0_u64;
35        self.render_text_tree_verbose_into(0, &mut node_id_counter, &mut lines);
36        lines.join("\n")
37    }
38
39    fn render_text_tree_into(
40        &self,
41        depth: usize,
42        node_id_counter: &mut u64,
43        lines: &mut Vec<String>,
44    ) {
45        let node_id = next_node_id(node_id_counter);
46        let mut line = format!(
47            "{}{} execution_mode={}",
48            "  ".repeat(depth),
49            self.node_type.as_str(),
50            execution_mode_label(self.execution_mode)
51        );
52        let _ = write!(line, " node_id={node_id}");
53        let _ = write!(line, " layer={}", self.node_type.layer_label());
54        let _ = write!(
55            line,
56            " execution_mode_detail={}",
57            execution_mode_detail_label(self.execution_mode)
58        );
59        let _ = write!(
60            line,
61            " predicate_pushdown_mode={}",
62            predicate_pushdown_mode(self)
63        );
64        if let Some(fast_path_selected) = fast_path_selected(self) {
65            let _ = write!(line, " fast_path_selected={fast_path_selected}");
66        }
67        if let Some(fast_path_reason) = fast_path_reason(self) {
68            let _ = write!(line, " fast_path_reason={fast_path_reason}");
69        }
70
71        if let Some(access_strategy) = self.access_strategy.as_ref() {
72            let _ = write!(line, " access={}", access_strategy_label(access_strategy));
73        }
74        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
75            let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
76        }
77        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
78            let _ = write!(line, " residual_predicate={residual_predicate:?}");
79        }
80        if let Some(projection) = self.projection.as_ref() {
81            let _ = write!(line, " projection={projection}");
82        }
83        if let Some(ordering_source) = self.ordering_source {
84            let _ = write!(
85                line,
86                " ordering_source={}",
87                ordering_source_label(ordering_source)
88            );
89        }
90        if let Some(limit) = self.limit {
91            let _ = write!(line, " limit={limit}");
92        }
93        if let Some(cursor) = self.cursor {
94            let _ = write!(line, " cursor={cursor}");
95        }
96        if let Some(covering_scan) = self.covering_scan {
97            let _ = write!(line, " covering_scan={covering_scan}");
98        }
99        if let Some(rows_expected) = self.rows_expected {
100            let _ = write!(line, " rows_expected={rows_expected}");
101        }
102        if !self.node_properties.is_empty() {
103            let _ = write!(
104                line,
105                " node_properties={}",
106                render_node_properties(&self.node_properties)
107            );
108        }
109
110        lines.push(line);
111
112        for child in &self.children {
113            child.render_text_tree_into(depth.saturating_add(1), node_id_counter, lines);
114        }
115    }
116
117    fn render_text_tree_verbose_into(
118        &self,
119        depth: usize,
120        node_id_counter: &mut u64,
121        lines: &mut Vec<String>,
122    ) {
123        let node_id = next_node_id(node_id_counter);
124        // Emit the node heading line first so child metadata stays visually scoped.
125        let node_indent = "  ".repeat(depth);
126        let field_indent = "  ".repeat(depth.saturating_add(1));
127        lines.push(format!(
128            "{}{} execution_mode={}",
129            node_indent,
130            self.node_type.as_str(),
131            execution_mode_label(self.execution_mode)
132        ));
133        lines.push(format!("{field_indent}node_id={node_id}"));
134        lines.push(format!(
135            "{}layer={}",
136            field_indent,
137            self.node_type.layer_label()
138        ));
139        lines.push(format!(
140            "{}execution_mode_detail={}",
141            field_indent,
142            execution_mode_detail_label(self.execution_mode)
143        ));
144        lines.push(format!(
145            "{}predicate_pushdown_mode={}",
146            field_indent,
147            predicate_pushdown_mode(self)
148        ));
149        if let Some(fast_path_selected) = fast_path_selected(self) {
150            lines.push(format!(
151                "{field_indent}fast_path_selected={fast_path_selected}"
152            ));
153        }
154        if let Some(fast_path_reason) = fast_path_reason(self) {
155            lines.push(format!("{field_indent}fast_path_reason={fast_path_reason}"));
156        }
157
158        // Emit all optional node-local fields in a deterministic order.
159        if let Some(access_strategy) = self.access_strategy.as_ref() {
160            lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
161        }
162        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
163            lines.push(format!(
164                "{field_indent}predicate_pushdown={predicate_pushdown}"
165            ));
166        }
167        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
168            lines.push(format!(
169                "{field_indent}residual_predicate={residual_predicate:?}"
170            ));
171        }
172        if let Some(projection) = self.projection.as_ref() {
173            lines.push(format!("{field_indent}projection={projection}"));
174        }
175        if let Some(ordering_source) = self.ordering_source {
176            lines.push(format!(
177                "{}ordering_source={}",
178                field_indent,
179                ordering_source_label(ordering_source)
180            ));
181        }
182        if let Some(limit) = self.limit {
183            lines.push(format!("{field_indent}limit={limit}"));
184        }
185        if let Some(cursor) = self.cursor {
186            lines.push(format!("{field_indent}cursor={cursor}"));
187        }
188        if let Some(covering_scan) = self.covering_scan {
189            lines.push(format!("{field_indent}covering_scan={covering_scan}"));
190        }
191        if let Some(rows_expected) = self.rows_expected {
192            lines.push(format!("{field_indent}rows_expected={rows_expected}"));
193        }
194        if !self.node_properties.is_empty() {
195            lines.push(format!(
196                "{}node_properties={}",
197                field_indent,
198                render_node_properties(&self.node_properties)
199            ));
200        }
201
202        // Recurse in execution order to preserve stable tree topology.
203        for child in &self.children {
204            child.render_text_tree_verbose_into(depth.saturating_add(1), node_id_counter, lines);
205        }
206    }
207}
208
209fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
210    let mut rendered = String::new();
211    let mut first = true;
212    for (key, value) in node_properties {
213        if first {
214            first = false;
215        } else {
216            rendered.push(',');
217        }
218        let _ = write!(rendered, "{key}={value:?}");
219    }
220    rendered
221}
222
223const fn next_node_id(node_id_counter: &mut u64) -> u64 {
224    let node_id = *node_id_counter;
225    *node_id_counter = node_id_counter.saturating_add(1);
226    node_id
227}