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::{
7    db::query::explain::{
8        ExplainExecutionNodeDescriptor,
9        access_projection::{access_strategy_label, write_access_json},
10        execution::{execution_mode_label, ordering_source_label},
11        writer::JsonWriter,
12    },
13    value::Value,
14};
15use std::{collections::BTreeMap, fmt::Write};
16
17impl ExplainExecutionNodeDescriptor {
18    /// Render this execution subtree as a compact text tree.
19    #[must_use]
20    pub fn render_text_tree(&self) -> String {
21        let mut lines = Vec::new();
22        self.render_text_tree_into(0, &mut lines);
23        lines.join("\n")
24    }
25
26    /// Render this execution subtree as canonical JSON.
27    #[must_use]
28    pub fn render_json_canonical(&self) -> String {
29        let mut out = String::new();
30        write_execution_node_json(self, &mut out);
31        out
32    }
33
34    /// Render this execution subtree as a verbose text tree with properties.
35    #[must_use]
36    pub fn render_text_tree_verbose(&self) -> String {
37        let mut lines = Vec::new();
38        self.render_text_tree_verbose_into(0, &mut lines);
39        lines.join("\n")
40    }
41
42    fn render_text_tree_into(&self, depth: usize, lines: &mut Vec<String>) {
43        let mut line = format!(
44            "{}{} execution_mode={}",
45            "  ".repeat(depth),
46            self.node_type.as_str(),
47            execution_mode_label(self.execution_mode)
48        );
49
50        if let Some(access_strategy) = self.access_strategy.as_ref() {
51            let _ = write!(line, " access={}", access_strategy_label(access_strategy));
52        }
53        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
54            let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
55        }
56        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
57            let _ = write!(line, " residual_predicate={residual_predicate:?}");
58        }
59        if let Some(projection) = self.projection.as_ref() {
60            let _ = write!(line, " projection={projection}");
61        }
62        if let Some(ordering_source) = self.ordering_source {
63            let _ = write!(
64                line,
65                " ordering_source={}",
66                ordering_source_label(ordering_source)
67            );
68        }
69        if let Some(limit) = self.limit {
70            let _ = write!(line, " limit={limit}");
71        }
72        if let Some(cursor) = self.cursor {
73            let _ = write!(line, " cursor={cursor}");
74        }
75        if let Some(covering_scan) = self.covering_scan {
76            let _ = write!(line, " covering_scan={covering_scan}");
77        }
78        if let Some(rows_expected) = self.rows_expected {
79            let _ = write!(line, " rows_expected={rows_expected}");
80        }
81        if !self.node_properties.is_empty() {
82            let _ = write!(
83                line,
84                " node_properties={}",
85                render_node_properties(&self.node_properties)
86            );
87        }
88
89        lines.push(line);
90
91        for child in &self.children {
92            child.render_text_tree_into(depth.saturating_add(1), lines);
93        }
94    }
95
96    fn render_text_tree_verbose_into(&self, depth: usize, lines: &mut Vec<String>) {
97        // Emit the node heading line first so child metadata stays visually scoped.
98        let node_indent = "  ".repeat(depth);
99        let field_indent = "  ".repeat(depth.saturating_add(1));
100        lines.push(format!(
101            "{}{} execution_mode={}",
102            node_indent,
103            self.node_type.as_str(),
104            execution_mode_label(self.execution_mode)
105        ));
106
107        // Emit all optional node-local fields in a deterministic order.
108        if let Some(access_strategy) = self.access_strategy.as_ref() {
109            lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
110        }
111        if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
112            lines.push(format!(
113                "{field_indent}predicate_pushdown={predicate_pushdown}"
114            ));
115        }
116        if let Some(residual_predicate) = self.residual_predicate.as_ref() {
117            lines.push(format!(
118                "{field_indent}residual_predicate={residual_predicate:?}"
119            ));
120        }
121        if let Some(projection) = self.projection.as_ref() {
122            lines.push(format!("{field_indent}projection={projection}"));
123        }
124        if let Some(ordering_source) = self.ordering_source {
125            lines.push(format!(
126                "{}ordering_source={}",
127                field_indent,
128                ordering_source_label(ordering_source)
129            ));
130        }
131        if let Some(limit) = self.limit {
132            lines.push(format!("{field_indent}limit={limit}"));
133        }
134        if let Some(cursor) = self.cursor {
135            lines.push(format!("{field_indent}cursor={cursor}"));
136        }
137        if let Some(covering_scan) = self.covering_scan {
138            lines.push(format!("{field_indent}covering_scan={covering_scan}"));
139        }
140        if let Some(rows_expected) = self.rows_expected {
141            lines.push(format!("{field_indent}rows_expected={rows_expected}"));
142        }
143        if !self.node_properties.is_empty() {
144            lines.push(format!(
145                "{}node_properties={}",
146                field_indent,
147                render_node_properties(&self.node_properties)
148            ));
149        }
150
151        // Recurse in execution order to preserve stable tree topology.
152        for child in &self.children {
153            child.render_text_tree_verbose_into(depth.saturating_add(1), lines);
154        }
155    }
156}
157
158fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
159    let mut rendered = String::new();
160    let mut first = true;
161    for (key, value) in node_properties {
162        if first {
163            first = false;
164        } else {
165            rendered.push(',');
166        }
167        let _ = write!(rendered, "{key}={value:?}");
168    }
169    rendered
170}
171
172fn write_execution_node_json(node: &ExplainExecutionNodeDescriptor, out: &mut String) {
173    let mut object = JsonWriter::begin_object(out);
174
175    object.field_str("node_type", node.node_type.as_str());
176    object.field_str("execution_mode", execution_mode_label(node.execution_mode));
177    object.field_with("access_strategy", |out| {
178        match node.access_strategy.as_ref() {
179            Some(access) => write_access_json(access, out),
180            None => out.push_str("null"),
181        }
182    });
183    match node.predicate_pushdown.as_deref() {
184        Some(predicate_pushdown) => object.field_str("predicate_pushdown", predicate_pushdown),
185        None => object.field_null("predicate_pushdown"),
186    }
187    match node.residual_predicate.as_ref() {
188        Some(residual_predicate) => {
189            object.field_value_debug("residual_predicate", residual_predicate);
190        }
191        None => object.field_null("residual_predicate"),
192    }
193    match node.projection.as_deref() {
194        Some(projection) => object.field_str("projection", projection),
195        None => object.field_null("projection"),
196    }
197    match node.ordering_source {
198        Some(ordering_source) => {
199            object.field_str("ordering_source", ordering_source_label(ordering_source));
200        }
201        None => object.field_null("ordering_source"),
202    }
203    match node.limit {
204        Some(limit) => object.field_u64("limit", u64::from(limit)),
205        None => object.field_null("limit"),
206    }
207    match node.cursor {
208        Some(cursor) => object.field_bool("cursor", cursor),
209        None => object.field_null("cursor"),
210    }
211    match node.covering_scan {
212        Some(covering_scan) => object.field_bool("covering_scan", covering_scan),
213        None => object.field_null("covering_scan"),
214    }
215    match node.rows_expected {
216        Some(rows_expected) => object.field_u64("rows_expected", rows_expected),
217        None => object.field_null("rows_expected"),
218    }
219    object.field_with("children", |out| {
220        out.push('[');
221        for (index, child) in node.children.iter().enumerate() {
222            if index > 0 {
223                out.push(',');
224            }
225            write_execution_node_json(child, out);
226        }
227        out.push(']');
228    });
229    object.field_debug_map("node_properties", &node.node_properties);
230
231    object.finish();
232}