icydb_core/db/query/explain/
render.rs1use 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 #[must_use]
20 pub fn render_text_tree(&self) -> String {
21 let mut lines = Vec::new();
22 let mut node_id_counter = 0_u64;
23 self.render_text_tree_into(0, &mut node_id_counter, &mut lines);
24 lines.join("\n")
25 }
26
27 #[must_use]
29 pub fn render_json_canonical(&self) -> String {
30 let mut out = String::new();
31 let mut node_id_counter = 0_u64;
32 write_execution_node_json(self, &mut node_id_counter, &mut out);
33 out
34 }
35
36 #[must_use]
38 pub fn render_text_tree_verbose(&self) -> String {
39 let mut lines = Vec::new();
40 let mut node_id_counter = 0_u64;
41 self.render_text_tree_verbose_into(0, &mut node_id_counter, &mut lines);
42 lines.join("\n")
43 }
44
45 fn render_text_tree_into(
46 &self,
47 depth: usize,
48 node_id_counter: &mut u64,
49 lines: &mut Vec<String>,
50 ) {
51 let node_id = next_node_id(node_id_counter);
52 let mut line = format!(
53 "{}{} execution_mode={}",
54 " ".repeat(depth),
55 self.node_type.as_str(),
56 execution_mode_label(self.execution_mode)
57 );
58 let _ = write!(line, " node_id={node_id}");
59 let _ = write!(
60 line,
61 " execution_mode_detail={}",
62 execution_mode_detail_label(self.execution_mode)
63 );
64 let _ = write!(
65 line,
66 " predicate_pushdown_mode={}",
67 predicate_pushdown_mode(self)
68 );
69 if let Some(fast_path_selected) = fast_path_selected(self) {
70 let _ = write!(line, " fast_path_selected={fast_path_selected}");
71 }
72 if let Some(fast_path_reason) = fast_path_reason(self) {
73 let _ = write!(line, " fast_path_reason={fast_path_reason}");
74 }
75
76 if let Some(access_strategy) = self.access_strategy.as_ref() {
77 let _ = write!(line, " access={}", access_strategy_label(access_strategy));
78 }
79 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
80 let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
81 }
82 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
83 let _ = write!(line, " residual_predicate={residual_predicate:?}");
84 }
85 if let Some(projection) = self.projection.as_ref() {
86 let _ = write!(line, " projection={projection}");
87 }
88 if let Some(ordering_source) = self.ordering_source {
89 let _ = write!(
90 line,
91 " ordering_source={}",
92 ordering_source_label(ordering_source)
93 );
94 }
95 if let Some(limit) = self.limit {
96 let _ = write!(line, " limit={limit}");
97 }
98 if let Some(cursor) = self.cursor {
99 let _ = write!(line, " cursor={cursor}");
100 }
101 if let Some(covering_scan) = self.covering_scan {
102 let _ = write!(line, " covering_scan={covering_scan}");
103 }
104 if let Some(rows_expected) = self.rows_expected {
105 let _ = write!(line, " rows_expected={rows_expected}");
106 }
107 if !self.node_properties.is_empty() {
108 let _ = write!(
109 line,
110 " node_properties={}",
111 render_node_properties(&self.node_properties)
112 );
113 }
114
115 lines.push(line);
116
117 for child in &self.children {
118 child.render_text_tree_into(depth.saturating_add(1), node_id_counter, lines);
119 }
120 }
121
122 fn render_text_tree_verbose_into(
123 &self,
124 depth: usize,
125 node_id_counter: &mut u64,
126 lines: &mut Vec<String>,
127 ) {
128 let node_id = next_node_id(node_id_counter);
129 let node_indent = " ".repeat(depth);
131 let field_indent = " ".repeat(depth.saturating_add(1));
132 lines.push(format!(
133 "{}{} execution_mode={}",
134 node_indent,
135 self.node_type.as_str(),
136 execution_mode_label(self.execution_mode)
137 ));
138 lines.push(format!("{field_indent}node_id={node_id}"));
139 lines.push(format!(
140 "{}execution_mode_detail={}",
141 field_indent,
142 execution_mode_detail_label(self.execution_mode)
143 ));
144 lines.push(format!(
145 "{}predicate_pushdown_mode={}",
146 field_indent,
147 predicate_pushdown_mode(self)
148 ));
149 if let Some(fast_path_selected) = fast_path_selected(self) {
150 lines.push(format!(
151 "{field_indent}fast_path_selected={fast_path_selected}"
152 ));
153 }
154 if let Some(fast_path_reason) = fast_path_reason(self) {
155 lines.push(format!("{field_indent}fast_path_reason={fast_path_reason}"));
156 }
157
158 if let Some(access_strategy) = self.access_strategy.as_ref() {
160 lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
161 }
162 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
163 lines.push(format!(
164 "{field_indent}predicate_pushdown={predicate_pushdown}"
165 ));
166 }
167 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
168 lines.push(format!(
169 "{field_indent}residual_predicate={residual_predicate:?}"
170 ));
171 }
172 if let Some(projection) = self.projection.as_ref() {
173 lines.push(format!("{field_indent}projection={projection}"));
174 }
175 if let Some(ordering_source) = self.ordering_source {
176 lines.push(format!(
177 "{}ordering_source={}",
178 field_indent,
179 ordering_source_label(ordering_source)
180 ));
181 }
182 if let Some(limit) = self.limit {
183 lines.push(format!("{field_indent}limit={limit}"));
184 }
185 if let Some(cursor) = self.cursor {
186 lines.push(format!("{field_indent}cursor={cursor}"));
187 }
188 if let Some(covering_scan) = self.covering_scan {
189 lines.push(format!("{field_indent}covering_scan={covering_scan}"));
190 }
191 if let Some(rows_expected) = self.rows_expected {
192 lines.push(format!("{field_indent}rows_expected={rows_expected}"));
193 }
194 if !self.node_properties.is_empty() {
195 lines.push(format!(
196 "{}node_properties={}",
197 field_indent,
198 render_node_properties(&self.node_properties)
199 ));
200 }
201
202 for child in &self.children {
204 child.render_text_tree_verbose_into(depth.saturating_add(1), node_id_counter, lines);
205 }
206 }
207}
208
209fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
210 let mut rendered = String::new();
211 let mut first = true;
212 for (key, value) in node_properties {
213 if first {
214 first = false;
215 } else {
216 rendered.push(',');
217 }
218 let _ = write!(rendered, "{key}={value:?}");
219 }
220 rendered
221}
222
223fn write_execution_node_json(
224 node: &ExplainExecutionNodeDescriptor,
225 node_id_counter: &mut u64,
226 out: &mut String,
227) {
228 let node_id = next_node_id(node_id_counter);
229 let mut object = JsonWriter::begin_object(out);
230
231 object.field_u64("node_id", node_id);
232 object.field_str("node_type", node.node_type.as_str());
233 object.field_str("execution_mode", execution_mode_label(node.execution_mode));
234 object.field_str(
235 "execution_mode_detail",
236 execution_mode_detail_label(node.execution_mode),
237 );
238 object.field_with("access_strategy", |out| {
239 match node.access_strategy.as_ref() {
240 Some(access) => write_access_json(access, out),
241 None => out.push_str("null"),
242 }
243 });
244 object.field_str("predicate_pushdown_mode", predicate_pushdown_mode(node));
245 match node.predicate_pushdown.as_deref() {
246 Some(predicate_pushdown) => object.field_str("predicate_pushdown", predicate_pushdown),
247 None => object.field_null("predicate_pushdown"),
248 }
249 match fast_path_selected(node) {
250 Some(selected) => object.field_bool("fast_path_selected", selected),
251 None => object.field_null("fast_path_selected"),
252 }
253 match fast_path_reason(node) {
254 Some(reason) => object.field_str("fast_path_reason", reason),
255 None => object.field_null("fast_path_reason"),
256 }
257 match node.residual_predicate.as_ref() {
258 Some(residual_predicate) => {
259 object.field_value_debug("residual_predicate", residual_predicate);
260 }
261 None => object.field_null("residual_predicate"),
262 }
263 match node.projection.as_deref() {
264 Some(projection) => object.field_str("projection", projection),
265 None => object.field_null("projection"),
266 }
267 match node.ordering_source {
268 Some(ordering_source) => {
269 object.field_str("ordering_source", ordering_source_label(ordering_source));
270 }
271 None => object.field_null("ordering_source"),
272 }
273 match node.limit {
274 Some(limit) => object.field_u64("limit", u64::from(limit)),
275 None => object.field_null("limit"),
276 }
277 match node.cursor {
278 Some(cursor) => object.field_bool("cursor", cursor),
279 None => object.field_null("cursor"),
280 }
281 match node.covering_scan {
282 Some(covering_scan) => object.field_bool("covering_scan", covering_scan),
283 None => object.field_null("covering_scan"),
284 }
285 match node.rows_expected {
286 Some(rows_expected) => object.field_u64("rows_expected", rows_expected),
287 None => object.field_null("rows_expected"),
288 }
289 object.field_with("children", |out| {
290 out.push('[');
291 for (index, child) in node.children.iter().enumerate() {
292 if index > 0 {
293 out.push(',');
294 }
295 write_execution_node_json(child, node_id_counter, out);
296 }
297 out.push(']');
298 });
299 object.field_debug_map("node_properties", &node.node_properties);
300
301 object.finish();
302}
303
304const fn next_node_id(node_id_counter: &mut u64) -> u64 {
305 let node_id = *node_id_counter;
306 *node_id_counter = node_id_counter.saturating_add(1);
307 node_id
308}
309
310const fn execution_mode_detail_label(
311 mode: crate::db::query::explain::ExplainExecutionMode,
312) -> &'static str {
313 match mode {
314 crate::db::query::explain::ExplainExecutionMode::Streaming => "streaming",
315 crate::db::query::explain::ExplainExecutionMode::Materialized => "materialized",
316 }
317}
318
319fn predicate_pushdown_mode(node: &ExplainExecutionNodeDescriptor) -> &'static str {
320 match node.predicate_pushdown.as_deref() {
321 None => "none",
322 Some("strict_all_or_none") => "full",
323 Some(_) => {
324 if node.residual_predicate.is_some() {
325 "partial"
326 } else {
327 "full"
328 }
329 }
330 }
331}
332
333fn fast_path_selected(node: &ExplainExecutionNodeDescriptor) -> Option<bool> {
334 let selected = node.node_properties.get("fast_path_selected")?;
335 match selected {
336 Value::Text(path) => Some(path.as_str() != "none"),
337 _ => None,
338 }
339}
340
341fn fast_path_reason(node: &ExplainExecutionNodeDescriptor) -> Option<&str> {
342 let reason = node.node_properties.get("fast_path_selected_reason")?;
343 match reason {
344 Value::Text(reason) => Some(reason.as_str()),
345 _ => None,
346 }
347}