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