1use colored::*;
2use phf::phf_map;
3pub mod structure;
4use structure::explain::explain;
5use structure::plan::plan;
6
7const UNDER: &str = "Under";
8const OVER: &str = "Over";
9const CTE_SCAN: &str = "CTE Scan";
10
11const DELTA_ERROR: f64 = 0.001;
12
13static DESCRIPTIONS: phf::Map<&'static str, &'static str> = phf_map! {
14 "Append" => "Used in a UNION to merge multiple record sets by appending them together.",
15 "Limit" => "Returns a specified number of rows from a record set.",
16 "Sort" => "Sorts a record set based on the specified sort key.",
17 "Nested Loop" => "Merges two record sets by looping through every record in the first set and trying to find a match in the second set. All matching records are returned.",
18 "Merge Join" => "Merges two record sets by first sorting them on a join key.",
19 "Hash" => "Generates a hash table from the records in the input recordset. Hash is used by Hash Join.",
20 "Hash Join" => "Joins to record sets by hashing one of them (using a Hash Scan).",
21 "Aggregate" => "Groups records together based on a GROUP BY or aggregate function (e.g. sum()).",
22 "Hashaggregate" => "Groups records together based on a GROUP BY or aggregate function (e.g. sum()). Hash Aggregate uses a hash to first organize the records by a key.",
23 "Seq Scan" => "Finds relevant records by sequentially scanning the input record set. When reading from a table, Seq Scans (unlike Index Scans) perform a single read operation (only the table is read).",
24 "Index Scan" => "Finds relevant records based on an Index. Index Scans perform 2 read operations: one to read the index and another to read the actual value from the table.",
25 "Index Only Scan" => "Finds relevant records based on an Index. Index Only Scans perform a single read operation from the index and do not read from the corresponding table.",
26 "Bitmap Heap Scan" => "Searches through the pages returned by the Bitmap Index Scan for relevant rows.",
27 "Bitmap Index Scan" => "Uses a Bitmap Index (index which uses 1 bit per page) to find all relevant pages. Results of this node are fed to the Bitmap Heap Scan.",
28 "CTE Scan" => "Performs a sequential scan of Common Table Expression (CTE) query results. Note that results of a CTE are materialized (calculated and temporarily stored).",
29 "" => "" };
31
32pub fn calculate_planner_estimate(plan: plan::Plan) -> plan::Plan {
34 let mut new_plan: plan::Plan = plan;
35 new_plan.planner_row_estimate_factor = 0.0;
36
37 if new_plan.plan_rows == new_plan.actual_rows {
38 return new_plan;
39 }
40
41 new_plan.planner_row_estimate_direction = UNDER.to_string();
42 if new_plan.plan_rows != 0 {
43 new_plan.planner_row_estimate_factor =
44 new_plan.actual_rows as f64 / new_plan.plan_rows as f64;
45 }
46
47 if new_plan.planner_row_estimate_factor < 1.0 {
48 new_plan.planner_row_estimate_factor = 0.0;
49 new_plan.planner_row_estimate_direction = OVER.to_string();
50 if new_plan.actual_rows != 0 {
51 new_plan.planner_row_estimate_factor =
52 new_plan.plan_rows as f64 / new_plan.actual_rows as f64;
53 }
54 }
55 new_plan
56}
57
58pub fn calculate_actuals(
59 explain: explain::Explain,
60 plan: plan::Plan,
61) -> (explain::Explain, plan::Plan) {
62 let mut new_plan: plan::Plan = plan;
63 let mut new_explain: explain::Explain = explain;
64 new_plan.actual_duration = new_plan.actual_total_time;
65 new_plan.actual_cost = new_plan.total_cost;
66 for child_plan in new_plan.plans.iter() {
67 if child_plan.node_type != CTE_SCAN {
68 new_plan.actual_duration -= child_plan.actual_total_time;
69 new_plan.actual_cost -= child_plan.total_cost;
70 }
71 }
72 if new_plan.actual_cost < 0.0 {
73 new_plan.actual_cost = 0.0;
74 }
75
76 new_explain.total_cost += new_plan.actual_cost;
77 new_plan.actual_duration *= new_plan.actual_loops as f64;
78 (new_explain, new_plan)
79}
80
81pub fn calculate_maximums(explain: explain::Explain, plan: plan::Plan) -> explain::Explain {
82 let mut new_explain: explain::Explain = explain;
83 if new_explain.max_rows < plan.actual_rows {
84 new_explain.max_rows = plan.actual_rows
85 }
86 if new_explain.max_cost < plan.actual_cost {
87 new_explain.max_cost = plan.actual_cost
88 }
89 if new_explain.max_duration < plan.actual_duration {
90 new_explain.max_duration = plan.actual_duration
91 }
92 new_explain
93}
94
95pub fn calculate_outlier_nodes(explain: explain::Explain, plan: plan::Plan) -> plan::Plan {
96 let mut new_plan: plan::Plan = plan;
97 new_plan.costliest = (new_plan.actual_cost - explain.max_cost).abs() < DELTA_ERROR;
98 new_plan.largest = new_plan.actual_rows == explain.max_rows;
99 new_plan.slowest = (new_plan.actual_duration - explain.max_duration).abs() < DELTA_ERROR;
100 for child_plan in new_plan.plans.iter_mut() {
101 *child_plan = calculate_outlier_nodes(explain.clone(), child_plan.clone());
102 }
103 new_plan
104}
105
106fn process_explain(explain: explain::Explain) -> explain::Explain {
107 let mut new_explain: explain::Explain = explain;
108 new_explain.plan = calculate_planner_estimate(new_explain.plan);
109 let (e, p) = calculate_actuals(new_explain.clone(), new_explain.clone().plan);
110 new_explain = e;
111 new_explain.plan = p;
112 new_explain = calculate_maximums(new_explain.clone(), new_explain.plan);
113 new_explain
114}
115
116fn process_child_plans(
117 explain: explain::Explain,
118 plans: Vec<plan::Plan>,
119) -> (explain::Explain, Vec<plan::Plan>) {
120 let mut new_explain: explain::Explain = explain;
121 let mut new_plans: Vec<plan::Plan> = plans;
122 for mut child_plan in new_plans.iter_mut() {
123 *child_plan = calculate_planner_estimate(child_plan.clone());
124 let (e, p) = calculate_actuals(new_explain.clone(), child_plan.clone());
125 new_explain = e;
126 *child_plan = p;
127 new_explain = calculate_maximums(new_explain, child_plan.clone());
128 if !(child_plan.plans).is_empty() {
129 let (e, ps) = process_child_plans(new_explain.clone(), child_plan.plans.clone());
130 child_plan.plans = ps;
131 new_explain = e;
132 }
133 }
134 (new_explain, new_plans)
135}
136
137pub fn process_all(explain: explain::Explain) -> explain::Explain {
138 let mut new_explain: explain::Explain = explain;
139 new_explain = process_explain(new_explain.clone());
140 if !new_explain.plan.plans.is_empty() {
141 let (e, ps) = process_child_plans(new_explain.clone(), new_explain.plan.plans.clone());
142 new_explain = e;
143 new_explain.plan.plans = ps;
144 }
145 let outlier_plan: plan::Plan =
146 calculate_outlier_nodes(new_explain.clone(), new_explain.clone().plan);
147 new_explain.plan = outlier_plan;
148 new_explain
149}
150
151fn duration_to_string(value: f64) -> colored::ColoredString {
152 if value < 100.0 {
153 format!("{0:.2} ms", value).green()
154 } else if value < 1000.0 {
155 format!("{0:.2} ms", value).yellow()
156 } else if value < 60000.0 {
157 format!("{0:.2} s", value / 2000.0).red()
158 } else {
159 format!("{0:.2} m", value / 60000.0).red()
160 }
161}
162
163pub fn write_explain(explain: explain::Explain, width: usize) {
164 println!("○ Total Cost {}", explain.total_cost);
165 println!(
166 "○ Planning Time: {}",
167 duration_to_string(explain.planning_time)
168 );
169 println!(
170 "○ Execution Time: {}",
171 duration_to_string(explain.execution_time)
172 );
173 println!("{}", color_format("┬".to_string(), "output"));
174 let p: plan::Plan = explain.clone().plan;
175 write_plan(
176 explain.clone(),
177 &p,
178 "".to_string(),
179 0,
180 width,
181 explain.plan.plans.len() == 1,
182 )
183}
184
185pub fn color_format(s: String, format: &str) -> colored::ColoredString {
186 match format {
187 "prefix" => s.bright_black(),
188 "muted" => s.bright_black(),
189 "bold" => s.bright_white(),
190 "good" => s.green(),
191 "warning" => s.yellow(),
192 "critical" => s.red(),
193 "output" => s.cyan(),
194 "tag" => s.on_bright_red().bright_white(),
195 _ => s.green(),
196 }
197}
198
199fn format_details(plan: plan::Plan) -> String {
200 let mut details = vec![];
201
202 if plan.scan_direction != "" {
203 details.push(plan.scan_direction);
204 }
205
206 if plan.strategy != "" {
207 details.push(plan.strategy);
208 }
209
210 if !details.is_empty() {
211 details.join(", ");
212 }
213
214 "".to_string()
215}
216
217fn format_tags(plan: plan::Plan) -> String {
218 let mut tags = vec![];
219
220 if plan.slowest {
221 tags.push(" slowest ");
222 }
223 if plan.costliest {
224 tags.push(" costliest ");
225 }
226 if plan.largest {
227 tags.push(" largest ");
228 }
229 if plan.planner_row_estimate_factor >= 100.0 {
230 tags.push(" bad estimate ");
231 }
232 tags.join(" ")
233}
234
235fn get_terminator(index: usize, plan: plan::Plan) -> String {
236 if index == 0 {
237 if plan.plans.is_empty() {
238 "⌡► ".to_string()
239 } else {
240 "├► ".to_string()
241 }
242 } else if plan.plans.is_empty() {
243 " ".to_string()
244 } else {
245 "│ ".to_string()
246 }
247}
248
249pub fn format_percent(number: f64, precision: usize) -> String {
250 return format!("{:.1$}%", number, precision);
251}
252
253pub fn write_plan(
254 explain: explain::Explain,
255 plan: &plan::Plan,
256 prefix: String,
257 depth: i32,
258 width: usize,
259 last_child: bool,
260) {
261 let mut source_prefix: String = prefix.clone();
262 let mut current_prefix: String = prefix;
263 println!(
264 "{}{}",
265 color_format(source_prefix.clone(), "prefix"),
266 color_format("│".to_string(), "prefix")
267 );
268 let joint = if plan.plans.len() > 1 || last_child {
269 "└".to_string()
270 } else {
271 String::from("├")
272 };
273 println!(
274 "{}{} {}{} {}",
275 color_format(current_prefix.clone(), "prefix"),
276 color_format(joint + "─⌠", "prefix"),
277 color_format(plan.clone().node_type, "bold"),
278 color_format(format_details(plan.clone()), "muted"),
279 color_format(format_tags(plan.clone()), "tag")
280 );
281
282 if plan.plans.len() > 1 || last_child {
283 source_prefix += " "
284 } else {
285 source_prefix += "│ "
286 }
287 current_prefix = source_prefix.clone() + "│ ";
288 let cols: usize = width - current_prefix.len();
289 for line in textwrap::fill(DESCRIPTIONS[plan.node_type.as_str()], cols)
290 .split('\n')
291 .collect::<Vec<&str>>()
292 {
293 println!(
294 "{}{}",
295 color_format(current_prefix.clone(), "prefix"),
296 color_format(line.to_string(), "muted")
297 )
298 }
299 println!(
300 "{}○ Duration: {} {}",
301 color_format(current_prefix.clone(), "prefix"),
302 duration_to_string(plan.actual_duration),
303 format_percent((plan.actual_duration / explain.execution_time) * 100.0, 1)
304 );
305 println!(
306 "{}○ Cost: {} {}",
307 color_format(current_prefix.clone(), "prefix"),
308 plan.actual_cost,
309 format_percent((plan.actual_cost / explain.total_cost) * 100.0, 1)
310 );
311 println!(
312 "{}○ Rows: {}",
313 color_format(current_prefix.clone(), "prefix"),
314 plan.actual_rows
315 );
316 current_prefix += " ";
317
318 if plan.join_type != "" {
319 println!(
320 "{}{} {}",
321 color_format(current_prefix.clone(), "prefix"),
322 color_format("join".to_string(), "muted"),
323 color_format(plan.clone().join_type, "muted")
324 )
325 }
326 if plan.relation_name != "" {
327 println!(
328 "{}{} {} {}",
329 color_format(current_prefix.clone(), "prefix"),
330 color_format("on".to_string(), "muted"),
331 color_format(plan.clone().schema, "muted"),
332 color_format(plan.clone().relation_name, "muted")
333 )
334 }
335
336 if plan.index_name != "" {
337 println!(
338 "{}{} {}",
339 color_format(current_prefix.clone(), "prefix"),
340 color_format("using".to_string(), "muted"),
341 plan.index_name
342 )
343 }
344
345 if plan.index_condition != "" {
346 println!(
347 "{}{} {}",
348 color_format(current_prefix.clone(), "prefix"),
349 color_format("condition".to_string(), "muted"),
350 plan.index_condition
351 )
352 }
353
354 if plan.filter != "" {
355 println!(
356 "{}{} {} [-{} rows]",
357 color_format(current_prefix.clone(), "prefix"),
358 color_format("filter".to_string(), "muted"),
359 plan.filter,
360 color_format(plan.rows_removed_by_filter.to_string(), "muted")
361 );
362 }
363 if plan.hash_condition != "" {
364 println!(
365 "{}{} {}",
366 color_format(current_prefix.clone(), "prefix"),
367 color_format("on".to_string(), "muted"),
368 plan.hash_condition
369 )
370 }
371 if plan.cte_name != "" {
372 println!(
373 "{}CTE {}",
374 color_format(current_prefix.clone(), "prefix"),
375 plan.cte_name
376 )
377 }
378
379 if plan.planner_row_estimate_factor != 0.0 {
380 println!(
381 "{}{} {}estimated {} {:.2}x",
382 color_format(current_prefix.clone(), "prefix"),
383 color_format("rows".to_string(), "muted"),
384 plan.planner_row_estimate_direction,
385 color_format("by".to_string(), "muted"),
386 plan.planner_row_estimate_factor
387 )
388 }
389
390 current_prefix = source_prefix.clone();
391
392 if !plan.output.is_empty() {
393 let joined_output: String = plan.output.join(" + ");
394 let wrapped_output: String = textwrap::fill(joined_output.as_str(), cols);
395 let split_output: Vec<&str> = wrapped_output.split('\n').collect();
396 for (index, line) in split_output.iter().enumerate() {
397 println!(
398 "{}{}",
399 color_format(current_prefix.clone(), "prefix"),
400 color_format(get_terminator(index, plan.clone()) + line, "output")
401 )
402 }
403 }
404 for (index, child_plan) in plan.plans.iter().enumerate() {
405 write_plan(
406 explain.clone(),
407 child_plan,
408 source_prefix.clone(),
409 depth + 1,
410 width,
411 index == plan.plans.len() - 1,
412 )
413 }
414}
415
416pub fn visualize(input: String, width: usize) -> explain::Explain {
417 let explains: Vec<explain::Explain> = serde_json::from_str(input.as_str()).unwrap();
418 let mut explain: explain::Explain = explains.into_iter().next().unwrap();
419 explain = process_all(explain);
420 write_explain(explain.clone(), width);
421 explain
422}