sqlx_sqlite/
logger.rs

1// Bad casts in this module SHOULD NOT result in a SQL injection
2// https://github.com/launchbadge/sqlx/issues/3440
3#![allow(
4    clippy::cast_possible_truncation,
5    clippy::cast_possible_wrap,
6    clippy::cast_sign_loss
7)]
8
9use crate::connection::intmap::IntMap;
10use std::collections::HashSet;
11use std::fmt::Debug;
12use std::hash::Hash;
13
14pub(crate) use sqlx_core::logger::*;
15
16#[derive(Debug)]
17pub(crate) enum BranchResult<R: Debug + 'static> {
18    Result(R),
19    Dedup(BranchParent),
20    Halt,
21    Error,
22    GasLimit,
23    LoopLimit,
24    Branched,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, Ord, PartialOrd)]
28pub(crate) struct BranchParent {
29    pub id: i64,
30    pub idx: i64,
31}
32
33#[derive(Debug)]
34pub(crate) struct InstructionHistory<S: Debug + DebugDiff> {
35    pub program_i: usize,
36    pub state: S,
37}
38
39pub(crate) trait DebugDiff {
40    fn diff(&self, prev: &Self) -> String;
41}
42
43pub struct QueryPlanLogger<'q, R: Debug + 'static, S: Debug + DebugDiff + 'static, P: Debug> {
44    sql: &'q str,
45    unknown_operations: HashSet<usize>,
46    branch_origins: IntMap<BranchParent>,
47    branch_results: IntMap<BranchResult<R>>,
48    branch_operations: IntMap<IntMap<InstructionHistory<S>>>,
49    program: &'q [P],
50}
51
52/// convert a string into dot format
53fn dot_escape_string(value: impl AsRef<str>) -> String {
54    value
55        .as_ref()
56        .replace('\\', r#"\\"#)
57        .replace('"', "'")
58        .replace('\n', r#"\n"#)
59        .to_string()
60}
61
62impl<R: Debug, S: Debug + DebugDiff, P: Debug> core::fmt::Display for QueryPlanLogger<'_, R, S, P> {
63    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64        //writes query plan history in dot format
65        f.write_str("digraph {\n")?;
66
67        f.write_str("subgraph operations {\n")?;
68        f.write_str("style=\"rounded\";\nnode [shape=\"point\"];\n")?;
69
70        let all_states: std::collections::HashMap<BranchParent, &InstructionHistory<S>> = self
71            .branch_operations
72            .iter_entries()
73            .flat_map(
74                |(branch_id, instructions): (i64, &IntMap<InstructionHistory<S>>)| {
75                    instructions.iter_entries().map(
76                        move |(idx, ih): (i64, &InstructionHistory<S>)| {
77                            (BranchParent { id: branch_id, idx }, ih)
78                        },
79                    )
80                },
81            )
82            .collect();
83
84        let mut instruction_uses: IntMap<Vec<BranchParent>> = Default::default();
85        for (k, state) in all_states.iter() {
86            let entry = instruction_uses.get_mut_or_default(&(state.program_i as i64));
87            entry.push(*k);
88        }
89
90        let mut branch_children: std::collections::HashMap<BranchParent, Vec<BranchParent>> =
91            Default::default();
92
93        let mut branched_with_state: std::collections::HashSet<BranchParent> = Default::default();
94
95        for (branch_id, branch_parent) in self.branch_origins.iter_entries() {
96            let entry = branch_children.entry(*branch_parent).or_default();
97            entry.push(BranchParent {
98                id: branch_id,
99                idx: 0,
100            });
101        }
102
103        for (idx, instruction) in self.program.iter().enumerate() {
104            let escaped_instruction = dot_escape_string(format!("{:?}", instruction));
105            write!(
106                f,
107                "subgraph cluster_{} {{ label=\"{}\"",
108                idx, escaped_instruction
109            )?;
110
111            if self.unknown_operations.contains(&idx) {
112                f.write_str(" style=dashed")?;
113            }
114
115            f.write_str(";\n")?;
116
117            let mut state_list: std::collections::BTreeMap<
118                String,
119                Vec<(BranchParent, Option<BranchParent>)>,
120            > = Default::default();
121
122            write!(f, "i{}[style=invis];", idx)?;
123
124            if let Some(this_instruction_uses) = instruction_uses.get(&(idx as i64)) {
125                for curr_ref in this_instruction_uses.iter() {
126                    if let Some(curr_state) = all_states.get(curr_ref) {
127                        let next_ref = BranchParent {
128                            id: curr_ref.id,
129                            idx: curr_ref.idx + 1,
130                        };
131
132                        if let Some(next_state) = all_states.get(&next_ref) {
133                            let state_diff = next_state.state.diff(&curr_state.state);
134
135                            state_list
136                                .entry(state_diff)
137                                .or_default()
138                                .push((*curr_ref, Some(next_ref)));
139                        } else {
140                            state_list
141                                .entry(Default::default())
142                                .or_default()
143                                .push((*curr_ref, None));
144                        };
145
146                        if let Some(children) = branch_children.get(curr_ref) {
147                            for next_ref in children {
148                                if let Some(next_state) = all_states.get(next_ref) {
149                                    let state_diff = next_state.state.diff(&curr_state.state);
150
151                                    if !state_diff.is_empty() {
152                                        branched_with_state.insert(*next_ref);
153                                    }
154
155                                    state_list
156                                        .entry(state_diff)
157                                        .or_default()
158                                        .push((*curr_ref, Some(*next_ref)));
159                                }
160                            }
161                        };
162                    }
163                }
164
165                for curr_ref in this_instruction_uses {
166                    if branch_children.contains_key(curr_ref) {
167                        write!(f, "\"b{}p{}\";", curr_ref.id, curr_ref.idx)?;
168                    }
169                }
170            } else {
171                write!(f, "i{}->i{}[style=invis];", idx - 1, idx)?;
172            }
173
174            for (state_num, (state_diff, ref_list)) in state_list.iter().enumerate() {
175                if !state_diff.is_empty() {
176                    let escaped_state = dot_escape_string(state_diff);
177                    write!(
178                        f,
179                        "subgraph \"cluster_i{}s{}\" {{\nlabel=\"{}\"\n",
180                        idx, state_num, escaped_state
181                    )?;
182                }
183
184                for (curr_ref, next_ref) in ref_list {
185                    if let Some(next_ref) = next_ref {
186                        let next_program_i = all_states
187                            .get(next_ref)
188                            .map(|s| s.program_i.to_string())
189                            .unwrap_or_default();
190
191                        if branched_with_state.contains(next_ref) {
192                            write!(
193                                f,
194                                "\"b{}p{}_b{}p{}\"[tooltip=\"next:{}\"];",
195                                curr_ref.id,
196                                curr_ref.idx,
197                                next_ref.id,
198                                next_ref.idx,
199                                next_program_i
200                            )?;
201                            continue;
202                        } else {
203                            write!(
204                                f,
205                                "\"b{}p{}\"[tooltip=\"next:{}\"];",
206                                curr_ref.id, curr_ref.idx, next_program_i
207                            )?;
208                        }
209                    } else {
210                        write!(f, "\"b{}p{}\";", curr_ref.id, curr_ref.idx)?;
211                    }
212                }
213
214                if !state_diff.is_empty() {
215                    f.write_str("}\n")?;
216                }
217            }
218
219            f.write_str("}\n")?;
220        }
221
222        f.write_str("};\n")?; //subgraph operations
223
224        let max_branch_id: i64 = [
225            self.branch_operations.last_index().unwrap_or(0),
226            self.branch_results.last_index().unwrap_or(0),
227            self.branch_results.last_index().unwrap_or(0),
228        ]
229        .into_iter()
230        .max()
231        .unwrap_or(0);
232
233        f.write_str("subgraph branches {\n")?;
234        for branch_id in 0..=max_branch_id {
235            write!(f, "subgraph b{}{{", branch_id)?;
236
237            let branch_num = branch_id as usize;
238            let color_names = [
239                "blue",
240                "red",
241                "cyan",
242                "yellow",
243                "green",
244                "magenta",
245                "orange",
246                "purple",
247                "orangered",
248                "sienna",
249                "olivedrab",
250                "pink",
251            ];
252            let color_name_root = color_names[branch_num % color_names.len()];
253            let color_name_suffix = match (branch_num / color_names.len()) % 4 {
254                0 => "1",
255                1 => "4",
256                2 => "3",
257                3 => "2",
258                _ => "",
259            }; //colors are easily confused after color_names.len() * 2, and outright reused after color_names.len() * 4
260            write!(
261                f,
262                "edge [colorscheme=x11 color={}{}];",
263                color_name_root, color_name_suffix
264            )?;
265
266            let mut instruction_list: Vec<(BranchParent, &InstructionHistory<S>)> = Vec::new();
267            if let Some(parent) = self.branch_origins.get(&branch_id) {
268                if let Some(parent_state) = all_states.get(parent) {
269                    instruction_list.push((*parent, parent_state));
270                }
271            }
272            if let Some(instructions) = self.branch_operations.get(&branch_id) {
273                for instruction in instructions.iter_entries() {
274                    instruction_list.push((
275                        BranchParent {
276                            id: branch_id,
277                            idx: instruction.0,
278                        },
279                        instruction.1,
280                    ))
281                }
282            }
283
284            let mut instructions_iter = instruction_list.into_iter();
285
286            if let Some((cur_ref, _)) = instructions_iter.next() {
287                let mut prev_ref = cur_ref;
288
289                for (cur_ref, _) in instructions_iter {
290                    if branched_with_state.contains(&cur_ref) {
291                        writeln!(
292                            f,
293                            "\"b{}p{}\" -> \"b{}p{}_b{}p{}\" -> \"b{}p{}\"",
294                            prev_ref.id,
295                            prev_ref.idx,
296                            prev_ref.id,
297                            prev_ref.idx,
298                            cur_ref.id,
299                            cur_ref.idx,
300                            cur_ref.id,
301                            cur_ref.idx
302                        )?;
303                    } else {
304                        write!(
305                            f,
306                            "\"b{}p{}\" -> \"b{}p{}\";",
307                            prev_ref.id, prev_ref.idx, cur_ref.id, cur_ref.idx
308                        )?;
309                    }
310                    prev_ref = cur_ref;
311                }
312
313                //draw edge to the result of this branch
314                if let Some(result) = self.branch_results.get(&branch_id) {
315                    if let BranchResult::Dedup(dedup_ref) = result {
316                        write!(
317                            f,
318                            "\"b{}p{}\"->\"b{}p{}\" [style=dotted]",
319                            prev_ref.id, prev_ref.idx, dedup_ref.id, dedup_ref.idx
320                        )?;
321                    } else {
322                        let escaped_result = dot_escape_string(format!("{:?}", result));
323                        write!(
324                            f,
325                            "\"b{}p{}\" ->\"{}\"; \"{}\" [shape=box];",
326                            prev_ref.id, prev_ref.idx, escaped_result, escaped_result
327                        )?;
328                    }
329                } else {
330                    write!(
331                        f,
332                        "\"b{}p{}\" ->\"NoResult\"; \"NoResult\" [shape=box];",
333                        prev_ref.id, prev_ref.idx
334                    )?;
335                }
336            }
337            f.write_str("};\n")?;
338        }
339        f.write_str("};\n")?; //branches
340
341        f.write_str("}\n")?;
342        Ok(())
343    }
344}
345
346impl<'q, R: Debug, S: Debug + DebugDiff, P: Debug> QueryPlanLogger<'q, R, S, P> {
347    pub fn new(sql: &'q str, program: &'q [P]) -> Self {
348        Self {
349            sql,
350            unknown_operations: HashSet::new(),
351            branch_origins: IntMap::new(),
352            branch_results: IntMap::new(),
353            branch_operations: IntMap::new(),
354            program,
355        }
356    }
357
358    pub fn log_enabled(&self) -> bool {
359        log::log_enabled!(target: "sqlx::explain", log::Level::Trace)
360            || private_tracing_dynamic_enabled!(target: "sqlx::explain", tracing::Level::TRACE)
361    }
362
363    pub fn add_branch<I: Copy>(&mut self, state: I, parent: &BranchParent)
364    where
365        BranchParent: From<I>,
366    {
367        if !self.log_enabled() {
368            return;
369        }
370        let branch: BranchParent = BranchParent::from(state);
371        self.branch_origins.insert(branch.id, *parent);
372    }
373
374    pub fn add_operation<I: Copy>(&mut self, program_i: usize, state: I)
375    where
376        BranchParent: From<I>,
377        S: From<I>,
378    {
379        if !self.log_enabled() {
380            return;
381        }
382        let branch: BranchParent = BranchParent::from(state);
383        let state: S = S::from(state);
384        self.branch_operations
385            .get_mut_or_default(&branch.id)
386            .insert(branch.idx, InstructionHistory { program_i, state });
387    }
388
389    pub fn add_result<I>(&mut self, state: I, result: BranchResult<R>)
390    where
391        BranchParent: for<'a> From<&'a I>,
392        S: From<I>,
393    {
394        if !self.log_enabled() {
395            return;
396        }
397        let branch: BranchParent = BranchParent::from(&state);
398        self.branch_results.insert(branch.id, result);
399    }
400
401    pub fn add_unknown_operation(&mut self, operation: usize) {
402        if !self.log_enabled() {
403            return;
404        }
405        self.unknown_operations.insert(operation);
406    }
407
408    pub fn finish(&self) {
409        if !self.log_enabled() {
410            return;
411        }
412
413        let mut summary = parse_query_summary(self.sql);
414
415        let sql = if summary != self.sql {
416            summary.push_str(" …");
417            format!(
418                "\n\n{}\n",
419                self.sql /*
420                         sqlformat::format(
421                             self.sql,
422                             &sqlformat::QueryParams::None,
423                             sqlformat::FormatOptions::default()
424                         )
425                         */
426            )
427        } else {
428            String::new()
429        };
430
431        sqlx_core::private_tracing_dynamic_event!(
432            target: "sqlx::explain",
433            tracing::Level::TRACE,
434            "{}; program:\n{}\n\n{:?}", summary, self, sql
435        );
436    }
437}
438
439impl<'q, R: Debug, S: Debug + DebugDiff, P: Debug> Drop for QueryPlanLogger<'q, R, S, P> {
440    fn drop(&mut self) {
441        self.finish();
442    }
443}