1#![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
52fn 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 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")?; 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 }; 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 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")?; 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 )
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}