Skip to main content

sqlitegraph/backend/native/v2/wal/recovery/errors/
validation.rs

1//! V2 WAL Recovery Validation Errors
2//!
3//! This module provides validation-specific error handling for the V2 WAL recovery system,
4//! including error factories for validation failures, context builders for validation scenarios,
5//! and specialized validation error types.
6
7use super::core::{
8    ErrorContext, ErrorSeverity, RecoveryError, RecoveryErrorKind, RecoverySuggestion,
9};
10
11/// Validation-specific error context builders
12pub struct ValidationErrorContext;
13
14impl ValidationErrorContext {
15    /// Create context for V2 format validation errors
16    pub fn v2_format(
17        lsn_range: Option<(u64, u64)>,
18        expected_format: &str,
19        actual_format: Option<&str>,
20    ) -> ErrorContext {
21        let mut context = ErrorContext::default();
22        context.lsn_range = lsn_range;
23        context.recovery_state = Some("V2 Format Validation".to_string());
24
25        if let Some(format) = actual_format {
26            context
27                .metadata
28                .insert("expected_format".to_string(), expected_format.to_string());
29            context
30                .metadata
31                .insert("actual_format".to_string(), format.to_string());
32        } else {
33            context
34                .metadata
35                .insert("expected_format".to_string(), expected_format.to_string());
36            context
37                .metadata
38                .insert("actual_format".to_string(), "None".to_string());
39        }
40
41        context
42    }
43
44    /// Create context for checksum validation errors
45    pub fn checksum_validation(
46        lsn: u64,
47        expected_checksum: u64,
48        actual_checksum: u64,
49        algorithm: &str,
50    ) -> ErrorContext {
51        let mut context = ErrorContext::default();
52        context.lsn_range = Some((lsn, lsn));
53        context.recovery_state = Some("Checksum Validation".to_string());
54
55        context.metadata.insert("lsn".to_string(), lsn.to_string());
56        context.metadata.insert(
57            "expected_checksum".to_string(),
58            format!("{:x}", expected_checksum),
59        );
60        context.metadata.insert(
61            "actual_checksum".to_string(),
62            format!("{:x}", actual_checksum),
63        );
64        context
65            .metadata
66            .insert("checksum_algorithm".to_string(), algorithm.to_string());
67
68        context
69    }
70
71    /// Create context for consistency validation errors
72    pub fn consistency_validation(
73        node_count_mismatch: Option<(u64, u64)>,
74        edge_count_mismatch: Option<(u64, u64)>,
75        cluster_inconsistency: Option<(u64, u64)>,
76    ) -> ErrorContext {
77        let mut context = ErrorContext::default();
78        context.recovery_state = Some("Consistency Validation".to_string());
79
80        if let Some((expected, actual)) = node_count_mismatch {
81            context
82                .metadata
83                .insert("expected_node_count".to_string(), expected.to_string());
84            context
85                .metadata
86                .insert("actual_node_count".to_string(), actual.to_string());
87        }
88
89        if let Some((expected, actual)) = edge_count_mismatch {
90            context
91                .metadata
92                .insert("expected_edge_count".to_string(), expected.to_string());
93            context
94                .metadata
95                .insert("actual_edge_count".to_string(), actual.to_string());
96        }
97
98        if let Some((cluster_id, error_type)) = cluster_inconsistency {
99            context
100                .metadata
101                .insert("cluster_id".to_string(), cluster_id.to_string());
102            context
103                .metadata
104                .insert("cluster_error_type".to_string(), error_type.to_string());
105        }
106
107        context
108    }
109
110    /// Create context for structural validation errors
111    pub fn structural_validation(
112        record_type: &str,
113        field_name: &str,
114        expected_type: &str,
115        actual_value: Option<&str>,
116    ) -> ErrorContext {
117        let mut context = ErrorContext::default();
118        context.recovery_state = Some("Structural Validation".to_string());
119
120        context
121            .metadata
122            .insert("record_type".to_string(), record_type.to_string());
123        context
124            .metadata
125            .insert("field_name".to_string(), field_name.to_string());
126        context
127            .metadata
128            .insert("expected_type".to_string(), expected_type.to_string());
129
130        if let Some(value) = actual_value {
131            context
132                .metadata
133                .insert("actual_value".to_string(), value.to_string());
134        } else {
135            context
136                .metadata
137                .insert("actual_value".to_string(), "None".to_string());
138        }
139
140        context
141    }
142
143    /// Create context for range validation errors
144    pub fn range_validation(
145        field_name: &str,
146        value: u64,
147        min_allowed: u64,
148        max_allowed: u64,
149    ) -> ErrorContext {
150        let mut context = ErrorContext::default();
151        context.recovery_state = Some("Range Validation".to_string());
152
153        context
154            .metadata
155            .insert("field_name".to_string(), field_name.to_string());
156        context
157            .metadata
158            .insert("value".to_string(), value.to_string());
159        context
160            .metadata
161            .insert("min_allowed".to_string(), min_allowed.to_string());
162        context
163            .metadata
164            .insert("max_allowed".to_string(), max_allowed.to_string());
165
166        context
167    }
168
169    /// Create context for dependency validation errors
170    pub fn dependency_validation(
171        dependent_type: &str,
172        dependency_type: &str,
173        dependency_id: u64,
174        missing_reference: bool,
175    ) -> ErrorContext {
176        let mut context = ErrorContext::default();
177        context.recovery_state = Some("Dependency Validation".to_string());
178
179        context
180            .metadata
181            .insert("dependent_type".to_string(), dependent_type.to_string());
182        context
183            .metadata
184            .insert("dependency_type".to_string(), dependency_type.to_string());
185        context
186            .metadata
187            .insert("dependency_id".to_string(), dependency_id.to_string());
188        context.metadata.insert(
189            "missing_reference".to_string(),
190            missing_reference.to_string(),
191        );
192
193        context
194    }
195}
196
197/// Validation-specific error factories
198pub struct ValidationErrorFactory;
199
200impl ValidationErrorFactory {
201    /// Create V2 format validation error
202    pub fn v2_format_error(
203        message: impl Into<String>,
204        expected_format: &str,
205        actual_format: Option<&str>,
206    ) -> RecoveryError {
207        RecoveryError::new(RecoveryErrorKind::Validation, message)
208            .with_context(ValidationErrorContext::v2_format(
209                None,
210                expected_format,
211                actual_format,
212            ))
213            .with_recovery(RecoverySuggestion::ValidateWalFile)
214            .with_severity(ErrorSeverity::Error)
215    }
216
217    /// Create checksum validation error
218    pub fn checksum_error(
219        lsn: u64,
220        expected_checksum: u64,
221        actual_checksum: u64,
222        algorithm: &str,
223    ) -> RecoveryError {
224        let message = format!(
225            "Checksum validation failed at LSN {}: expected {:x}, got {:x} ({})",
226            lsn, expected_checksum, actual_checksum, algorithm
227        );
228
229        RecoveryError::new(RecoveryErrorKind::Validation, message)
230            .with_context(ValidationErrorContext::checksum_validation(
231                lsn,
232                expected_checksum,
233                actual_checksum,
234                algorithm,
235            ))
236            .with_recovery(RecoverySuggestion::ForceRecovery)
237            .with_severity(ErrorSeverity::Error)
238    }
239
240    /// Create consistency validation error
241    pub fn consistency_error(
242        message: impl Into<String>,
243        node_count_mismatch: Option<(u64, u64)>,
244        edge_count_mismatch: Option<(u64, u64)>,
245    ) -> RecoveryError {
246        RecoveryError::new(RecoveryErrorKind::Consistency, message)
247            .with_context(ValidationErrorContext::consistency_validation(
248                node_count_mismatch,
249                edge_count_mismatch,
250                None,
251            ))
252            .with_recovery(RecoverySuggestion::ForceRecovery)
253            .with_severity(ErrorSeverity::Critical)
254    }
255
256    /// Create structural validation error
257    pub fn structural_error(
258        message: impl Into<String>,
259        record_type: &str,
260        field_name: &str,
261        expected_type: &str,
262    ) -> RecoveryError {
263        RecoveryError::new(RecoveryErrorKind::Validation, message)
264            .with_context(ValidationErrorContext::structural_validation(
265                record_type,
266                field_name,
267                expected_type,
268                None,
269            ))
270            .with_recovery(RecoverySuggestion::ForceRecovery)
271            .with_severity(ErrorSeverity::Error)
272    }
273
274    /// Create range validation error
275    pub fn range_error(
276        message: impl Into<String>,
277        field_name: &str,
278        value: u64,
279        min_allowed: u64,
280        max_allowed: u64,
281    ) -> RecoveryError {
282        RecoveryError::new(RecoveryErrorKind::Validation, message)
283            .with_context(ValidationErrorContext::range_validation(
284                field_name,
285                value,
286                min_allowed,
287                max_allowed,
288            ))
289            .with_recovery(RecoverySuggestion::ForceRecovery)
290            .with_severity(ErrorSeverity::Error)
291    }
292
293    /// Create dependency validation error
294    pub fn dependency_error(
295        message: impl Into<String>,
296        dependent_type: &str,
297        dependency_type: &str,
298        dependency_id: u64,
299    ) -> RecoveryError {
300        RecoveryError::new(RecoveryErrorKind::Consistency, message)
301            .with_context(ValidationErrorContext::dependency_validation(
302                dependent_type,
303                dependency_type,
304                dependency_id,
305                true,
306            ))
307            .with_recovery(RecoverySuggestion::ForceRecovery)
308            .with_severity(ErrorSeverity::Error)
309    }
310
311    /// Create integrity validation error
312    pub fn integrity_error(
313        message: impl Into<String>,
314        lsn_range: Option<(u64, u64)>,
315        affected_records: u64,
316    ) -> RecoveryError {
317        let mut context =
318            ValidationErrorContext::v2_format(lsn_range, "V2 Integrity", Some("Corrupted"));
319        context
320            .metadata
321            .insert("affected_records".to_string(), affected_records.to_string());
322
323        RecoveryError::new(RecoveryErrorKind::Corruption, message)
324            .with_context(context)
325            .with_recovery(RecoverySuggestion::RestoreFromBackup)
326            .with_severity(ErrorSeverity::Critical)
327    }
328
329    /// Create schema validation error
330    pub fn schema_error(
331        message: impl Into<String>,
332        expected_version: &str,
333        actual_version: &str,
334    ) -> RecoveryError {
335        let mut context =
336            ValidationErrorContext::v2_format(None, expected_version, Some(actual_version));
337        context.recovery_state = Some("Schema Validation".to_string());
338
339        RecoveryError::new(RecoveryErrorKind::V2Integration, message)
340            .with_context(context)
341            .with_recovery(RecoverySuggestion::ValidateWalFile)
342            .with_severity(ErrorSeverity::Critical)
343    }
344}
345
346/// Extension trait for RecoveryError to provide validation-specific methods
347pub trait ValidationErrorExt {
348    /// Convert to a validation error with context
349    fn as_validation_error(self, validation_stage: &str) -> Self;
350
351    /// Add validation-specific context
352    fn with_validation_context(
353        self,
354        stage: &str,
355        record_count: Option<u64>,
356        failure_count: Option<u64>,
357    ) -> Self;
358
359    /// Mark as recoverable validation error
360    fn as_recoverable_validation(self) -> Self;
361
362    /// Mark as critical validation error
363    fn as_critical_validation(self) -> Self;
364}
365
366impl ValidationErrorExt for RecoveryError {
367    fn as_validation_error(self, validation_stage: &str) -> Self {
368        let mut context = self.context.clone();
369        context.recovery_state = Some(format!("Validation: {}", validation_stage));
370
371        self.with_context(context)
372            .with_kind(RecoveryErrorKind::Validation)
373    }
374
375    fn with_validation_context(
376        self,
377        stage: &str,
378        record_count: Option<u64>,
379        failure_count: Option<u64>,
380    ) -> Self {
381        let mut context = self.context.clone();
382        context.recovery_state = Some(format!("Validation: {}", stage));
383
384        if let Some(count) = record_count {
385            context
386                .metadata
387                .insert("records_validated".to_string(), count.to_string());
388        }
389
390        if let Some(count) = failure_count {
391            context
392                .metadata
393                .insert("validation_failures".to_string(), count.to_string());
394        }
395
396        self.with_context(context)
397    }
398
399    fn as_recoverable_validation(self) -> Self {
400        self.with_severity(ErrorSeverity::Error)
401            .with_recovery(RecoverySuggestion::ForceRecovery)
402    }
403
404    fn as_critical_validation(self) -> Self {
405        self.with_severity(ErrorSeverity::Critical)
406            .with_recovery(RecoverySuggestion::RestoreFromBackup)
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::super::core::ErrorSeverity;
413    use super::*;
414
415    #[test]
416    fn test_validation_error_context_v2_format() {
417        let context = ValidationErrorContext::v2_format(Some((1000, 2000)), "V2.0", Some("V1.0"));
418
419        assert_eq!(context.lsn_range, Some((1000, 2000)));
420        assert_eq!(
421            context.recovery_state,
422            Some("V2 Format Validation".to_string())
423        );
424        assert_eq!(
425            context.metadata.get("expected_format"),
426            Some(&"V2.0".to_string())
427        );
428        assert_eq!(
429            context.metadata.get("actual_format"),
430            Some(&"V1.0".to_string())
431        );
432    }
433
434    #[test]
435    fn test_validation_error_context_checksum() {
436        let context = ValidationErrorContext::checksum_validation(1234, 0xABCD, 0xDCBA, "CRC32");
437
438        assert_eq!(context.lsn_range, Some((1234, 1234)));
439        assert_eq!(
440            context.recovery_state,
441            Some("Checksum Validation".to_string())
442        );
443        assert_eq!(context.metadata.get("lsn"), Some(&"1234".to_string()));
444        assert_eq!(
445            context.metadata.get("expected_checksum"),
446            Some(&"abcd".to_string())
447        );
448        assert_eq!(
449            context.metadata.get("actual_checksum"),
450            Some(&"dcba".to_string())
451        );
452        assert_eq!(
453            context.metadata.get("checksum_algorithm"),
454            Some(&"CRC32".to_string())
455        );
456    }
457
458    #[test]
459    fn test_validation_error_factory_checksum() {
460        let error = ValidationErrorFactory::checksum_error(1234, 0xABCD, 0xDCBA, "CRC32");
461
462        assert_eq!(error.kind, RecoveryErrorKind::Validation);
463        assert!(error.message.contains("1234"));
464        assert!(error.message.contains("abcd"));
465        assert!(error.message.contains("dcba"));
466        assert!(matches!(error.recovery, RecoverySuggestion::ForceRecovery));
467        assert_eq!(error.severity(), ErrorSeverity::Error);
468    }
469
470    #[test]
471    fn test_validation_error_factory_consistency() {
472        let error = ValidationErrorFactory::consistency_error(
473            "Node count mismatch",
474            Some((100, 90)),
475            Some((200, 180)),
476        );
477
478        assert_eq!(error.kind, RecoveryErrorKind::Consistency);
479        assert_eq!(error.severity(), ErrorSeverity::Critical);
480        assert_eq!(
481            error.context.metadata.get("expected_node_count"),
482            Some(&"100".to_string())
483        );
484        assert_eq!(
485            error.context.metadata.get("actual_node_count"),
486            Some(&"90".to_string())
487        );
488        assert_eq!(
489            error.context.metadata.get("expected_edge_count"),
490            Some(&"200".to_string())
491        );
492        assert_eq!(
493            error.context.metadata.get("actual_edge_count"),
494            Some(&"180".to_string())
495        );
496    }
497
498    #[test]
499    fn test_validation_error_factory_structural() {
500        let error = ValidationErrorFactory::structural_error(
501            "Invalid field type",
502            "NodeRecord",
503            "node_id",
504            "u64",
505        );
506
507        assert_eq!(error.kind, RecoveryErrorKind::Validation);
508        assert_eq!(
509            error.context.metadata.get("record_type"),
510            Some(&"NodeRecord".to_string())
511        );
512        assert_eq!(
513            error.context.metadata.get("field_name"),
514            Some(&"node_id".to_string())
515        );
516        assert_eq!(
517            error.context.metadata.get("expected_type"),
518            Some(&"u64".to_string())
519        );
520    }
521
522    #[test]
523    fn test_validation_error_extension() {
524        let base_error = RecoveryError::new(RecoveryErrorKind::Io, "Test error");
525
526        let validation_error = base_error
527            .as_validation_error("Format Check")
528            .with_validation_context("V2", Some(100), Some(5));
529
530        assert_eq!(
531            validation_error.context.recovery_state,
532            Some("Validation: V2".to_string())
533        );
534        assert_eq!(
535            validation_error.context.metadata.get("records_validated"),
536            Some(&"100".to_string())
537        );
538        assert_eq!(
539            validation_error.context.metadata.get("validation_failures"),
540            Some(&"5".to_string())
541        );
542    }
543
544    #[test]
545    fn test_validation_error_recovery_levels() {
546        let base_error = RecoveryError::new(RecoveryErrorKind::Validation, "Test error");
547
548        let recoverable = base_error.clone().as_recoverable_validation();
549        let critical = base_error.as_critical_validation();
550
551        assert_eq!(recoverable.severity(), ErrorSeverity::Error);
552        assert!(matches!(
553            recoverable.recovery,
554            RecoverySuggestion::ForceRecovery
555        ));
556
557        assert_eq!(critical.severity(), ErrorSeverity::Critical);
558        assert!(matches!(
559            critical.recovery,
560            RecoverySuggestion::RestoreFromBackup
561        ));
562    }
563
564    #[test]
565    fn test_validation_error_factory_schema() {
566        let error = ValidationErrorFactory::schema_error("Version mismatch", "2.0", "1.0");
567
568        assert_eq!(error.kind, RecoveryErrorKind::V2Integration);
569        assert_eq!(error.severity(), ErrorSeverity::Critical);
570        assert_eq!(
571            error.context.recovery_state,
572            Some("Schema Validation".to_string())
573        );
574        assert_eq!(
575            error.context.metadata.get("expected_format"),
576            Some(&"2.0".to_string())
577        );
578        assert_eq!(
579            error.context.metadata.get("actual_format"),
580            Some(&"1.0".to_string())
581        );
582    }
583}