// Bad casts in this module SHOULD NOT result in a SQL injection // https://github.com/launchbadge/sqlx/issues/3440 #![allow( clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss )] use crate::connection::intmap::IntMap; use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; pub(crate) use sqlx_core::logger::*; #[derive(Debug)] pub(crate) enum BranchResult { Result(R), Dedup(BranchParent), Halt, Error, GasLimit, LoopLimit, Branched, } #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, Ord, PartialOrd)] pub(crate) struct BranchParent { pub id: i64, pub idx: i64, } #[derive(Debug)] pub(crate) struct InstructionHistory { pub program_i: usize, pub state: S, } pub(crate) trait DebugDiff { fn diff(&self, prev: &Self) -> String; } pub struct QueryPlanLogger<'q, R: Debug + 'static, S: Debug + DebugDiff + 'static, P: Debug> { sql: &'q str, unknown_operations: HashSet, branch_origins: IntMap, branch_results: IntMap>, branch_operations: IntMap>>, program: &'q [P], } /// convert a string into dot format fn dot_escape_string(value: impl AsRef) -> String { value .as_ref() .replace('\\', r#"\\"#) .replace('"', "'") .replace('\n', r#"\n"#) .to_string() } impl core::fmt::Display for QueryPlanLogger<'_, R, S, P> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { //writes query plan history in dot format f.write_str("digraph {\n")?; f.write_str("subgraph operations {\n")?; f.write_str("style=\"rounded\";\nnode [shape=\"point\"];\n")?; let all_states: std::collections::HashMap> = self .branch_operations .iter_entries() .flat_map( |(branch_id, instructions): (i64, &IntMap>)| { instructions.iter_entries().map( move |(idx, ih): (i64, &InstructionHistory)| { (BranchParent { id: branch_id, idx }, ih) }, ) }, ) .collect(); let mut instruction_uses: IntMap> = Default::default(); for (k, state) in all_states.iter() { let entry = instruction_uses.get_mut_or_default(&(state.program_i as i64)); entry.push(*k); } let mut branch_children: std::collections::HashMap> = Default::default(); let mut branched_with_state: std::collections::HashSet = Default::default(); for (branch_id, branch_parent) in self.branch_origins.iter_entries() { let entry = branch_children.entry(*branch_parent).or_default(); entry.push(BranchParent { id: branch_id, idx: 0, }); } for (idx, instruction) in self.program.iter().enumerate() { let escaped_instruction = dot_escape_string(format!("{:?}", instruction)); write!( f, "subgraph cluster_{} {{ label=\"{}\"", idx, escaped_instruction )?; if self.unknown_operations.contains(&idx) { f.write_str(" style=dashed")?; } f.write_str(";\n")?; let mut state_list: std::collections::BTreeMap< String, Vec<(BranchParent, Option)>, > = Default::default(); write!(f, "i{}[style=invis];", idx)?; if let Some(this_instruction_uses) = instruction_uses.get(&(idx as i64)) { for curr_ref in this_instruction_uses.iter() { if let Some(curr_state) = all_states.get(curr_ref) { let next_ref = BranchParent { id: curr_ref.id, idx: curr_ref.idx + 1, }; if let Some(next_state) = all_states.get(&next_ref) { let state_diff = next_state.state.diff(&curr_state.state); state_list .entry(state_diff) .or_default() .push((*curr_ref, Some(next_ref))); } else { state_list .entry(Default::default()) .or_default() .push((*curr_ref, None)); }; if let Some(children) = branch_children.get(curr_ref) { for next_ref in children { if let Some(next_state) = all_states.get(next_ref) { let state_diff = next_state.state.diff(&curr_state.state); if !state_diff.is_empty() { branched_with_state.insert(*next_ref); } state_list .entry(state_diff) .or_default() .push((*curr_ref, Some(*next_ref))); } } }; } } for curr_ref in this_instruction_uses { if branch_children.contains_key(curr_ref) { write!(f, "\"b{}p{}\";", curr_ref.id, curr_ref.idx)?; } } } else { write!(f, "i{}->i{}[style=invis];", idx - 1, idx)?; } for (state_num, (state_diff, ref_list)) in state_list.iter().enumerate() { if !state_diff.is_empty() { let escaped_state = dot_escape_string(state_diff); write!( f, "subgraph \"cluster_i{}s{}\" {{\nlabel=\"{}\"\n", idx, state_num, escaped_state )?; } for (curr_ref, next_ref) in ref_list { if let Some(next_ref) = next_ref { let next_program_i = all_states .get(next_ref) .map(|s| s.program_i.to_string()) .unwrap_or_default(); if branched_with_state.contains(next_ref) { write!( f, "\"b{}p{}_b{}p{}\"[tooltip=\"next:{}\"];", curr_ref.id, curr_ref.idx, next_ref.id, next_ref.idx, next_program_i )?; continue; } else { write!( f, "\"b{}p{}\"[tooltip=\"next:{}\"];", curr_ref.id, curr_ref.idx, next_program_i )?; } } else { write!(f, "\"b{}p{}\";", curr_ref.id, curr_ref.idx)?; } } if !state_diff.is_empty() { f.write_str("}\n")?; } } f.write_str("}\n")?; } f.write_str("};\n")?; //subgraph operations let max_branch_id: i64 = [ self.branch_operations.last_index().unwrap_or(0), self.branch_results.last_index().unwrap_or(0), self.branch_results.last_index().unwrap_or(0), ] .into_iter() .max() .unwrap_or(0); f.write_str("subgraph branches {\n")?; for branch_id in 0..=max_branch_id { write!(f, "subgraph b{}{{", branch_id)?; let branch_num = branch_id as usize; let color_names = [ "blue", "red", "cyan", "yellow", "green", "magenta", "orange", "purple", "orangered", "sienna", "olivedrab", "pink", ]; let color_name_root = color_names[branch_num % color_names.len()]; let color_name_suffix = match (branch_num / color_names.len()) % 4 { 0 => "1", 1 => "4", 2 => "3", 3 => "2", _ => "", }; //colors are easily confused after color_names.len() * 2, and outright reused after color_names.len() * 4 write!( f, "edge [colorscheme=x11 color={}{}];", color_name_root, color_name_suffix )?; let mut instruction_list: Vec<(BranchParent, &InstructionHistory)> = Vec::new(); if let Some(parent) = self.branch_origins.get(&branch_id) { if let Some(parent_state) = all_states.get(parent) { instruction_list.push((*parent, parent_state)); } } if let Some(instructions) = self.branch_operations.get(&branch_id) { for instruction in instructions.iter_entries() { instruction_list.push(( BranchParent { id: branch_id, idx: instruction.0, }, instruction.1, )) } } let mut instructions_iter = instruction_list.into_iter(); if let Some((cur_ref, _)) = instructions_iter.next() { let mut prev_ref = cur_ref; for (cur_ref, _) in instructions_iter { if branched_with_state.contains(&cur_ref) { writeln!( f, "\"b{}p{}\" -> \"b{}p{}_b{}p{}\" -> \"b{}p{}\"", prev_ref.id, prev_ref.idx, prev_ref.id, prev_ref.idx, cur_ref.id, cur_ref.idx, cur_ref.id, cur_ref.idx )?; } else { write!( f, "\"b{}p{}\" -> \"b{}p{}\";", prev_ref.id, prev_ref.idx, cur_ref.id, cur_ref.idx )?; } prev_ref = cur_ref; } //draw edge to the result of this branch if let Some(result) = self.branch_results.get(&branch_id) { if let BranchResult::Dedup(dedup_ref) = result { write!( f, "\"b{}p{}\"->\"b{}p{}\" [style=dotted]", prev_ref.id, prev_ref.idx, dedup_ref.id, dedup_ref.idx )?; } else { let escaped_result = dot_escape_string(format!("{:?}", result)); write!( f, "\"b{}p{}\" ->\"{}\"; \"{}\" [shape=box];", prev_ref.id, prev_ref.idx, escaped_result, escaped_result )?; } } else { write!( f, "\"b{}p{}\" ->\"NoResult\"; \"NoResult\" [shape=box];", prev_ref.id, prev_ref.idx )?; } } f.write_str("};\n")?; } f.write_str("};\n")?; //branches f.write_str("}\n")?; Ok(()) } } impl<'q, R: Debug, S: Debug + DebugDiff, P: Debug> QueryPlanLogger<'q, R, S, P> { pub fn new(sql: &'q str, program: &'q [P]) -> Self { Self { sql, unknown_operations: HashSet::new(), branch_origins: IntMap::new(), branch_results: IntMap::new(), branch_operations: IntMap::new(), program, } } pub fn log_enabled(&self) -> bool { log::log_enabled!(target: "sqlx::explain", log::Level::Trace) || private_tracing_dynamic_enabled!(target: "sqlx::explain", tracing::Level::TRACE) } pub fn add_branch(&mut self, state: I, parent: &BranchParent) where BranchParent: From, { if !self.log_enabled() { return; } let branch: BranchParent = BranchParent::from(state); self.branch_origins.insert(branch.id, *parent); } pub fn add_operation(&mut self, program_i: usize, state: I) where BranchParent: From, S: From, { if !self.log_enabled() { return; } let branch: BranchParent = BranchParent::from(state); let state: S = S::from(state); self.branch_operations .get_mut_or_default(&branch.id) .insert(branch.idx, InstructionHistory { program_i, state }); } pub fn add_result(&mut self, state: I, result: BranchResult) where BranchParent: for<'a> From<&'a I>, S: From, { if !self.log_enabled() { return; } let branch: BranchParent = BranchParent::from(&state); self.branch_results.insert(branch.id, result); } pub fn add_unknown_operation(&mut self, operation: usize) { if !self.log_enabled() { return; } self.unknown_operations.insert(operation); } pub fn finish(&self) { if !self.log_enabled() { return; } let mut summary = parse_query_summary(self.sql); let sql = if summary != self.sql { summary.push_str(" …"); format!( "\n\n{}\n", sqlformat::format( self.sql, &sqlformat::QueryParams::None, sqlformat::FormatOptions::default() ) ) } else { String::new() }; sqlx_core::private_tracing_dynamic_event!( target: "sqlx::explain", tracing::Level::TRACE, "{}; program:\n{}\n\n{:?}", summary, self, sql ); } } impl<'q, R: Debug, S: Debug + DebugDiff, P: Debug> Drop for QueryPlanLogger<'q, R, S, P> { fn drop(&mut self) { self.finish(); } }