rustcmdpev/
lib.rs

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    "" => "" // handle case where key isn't populated in initial input
30};
31
32// https://www.forrestthewoods.com/blog/should-small-rust-structs-be-passed-by-copy-or-by-borrow/
33pub 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}