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 self.render_text_tree_into(0, &mut lines);
23 lines.join("\n")
24 }
25
26 #[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 #[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 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 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 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}