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::write_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 out = String::new();
25        let mut node_id_counter = 0_u64;
26        self.render_text_tree_into(0, &mut node_id_counter, &mut out);
27        out
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 out = String::new();
34        let mut node_id_counter = 0_u64;
35        self.render_text_tree_verbose_into(0, &mut node_id_counter, &mut out);
36        out
37    }
38
39    fn render_text_tree_into(&self, depth: usize, node_id_counter: &mut u64, out: &mut String) {
40        let node_id = next_node_id(node_id_counter);
41        push_rendered_line_prefix(out, depth);
42        let _ = write!(
43            out,
44            "{} execution_mode={}",
45            self.node_type.as_str(),
46            execution_mode_label(self.execution_mode)
47        );
48        let _ = write!(out, " node_id={node_id}");
49        let _ = write!(out, " layer={}", self.node_type.layer_label());
50        let _ = write!(
51            out,
52            " execution_mode_detail={}",
53            execution_mode_detail_label(self.execution_mode)
54        );
55        let _ = write!(
56            out,
57            " predicate_pushdown_mode={}",
58            predicate_pushdown_mode(self)
59        );
60        if let Some(fast_path_selected) = fast_path_selected(self) {
61            let _ = write!(out, " fast_path_selected={fast_path_selected}");
62        }
63        if let Some(fast_path_reason) = fast_path_reason(self) {
64            let _ = write!(out, " fast_path_reason={fast_path_reason}");
65        }
66
67        if let Some(access_strategy) = self.access_strategy.as_ref() {
68            out.push_str(" access=");
69            write_access_strategy_label(out, access_strategy);
70        }
71        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
72            let _ = write!(out, " predicate_pushdown={predicate_pushdown}");
73        }
74        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
75            let _ = write!(out, " residual_predicate={residual_predicate:?}");
76        }
77        if let Some(projection) = self.projection.as_ref() {
78            let _ = write!(out, " projection={projection}");
79        }
80        if let Some(ordering_source) = self.ordering_source {
81            let _ = write!(
82                out,
83                " ordering_source={}",
84                ordering_source_label(ordering_source)
85            );
86        }
87        if let Some(limit) = self.limit {
88            let _ = write!(out, " limit={limit}");
89        }
90        if let Some(cursor) = self.cursor {
91            let _ = write!(out, " cursor={cursor}");
92        }
93        if let Some(covering_scan) = self.covering_scan {
94            let _ = write!(out, " covering_scan={covering_scan}");
95        }
96        if let Some(rows_expected) = self.rows_expected {
97            let _ = write!(out, " rows_expected={rows_expected}");
98        }
99        if !self.node_properties.is_empty() {
100            let _ = write!(
101                out,
102                " node_properties={}",
103                render_node_properties(&self.node_properties)
104            );
105        }
106
107        for child in &self.children {
108            child.render_text_tree_into(depth.saturating_add(1), node_id_counter, out);
109        }
110    }
111
112    fn render_text_tree_verbose_into(
113        &self,
114        depth: usize,
115        node_id_counter: &mut u64,
116        out: &mut String,
117    ) {
118        let node_id = next_node_id(node_id_counter);
119        // Emit the node heading line first so child metadata stays visually scoped.
120        let node_indent = "  ".repeat(depth);
121        let field_indent = "  ".repeat(depth.saturating_add(1));
122        push_rendered_line_prefix_with_indent(out, &node_indent);
123        let _ = write!(
124            out,
125            "{} execution_mode={}",
126            self.node_type.as_str(),
127            execution_mode_label(self.execution_mode)
128        );
129        push_rendered_line_prefix_with_indent(out, &field_indent);
130        let _ = write!(out, "node_id={node_id}");
131        push_rendered_line_prefix_with_indent(out, &field_indent);
132        let _ = write!(out, "layer={}", self.node_type.layer_label());
133        push_rendered_line_prefix_with_indent(out, &field_indent);
134        let _ = write!(
135            out,
136            "execution_mode_detail={}",
137            execution_mode_detail_label(self.execution_mode)
138        );
139        push_rendered_line_prefix_with_indent(out, &field_indent);
140        let _ = write!(
141            out,
142            "predicate_pushdown_mode={}",
143            predicate_pushdown_mode(self)
144        );
145        if let Some(fast_path_selected) = fast_path_selected(self) {
146            push_rendered_line_prefix_with_indent(out, &field_indent);
147            let _ = write!(out, "fast_path_selected={fast_path_selected}");
148        }
149        if let Some(fast_path_reason) = fast_path_reason(self) {
150            push_rendered_line_prefix_with_indent(out, &field_indent);
151            let _ = write!(out, "fast_path_reason={fast_path_reason}");
152        }
153
154        // Emit all optional node-local fields in a deterministic order.
155        self.render_text_tree_verbose_node_fields(&field_indent, out);
156
157        // Recurse in execution order to preserve stable tree topology.
158        for child in &self.children {
159            child.render_text_tree_verbose_into(depth.saturating_add(1), node_id_counter, out);
160        }
161    }
162
163    fn render_text_tree_verbose_node_fields(&self, field_indent: &str, out: &mut String) {
164        if let Some(access_strategy) = self.access_strategy.as_ref() {
165            push_rendered_line_prefix_with_indent(out, field_indent);
166            let _ = write!(out, "access_strategy={access_strategy:?}");
167        }
168        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
169            push_rendered_line_prefix_with_indent(out, field_indent);
170            let _ = write!(out, "predicate_pushdown={predicate_pushdown}");
171        }
172        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
173            push_rendered_line_prefix_with_indent(out, field_indent);
174            let _ = write!(out, "residual_predicate={residual_predicate:?}");
175        }
176        if let Some(projection) = self.projection.as_ref() {
177            push_rendered_line_prefix_with_indent(out, field_indent);
178            let _ = write!(out, "projection={projection}");
179        }
180        if let Some(ordering_source) = self.ordering_source {
181            push_rendered_line_prefix_with_indent(out, field_indent);
182            let _ = write!(
183                out,
184                "ordering_source={}",
185                ordering_source_label(ordering_source)
186            );
187        }
188        if let Some(limit) = self.limit {
189            push_rendered_line_prefix_with_indent(out, field_indent);
190            let _ = write!(out, "limit={limit}");
191        }
192        if let Some(cursor) = self.cursor {
193            push_rendered_line_prefix_with_indent(out, field_indent);
194            let _ = write!(out, "cursor={cursor}");
195        }
196        if let Some(covering_scan) = self.covering_scan {
197            push_rendered_line_prefix_with_indent(out, field_indent);
198            let _ = write!(out, "covering_scan={covering_scan}");
199        }
200        if let Some(rows_expected) = self.rows_expected {
201            push_rendered_line_prefix_with_indent(out, field_indent);
202            let _ = write!(out, "rows_expected={rows_expected}");
203        }
204        if !self.node_properties.is_empty() {
205            push_rendered_line_prefix_with_indent(out, field_indent);
206            let _ = write!(
207                out,
208                "node_properties={}",
209                render_node_properties(&self.node_properties)
210            );
211        }
212    }
213}
214
215fn push_rendered_line_prefix(out: &mut String, depth: usize) {
216    if !out.is_empty() {
217        out.push('\n');
218    }
219
220    for _ in 0..depth {
221        out.push_str("  ");
222    }
223}
224
225fn push_rendered_line_prefix_with_indent(out: &mut String, indent: &str) {
226    if !out.is_empty() {
227        out.push('\n');
228    }
229    out.push_str(indent);
230}
231
232fn render_node_properties(node_properties: &BTreeMap<&'static str, Value>) -> String {
233    let mut rendered = String::new();
234    let mut first = true;
235    for (key, value) in node_properties {
236        if first {
237            first = false;
238        } else {
239            rendered.push(',');
240        }
241        let _ = write!(rendered, "{key}={value:?}");
242    }
243    rendered
244}
245
246const fn next_node_id(node_id_counter: &mut u64) -> u64 {
247    let node_id = *node_id_counter;
248    *node_id_counter = node_id_counter.saturating_add(1);
249    node_id
250}