use crate::core::ConstraintResult;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tracing::{debug, trace};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DebugLevel {
None,
Basic,
Detailed,
Verbose,
}
#[derive(Debug, Clone)]
pub struct DebugContext {
level: DebugLevel,
log_queries: bool,
track_performance: bool,
capture_intermediate_results: bool,
collector: Arc<Mutex<DebugCollector>>,
}
impl Default for DebugContext {
fn default() -> Self {
Self::new()
}
}
impl DebugContext {
pub fn new() -> Self {
Self {
level: DebugLevel::None,
log_queries: false,
track_performance: false,
capture_intermediate_results: false,
collector: Arc::new(Mutex::new(DebugCollector::new())),
}
}
pub fn with_level(mut self, level: DebugLevel) -> Self {
self.level = level;
match level {
DebugLevel::None => {
self.log_queries = false;
self.track_performance = false;
self.capture_intermediate_results = false;
}
DebugLevel::Basic => {
self.track_performance = true;
}
DebugLevel::Detailed => {
self.log_queries = true;
self.track_performance = true;
}
DebugLevel::Verbose => {
self.log_queries = true;
self.track_performance = true;
self.capture_intermediate_results = true;
}
}
self
}
pub fn with_query_logging(mut self, enable: bool) -> Self {
self.log_queries = enable;
self
}
pub fn with_performance_tracking(mut self, enable: bool) -> Self {
self.track_performance = enable;
self
}
pub fn log_query(&self, query: &str, table_context: &str) {
if self.log_queries && self.level != DebugLevel::None {
let mut collector = self.collector.lock().unwrap();
collector.add_query(query.to_string(), table_context.to_string());
trace!("SQL Query for {}: {}", table_context, query);
}
}
pub fn start_constraint(&self, constraint_name: &str) -> Option<ConstraintTracker> {
if self.track_performance && self.level != DebugLevel::None {
Some(ConstraintTracker {
name: constraint_name.to_string(),
start: Instant::now(),
context: self.clone(),
})
} else {
None
}
}
pub fn record_result(&self, constraint_name: &str, result: &ConstraintResult) {
if self.level != DebugLevel::None {
let mut collector = self.collector.lock().unwrap();
collector.add_result(constraint_name.to_string(), result.clone());
}
}
pub fn get_debug_info(&self) -> DebugInfo {
let collector = self.collector.lock().unwrap();
collector.to_debug_info()
}
}
pub struct ConstraintTracker {
name: String,
start: Instant,
context: DebugContext,
}
impl Drop for ConstraintTracker {
fn drop(&mut self) {
let duration = self.start.elapsed();
let mut collector = self.context.collector.lock().unwrap();
collector.add_timing(self.name.clone(), duration);
debug!("Constraint '{}' executed in {:?}", self.name, duration);
}
}
#[derive(Debug)]
struct DebugCollector {
queries: Vec<QueryExecution>,
timings: Vec<ConstraintTiming>,
results: HashMap<String, ConstraintResult>,
timeline: Vec<TimelineEvent>,
}
impl DebugCollector {
fn new() -> Self {
Self {
queries: Vec::new(),
timings: Vec::new(),
results: HashMap::new(),
timeline: Vec::new(),
}
}
fn add_query(&mut self, query: String, context: String) {
let event = QueryExecution {
query: query.clone(),
context,
timestamp: Some(Instant::now()),
};
self.queries.push(event.clone());
self.timeline.push(TimelineEvent::QueryExecuted(event));
}
fn add_timing(&mut self, constraint: String, duration: Duration) {
let timing = ConstraintTiming {
constraint: constraint.clone(),
duration,
};
self.timings.push(timing.clone());
self.timeline
.push(TimelineEvent::ConstraintCompleted(timing));
}
fn add_result(&mut self, constraint: String, result: ConstraintResult) {
self.results.insert(constraint.clone(), result.clone());
self.timeline.push(TimelineEvent::ResultRecorded {
constraint,
success: matches!(result.status, crate::core::ConstraintStatus::Success),
});
}
fn to_debug_info(&self) -> DebugInfo {
DebugInfo {
queries: self.queries.clone(),
timings: self.timings.clone(),
results: self.results.clone(),
timeline: self.timeline.clone(),
summary: self.generate_summary(),
}
}
fn generate_summary(&self) -> DebugSummary {
let total_queries = self.queries.len();
let total_constraints = self.timings.len();
let total_duration: Duration = self.timings.iter().map(|t| t.duration).sum();
let failed_constraints = self
.results
.values()
.filter(|r| matches!(r.status, crate::core::ConstraintStatus::Failure))
.count();
DebugSummary {
total_queries,
total_constraints,
total_duration,
failed_constraints,
avg_constraint_time: if total_constraints > 0 {
total_duration / total_constraints as u32
} else {
Duration::from_secs(0)
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DebugInfo {
pub queries: Vec<QueryExecution>,
pub timings: Vec<ConstraintTiming>,
pub results: HashMap<String, ConstraintResult>,
pub timeline: Vec<TimelineEvent>,
pub summary: DebugSummary,
}
impl DebugInfo {
pub fn generate_error_report(&self) -> ErrorReport {
let mut failed_constraints = Vec::new();
for (name, result) in &self.results {
if matches!(result.status, crate::core::ConstraintStatus::Failure) {
let related_queries = self
.queries
.iter()
.filter(|q| q.context.contains(name))
.cloned()
.collect();
let timing = self.timings.iter().find(|t| t.constraint == *name).cloned();
failed_constraints.push(FailedConstraintDetail {
name: name.clone(),
result: result.clone(),
related_queries,
timing,
suggestions: self.generate_suggestions_for(name, result),
});
}
}
let total_failures = failed_constraints.len();
ErrorReport {
failed_constraints,
total_failures,
execution_summary: self.summary.clone(),
}
}
fn generate_suggestions_for(
&self,
constraint_name: &str,
result: &ConstraintResult,
) -> Vec<String> {
let mut suggestions = Vec::new();
if constraint_name.contains("foreign_key") {
suggestions.push("Check that both tables are properly registered".to_string());
suggestions.push(
"Verify that the referenced columns exist and have compatible types".to_string(),
);
suggestions.push("Consider allowing nulls if the relationship is optional".to_string());
}
if constraint_name.contains("cross_table_sum") {
suggestions.push("Verify that numeric columns have the same precision".to_string());
suggestions.push(
"Check for floating-point precision issues - consider using tolerance".to_string(),
);
suggestions.push("Ensure GROUP BY columns exist in both tables".to_string());
}
if constraint_name.contains("join_coverage") {
suggestions
.push("Review the expected coverage rate - it might be too high".to_string());
suggestions.push("Check for data quality issues in join keys".to_string());
suggestions
.push("Consider using distinct counts if duplicates are expected".to_string());
}
if constraint_name.contains("temporal") {
suggestions.push("Verify timestamp formats are consistent".to_string());
suggestions.push("Check timezone handling".to_string());
suggestions.push("Consider allowing small time differences with tolerance".to_string());
}
if result.message.is_some() {
suggestions.push("Review the error message for specific details".to_string());
}
suggestions.push("Enable verbose debug logging for more details".to_string());
suggestions
}
pub fn visualize_relationships(&self) -> String {
let mut output = String::new();
output.push_str("Table Relationships Detected:\n");
output.push_str("============================\n\n");
let mut relationships: HashMap<String, Vec<String>> = HashMap::new();
for query in &self.queries {
if query.query.contains("JOIN") {
if let Some(tables) = self.extract_join_tables(&query.query) {
relationships
.entry(tables.0.clone())
.or_default()
.push(tables.1.clone());
}
}
}
for (left_table, right_tables) in relationships {
for right_table in right_tables {
output.push_str(&format!("{left_table} ──────> {right_table}\n"));
}
}
output
}
fn extract_join_tables(&self, query: &str) -> Option<(String, String)> {
if query.contains("JOIN") {
None
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryExecution {
pub query: String,
pub context: String,
#[serde(skip_deserializing, skip_serializing)]
pub timestamp: Option<Instant>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintTiming {
pub constraint: String,
pub duration: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TimelineEvent {
QueryExecuted(QueryExecution),
ConstraintCompleted(ConstraintTiming),
ResultRecorded { constraint: String, success: bool },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DebugSummary {
pub total_queries: usize,
pub total_constraints: usize,
pub total_duration: Duration,
pub failed_constraints: usize,
pub avg_constraint_time: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorReport {
pub failed_constraints: Vec<FailedConstraintDetail>,
pub total_failures: usize,
pub execution_summary: DebugSummary,
}
impl fmt::Display for ErrorReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "═══════════════════════════════════════")?;
writeln!(f, " Validation Error Report")?;
writeln!(f, "═══════════════════════════════════════")?;
writeln!(f)?;
writeln!(f, "Summary:")?;
writeln!(f, " Total Failures: {}", self.total_failures)?;
writeln!(
f,
" Total Constraints: {}",
self.execution_summary.total_constraints
)?;
writeln!(
f,
" Total Duration: {:?}",
self.execution_summary.total_duration
)?;
writeln!(f)?;
for (i, failed) in self.failed_constraints.iter().enumerate() {
writeln!(f, "Failure #{}: {}", i + 1, failed.name)?;
writeln!(f, "───────────────────────────────────────")?;
if let Some(ref message) = failed.result.message {
writeln!(f, " Error: {message}")?;
}
if let Some(ref timing) = failed.timing {
writeln!(f, " Duration: {:?}", timing.duration)?;
}
if !failed.suggestions.is_empty() {
writeln!(f, " Suggestions:")?;
for suggestion in &failed.suggestions {
writeln!(f, " • {suggestion}")?;
}
}
if !failed.related_queries.is_empty() {
writeln!(f, " Related Queries:")?;
for query in &failed.related_queries {
writeln!(f, " {}", query.query)?;
}
}
writeln!(f)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailedConstraintDetail {
pub name: String,
pub result: ConstraintResult,
pub related_queries: Vec<QueryExecution>,
pub timing: Option<ConstraintTiming>,
pub suggestions: Vec<String>,
}
pub trait ValidationResultDebugExt {
fn debug_info(&self) -> Option<&DebugInfo>;
fn error_report(&self) -> Option<ErrorReport>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug_context_creation() {
let ctx = DebugContext::new()
.with_level(DebugLevel::Detailed)
.with_query_logging(true);
assert!(ctx.log_queries);
assert!(ctx.track_performance);
}
#[test]
fn test_debug_collector() {
let mut collector = DebugCollector::new();
collector.add_query(
"SELECT * FROM users".to_string(),
"test_constraint".to_string(),
);
collector.add_timing("test_constraint".to_string(), Duration::from_millis(100));
let info = collector.to_debug_info();
assert_eq!(info.queries.len(), 1);
assert_eq!(info.timings.len(), 1);
assert_eq!(info.summary.total_queries, 1);
assert_eq!(info.summary.total_constraints, 1);
}
#[test]
fn test_error_report_generation() {
let mut collector = DebugCollector::new();
collector.add_result(
"foreign_key_check".to_string(),
ConstraintResult {
status: crate::core::ConstraintStatus::Failure,
message: Some("Foreign key violation found".to_string()),
metric: None,
},
);
let info = collector.to_debug_info();
let report = info.generate_error_report();
assert_eq!(report.total_failures, 1);
assert!(!report.failed_constraints[0].suggestions.is_empty());
}
}