term_guard/core/
debug_context.rs

1//! Debug context and comprehensive error reporting for multi-table validation.
2//!
3//! This module provides detailed debugging and error reporting capabilities for
4//! cross-table validation scenarios, helping developers quickly identify and resolve
5//! issues in complex validation setups. Part of Phase 3: UX & Integration.
6//!
7//! # Features
8//!
9//! - SQL query logging and explanation
10//! - Constraint execution timeline
11//! - Performance profiling
12//! - Detailed error context with suggestions
13//! - Visual representation of table relationships
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use term_guard::core::{DebugContext, DebugLevel};
19//! use term_guard::core::{ValidationSuite, Check};
20//! use datafusion::prelude::*;
21//!
22//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
23//! let ctx = SessionContext::new();
24//!
25//! // Enable debug context
26//! let debug_ctx = DebugContext::new()
27//!     .with_level(DebugLevel::Detailed)
28//!     .with_query_logging(true)
29//!     .with_performance_tracking(true);
30//!
31//! let suite = ValidationSuite::builder("validation")
32//!     .with_debug_context(debug_ctx)
33//!     .check(/* ... */)
34//!     .build();
35//!
36//! let result = suite.run(&ctx).await?;
37//!
38//! // Access debug information
39//! if let Some(debug_info) = result.debug_info() {
40//!     println!("Execution timeline: {:#?}", debug_info.timeline);
41//!     println!("SQL queries executed: {:#?}", debug_info.queries);
42//! }
43//! # Ok(())
44//! # }
45//! ```
46
47use 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/// Debug level for validation execution.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum DebugLevel {
58    /// No debug information collected
59    None,
60    /// Basic information (constraint names, pass/fail)
61    Basic,
62    /// Detailed information (SQL queries, timings)
63    Detailed,
64    /// Verbose information (all intermediate results)
65    Verbose,
66}
67
68/// Debug context for validation execution.
69#[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    /// Create a new debug context with default settings.
86    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    /// Set the debug level.
97    pub fn with_level(mut self, level: DebugLevel) -> Self {
98        self.level = level;
99        // Auto-enable features based on level
100        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    /// Enable or disable SQL query logging.
123    pub fn with_query_logging(mut self, enable: bool) -> Self {
124        self.log_queries = enable;
125        self
126    }
127
128    /// Enable or disable performance tracking.
129    pub fn with_performance_tracking(mut self, enable: bool) -> Self {
130        self.track_performance = enable;
131        self
132    }
133
134    /// Log a SQL query execution.
135    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    /// Start tracking a constraint execution.
144    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    /// Record a constraint result.
157    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    /// Get the collected debug information.
165    pub fn get_debug_info(&self) -> DebugInfo {
166        let collector = self.collector.lock().unwrap();
167        collector.to_debug_info()
168    }
169}
170
171/// Tracker for constraint execution timing.
172pub 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/// Collector for debug information during execution.
188#[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/// Debug information collected during validation execution.
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct DebugInfo {
271    /// SQL queries executed
272    pub queries: Vec<QueryExecution>,
273    /// Constraint execution timings
274    pub timings: Vec<ConstraintTiming>,
275    /// Constraint results
276    pub results: HashMap<String, ConstraintResult>,
277    /// Execution timeline
278    pub timeline: Vec<TimelineEvent>,
279    /// Summary statistics
280    pub summary: DebugSummary,
281}
282
283impl DebugInfo {
284    /// Generate a detailed error report for failed constraints.
285    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    /// Generate debugging suggestions for a failed constraint.
318    fn generate_suggestions_for(
319        &self,
320        constraint_name: &str,
321        result: &ConstraintResult,
322    ) -> Vec<String> {
323        let mut suggestions = Vec::new();
324
325        // Analyze the constraint type and provide specific suggestions
326        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        // Add generic suggestions
357        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    /// Generate a visual representation of table relationships.
366    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        // Extract table relationships from queries
372        let mut relationships: HashMap<String, Vec<String>> = HashMap::new();
373
374        for query in &self.queries {
375            if query.query.contains("JOIN") {
376                // Simple extraction of table names from JOIN clauses
377                // In production, this would use proper SQL parsing
378                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        // Generate ASCII art representation
388        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    /// Extract table names from a JOIN query (simplified).
398    fn extract_join_tables(&self, query: &str) -> Option<(String, String)> {
399        // This is a simplified extraction - in production, use a proper SQL parser
400        if query.contains("JOIN") {
401            // Extract patterns like "FROM table1 JOIN table2"
402            // This is a placeholder implementation
403            None
404        } else {
405            None
406        }
407    }
408}
409
410/// Query execution record.
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct QueryExecution {
413    /// The SQL query executed
414    pub query: String,
415    /// Context (e.g., constraint name)
416    pub context: String,
417    /// When the query was executed (not serialized)
418    #[serde(skip_deserializing, skip_serializing)]
419    pub timestamp: Option<Instant>,
420}
421
422/// Constraint timing information.
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct ConstraintTiming {
425    /// Constraint name
426    pub constraint: String,
427    /// Execution duration
428    pub duration: Duration,
429}
430
431/// Timeline event during execution.
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub enum TimelineEvent {
434    /// A SQL query was executed
435    QueryExecuted(QueryExecution),
436    /// A constraint completed execution
437    ConstraintCompleted(ConstraintTiming),
438    /// A constraint result was recorded
439    ResultRecorded { constraint: String, success: bool },
440}
441
442/// Summary of debug information.
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct DebugSummary {
445    /// Total number of SQL queries executed
446    pub total_queries: usize,
447    /// Total number of constraints evaluated
448    pub total_constraints: usize,
449    /// Total execution time
450    pub total_duration: Duration,
451    /// Number of failed constraints
452    pub failed_constraints: usize,
453    /// Average time per constraint
454    pub avg_constraint_time: Duration,
455}
456
457/// Detailed error report for failed validations.
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct ErrorReport {
460    /// Details of each failed constraint
461    pub failed_constraints: Vec<FailedConstraintDetail>,
462    /// Total number of failures
463    pub total_failures: usize,
464    /// Execution summary
465    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/// Detailed information about a failed constraint.
522#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct FailedConstraintDetail {
524    /// Constraint name
525    pub name: String,
526    /// The failure result
527    pub result: ConstraintResult,
528    /// Related SQL queries
529    pub related_queries: Vec<QueryExecution>,
530    /// Execution timing
531    pub timing: Option<ConstraintTiming>,
532    /// Debugging suggestions
533    pub suggestions: Vec<String>,
534}
535
536/// Extension trait for ValidationResult to add debug information.
537pub trait ValidationResultDebugExt {
538    /// Get debug information if available.
539    fn debug_info(&self) -> Option<&DebugInfo>;
540
541    /// Generate an error report for failures.
542    fn error_report(&self) -> Option<ErrorReport>;
543}
544
545// Note: In production, this would be implemented on ValidationResult
546// For now, we'll leave it as a trait definition
547
548#[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}