use crate::algebra::Algebra;
use anyhow::Result;
use scirs2_core::metrics::MetricsRegistry;
use scirs2_core::profiling::Profiler;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use tracing::info;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DebugConfig {
pub enable_tracing: bool,
pub enable_breakpoints: bool,
pub enable_variable_tracking: bool,
pub enable_profiling: bool,
pub enable_plan_visualization: bool,
pub max_trace_entries: usize,
pub trace_detail_level: usize,
pub track_rewrites: bool,
pub track_memory: bool,
pub visualization_format: VisualizationFormat,
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
enable_tracing: true,
enable_breakpoints: true,
enable_variable_tracking: true,
enable_profiling: true,
enable_plan_visualization: true,
max_trace_entries: 10000,
trace_detail_level: 2,
track_rewrites: true,
track_memory: true,
visualization_format: VisualizationFormat::Text,
}
}
}
impl DebugConfig {
pub fn with_breakpoints(mut self, enabled: bool) -> Self {
self.enable_breakpoints = enabled;
self
}
pub fn with_variable_tracking(mut self, enabled: bool) -> Self {
self.enable_variable_tracking = enabled;
self
}
pub fn with_trace_level(mut self, level: usize) -> Self {
self.trace_detail_level = level.min(3);
self
}
pub fn with_visualization_format(mut self, format: VisualizationFormat) -> Self {
self.visualization_format = format;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VisualizationFormat {
Text,
Dot,
Json,
Mermaid,
}
pub struct QueryDebugger {
config: DebugConfig,
breakpoints: Vec<DebugBreakpoint>,
trace: VecDeque<TraceEntry>,
variable_history: HashMap<String, Vec<VariableBinding>>,
#[allow(dead_code)]
profiler: Arc<Profiler>,
#[allow(dead_code)]
metrics: Arc<MetricsRegistry>,
rewrite_history: Vec<RewriteStep>,
state: ExecutionState,
breakpoint_hits: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DebugBreakpoint {
BeforeTriplePattern { pattern_id: usize },
AfterTriplePattern { pattern_id: usize },
BeforeJoin { join_id: usize },
AfterJoin { join_id: usize },
OnFilter { filter_id: usize },
OnVariableBound { variable: String },
OnResultCountExceeds { threshold: usize },
OnTimeExceeds { threshold: Duration },
OnMemoryExceeds { threshold_bytes: usize },
Conditional { condition: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceEntry {
pub seq: usize,
pub timestamp: Duration,
pub operation: Operation,
pub duration: Duration,
pub result_count: usize,
pub memory_usage: usize,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Operation {
Parse,
GenerateAlgebra,
Optimize,
GeneratePlan,
ScanPattern { pattern_id: usize },
Join { join_id: usize, join_type: JoinType },
EvaluateFilter { filter_id: usize },
Project { variables: Vec<String> },
Aggregate { functions: Vec<String> },
Sort { variables: Vec<String> },
Distinct,
Slice { offset: usize, limit: usize },
Custom { name: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JoinType {
Inner,
LeftOuter,
Minus,
Union,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableBinding {
pub variable: String,
pub value: String,
pub timestamp: Duration,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewriteStep {
pub step: usize,
pub rule: String,
pub description: String,
pub before: String,
pub after: String,
pub improvement: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionState {
Idle,
Running,
Paused,
Completed,
Failed,
}
impl QueryDebugger {
pub fn new(config: DebugConfig) -> Result<Self> {
let profiler = Arc::new(Profiler::new());
let metrics = Arc::new(MetricsRegistry::new());
Ok(Self {
config,
breakpoints: Vec::new(),
trace: VecDeque::new(),
variable_history: HashMap::new(),
profiler,
metrics,
rewrite_history: Vec::new(),
state: ExecutionState::Idle,
breakpoint_hits: 0,
})
}
pub fn add_breakpoint(&mut self, breakpoint: DebugBreakpoint) {
if self.config.enable_breakpoints {
info!("Adding breakpoint: {:?}", breakpoint);
self.breakpoints.push(breakpoint);
}
}
pub fn remove_breakpoint(&mut self, index: usize) -> Option<DebugBreakpoint> {
if index < self.breakpoints.len() {
Some(self.breakpoints.remove(index))
} else {
None
}
}
pub fn clear_breakpoints(&mut self) {
self.breakpoints.clear();
info!("Cleared all breakpoints");
}
pub fn record_trace(&mut self, operation: Operation, duration: Duration, result_count: usize) {
if !self.config.enable_tracing {
return;
}
let entry = TraceEntry {
seq: self.trace.len(),
timestamp: self.get_elapsed_time(),
operation,
duration,
result_count,
memory_usage: self.estimate_memory_usage(),
metadata: HashMap::new(),
};
self.trace.push_back(entry);
while self.trace.len() > self.config.max_trace_entries {
self.trace.pop_front();
}
}
pub fn track_variable(&mut self, variable: String, value: String, source: String) {
if !self.config.enable_variable_tracking {
return;
}
let binding = VariableBinding {
variable: variable.clone(),
value,
timestamp: self.get_elapsed_time(),
source,
};
self.variable_history
.entry(variable.clone())
.or_default()
.push(binding);
if let Some(_bp) = self.breakpoints.iter().find(
|bp| matches!(bp, DebugBreakpoint::OnVariableBound { variable: v } if v == &variable),
) {
self.hit_breakpoint();
}
}
pub fn record_rewrite(
&mut self,
rule: String,
description: String,
before: &Algebra,
after: &Algebra,
) {
if !self.config.track_rewrites {
return;
}
let step = RewriteStep {
step: self.rewrite_history.len(),
rule,
description,
before: format!("{:?}", before),
after: format!("{:?}", after),
improvement: None, };
self.rewrite_history.push(step);
}
pub fn should_break(&self, operation: &Operation, result_count: usize) -> bool {
if !self.config.enable_breakpoints {
return false;
}
for bp in &self.breakpoints {
let should_break = match (bp, operation) {
(
DebugBreakpoint::BeforeTriplePattern { pattern_id: bp_id },
Operation::ScanPattern { pattern_id: op_id },
) => bp_id == op_id,
(
DebugBreakpoint::BeforeJoin { join_id: bp_id },
Operation::Join { join_id: op_id, .. },
) => bp_id == op_id,
(DebugBreakpoint::OnResultCountExceeds { threshold }, _) => {
result_count > *threshold
}
_ => false,
};
if should_break {
return true;
}
}
false
}
fn hit_breakpoint(&mut self) {
self.breakpoint_hits += 1;
self.state = ExecutionState::Paused;
info!("Breakpoint hit #{}", self.breakpoint_hits);
}
pub fn resume(&mut self) {
if self.state == ExecutionState::Paused {
self.state = ExecutionState::Running;
info!("Resuming execution");
}
}
pub fn get_execution_trace(&self) -> &VecDeque<TraceEntry> {
&self.trace
}
pub fn get_variable_history(&self, variable: &str) -> Option<&Vec<VariableBinding>> {
self.variable_history.get(variable)
}
pub fn get_all_variable_histories(&self) -> &HashMap<String, Vec<VariableBinding>> {
&self.variable_history
}
pub fn get_rewrite_history(&self) -> &[RewriteStep] {
&self.rewrite_history
}
pub fn get_state(&self) -> ExecutionState {
self.state
}
pub fn get_breakpoint_hits(&self) -> usize {
self.breakpoint_hits
}
pub fn visualize_plan(&self, algebra: &Algebra) -> Result<String> {
if !self.config.enable_plan_visualization {
return Ok("Plan visualization disabled".to_string());
}
match self.config.visualization_format {
VisualizationFormat::Text => self.visualize_as_text(algebra),
VisualizationFormat::Dot => self.visualize_as_dot(algebra),
VisualizationFormat::Json => self.visualize_as_json(algebra),
VisualizationFormat::Mermaid => self.visualize_as_mermaid(algebra),
}
}
fn visualize_as_text(&self, algebra: &Algebra) -> Result<String> {
let mut output = String::new();
self.visualize_algebra_recursive(algebra, 0, &mut output);
Ok(output)
}
#[allow(clippy::only_used_in_recursion)]
fn visualize_algebra_recursive(&self, algebra: &Algebra, depth: usize, output: &mut String) {
let indent = " ".repeat(depth);
match algebra {
Algebra::Bgp(patterns) => {
output.push_str(&format!("{}BGP ({} patterns)\n", indent, patterns.len()));
}
Algebra::Join { left, right } => {
output.push_str(&format!("{}JOIN\n", indent));
self.visualize_algebra_recursive(left, depth + 1, output);
self.visualize_algebra_recursive(right, depth + 1, output);
}
Algebra::LeftJoin { left, right, .. } => {
output.push_str(&format!("{}LEFT JOIN (OPTIONAL)\n", indent));
self.visualize_algebra_recursive(left, depth + 1, output);
self.visualize_algebra_recursive(right, depth + 1, output);
}
Algebra::Union { left, right } => {
output.push_str(&format!("{}UNION\n", indent));
self.visualize_algebra_recursive(left, depth + 1, output);
self.visualize_algebra_recursive(right, depth + 1, output);
}
Algebra::Filter { pattern, condition } => {
output.push_str(&format!("{}FILTER: {:?}\n", indent, condition));
self.visualize_algebra_recursive(pattern, depth + 1, output);
}
Algebra::Project { pattern, variables } => {
output.push_str(&format!(
"{}PROJECT: [{}]\n",
indent,
variables
.iter()
.map(|v| v.name())
.collect::<Vec<_>>()
.join(", ")
));
self.visualize_algebra_recursive(pattern, depth + 1, output);
}
Algebra::Distinct { pattern } => {
output.push_str(&format!("{}DISTINCT\n", indent));
self.visualize_algebra_recursive(pattern, depth + 1, output);
}
Algebra::OrderBy {
pattern,
conditions,
} => {
output.push_str(&format!("{}ORDER BY ({} keys)\n", indent, conditions.len()));
self.visualize_algebra_recursive(pattern, depth + 1, output);
}
Algebra::Slice {
pattern,
offset,
limit,
} => {
output.push_str(&format!(
"{}SLICE (offset={:?}, limit={:?})\n",
indent, offset, limit
));
self.visualize_algebra_recursive(pattern, depth + 1, output);
}
Algebra::Group { pattern, .. } => {
output.push_str(&format!("{}GROUP\n", indent));
self.visualize_algebra_recursive(pattern, depth + 1, output);
}
_ => {
output.push_str(&format!("{}{:?}\n", indent, algebra));
}
}
}
fn visualize_as_dot(&self, algebra: &Algebra) -> Result<String> {
let mut output = String::from("digraph query_plan {\n");
output.push_str(" rankdir=TB;\n");
output.push_str(" node [shape=box];\n\n");
let mut node_id = 0;
self.visualize_dot_recursive(algebra, &mut node_id, None, &mut output);
output.push_str("}\n");
Ok(output)
}
#[allow(clippy::only_used_in_recursion)]
fn visualize_dot_recursive(
&self,
algebra: &Algebra,
node_id: &mut usize,
parent_id: Option<usize>,
output: &mut String,
) -> usize {
let current_id = *node_id;
*node_id += 1;
let label = match algebra {
Algebra::Bgp(patterns) => format!("BGP\\n{} patterns", patterns.len()),
Algebra::Join { .. } => "JOIN".to_string(),
Algebra::LeftJoin { .. } => "LEFT JOIN".to_string(),
Algebra::Union { .. } => "UNION".to_string(),
Algebra::Filter { .. } => "FILTER".to_string(),
Algebra::Project { variables, .. } => {
format!("PROJECT\\n{} vars", variables.len())
}
Algebra::Distinct { .. } => "DISTINCT".to_string(),
Algebra::OrderBy { .. } => "ORDER BY".to_string(),
_ => format!("{:?}", algebra)
.split('{')
.next()
.unwrap_or("Unknown")
.to_string(),
};
output.push_str(&format!(" n{} [label=\"{}\"];\n", current_id, label));
if let Some(pid) = parent_id {
output.push_str(&format!(" n{} -> n{};\n", pid, current_id));
}
match algebra {
Algebra::Join { left, right }
| Algebra::LeftJoin { left, right, .. }
| Algebra::Union { left, right } => {
self.visualize_dot_recursive(left, node_id, Some(current_id), output);
self.visualize_dot_recursive(right, node_id, Some(current_id), output);
}
Algebra::Filter { pattern, .. }
| Algebra::Project { pattern, .. }
| Algebra::Distinct { pattern }
| Algebra::OrderBy { pattern, .. }
| Algebra::Slice { pattern, .. }
| Algebra::Group { pattern, .. } => {
self.visualize_dot_recursive(pattern, node_id, Some(current_id), output);
}
_ => {}
}
current_id
}
fn visualize_as_json(&self, algebra: &Algebra) -> Result<String> {
Ok(serde_json::to_string_pretty(&format!("{:?}", algebra))?)
}
fn visualize_as_mermaid(&self, algebra: &Algebra) -> Result<String> {
let mut output = String::from("graph TD\n");
let mut node_id = 0;
self.visualize_mermaid_recursive(algebra, &mut node_id, None, &mut output);
Ok(output)
}
#[allow(clippy::only_used_in_recursion)]
fn visualize_mermaid_recursive(
&self,
algebra: &Algebra,
node_id: &mut usize,
parent_id: Option<usize>,
output: &mut String,
) -> usize {
let current_id = *node_id;
*node_id += 1;
let label = match algebra {
Algebra::Bgp(patterns) => format!("BGP<br/>{} patterns", patterns.len()),
Algebra::Join { .. } => "JOIN".to_string(),
Algebra::Filter { .. } => "FILTER".to_string(),
_ => format!("{:?}", algebra)
.split('{')
.next()
.unwrap_or("Unknown")
.to_string(),
};
output.push_str(&format!(" N{}[{}]\n", current_id, label));
if let Some(pid) = parent_id {
output.push_str(&format!(" N{} --> N{}\n", pid, current_id));
}
match algebra {
Algebra::Join { left, right } | Algebra::Union { left, right } => {
self.visualize_mermaid_recursive(left, node_id, Some(current_id), output);
self.visualize_mermaid_recursive(right, node_id, Some(current_id), output);
}
Algebra::Filter { pattern, .. } | Algebra::Project { pattern, .. } => {
self.visualize_mermaid_recursive(pattern, node_id, Some(current_id), output);
}
_ => {}
}
current_id
}
pub fn generate_report(&self) -> DebugReport {
DebugReport {
total_operations: self.trace.len(),
total_duration: self.get_elapsed_time(),
breakpoint_hits: self.breakpoint_hits,
variables_tracked: self.variable_history.len(),
rewrites_applied: self.rewrite_history.len(),
state: self.state,
slowest_operations: self.get_slowest_operations(5),
memory_peak: self.trace.iter().map(|e| e.memory_usage).max().unwrap_or(0),
}
}
fn get_slowest_operations(&self, count: usize) -> Vec<(Operation, Duration)> {
let mut ops: Vec<_> = self
.trace
.iter()
.map(|e| (e.operation.clone(), e.duration))
.collect();
ops.sort_by_key(|b| std::cmp::Reverse(b.1));
ops.truncate(count);
ops
}
fn get_elapsed_time(&self) -> Duration {
self.trace
.back()
.map(|e| e.timestamp)
.unwrap_or(Duration::ZERO)
}
fn estimate_memory_usage(&self) -> usize {
let trace_mem = self.trace.len() * std::mem::size_of::<TraceEntry>();
let var_mem = self.variable_history.len() * 256; trace_mem + var_mem
}
pub fn reset(&mut self) {
self.trace.clear();
self.variable_history.clear();
self.rewrite_history.clear();
self.state = ExecutionState::Idle;
self.breakpoint_hits = 0;
info!("Debugger reset");
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DebugReport {
pub total_operations: usize,
pub total_duration: Duration,
pub breakpoint_hits: usize,
pub variables_tracked: usize,
pub rewrites_applied: usize,
pub state: ExecutionState,
pub slowest_operations: Vec<(Operation, Duration)>,
pub memory_peak: usize,
}
impl fmt::Display for DebugReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "=== Query Debug Report ===")?;
writeln!(f, "Total Operations: {}", self.total_operations)?;
writeln!(f, "Total Duration: {:?}", self.total_duration)?;
writeln!(f, "Breakpoint Hits: {}", self.breakpoint_hits)?;
writeln!(f, "Variables Tracked: {}", self.variables_tracked)?;
writeln!(f, "Rewrites Applied: {}", self.rewrites_applied)?;
writeln!(f, "Execution State: {:?}", self.state)?;
writeln!(f, "Peak Memory: {} bytes", self.memory_peak)?;
writeln!(f, "\nSlowest Operations:")?;
for (i, (op, duration)) in self.slowest_operations.iter().enumerate() {
writeln!(f, " {}. {:?} - {:?}", i + 1, op, duration)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debugger_creation() {
let config = DebugConfig::default();
let debugger = QueryDebugger::new(config);
assert!(debugger.is_ok());
}
#[test]
fn test_breakpoint_management() {
let mut debugger = QueryDebugger::new(DebugConfig::default()).unwrap();
debugger.add_breakpoint(DebugBreakpoint::BeforeJoin { join_id: 0 });
assert_eq!(debugger.breakpoints.len(), 1);
let removed = debugger.remove_breakpoint(0);
assert!(removed.is_some());
assert_eq!(debugger.breakpoints.len(), 0);
}
#[test]
fn test_trace_recording() {
let mut debugger = QueryDebugger::new(DebugConfig::default()).unwrap();
let op = Operation::Parse;
debugger.record_trace(op.clone(), Duration::from_millis(10), 0);
assert_eq!(debugger.trace.len(), 1);
assert_eq!(debugger.trace[0].operation, op);
}
#[test]
fn test_variable_tracking() {
let mut debugger = QueryDebugger::new(DebugConfig::default()).unwrap();
debugger.track_variable(
"x".to_string(),
"value1".to_string(),
"pattern_0".to_string(),
);
let history = debugger.get_variable_history("x");
assert!(history.is_some());
assert_eq!(history.unwrap().len(), 1);
}
#[test]
fn test_rewrite_tracking() {
let mut debugger = QueryDebugger::new(DebugConfig::default()).unwrap();
use crate::algebra::Algebra;
let before = Algebra::Bgp(vec![]);
let after = Algebra::Bgp(vec![]);
debugger.record_rewrite(
"test_rule".to_string(),
"Test rewrite".to_string(),
&before,
&after,
);
assert_eq!(debugger.rewrite_history.len(), 1);
}
#[test]
fn test_execution_state_transitions() {
let mut debugger = QueryDebugger::new(DebugConfig::default()).unwrap();
assert_eq!(debugger.get_state(), ExecutionState::Idle);
debugger.state = ExecutionState::Running;
assert_eq!(debugger.get_state(), ExecutionState::Running);
debugger.hit_breakpoint();
assert_eq!(debugger.get_state(), ExecutionState::Paused);
debugger.resume();
assert_eq!(debugger.get_state(), ExecutionState::Running);
}
#[test]
fn test_debug_report_generation() {
let mut debugger = QueryDebugger::new(DebugConfig::default()).unwrap();
debugger.record_trace(Operation::Parse, Duration::from_millis(5), 0);
debugger.record_trace(Operation::GenerateAlgebra, Duration::from_millis(10), 0);
let report = debugger.generate_report();
assert_eq!(report.total_operations, 2);
}
#[test]
fn test_config_builder() {
let config = DebugConfig::default()
.with_breakpoints(false)
.with_variable_tracking(true)
.with_trace_level(3)
.with_visualization_format(VisualizationFormat::Dot);
assert!(!config.enable_breakpoints);
assert!(config.enable_variable_tracking);
assert_eq!(config.trace_detail_level, 3);
assert_eq!(config.visualization_format, VisualizationFormat::Dot);
}
#[test]
fn test_debugger_reset() {
let mut debugger = QueryDebugger::new(DebugConfig::default()).unwrap();
debugger.record_trace(Operation::Parse, Duration::from_millis(5), 0);
debugger.track_variable("x".to_string(), "val".to_string(), "src".to_string());
debugger.reset();
assert_eq!(debugger.trace.len(), 0);
assert_eq!(debugger.variable_history.len(), 0);
assert_eq!(debugger.get_state(), ExecutionState::Idle);
}
}