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