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