""" Migration validator for ensuring migration safety and integrity. This module provides validation utilities to check migrations before they are executed, ensuring they meet quality standards. """ import logging from typing import List, Optional, Set from .base import Migration, MigrationError logger = logging.getLogger(__name__) class MigrationValidator: """ Validates migrations before execution. Performs various checks to ensure migrations are safe to run, including version uniqueness, naming conventions, and dependency resolution. """ def __init__(self): """Initialize migration validator.""" self.errors: List[str] = [] self.warnings: List[str] = [] def reset(self) -> None: """Clear validation results.""" self.errors.clear() self.warnings.clear() def validate_migration(self, migration: Migration) -> bool: """ Validate a single migration. Args: migration: Migration to validate Returns: True if migration is valid, False otherwise """ self.reset() # Check version format if not self._validate_version_format(migration.version): self.errors.append( f"Invalid version format: {migration.version}. " "Expected format: YYYYMMDD_NNN" ) # Check description if not migration.description or len(migration.description) < 5: self.errors.append( f"Migration {migration.version} has invalid " f"description: '{migration.description}'" ) # Check for implementation if not hasattr(migration, "upgrade") or not callable( getattr(migration, "upgrade") ): self.errors.append( f"Migration {migration.version} missing upgrade method" ) if not hasattr(migration, "downgrade") or not callable( getattr(migration, "downgrade") ): self.errors.append( f"Migration {migration.version} missing downgrade method" ) return len(self.errors) == 0 def validate_migrations(self, migrations: List[Migration]) -> bool: """ Validate a list of migrations. Args: migrations: List of migrations to validate Returns: True if all migrations are valid, False otherwise """ self.reset() if not migrations: self.warnings.append("No migrations to validate") return True # Check for duplicate versions versions: Set[str] = set() for migration in migrations: if migration.version in versions: self.errors.append( f"Duplicate migration version: {migration.version}" ) versions.add(migration.version) # Return early if duplicates found if self.errors: return False # Validate each migration for migration in migrations: if not self.validate_migration(migration): logger.error( f"Migration {migration.version} " f"validation failed: {self.errors}" ) return False # Check version ordering sorted_versions = sorted([m.version for m in migrations]) actual_versions = [m.version for m in migrations] if sorted_versions != actual_versions: self.warnings.append( "Migrations are not in chronological order" ) return len(self.errors) == 0 def _validate_version_format(self, version: str) -> bool: """ Validate version string format. Args: version: Version string to validate Returns: True if format is valid """ # Expected format: YYYYMMDD_NNN or YYYYMMDD_NNN_description if not version: return False parts = version.split("_") if len(parts) < 2: return False # Check date part (YYYYMMDD) date_part = parts[0] if len(date_part) != 8 or not date_part.isdigit(): return False # Check sequence part (NNN) seq_part = parts[1] if not seq_part.isdigit(): return False return True def check_migration_conflicts( self, pending: List[Migration], applied: List[str], ) -> Optional[str]: """ Check for conflicts between pending and applied migrations. Args: pending: List of pending migrations applied: List of applied migration versions Returns: Error message if conflicts found, None otherwise """ # Check if any pending migration has version lower than applied if not applied: return None latest_applied = max(applied) for migration in pending: if migration.version < latest_applied: return ( f"Migration {migration.version} is older than " f"latest applied migration {latest_applied}. " "This may indicate a merge conflict." ) return None def get_validation_report(self) -> str: """ Get formatted validation report. Returns: Formatted report string """ report = [] if self.errors: report.append("Validation Errors:") for error in self.errors: report.append(f" - {error}") if self.warnings: report.append("Validation Warnings:") for warning in self.warnings: report.append(f" - {warning}") if not self.errors and not self.warnings: report.append("All validations passed") return "\n".join(report) def raise_if_invalid(self) -> None: """ Raise exception if validation failed. Raises: MigrationError: If validation errors exist """ if self.errors: error_msg = "\n".join(self.errors) raise MigrationError( f"Migration validation failed:\n{error_msg}" )