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        self.render_text_tree_verbose_with_indent("")
30    }
31
32    /// Render this execution subtree as one verbose text tree with one
33    /// caller-owned line prefix applied to every emitted line.
34    #[must_use]
35    pub fn render_text_tree_verbose_with_indent(&self, indent: &str) -> String {
36        let mut out = String::new();
37        let mut node_id_counter = 0_u64;
38        self.render_text_tree_verbose_into(indent, 0, &mut node_id_counter, &mut out);
39        out
40    }
41
42    fn render_text_tree_into(&self, depth: usize, node_id_counter: &mut u64, out: &mut String) {
43        let node_id = *node_id_counter;
44        *node_id_counter = node_id_counter.saturating_add(1);
45        push_rendered_line_prefix(out, depth);
46        let _ = write!(
47            out,
48            "{} execution_mode={}",
49            self.node_type.as_str(),
50            execution_mode_label(self.execution_mode)
51        );
52        let _ = write!(out, " node_id={node_id}");
53        let _ = write!(out, " layer={}", self.node_type.layer_label());
54        let _ = write!(
55            out,
56            " execution_mode_detail={}",
57            execution_mode_detail_label(self.execution_mode)
58        );
59        let _ = write!(
60            out,
61            " predicate_pushdown_mode={}",
62            predicate_pushdown_mode(self)
63        );
64        if let Some(fast_path_selected) = fast_path_selected(self) {
65            let _ = write!(out, " fast_path_selected={fast_path_selected}");
66        }
67        if let Some(fast_path_reason) = fast_path_reason(self) {
68            let _ = write!(out, " fast_path_reason={fast_path_reason}");
69        }
70
71        if let Some(access_strategy) = self.access_strategy.as_ref() {
72            out.push_str(" access=");
73            write_access_strategy_label(out, access_strategy);
74        }
75        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
76            let _ = write!(out, " predicate_pushdown={predicate_pushdown}");
77        }
78        if let Some(filter_expr) = self.filter_expr.as_ref() {
79            let _ = write!(out, " filter_expr={filter_expr}");
80        }
81        if let Some(residual_filter_predicate) = self.residual_filter_predicate.as_ref() {
82            let _ = write!(
83                out,
84                " residual_filter_predicate={residual_filter_predicate:?}"
85            );
86        }
87        if let Some(projection) = self.projection.as_ref() {
88            let _ = write!(out, " projection={projection}");
89        }
90        if let Some(ordering_source) = self.ordering_source {
91            let _ = write!(
92                out,
93                " ordering_source={}",
94                ordering_source_label(ordering_source)
95            );
96        }
97        if let Some(limit) = self.limit {
98            let _ = write!(out, " limit={limit}");
99        }
100        if let Some(cursor) = self.cursor {
101            let _ = write!(out, " cursor={cursor}");
102        }
103        if let Some(covering_scan) = self.covering_scan {
104            let _ = write!(out, " covering_scan={covering_scan}");
105        }
106        if let Some(rows_expected) = self.rows_expected {
107            let _ = write!(out, " rows_expected={rows_expected}");
108        }
109        if !self.node_properties.is_empty() {
110            out.push_str(" node_properties=");
111            write_node_properties(out, &self.node_properties);
112        }
113
114        for child in &self.children {
115            child.render_text_tree_into(depth.saturating_add(1), node_id_counter, out);
116        }
117    }
118
119    fn render_text_tree_verbose_into(
120        &self,
121        base_indent: &str,
122        depth: usize,
123        node_id_counter: &mut u64,
124        out: &mut String,
125    ) {
126        let node_id = *node_id_counter;
127        *node_id_counter = node_id_counter.saturating_add(1);
128
129        // Emit the node heading line first so child metadata stays visually scoped
130        // without rebuilding indentation strings per node.
131        push_rendered_line_prefix_with_base_depth(out, base_indent, depth);
132        let _ = write!(
133            out,
134            "{} execution_mode={}",
135            self.node_type.as_str(),
136            execution_mode_label(self.execution_mode)
137        );
138        push_rendered_line_prefix_with_base_depth(out, base_indent, depth.saturating_add(1));
139        let _ = write!(out, "node_id={node_id}");
140        push_rendered_line_prefix_with_base_depth(out, base_indent, depth.saturating_add(1));
141        let _ = write!(out, "layer={}", self.node_type.layer_label());
142        push_rendered_line_prefix_with_base_depth(out, base_indent, depth.saturating_add(1));
143        let _ = write!(
144            out,
145            "execution_mode_detail={}",
146            execution_mode_detail_label(self.execution_mode)
147        );
148        push_rendered_line_prefix_with_base_depth(out, base_indent, depth.saturating_add(1));
149        let _ = write!(
150            out,
151            "predicate_pushdown_mode={}",
152            predicate_pushdown_mode(self)
153        );
154        if let Some(fast_path_selected) = fast_path_selected(self) {
155            push_rendered_line_prefix_with_base_depth(out, base_indent, depth.saturating_add(1));
156            let _ = write!(out, "fast_path_selected={fast_path_selected}");
157        }
158        if let Some(fast_path_reason) = fast_path_reason(self) {
159            push_rendered_line_prefix_with_base_depth(out, base_indent, depth.saturating_add(1));
160            let _ = write!(out, "fast_path_reason={fast_path_reason}");
161        }
162
163        // Emit all optional node-local fields in a deterministic order.
164        self.render_text_tree_verbose_node_fields(base_indent, depth.saturating_add(1), out);
165
166        // Recurse in execution order to preserve stable tree topology.
167        for child in &self.children {
168            child.render_text_tree_verbose_into(
169                base_indent,
170                depth.saturating_add(1),
171                node_id_counter,
172                out,
173            );
174        }
175    }
176
177    fn render_text_tree_verbose_node_fields(
178        &self,
179        base_indent: &str,
180        field_depth: usize,
181        out: &mut String,
182    ) {
183        if let Some(access_strategy) = self.access_strategy.as_ref() {
184            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
185            out.push_str("access_strategy=");
186            write_access_strategy_label(out, access_strategy);
187        }
188        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
189            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
190            let _ = write!(out, "predicate_pushdown={predicate_pushdown}");
191        }
192        if let Some(filter_expr) = self.filter_expr.as_ref() {
193            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
194            let _ = write!(out, "filter_expr={filter_expr}");
195        }
196        if let Some(residual_filter_predicate) = self.residual_filter_predicate.as_ref() {
197            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
198            let _ = write!(
199                out,
200                "residual_filter_predicate={residual_filter_predicate:?}"
201            );
202        }
203        if let Some(projection) = self.projection.as_ref() {
204            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
205            let _ = write!(out, "projection={projection}");
206        }
207        if let Some(ordering_source) = self.ordering_source {
208            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
209            let _ = write!(
210                out,
211                "ordering_source={}",
212                ordering_source_label(ordering_source)
213            );
214        }
215        if let Some(limit) = self.limit {
216            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
217            let _ = write!(out, "limit={limit}");
218        }
219        if let Some(cursor) = self.cursor {
220            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
221            let _ = write!(out, "cursor={cursor}");
222        }
223        if let Some(covering_scan) = self.covering_scan {
224            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
225            let _ = write!(out, "covering_scan={covering_scan}");
226        }
227        if let Some(rows_expected) = self.rows_expected {
228            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
229            let _ = write!(out, "rows_expected={rows_expected}");
230        }
231        if !self.node_properties.is_empty() {
232            push_rendered_line_prefix_with_base_depth(out, base_indent, field_depth);
233            out.push_str("node_properties:");
234
235            // Expand each stable property onto its own line so verbose explain
236            // stays readable even when route diagnostics grow.
237            for (key, value) in self.node_properties.iter() {
238                push_rendered_line_prefix_with_base_depth(
239                    out,
240                    base_indent,
241                    field_depth.saturating_add(1),
242                );
243                let _ = write!(out, "{key}={value:?}");
244            }
245        }
246    }
247}
248
249fn push_rendered_line_prefix(out: &mut String, depth: usize) {
250    if !out.is_empty() {
251        out.push('\n');
252    }
253
254    for _ in 0..depth {
255        out.push_str("  ");
256    }
257}
258
259fn push_rendered_line_prefix_with_base_depth(out: &mut String, base_indent: &str, depth: usize) {
260    if !out.is_empty() {
261        out.push('\n');
262    }
263    out.push_str(base_indent);
264
265    for _ in 0..depth {
266        out.push_str("  ");
267    }
268}
269
270fn write_node_properties(out: &mut String, node_properties: &ExplainPropertyMap) {
271    let mut first = true;
272    for (key, value) in node_properties.iter() {
273        if first {
274            first = false;
275        } else {
276            out.push(',');
277        }
278        let _ = write!(out, "{key}={value:?}");
279    }
280}