//! Schema management with inference, evolution tracking, and drift detection //! //! This module provides comprehensive schema management capabilities including //! automatic schema inference, schema evolution tracking, drift detection, //! and schema validation. use std::collections::{HashMap, HashSet}; use std::fmt; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::{DataRecord, DataValue, Result, ValidationError}; /// Schema manager for handling schema operations #[derive(Debug, Clone)] pub struct SchemaManager { /// Current active schemas by name pub schemas: HashMap, /// Schema evolution history pub evolution_history: HashMap>, /// Configuration for schema operations pub config: SchemaConfig, /// Schema inference engine pub inference_engine: SchemaInference, } /// Configuration for schema management #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaConfig { /// Enable automatic schema evolution pub auto_evolution: bool, /// Strictness level for schema validation pub validation_strictness: ValidationStrictness, /// Minimum confidence for schema inference pub inference_confidence_threshold: f64, /// Enable drift detection pub drift_detection_enabled: bool, /// Sample size for schema inference pub inference_sample_size: usize, /// Maximum number of schema versions to keep pub max_schema_versions: usize, } /// Schema validation strictness levels #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum ValidationStrictness { /// Lenient validation (warnings only) Lenient, /// Standard validation (errors for major violations) Standard, /// Strict validation (errors for any violations) Strict, } /// Data schema representation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Schema { /// Schema name/identifier pub name: String, /// Schema version pub version: String, /// Schema description pub description: String, /// Field definitions pub fields: HashMap, /// Schema-level constraints pub constraints: Vec, /// Schema metadata pub metadata: SchemaMetadata, /// JSON Schema representation (if available) pub json_schema: Option, /// Creation timestamp pub created_at: DateTime, /// Last modified timestamp pub modified_at: DateTime, } /// Field definition in a schema #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FieldDefinition { /// Field name pub name: String, /// Data type pub data_type: DataType, /// Whether field is required pub required: bool, /// Whether field allows null values pub nullable: bool, /// Field description pub description: String, /// Field constraints pub constraints: Vec, /// Default value (if any) pub default_value: Option, /// Field metadata pub metadata: HashMap, } /// Supported data types #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum DataType { /// Boolean type Boolean, /// Integer type Integer, /// Floating point type Float, /// String type String, /// Date/timestamp type Timestamp, /// Array type (with element type) Array(Box), /// Object type Object, /// Union type (multiple possible types) Union(Vec), /// Null type Null, /// Unknown/any type Any, } /// Field-level constraints #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum FieldConstraint { /// Minimum value constraint MinValue(f64), /// Maximum value constraint MaxValue(f64), /// Minimum length constraint (for strings/arrays) MinLength(usize), /// Maximum length constraint (for strings/arrays) MaxLength(usize), /// Pattern constraint (regex) Pattern(String), /// Enumeration constraint (allowed values) Enum(Vec), /// Unique constraint Unique, /// Custom constraint with description Custom(String), } /// Schema-level constraints #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SchemaConstraint { /// Primary key constraint PrimaryKey(Vec), /// Foreign key constraint ForeignKey { fields: Vec, references: String, }, /// Unique constraint across multiple fields UniqueComposite(Vec), /// Check constraint with condition Check(String), /// Custom schema constraint Custom(String), } /// Schema metadata #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaMetadata { /// Schema author pub author: String, /// Schema tags pub tags: Vec, /// Schema category pub category: String, /// Data source information pub data_source: Option, /// Usage statistics pub usage_stats: UsageStats, /// Schema quality metrics pub quality_metrics: SchemaQualityMetrics, } /// Usage statistics for a schema #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UsageStats { /// Number of times schema was used for validation pub validation_count: usize, /// Number of validation failures pub failure_count: usize, /// Last used timestamp pub last_used: Option>, /// Average validation time in milliseconds pub avg_validation_time_ms: f64, } /// Schema quality metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaQualityMetrics { /// Schema completeness score (0-1) pub completeness: f64, /// Schema consistency score (0-1) pub consistency: f64, /// Schema clarity score (0-1) pub clarity: f64, /// Overall schema quality score (0-1) pub overall_quality: f64, } /// Schema version information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaVersion { /// Version number pub version: String, /// Schema at this version pub schema: Schema, /// Changes made in this version pub changes: Vec, /// Version timestamp pub timestamp: DateTime, /// Migration script (if needed) pub migration_script: Option, } /// Types of schema changes #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SchemaChange { /// Field was added FieldAdded { name: String, definition: FieldDefinition, }, /// Field was removed FieldRemoved { name: String }, /// Field was modified FieldModified { name: String, old_def: FieldDefinition, new_def: FieldDefinition, }, /// Field was renamed FieldRenamed { old_name: String, new_name: String }, /// Constraint was added ConstraintAdded(SchemaConstraint), /// Constraint was removed ConstraintRemoved(SchemaConstraint), /// Schema metadata changed MetadataChanged { old: SchemaMetadata, new: SchemaMetadata, }, } /// Schema inference engine #[derive(Debug, Clone)] pub struct SchemaInference { /// Configuration for inference pub config: InferenceConfig, /// Field type inference statistics pub field_stats: HashMap, } /// Configuration for schema inference #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InferenceConfig { /// Minimum confidence threshold for type inference pub confidence_threshold: f64, /// Sample size for inference pub sample_size: usize, /// Enable nullable field detection pub detect_nullable: bool, /// Enable constraint inference pub infer_constraints: bool, /// Enable pattern detection for strings pub detect_patterns: bool, } /// Statistics for field type inference #[derive(Debug, Clone)] pub struct FieldInferenceStats { /// Field name pub field_name: String, /// Type frequency counts pub type_counts: HashMap, /// Total samples seen pub total_samples: usize, /// Null value count pub null_count: usize, /// Sample values for analysis pub sample_values: Vec, } /// Schema evolution tracking #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaEvolution { /// Schema name pub schema_name: String, /// Evolution timeline pub timeline: Vec, /// Evolution statistics pub stats: EvolutionStats, } /// Schema evolution event #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EvolutionEvent { /// Event timestamp pub timestamp: DateTime, /// Event type pub event_type: EvolutionEventType, /// Event description pub description: String, /// Impact level pub impact_level: ImpactLevel, /// Affected components pub affected_components: Vec, } /// Types of evolution events #[derive(Debug, Clone, Serialize, Deserialize)] pub enum EvolutionEventType { /// Schema creation Created, /// Schema update Updated, /// Schema deprecated Deprecated, /// Schema deleted Deleted, /// Breaking change detected BreakingChange, /// Non-breaking change detected NonBreakingChange, /// Schema migration Migration, } /// Impact level of schema changes #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ImpactLevel { /// No impact None, /// Low impact Low, /// Medium impact Medium, /// High impact High, /// Critical impact Critical, } /// Evolution statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EvolutionStats { /// Total number of changes pub total_changes: usize, /// Breaking changes count pub breaking_changes: usize, /// Non-breaking changes count pub non_breaking_changes: usize, /// Average time between changes (in days) pub avg_change_interval_days: f64, /// Schema stability score (0-1) pub stability_score: f64, } /// Schema drift detection result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaDrift { /// Schema name pub schema_name: String, /// Drift detection timestamp pub detected_at: DateTime, /// Drift severity pub severity: DriftSeverity, /// Detected drift types pub drift_types: Vec, /// Drift score (0-1, higher = more drift) pub drift_score: f64, /// Recommended actions pub recommendations: Vec, } /// Schema drift severity levels #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum DriftSeverity { /// No significant drift None, /// Minor drift detected Minor, /// Moderate drift detected Moderate, /// Major drift detected Major, /// Critical drift detected Critical, } /// Types of schema drift #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DriftType { /// New fields detected NewFields(Vec), /// Fields removed RemovedFields(Vec), /// Type changes detected TypeChanges(HashMap), /// Constraint violations ConstraintViolations(Vec), /// Pattern changes in string fields PatternChanges(HashMap), /// Value distribution changes DistributionChanges(HashMap), } /// Schema validation result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaValidation { /// Whether validation passed pub is_valid: bool, /// Validation errors pub errors: Vec, /// Validation warnings pub warnings: Vec, /// Validation metadata pub metadata: ValidationMetadata, } /// Schema validation error #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaValidationError { /// Error code pub code: String, /// Error message pub message: String, /// Field path where error occurred pub field_path: String, /// Expected value or constraint pub expected: String, /// Actual value found pub actual: String, /// Error severity pub severity: ErrorSeverity, } /// Schema validation warning #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaValidationWarning { /// Warning code pub code: String, /// Warning message pub message: String, /// Field path where warning occurred pub field_path: String, /// Recommendation pub recommendation: String, } /// Error severity levels #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ErrorSeverity { /// Information level Info, /// Warning level Warning, /// Error level Error, /// Critical error level Critical, } /// Validation metadata #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationMetadata { /// Schema version used for validation pub schema_version: String, /// Validation timestamp pub validated_at: DateTime, /// Validation duration in milliseconds pub duration_ms: u64, /// Number of records validated pub records_validated: usize, /// Validation engine version pub engine_version: String, } /// Schema comparison result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaDiff { /// Source schema name pub source_schema: String, /// Target schema name pub target_schema: String, /// Detected differences pub differences: Vec, /// Compatibility assessment pub compatibility: CompatibilityLevel, /// Migration complexity pub migration_complexity: MigrationComplexity, } /// Individual schema difference #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SchemaDifference { /// Field added FieldAdded { name: String }, /// Field removed FieldRemoved { name: String }, /// Field type changed FieldTypeChanged { name: String, from: DataType, to: DataType, }, /// Field constraint changed FieldConstraintChanged { field: String, constraint: String }, /// Schema constraint added SchemaConstraintAdded(SchemaConstraint), /// Schema constraint removed SchemaConstraintRemoved(SchemaConstraint), } /// Schema compatibility levels #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum CompatibilityLevel { /// Fully compatible (no breaking changes) FullyCompatible, /// Backward compatible (new schema can read old data) BackwardCompatible, /// Forward compatible (old schema can read new data) ForwardCompatible, /// Incompatible (breaking changes) Incompatible, } /// Migration complexity levels #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum MigrationComplexity { /// No migration needed None, /// Simple migration (automatic) Simple, /// Moderate migration (some manual steps) Moderate, /// Complex migration (significant changes) Complex, /// Very complex migration (major restructuring) VeryComplex, } impl Default for SchemaConfig { fn default() -> Self { Self { auto_evolution: false, validation_strictness: ValidationStrictness::Standard, inference_confidence_threshold: 0.8, drift_detection_enabled: true, inference_sample_size: 1000, max_schema_versions: 10, } } } impl Default for InferenceConfig { fn default() -> Self { Self { confidence_threshold: 0.8, sample_size: 1000, detect_nullable: true, infer_constraints: true, detect_patterns: true, } } } impl SchemaManager { /// Create a new schema manager pub fn new() -> Self { Self { schemas: HashMap::new(), evolution_history: HashMap::new(), config: SchemaConfig::default(), inference_engine: SchemaInference::new(), } } /// Create schema manager with custom configuration pub fn with_config(config: SchemaConfig) -> Self { Self { schemas: HashMap::new(), evolution_history: HashMap::new(), inference_engine: SchemaInference::with_config(InferenceConfig { confidence_threshold: config.inference_confidence_threshold, sample_size: config.inference_sample_size, ..InferenceConfig::default() }), config, } } /// Register a new schema pub fn register_schema(&mut self, schema: Schema) -> Result<()> { let schema_name = schema.name.clone(); // Create initial version let version = SchemaVersion { version: schema.version.clone(), schema: schema.clone(), changes: vec![], timestamp: Utc::now(), migration_script: None, }; // Add to history self.evolution_history .entry(schema_name.clone()) .or_insert_with(Vec::new) .push(version); // Set as active schema self.schemas.insert(schema_name, schema); Ok(()) } /// Infer schema from sample data pub fn infer_schema(&mut self, name: String, records: &[DataRecord]) -> Result { self.inference_engine.infer_schema(name, records) } /// Validate a record against a schema pub fn validate_record(&self, record: &DataRecord) -> Result { // For this implementation, we'll do basic validation // In a full implementation, this would use the JSON Schema validator let mut errors = Vec::new(); let warnings = Vec::new(); // Check if we have a schema (simplified - would normally determine schema from record) if self.schemas.is_empty() { errors.push(SchemaValidationError { code: "NO_SCHEMA".to_string(), message: "No schema available for validation".to_string(), field_path: "".to_string(), expected: "Schema definition".to_string(), actual: "No schema".to_string(), severity: ErrorSeverity::Warning, }); } let is_valid = errors .iter() .all(|e| e.severity != ErrorSeverity::Error && e.severity != ErrorSeverity::Critical); Ok(SchemaValidation { is_valid, errors, warnings, metadata: ValidationMetadata { schema_version: "1.0.0".to_string(), validated_at: Utc::now(), duration_ms: 1, records_validated: 1, engine_version: "1.0.0".to_string(), }, }) } /// Detect schema drift pub fn detect_drift( &self, schema_name: &str, recent_records: &[DataRecord], ) -> Result> { if !self.config.drift_detection_enabled { return Ok(None); } if let Some(schema) = self.schemas.get(schema_name) { // Simplified drift detection let mut drift_types = Vec::new(); let mut drift_score = 0.0; // Check for new fields not in schema let mut all_fields = HashSet::new(); for record in recent_records { all_fields.extend(record.fields.keys().cloned()); } let schema_fields: HashSet = schema.fields.keys().cloned().collect(); let new_fields: Vec = all_fields.difference(&schema_fields).cloned().collect(); if !new_fields.is_empty() { drift_types.push(DriftType::NewFields(new_fields)); drift_score += 0.3; } let severity = match drift_score { s if s > 0.8 => DriftSeverity::Critical, s if s > 0.6 => DriftSeverity::Major, s if s > 0.4 => DriftSeverity::Moderate, s if s > 0.2 => DriftSeverity::Minor, _ => DriftSeverity::None, }; if severity != DriftSeverity::None { return Ok(Some(SchemaDrift { schema_name: schema_name.to_string(), detected_at: Utc::now(), severity, drift_types, drift_score, recommendations: vec![ "Consider updating schema to include new fields".to_string(), ], })); } } Ok(None) } /// Compare two schemas pub fn compare_schemas(&self, source_name: &str, target_name: &str) -> Result { let source_schema = self.schemas.get(source_name).ok_or_else(|| { ValidationError::Schema(format!("Source schema '{}' not found", source_name)) })?; let target_schema = self.schemas.get(target_name).ok_or_else(|| { ValidationError::Schema(format!("Target schema '{}' not found", target_name)) })?; let mut differences = Vec::new(); // Compare fields for (field_name, source_field) in &source_schema.fields { if let Some(target_field) = target_schema.fields.get(field_name) { if source_field.data_type != target_field.data_type { differences.push(SchemaDifference::FieldTypeChanged { name: field_name.clone(), from: source_field.data_type.clone(), to: target_field.data_type.clone(), }); } } else { differences.push(SchemaDifference::FieldRemoved { name: field_name.clone(), }); } } // Check for new fields for field_name in target_schema.fields.keys() { if !source_schema.fields.contains_key(field_name) { differences.push(SchemaDifference::FieldAdded { name: field_name.clone(), }); } } // Determine compatibility let compatibility = if differences.is_empty() { CompatibilityLevel::FullyCompatible } else if differences.iter().any(|d| { matches!( d, SchemaDifference::FieldRemoved { .. } | SchemaDifference::FieldTypeChanged { .. } ) }) { CompatibilityLevel::Incompatible } else { CompatibilityLevel::BackwardCompatible }; // Determine migration complexity let migration_complexity = match differences.len() { 0 => MigrationComplexity::None, 1..=2 => MigrationComplexity::Simple, 3..=5 => MigrationComplexity::Moderate, 6..=10 => MigrationComplexity::Complex, _ => MigrationComplexity::VeryComplex, }; Ok(SchemaDiff { source_schema: source_name.to_string(), target_schema: target_name.to_string(), differences, compatibility, migration_complexity, }) } /// Get schema evolution history pub fn get_evolution_history(&self, schema_name: &str) -> Option<&Vec> { self.evolution_history.get(schema_name) } } impl Default for SchemaManager { fn default() -> Self { Self::new() } } impl SchemaInference { /// Create a new schema inference engine pub fn new() -> Self { Self { config: InferenceConfig::default(), field_stats: HashMap::new(), } } /// Create inference engine with custom configuration pub fn with_config(config: InferenceConfig) -> Self { Self { config, field_stats: HashMap::new(), } } /// Infer schema from sample records pub fn infer_schema(&mut self, name: String, records: &[DataRecord]) -> Result { if records.is_empty() { return Err(ValidationError::Schema( "Cannot infer schema from empty dataset".to_string(), )); } // Collect field statistics self.collect_field_stats(records); // Infer field definitions let mut fields = HashMap::new(); for (field_name, stats) in &self.field_stats { let field_def = self.infer_field_definition(field_name, stats)?; fields.insert(field_name.clone(), field_def); } // Create schema Ok(Schema { name, version: "1.0.0".to_string(), description: "Automatically inferred schema".to_string(), fields, constraints: Vec::new(), metadata: SchemaMetadata { author: "Schema Inference Engine".to_string(), tags: vec!["auto-generated".to_string()], category: "inferred".to_string(), data_source: None, usage_stats: UsageStats { validation_count: 0, failure_count: 0, last_used: None, avg_validation_time_ms: 0.0, }, quality_metrics: SchemaQualityMetrics { completeness: 0.8, consistency: 0.9, clarity: 0.7, overall_quality: 0.8, }, }, json_schema: None, created_at: Utc::now(), modified_at: Utc::now(), }) } fn collect_field_stats(&mut self, records: &[DataRecord]) { self.field_stats.clear(); let sample_limit = self.config.sample_size.min(records.len()); for record in records.iter().take(sample_limit) { for (field_name, value) in &record.fields { let stats = self .field_stats .entry(field_name.clone()) .or_insert_with(|| FieldInferenceStats { field_name: field_name.clone(), type_counts: HashMap::new(), total_samples: 0, null_count: 0, sample_values: Vec::new(), }); stats.total_samples += 1; if value.is_null() { stats.null_count += 1; } else { // Keep sample values for analysis (limit to 100) if stats.sample_values.len() < 100 { stats.sample_values.push(value.clone()); } } } } // Calculate type counts in a separate pass to avoid borrow checker issues for record in records.iter().take(sample_limit) { for (field_name, value) in &record.fields { if !value.is_null() { let data_type = self.value_to_data_type(value); if let Some(stats) = self.field_stats.get_mut(field_name) { *stats.type_counts.entry(data_type).or_insert(0) += 1; } } } } } fn infer_field_definition( &self, field_name: &str, stats: &FieldInferenceStats, ) -> Result { // Determine the most likely data type let data_type = if let Some((most_common_type, count)) = stats.type_counts.iter().max_by_key(|(_, count)| **count) { let confidence = *count as f64 / stats.total_samples as f64; if confidence >= self.config.confidence_threshold { most_common_type.clone() } else { DataType::Any // Mixed types, use Any } } else { DataType::Any }; // Determine if field is nullable let nullable = if self.config.detect_nullable { stats.null_count > 0 } else { false }; // Determine if field is required (simplified logic) let required = stats.null_count == 0 && stats.total_samples > 0; // Infer constraints if enabled let mut constraints = Vec::new(); if self.config.infer_constraints { constraints.extend(self.infer_field_constraints(&data_type, &stats.sample_values)); } Ok(FieldDefinition { name: field_name.to_string(), data_type, required, nullable, description: format!( "Inferred field (confidence: {:.2})", *stats.type_counts.values().max().unwrap_or(&0) as f64 / stats.total_samples as f64 ), constraints, default_value: None, metadata: HashMap::new(), }) } fn value_to_data_type(&self, value: &DataValue) -> DataType { match value { DataValue::Null => DataType::Null, DataValue::Bool(_) => DataType::Boolean, DataValue::Int(_) => DataType::Integer, DataValue::Float(_) => DataType::Float, DataValue::String(_) => DataType::String, DataValue::Array(_) => DataType::Array(Box::new(DataType::Any)), DataValue::Object(_) => DataType::Object, DataValue::Timestamp(_) => DataType::Timestamp, } } fn infer_field_constraints( &self, data_type: &DataType, sample_values: &[DataValue], ) -> Vec { let mut constraints = Vec::new(); match data_type { DataType::String => { if let Some(lengths) = self.collect_string_lengths(sample_values) { if let (Some(&min_len), Some(&max_len)) = (lengths.iter().min(), lengths.iter().max()) { if min_len == max_len && min_len > 0 { constraints.push(FieldConstraint::MinLength(min_len)); constraints.push(FieldConstraint::MaxLength(max_len)); } } } } DataType::Integer | DataType::Float => { if let Some(numbers) = self.collect_numeric_values(sample_values) { if let (Some(min_val), Some(max_val)) = ( numbers.iter().min_by(|a, b| a.total_cmp(b)), numbers.iter().max_by(|a, b| a.total_cmp(b)), ) { // Only add constraints if there's a reasonable range if (max_val - min_val) / min_val.abs() > 0.1 { constraints.push(FieldConstraint::MinValue(*min_val)); constraints.push(FieldConstraint::MaxValue(*max_val)); } } } } _ => {} // No constraints for other types in this simple implementation } constraints } fn collect_string_lengths(&self, values: &[DataValue]) -> Option> { let lengths: Vec = values .iter() .filter_map(|v| v.as_string().map(|s| s.len())) .collect(); if lengths.is_empty() { None } else { Some(lengths) } } fn collect_numeric_values(&self, values: &[DataValue]) -> Option> { let numbers: Vec = values.iter().filter_map(|v| v.as_f64()).collect(); if numbers.is_empty() { None } else { Some(numbers) } } } impl Default for SchemaInference { fn default() -> Self { Self::new() } } impl fmt::Display for DataType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { DataType::Boolean => write!(f, "boolean"), DataType::Integer => write!(f, "integer"), DataType::Float => write!(f, "float"), DataType::String => write!(f, "string"), DataType::Timestamp => write!(f, "timestamp"), DataType::Array(inner) => write!(f, "array[{}]", inner), DataType::Object => write!(f, "object"), DataType::Union(types) => { let type_names: Vec = types.iter().map(|t| t.to_string()).collect(); write!(f, "union[{}]", type_names.join("|")) } DataType::Null => write!(f, "null"), DataType::Any => write!(f, "any"), } } } #[cfg(test)] mod tests { use super::*; use crate::DataValue; use std::collections::HashMap; #[test] fn test_schema_manager_creation() { let manager = SchemaManager::new(); assert!(manager.schemas.is_empty()); assert!(manager.evolution_history.is_empty()); } #[test] fn test_schema_inference() { let mut inference = SchemaInference::new(); let mut records = Vec::new(); for i in 0..5 { let mut fields = HashMap::new(); fields.insert("id".to_string(), DataValue::Int(i)); fields.insert("name".to_string(), DataValue::String(format!("Name {}", i))); fields.insert("active".to_string(), DataValue::Bool(i % 2 == 0)); records.push(DataRecord { id: i.to_string(), timestamp: Utc::now(), fields, metadata: HashMap::new(), }); } let schema = inference .infer_schema("test_schema".to_string(), &records) .unwrap(); assert_eq!(schema.name, "test_schema"); assert_eq!(schema.fields.len(), 3); assert!(schema.fields.contains_key("id")); assert!(schema.fields.contains_key("name")); assert!(schema.fields.contains_key("active")); } #[test] fn test_data_type_inference() { let inference = SchemaInference::new(); assert_eq!( inference.value_to_data_type(&DataValue::Bool(true)), DataType::Boolean ); assert_eq!( inference.value_to_data_type(&DataValue::Int(42)), DataType::Integer ); assert_eq!( inference.value_to_data_type(&DataValue::Float(3.14)), DataType::Float ); assert_eq!( inference.value_to_data_type(&DataValue::String("test".to_string())), DataType::String ); assert_eq!( inference.value_to_data_type(&DataValue::Null), DataType::Null ); } #[test] fn test_schema_validation() { let manager = SchemaManager::new(); let record = DataRecord { id: "test".to_string(), timestamp: Utc::now(), fields: HashMap::new(), metadata: HashMap::new(), }; let validation = manager.validate_record(&record).unwrap(); // Should have warnings about no schema assert!(!validation.warnings.is_empty() || !validation.errors.is_empty()); } #[test] fn test_impact_level_ordering() { assert!(ImpactLevel::None < ImpactLevel::Low); assert!(ImpactLevel::Low < ImpactLevel::Medium); assert!(ImpactLevel::Medium < ImpactLevel::High); assert!(ImpactLevel::High < ImpactLevel::Critical); } #[test] fn test_migration_complexity_ordering() { assert!(MigrationComplexity::None < MigrationComplexity::Simple); assert!(MigrationComplexity::Simple < MigrationComplexity::Moderate); assert!(MigrationComplexity::Moderate < MigrationComplexity::Complex); assert!(MigrationComplexity::Complex < MigrationComplexity::VeryComplex); } }