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