1use crate::core::ConstraintResult;
48use serde::{Deserialize, Serialize};
49use std::collections::HashMap;
50use std::fmt;
51use std::sync::{Arc, Mutex};
52use std::time::{Duration, Instant};
53use tracing::{debug, trace};
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum DebugLevel {
58 None,
60 Basic,
62 Detailed,
64 Verbose,
66}
67
68#[derive(Debug, Clone)]
70pub struct DebugContext {
71 level: DebugLevel,
72 log_queries: bool,
73 track_performance: bool,
74 capture_intermediate_results: bool,
75 collector: Arc<Mutex<DebugCollector>>,
76}
77
78impl Default for DebugContext {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl DebugContext {
85 pub fn new() -> Self {
87 Self {
88 level: DebugLevel::None,
89 log_queries: false,
90 track_performance: false,
91 capture_intermediate_results: false,
92 collector: Arc::new(Mutex::new(DebugCollector::new())),
93 }
94 }
95
96 pub fn with_level(mut self, level: DebugLevel) -> Self {
98 self.level = level;
99 match level {
101 DebugLevel::None => {
102 self.log_queries = false;
103 self.track_performance = false;
104 self.capture_intermediate_results = false;
105 }
106 DebugLevel::Basic => {
107 self.track_performance = true;
108 }
109 DebugLevel::Detailed => {
110 self.log_queries = true;
111 self.track_performance = true;
112 }
113 DebugLevel::Verbose => {
114 self.log_queries = true;
115 self.track_performance = true;
116 self.capture_intermediate_results = true;
117 }
118 }
119 self
120 }
121
122 pub fn with_query_logging(mut self, enable: bool) -> Self {
124 self.log_queries = enable;
125 self
126 }
127
128 pub fn with_performance_tracking(mut self, enable: bool) -> Self {
130 self.track_performance = enable;
131 self
132 }
133
134 pub fn log_query(&self, query: &str, table_context: &str) {
136 if self.log_queries && self.level != DebugLevel::None {
137 let mut collector = self.collector.lock().unwrap();
138 collector.add_query(query.to_string(), table_context.to_string());
139 trace!("SQL Query for {}: {}", table_context, query);
140 }
141 }
142
143 pub fn start_constraint(&self, constraint_name: &str) -> Option<ConstraintTracker> {
145 if self.track_performance && self.level != DebugLevel::None {
146 Some(ConstraintTracker {
147 name: constraint_name.to_string(),
148 start: Instant::now(),
149 context: self.clone(),
150 })
151 } else {
152 None
153 }
154 }
155
156 pub fn record_result(&self, constraint_name: &str, result: &ConstraintResult) {
158 if self.level != DebugLevel::None {
159 let mut collector = self.collector.lock().unwrap();
160 collector.add_result(constraint_name.to_string(), result.clone());
161 }
162 }
163
164 pub fn get_debug_info(&self) -> DebugInfo {
166 let collector = self.collector.lock().unwrap();
167 collector.to_debug_info()
168 }
169}
170
171pub struct ConstraintTracker {
173 name: String,
174 start: Instant,
175 context: DebugContext,
176}
177
178impl Drop for ConstraintTracker {
179 fn drop(&mut self) {
180 let duration = self.start.elapsed();
181 let mut collector = self.context.collector.lock().unwrap();
182 collector.add_timing(self.name.clone(), duration);
183 debug!("Constraint '{}' executed in {:?}", self.name, duration);
184 }
185}
186
187#[derive(Debug)]
189struct DebugCollector {
190 queries: Vec<QueryExecution>,
191 timings: Vec<ConstraintTiming>,
192 results: HashMap<String, ConstraintResult>,
193 timeline: Vec<TimelineEvent>,
194}
195
196impl DebugCollector {
197 fn new() -> Self {
198 Self {
199 queries: Vec::new(),
200 timings: Vec::new(),
201 results: HashMap::new(),
202 timeline: Vec::new(),
203 }
204 }
205
206 fn add_query(&mut self, query: String, context: String) {
207 let event = QueryExecution {
208 query: query.clone(),
209 context,
210 timestamp: Some(Instant::now()),
211 };
212 self.queries.push(event.clone());
213 self.timeline.push(TimelineEvent::QueryExecuted(event));
214 }
215
216 fn add_timing(&mut self, constraint: String, duration: Duration) {
217 let timing = ConstraintTiming {
218 constraint: constraint.clone(),
219 duration,
220 };
221 self.timings.push(timing.clone());
222 self.timeline
223 .push(TimelineEvent::ConstraintCompleted(timing));
224 }
225
226 fn add_result(&mut self, constraint: String, result: ConstraintResult) {
227 self.results.insert(constraint.clone(), result.clone());
228 self.timeline.push(TimelineEvent::ResultRecorded {
229 constraint,
230 success: matches!(result.status, crate::core::ConstraintStatus::Success),
231 });
232 }
233
234 fn to_debug_info(&self) -> DebugInfo {
235 DebugInfo {
236 queries: self.queries.clone(),
237 timings: self.timings.clone(),
238 results: self.results.clone(),
239 timeline: self.timeline.clone(),
240 summary: self.generate_summary(),
241 }
242 }
243
244 fn generate_summary(&self) -> DebugSummary {
245 let total_queries = self.queries.len();
246 let total_constraints = self.timings.len();
247 let total_duration: Duration = self.timings.iter().map(|t| t.duration).sum();
248 let failed_constraints = self
249 .results
250 .values()
251 .filter(|r| matches!(r.status, crate::core::ConstraintStatus::Failure))
252 .count();
253
254 DebugSummary {
255 total_queries,
256 total_constraints,
257 total_duration,
258 failed_constraints,
259 avg_constraint_time: if total_constraints > 0 {
260 total_duration / total_constraints as u32
261 } else {
262 Duration::from_secs(0)
263 },
264 }
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct DebugInfo {
271 pub queries: Vec<QueryExecution>,
273 pub timings: Vec<ConstraintTiming>,
275 pub results: HashMap<String, ConstraintResult>,
277 pub timeline: Vec<TimelineEvent>,
279 pub summary: DebugSummary,
281}
282
283impl DebugInfo {
284 pub fn generate_error_report(&self) -> ErrorReport {
286 let mut failed_constraints = Vec::new();
287
288 for (name, result) in &self.results {
289 if matches!(result.status, crate::core::ConstraintStatus::Failure) {
290 let related_queries = self
291 .queries
292 .iter()
293 .filter(|q| q.context.contains(name))
294 .cloned()
295 .collect();
296
297 let timing = self.timings.iter().find(|t| t.constraint == *name).cloned();
298
299 failed_constraints.push(FailedConstraintDetail {
300 name: name.clone(),
301 result: result.clone(),
302 related_queries,
303 timing,
304 suggestions: self.generate_suggestions_for(name, result),
305 });
306 }
307 }
308
309 let total_failures = failed_constraints.len();
310 ErrorReport {
311 failed_constraints,
312 total_failures,
313 execution_summary: self.summary.clone(),
314 }
315 }
316
317 fn generate_suggestions_for(
319 &self,
320 constraint_name: &str,
321 result: &ConstraintResult,
322 ) -> Vec<String> {
323 let mut suggestions = Vec::new();
324
325 if constraint_name.contains("foreign_key") {
327 suggestions.push("Check that both tables are properly registered".to_string());
328 suggestions.push(
329 "Verify that the referenced columns exist and have compatible types".to_string(),
330 );
331 suggestions.push("Consider allowing nulls if the relationship is optional".to_string());
332 }
333
334 if constraint_name.contains("cross_table_sum") {
335 suggestions.push("Verify that numeric columns have the same precision".to_string());
336 suggestions.push(
337 "Check for floating-point precision issues - consider using tolerance".to_string(),
338 );
339 suggestions.push("Ensure GROUP BY columns exist in both tables".to_string());
340 }
341
342 if constraint_name.contains("join_coverage") {
343 suggestions
344 .push("Review the expected coverage rate - it might be too high".to_string());
345 suggestions.push("Check for data quality issues in join keys".to_string());
346 suggestions
347 .push("Consider using distinct counts if duplicates are expected".to_string());
348 }
349
350 if constraint_name.contains("temporal") {
351 suggestions.push("Verify timestamp formats are consistent".to_string());
352 suggestions.push("Check timezone handling".to_string());
353 suggestions.push("Consider allowing small time differences with tolerance".to_string());
354 }
355
356 if result.message.is_some() {
358 suggestions.push("Review the error message for specific details".to_string());
359 }
360 suggestions.push("Enable verbose debug logging for more details".to_string());
361
362 suggestions
363 }
364
365 pub fn visualize_relationships(&self) -> String {
367 let mut output = String::new();
368 output.push_str("Table Relationships Detected:\n");
369 output.push_str("============================\n\n");
370
371 let mut relationships: HashMap<String, Vec<String>> = HashMap::new();
373
374 for query in &self.queries {
375 if query.query.contains("JOIN") {
376 if let Some(tables) = self.extract_join_tables(&query.query) {
379 relationships
380 .entry(tables.0.clone())
381 .or_default()
382 .push(tables.1.clone());
383 }
384 }
385 }
386
387 for (left_table, right_tables) in relationships {
389 for right_table in right_tables {
390 output.push_str(&format!("{left_table} ──────> {right_table}\n"));
391 }
392 }
393
394 output
395 }
396
397 fn extract_join_tables(&self, query: &str) -> Option<(String, String)> {
399 if query.contains("JOIN") {
401 None
404 } else {
405 None
406 }
407 }
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct QueryExecution {
413 pub query: String,
415 pub context: String,
417 #[serde(skip_deserializing, skip_serializing)]
419 pub timestamp: Option<Instant>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct ConstraintTiming {
425 pub constraint: String,
427 pub duration: Duration,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433pub enum TimelineEvent {
434 QueryExecuted(QueryExecution),
436 ConstraintCompleted(ConstraintTiming),
438 ResultRecorded { constraint: String, success: bool },
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct DebugSummary {
445 pub total_queries: usize,
447 pub total_constraints: usize,
449 pub total_duration: Duration,
451 pub failed_constraints: usize,
453 pub avg_constraint_time: Duration,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct ErrorReport {
460 pub failed_constraints: Vec<FailedConstraintDetail>,
462 pub total_failures: usize,
464 pub execution_summary: DebugSummary,
466}
467
468impl fmt::Display for ErrorReport {
469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470 writeln!(f, "═══════════════════════════════════════")?;
471 writeln!(f, " Validation Error Report")?;
472 writeln!(f, "═══════════════════════════════════════")?;
473 writeln!(f)?;
474 writeln!(f, "Summary:")?;
475 writeln!(f, " Total Failures: {}", self.total_failures)?;
476 writeln!(
477 f,
478 " Total Constraints: {}",
479 self.execution_summary.total_constraints
480 )?;
481 writeln!(
482 f,
483 " Total Duration: {:?}",
484 self.execution_summary.total_duration
485 )?;
486 writeln!(f)?;
487
488 for (i, failed) in self.failed_constraints.iter().enumerate() {
489 writeln!(f, "Failure #{}: {}", i + 1, failed.name)?;
490 writeln!(f, "───────────────────────────────────────")?;
491
492 if let Some(ref message) = failed.result.message {
493 writeln!(f, " Error: {message}")?;
494 }
495
496 if let Some(ref timing) = failed.timing {
497 writeln!(f, " Duration: {:?}", timing.duration)?;
498 }
499
500 if !failed.suggestions.is_empty() {
501 writeln!(f, " Suggestions:")?;
502 for suggestion in &failed.suggestions {
503 writeln!(f, " • {suggestion}")?;
504 }
505 }
506
507 if !failed.related_queries.is_empty() {
508 writeln!(f, " Related Queries:")?;
509 for query in &failed.related_queries {
510 writeln!(f, " {}", query.query)?;
511 }
512 }
513
514 writeln!(f)?;
515 }
516
517 Ok(())
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct FailedConstraintDetail {
524 pub name: String,
526 pub result: ConstraintResult,
528 pub related_queries: Vec<QueryExecution>,
530 pub timing: Option<ConstraintTiming>,
532 pub suggestions: Vec<String>,
534}
535
536pub trait ValidationResultDebugExt {
538 fn debug_info(&self) -> Option<&DebugInfo>;
540
541 fn error_report(&self) -> Option<ErrorReport>;
543}
544
545#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn test_debug_context_creation() {
554 let ctx = DebugContext::new()
555 .with_level(DebugLevel::Detailed)
556 .with_query_logging(true);
557
558 assert!(ctx.log_queries);
559 assert!(ctx.track_performance);
560 }
561
562 #[test]
563 fn test_debug_collector() {
564 let mut collector = DebugCollector::new();
565
566 collector.add_query(
567 "SELECT * FROM users".to_string(),
568 "test_constraint".to_string(),
569 );
570 collector.add_timing("test_constraint".to_string(), Duration::from_millis(100));
571
572 let info = collector.to_debug_info();
573
574 assert_eq!(info.queries.len(), 1);
575 assert_eq!(info.timings.len(), 1);
576 assert_eq!(info.summary.total_queries, 1);
577 assert_eq!(info.summary.total_constraints, 1);
578 }
579
580 #[test]
581 fn test_error_report_generation() {
582 let mut collector = DebugCollector::new();
583
584 collector.add_result(
585 "foreign_key_check".to_string(),
586 ConstraintResult {
587 status: crate::core::ConstraintStatus::Failure,
588 message: Some("Foreign key violation found".to_string()),
589 metric: None,
590 },
591 );
592
593 let info = collector.to_debug_info();
594 let report = info.generate_error_report();
595
596 assert_eq!(report.total_failures, 1);
597 assert!(!report.failed_constraints[0].suggestions.is_empty());
598 }
599}