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            out.push_str("access_strategy=");
160            write_access_strategy_label(out, access_strategy);
161        }
162        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
163            push_rendered_line_prefix_with_indent(out, field_indent);
164            let _ = write!(out, "predicate_pushdown={predicate_pushdown}");
165        }
166        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
167            push_rendered_line_prefix_with_indent(out, field_indent);
168            let _ = write!(out, "residual_predicate={residual_predicate:?}");
169        }
170        if let Some(projection) = self.projection.as_ref() {
171            push_rendered_line_prefix_with_indent(out, field_indent);
172            let _ = write!(out, "projection={projection}");
173        }
174        if let Some(ordering_source) = self.ordering_source {
175            push_rendered_line_prefix_with_indent(out, field_indent);
176            let _ = write!(
177                out,
178                "ordering_source={}",
179                ordering_source_label(ordering_source)
180            );
181        }
182        if let Some(limit) = self.limit {
183            push_rendered_line_prefix_with_indent(out, field_indent);
184            let _ = write!(out, "limit={limit}");
185        }
186        if let Some(cursor) = self.cursor {
187            push_rendered_line_prefix_with_indent(out, field_indent);
188            let _ = write!(out, "cursor={cursor}");
189        }
190        if let Some(covering_scan) = self.covering_scan {
191            push_rendered_line_prefix_with_indent(out, field_indent);
192            let _ = write!(out, "covering_scan={covering_scan}");
193        }
194        if let Some(rows_expected) = self.rows_expected {
195            push_rendered_line_prefix_with_indent(out, field_indent);
196            let _ = write!(out, "rows_expected={rows_expected}");
197        }
198        if !self.node_properties.is_empty() {
199            push_rendered_line_prefix_with_indent(out, field_indent);
200            out.push_str("node_properties:");
201
202            // Expand each stable property onto its own line so verbose explain
203            // stays readable even when route diagnostics grow.
204            for (key, value) in self.node_properties.iter() {
205                push_rendered_line_prefix_with_indent(out, field_indent);
206                out.push_str("  ");
207                let _ = write!(out, "{key}={value:?}");
208            }
209        }
210    }
211}
212
213fn push_rendered_line_prefix(out: &mut String, depth: usize) {
214    if !out.is_empty() {
215        out.push('\n');
216    }
217
218    for _ in 0..depth {
219        out.push_str("  ");
220    }
221}
222
223fn push_rendered_line_prefix_with_indent(out: &mut String, indent: &str) {
224    if !out.is_empty() {
225        out.push('\n');
226    }
227    out.push_str(indent);
228}
229
230fn write_node_properties(out: &mut String, node_properties: &ExplainPropertyMap) {
231    let mut first = true;
232    for (key, value) in node_properties.iter() {
233        if first {
234            first = false;
235        } else {
236            out.push(',');
237        }
238        let _ = write!(out, "{key}={value:?}");
239    }
240}
241
242const fn next_node_id(node_id_counter: &mut u64) -> u64 {
243    let node_id = *node_id_counter;
244    *node_id_counter = node_id_counter.saturating_add(1);
245    node_id
246}