Migrations

Supporting Multiple Django Versions

Creating migrations that work across multiple Django versions requires understanding version differences, compatibility patterns, and migration system evolution. This section covers strategies for maintaining migration compatibility while supporting different Django releases.

Supporting Multiple Django Versions

Creating migrations that work across multiple Django versions requires understanding version differences, compatibility patterns, and migration system evolution. This section covers strategies for maintaining migration compatibility while supporting different Django releases.

Django Version Compatibility

Understanding Version Differences

# Migration compatibility across Django versions
import django
from django.db import migrations, models

class VersionAwareMigration(migrations.Migration):
    """Migration that adapts to different Django versions"""
    
    dependencies = [
        ('myapp', '0001_initial'),
    ]
    
    def __init__(self, name, app_label):
        super().__init__(name, app_label)
        
        # Adapt operations based on Django version
        self.operations = self._get_version_specific_operations()
    
    def _get_version_specific_operations(self):
        """Get operations adapted for current Django version"""
        
        operations = []
        
        # Django 3.2+ supports functional indexes
        if django.VERSION >= (3, 2):
            operations.append(
                migrations.AddIndex(
                    model_name='post',
                    index=models.Index(
                        models.functions.Upper('title'),
                        name='post_upper_title_idx'
                    ),
                )
            )
        else:
            # Fallback for older versions
            operations.append(
                migrations.RunSQL(
                    sql="CREATE INDEX post_upper_title_idx ON myapp_post (UPPER(title));",
                    reverse_sql="DROP INDEX post_upper_title_idx;",
                )
            )
        
        # Django 4.0+ supports covering indexes (PostgreSQL)
        if django.VERSION >= (4, 0):
            operations.append(
                migrations.AddIndex(
                    model_name='post',
                    index=models.Index(
                        fields=['status'],
                        include=['title', 'created_at'],
                        name='post_status_covering_idx'
                    ),
                )
            )
        
        # Django 3.1+ supports JSONField in all backends
        if django.VERSION >= (3, 1):
            operations.append(
                migrations.AddField(
                    model_name='post',
                    name='metadata',
                    field=models.JSONField(default=dict),
                )
            )
        else:
            # Use TextField with JSON for older versions
            operations.append(
                migrations.AddField(
                    model_name='post',
                    name='metadata',
                    field=models.TextField(default='{}'),
                )
            )
        
        return operations

# Field type compatibility
class FieldCompatibility:
    """Handle field type differences across versions"""
    
    @staticmethod
    def get_json_field():
        """Get appropriate JSON field for Django version"""
        
        if django.VERSION >= (3, 1):
            # Native JSONField available
            return models.JSONField(default=dict)
        else:
            # Use contrib.postgres.fields for PostgreSQL
            try:
                from django.contrib.postgres.fields import JSONField
                return JSONField(default=dict)
            except ImportError:
                # Fallback to TextField for other databases
                return models.TextField(default='{}')
    
    @staticmethod
    def get_big_auto_field():
        """Get appropriate auto field for Django version"""
        
        if django.VERSION >= (3, 2):
            # BigAutoField available
            return models.BigAutoField(primary_key=True)
        else:
            # Use regular AutoField
            return models.AutoField(primary_key=True)
    
    @staticmethod
    def get_positive_big_integer_field():
        """Get appropriate positive integer field"""
        
        if django.VERSION >= (3, 1):
            return models.PositiveBigIntegerField()
        else:
            return models.PositiveIntegerField()

# Migration operation compatibility
class OperationCompatibility:
    """Handle operation differences across versions"""
    
    @staticmethod
    def create_index_operation(model_name, fields, name=None, condition=None):
        """Create index operation compatible across versions"""
        
        if django.VERSION >= (2, 2):
            # Conditional indexes supported
            return migrations.AddIndex(
                model_name=model_name,
                index=models.Index(
                    fields=fields,
                    name=name,
                    condition=condition
                ),
            )
        else:
            # Use raw SQL for conditional indexes in older versions
            if condition:
                sql = f"CREATE INDEX {name} ON {model_name} ({', '.join(fields)}) WHERE {condition};"
                return migrations.RunSQL(
                    sql=sql,
                    reverse_sql=f"DROP INDEX {name};",
                )
            else:
                return migrations.AddIndex(
                    model_name=model_name,
                    index=models.Index(fields=fields, name=name),
                )
    
    @staticmethod
    def create_constraint_operation(model_name, constraint):
        """Create constraint operation compatible across versions"""
        
        if django.VERSION >= (2, 2):
            # AddConstraint operation available
            return migrations.AddConstraint(
                model_name=model_name,
                constraint=constraint,
            )
        else:
            # Use raw SQL for older versions
            if isinstance(constraint, models.CheckConstraint):
                sql = f"ALTER TABLE {model_name} ADD CONSTRAINT {constraint.name} CHECK ({constraint.check});"
                return migrations.RunSQL(
                    sql=sql,
                    reverse_sql=f"ALTER TABLE {model_name} DROP CONSTRAINT {constraint.name};",
                )
            else:
                raise ValueError(f"Constraint type {type(constraint)} not supported in Django {django.VERSION}")

Version Detection and Adaptation

class VersionDetectionMixin:
    """Mixin for version-aware migrations"""
    
    @property
    def django_version(self):
        """Get current Django version as tuple"""
        return django.VERSION
    
    @property
    def supports_json_field(self):
        """Check if native JSONField is supported"""
        return self.django_version >= (3, 1)
    
    @property
    def supports_functional_indexes(self):
        """Check if functional indexes are supported"""
        return self.django_version >= (3, 2)
    
    @property
    def supports_covering_indexes(self):
        """Check if covering indexes are supported"""
        return self.django_version >= (4, 0)
    
    @property
    def supports_constraints(self):
        """Check if AddConstraint operation is supported"""
        return self.django_version >= (2, 2)
    
    def get_compatible_operations(self):
        """Get operations compatible with current Django version"""
        
        operations = []
        
        # Add version-specific operations
        if self.supports_json_field:
            operations.extend(self._get_json_field_operations())
        else:
            operations.extend(self._get_text_field_operations())
        
        if self.supports_functional_indexes:
            operations.extend(self._get_functional_index_operations())
        else:
            operations.extend(self._get_raw_sql_index_operations())
        
        return operations
    
    def _get_json_field_operations(self):
        """Operations using native JSONField"""
        return [
            migrations.AddField(
                model_name='post',
                name='metadata',
                field=models.JSONField(default=dict),
            ),
        ]
    
    def _get_text_field_operations(self):
        """Fallback operations using TextField"""
        return [
            migrations.AddField(
                model_name='post',
                name='metadata',
                field=models.TextField(default='{}'),
            ),
        ]
    
    def _get_functional_index_operations(self):
        """Operations using functional indexes"""
        return [
            migrations.AddIndex(
                model_name='post',
                index=models.Index(
                    models.functions.Upper('title'),
                    name='post_upper_title_idx'
                ),
            ),
        ]
    
    def _get_raw_sql_index_operations(self):
        """Fallback operations using raw SQL"""
        return [
            migrations.RunSQL(
                sql="CREATE INDEX post_upper_title_idx ON myapp_post (UPPER(title));",
                reverse_sql="DROP INDEX post_upper_title_idx;",
            ),
        ]

class CompatibleMigration(VersionDetectionMixin, migrations.Migration):
    """Migration class with version compatibility"""
    
    dependencies = [
        ('myapp', '0001_initial'),
    ]
    
    def __init__(self, name, app_label):
        super().__init__(name, app_label)
        self.operations = self.get_compatible_operations()

# Database backend compatibility
class DatabaseCompatibility:
    """Handle database backend differences"""
    
    @staticmethod
    def get_database_specific_operations():
        """Get operations specific to database backend"""
        
        from django.db import connection
        
        operations = []
        
        if connection.vendor == 'postgresql':
            # PostgreSQL-specific operations
            operations.extend([
                migrations.RunSQL(
                    sql="CREATE EXTENSION IF NOT EXISTS pg_trgm;",
                    reverse_sql="-- Extension cleanup handled by PostgreSQL",
                ),
                migrations.AddIndex(
                    model_name='post',
                    index=models.Index(
                        fields=['title'],
                        name='post_title_gin_idx',
                        opclasses=['gin_trgm_ops']
                    ),
                ),
            ])
        
        elif connection.vendor == 'mysql':
            # MySQL-specific operations
            operations.extend([
                migrations.RunSQL(
                    sql="ALTER TABLE myapp_post ADD FULLTEXT(title, content);",
                    reverse_sql="ALTER TABLE myapp_post DROP INDEX title;",
                ),
            ])
        
        elif connection.vendor == 'sqlite':
            # SQLite-specific operations (limited)
            operations.extend([
                migrations.RunSQL(
                    sql="CREATE INDEX post_title_idx ON myapp_post(title);",
                    reverse_sql="DROP INDEX post_title_idx;",
                ),
            ])
        
        return operations
    
    @staticmethod
    def create_cross_database_migration():
        """Create migration that works across database backends"""
        
        def cross_db_operation(apps, schema_editor):
            """Operation that adapts to database backend"""
            
            from django.db import connection
            
            Post = apps.get_model('myapp', 'Post')
            
            if connection.vendor == 'postgresql':
                # Use PostgreSQL-specific features
                with connection.cursor() as cursor:
                    cursor.execute("""
                        UPDATE myapp_post 
                        SET search_vector = to_tsvector('english', title || ' ' || content)
                    """)
            
            elif connection.vendor == 'mysql':
                # Use MySQL-specific features
                with connection.cursor() as cursor:
                    cursor.execute("""
                        UPDATE myapp_post 
                        SET search_text = CONCAT(title, ' ', content)
                    """)
            
            else:
                # Generic approach for other databases
                for post in Post.objects.all():
                    post.search_text = f"{post.title} {post.content}"
                    post.save()
        
        return migrations.RunPython(
            code=cross_db_operation,
            reverse_code=migrations.RunPython.noop,
        )

Backward Compatibility Strategies

Maintaining Compatibility

class BackwardCompatibilityStrategies:
    """Strategies for maintaining backward compatibility"""
    
    @staticmethod
    def create_compatibility_layer():
        """Create compatibility layer for different Django versions"""
        
        # compatibility.py - Compatibility utilities
        compatibility_utils = '''
import django
from django.db import models

# Version-specific imports
if django.VERSION >= (3, 1):
    from django.db.models import JSONField
else:
    try:
        from django.contrib.postgres.fields import JSONField
    except ImportError:
        # Fallback for non-PostgreSQL databases
        JSONField = None

if django.VERSION >= (3, 2):
    from django.db.models import BigAutoField
else:
    BigAutoField = models.AutoField

# Compatibility functions
def get_json_field_class():
    """Get appropriate JSONField class for current Django version"""
    if JSONField is not None:
        return JSONField
    else:
        # Return TextField as fallback
        return models.TextField

def get_auto_field_class():
    """Get appropriate auto field class"""
    if django.VERSION >= (3, 2):
        return models.BigAutoField
    else:
        return models.AutoField

def create_index_with_condition(model_name, fields, name, condition=None):
    """Create index with optional condition, compatible across versions"""
    
    if django.VERSION >= (2, 2) and condition:
        return migrations.AddIndex(
            model_name=model_name,
            index=models.Index(
                fields=fields,
                name=name,
                condition=models.Q(**condition) if isinstance(condition, dict) else condition
            ),
        )
    elif condition:
        # Use raw SQL for conditional indexes in older versions
        condition_sql = " AND ".join([f"{k} = %s" for k in condition.keys()]) if isinstance(condition, dict) else str(condition)
        sql = f"CREATE INDEX {name} ON {model_name} ({', '.join(fields)}) WHERE {condition_sql};"
        
        return migrations.RunSQL(
            sql=sql,
            reverse_sql=f"DROP INDEX {name};",
        )
    else:
        return migrations.AddIndex(
            model_name=model_name,
            index=models.Index(fields=fields, name=name),
        )

# Model field compatibility
class CompatibleJSONField(models.Field):
    """JSONField that works across Django versions"""
    
    def __init__(self, *args, **kwargs):
        self.json_field_class = get_json_field_class()
        
        if self.json_field_class == models.TextField:
            # Convert JSONField kwargs to TextField kwargs
            kwargs.pop('encoder', None)
            kwargs.pop('decoder', None)
            
        super().__init__(*args, **kwargs)
    
    def contribute_to_class(self, cls, name, **kwargs):
        # Use the appropriate field class
        if self.json_field_class != models.TextField:
            field = self.json_field_class(**self.__dict__)
        else:
            field = models.TextField(default='{}', **{k: v for k, v in self.__dict__.items() if k != 'json_field_class'})
        
        field.contribute_to_class(cls, name, **kwargs)
'''
        
        return compatibility_utils
    
    @staticmethod
    def create_version_specific_migrations():
        """Create migrations that adapt to Django version"""
        
        # Base migration class
        class VersionAwareMigration(migrations.Migration):
            """Base class for version-aware migrations"""
            
            def __init__(self, name, app_label):
                super().__init__(name, app_label)
                
                # Modify operations based on Django version
                self.operations = self._adapt_operations_for_version()
            
            def _adapt_operations_for_version(self):
                """Adapt operations for current Django version"""
                
                adapted_operations = []
                
                for operation in self.get_base_operations():
                    adapted_op = self._adapt_single_operation(operation)
                    
                    if isinstance(adapted_op, list):
                        adapted_operations.extend(adapted_op)
                    else:
                        adapted_operations.append(adapted_op)
                
                return adapted_operations
            
            def _adapt_single_operation(self, operation):
                """Adapt a single operation for current Django version"""
                
                op_name = operation.__class__.__name__
                
                if op_name == 'AddField' and hasattr(operation.field, '__class__'):
                    # Adapt field types
                    field_type = operation.field.__class__.__name__
                    
                    if field_type == 'JSONField' and django.VERSION < (3, 1):
                        # Replace with TextField for older versions
                        return migrations.AddField(
                            model_name=operation.model_name,
                            name=operation.name,
                            field=models.TextField(default='{}'),
                        )
                
                elif op_name == 'AddIndex':
                    # Adapt index operations
                    index = operation.index
                    
                    if hasattr(index, 'expressions') and django.VERSION < (3, 2):
                        # Convert functional index to raw SQL
                        return migrations.RunSQL(
                            sql=f"CREATE INDEX {index.name} ON {operation.model_name} (UPPER({index.expressions[0].source_expressions[0].name}));",
                            reverse_sql=f"DROP INDEX {index.name};",
                        )
                
                return operation
            
            def get_base_operations(self):
                """Override in subclasses to provide base operations"""
                return []
        
        # Example usage
        class ExampleMigration(VersionAwareMigration):
            dependencies = [
                ('myapp', '0001_initial'),
            ]
            
            def get_base_operations(self):
                return [
                    migrations.AddField(
                        model_name='post',
                        name='metadata',
                        field=models.JSONField(default=dict),
                    ),
                    migrations.AddIndex(
                        model_name='post',
                        index=models.Index(
                            models.functions.Upper('title'),
                            name='post_upper_title_idx'
                        ),
                    ),
                ]
        
        return VersionAwareMigration, ExampleMigration
    
    @staticmethod
    def handle_deprecated_features():
        """Handle deprecated Django features in migrations"""
        
        def check_and_replace_deprecated_features():
            """Check for and replace deprecated features"""
            
            deprecated_replacements = {
                # Django 3.0 deprecations
                'django.utils.encoding.force_text': 'django.utils.encoding.force_str',
                'django.utils.encoding.smart_text': 'django.utils.encoding.smart_str',
                
                # Django 3.1 deprecations
                'django.conf.urls.url': 'django.urls.re_path',
                
                # Django 4.0 deprecations
                'django.utils.translation.ugettext': 'django.utils.translation.gettext',
                'django.utils.translation.ugettext_lazy': 'django.utils.translation.gettext_lazy',
            }
            
            # Migration-specific deprecated features
            migration_deprecations = {
                # Old-style foreign key definitions
                'models.ForeignKey(User)': 'models.ForeignKey(User, on_delete=models.CASCADE)',
                
                # Old-style index definitions
                'db_index=True': 'indexes=[models.Index(fields=[...])]',
            }
            
            return deprecated_replacements, migration_deprecations
        
        def create_deprecation_warning_migration():
            """Create migration that warns about deprecated features"""
            
            def check_deprecations(apps, schema_editor):
                """Check for deprecated features and warn"""
                
                import warnings
                
                # Check Django version and warn about upcoming deprecations
                if django.VERSION >= (4, 0):
                    warnings.warn(
                        "This migration was created for Django 4.0+. "
                        "Ensure compatibility with your target Django version.",
                        DeprecationWarning
                    )
                
                # Check for deprecated field usage
                Post = apps.get_model('myapp', 'Post')
                
                for field in Post._meta.fields:
                    if isinstance(field, models.ForeignKey) and not hasattr(field, 'on_delete'):
                        warnings.warn(
                            f"ForeignKey field '{field.name}' missing on_delete parameter. "
                            "This will be required in future Django versions.",
                            DeprecationWarning
                        )
            
            return migrations.RunPython(
                code=check_deprecations,
                reverse_code=migrations.RunPython.noop,
            )
        
        return check_and_replace_deprecated_features, create_deprecation_warning_migration

# Testing compatibility across versions
class CompatibilityTesting:
    """Test migration compatibility across Django versions"""
    
    @staticmethod
    def create_version_test_matrix():
        """Create test matrix for different Django versions"""
        
        test_matrix = {
            'django_versions': [
                '3.2',  # LTS
                '4.0',
                '4.1',
                '4.2',  # LTS
                '5.0',
            ],
            'python_versions': [
                '3.8',
                '3.9',
                '3.10',
                '3.11',
            ],
            'database_backends': [
                'postgresql',
                'mysql',
                'sqlite',
            ]
        }
        
        # Test scenarios
        test_scenarios = [
            'fresh_migration_apply',
            'migration_rollback',
            'squashed_migration_apply',
            'cross_app_dependencies',
            'data_migration_execution',
        ]
        
        return test_matrix, test_scenarios
    
    @staticmethod
    def create_compatibility_test_suite():
        """Create test suite for migration compatibility"""
        
        test_suite = '''
import django
from django.test import TestCase, override_settings
from django.db import connection
from django.core.management import call_command
from django.db.migrations.executor import MigrationExecutor

class MigrationCompatibilityTestCase(TestCase):
    """Test migration compatibility across Django versions"""
    
    def setUp(self):
        self.executor = MigrationExecutor(connection)
    
    def test_migration_applies_cleanly(self):
        """Test that migration applies without errors"""
        
        # Get migration
        migration = self.executor.loader.get_migration('myapp', '0002_version_compatible')
        
        # Apply migration
        try:
            self.executor.apply_migration(
                self.executor.loader.project_state(),
                migration
            )
            success = True
        except Exception as e:
            success = False
            error = str(e)
        
        self.assertTrue(success, f"Migration failed to apply: {error if not success else ''}")
    
    def test_migration_rollback(self):
        """Test that migration can be rolled back"""
        
        # Apply migration first
        call_command('migrate', 'myapp', '0002', verbosity=0)
        
        # Then rollback
        try:
            call_command('migrate', 'myapp', '0001', verbosity=0)
            success = True
        except Exception as e:
            success = False
            error = str(e)
        
        self.assertTrue(success, f"Migration rollback failed: {error if not success else ''}")
    
    def test_django_version_compatibility(self):
        """Test compatibility with current Django version"""
        
        # Check if migration uses features available in current version
        migration = self.executor.loader.get_migration('myapp', '0002_version_compatible')
        
        for operation in migration.operations:
            op_name = operation.__class__.__name__
            
            # Check for version-specific operations
            if op_name == 'AddConstraint' and django.VERSION < (2, 2):
                self.fail("Migration uses AddConstraint which is not available in Django < 2.2")
            
            if hasattr(operation, 'index') and hasattr(operation.index, 'expressions'):
                if django.VERSION < (3, 2):
                    self.fail("Migration uses functional indexes which are not available in Django < 3.2")
    
    @override_settings(DATABASES={
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': ':memory:',
        }
    })
    def test_sqlite_compatibility(self):
        """Test migration compatibility with SQLite"""
        self.test_migration_applies_cleanly()
    
    def test_field_compatibility(self):
        """Test that fields are compatible across versions"""
        
        from myapp.models import Post
        
        # Check JSONField compatibility
        metadata_field = Post._meta.get_field('metadata')
        
        if django.VERSION >= (3, 1):
            self.assertEqual(metadata_field.__class__.__name__, 'JSONField')
        else:
            # Should fallback to TextField or PostgreSQL JSONField
            self.assertIn(metadata_field.__class__.__name__, ['TextField', 'JSONField'])
'''
        
        return test_suite

Migration Deployment Strategies

Multi-Version Deployment

class MultiVersionDeployment:
    """Strategies for deploying migrations across multiple Django versions"""
    
    @staticmethod
    def create_staged_deployment_plan():
        """Create plan for staged deployment across versions"""
        
        deployment_plan = {
            'phase_1_preparation': {
                'description': 'Prepare migrations for multiple versions',
                'steps': [
                    'Audit current Django versions across environments',
                    'Identify version-specific features in migrations',
                    'Create compatibility layer for different versions',
                    'Test migrations on all target Django versions',
                    'Document version requirements for each migration'
                ]
            },
            
            'phase_2_compatibility': {
                'description': 'Ensure backward compatibility',
                'steps': [
                    'Replace version-specific operations with compatible alternatives',
                    'Add version detection to migrations',
                    'Create fallback operations for older versions',
                    'Test rollback procedures on all versions',
                    'Validate schema consistency across versions'
                ]
            },
            
            'phase_3_deployment': {
                'description': 'Deploy migrations progressively',
                'steps': [
                    'Deploy to oldest Django version first',
                    'Verify migration success and application stability',
                    'Deploy to intermediate versions',
                    'Finally deploy to newest Django version',
                    'Monitor for version-specific issues'
                ]
            },
            
            'phase_4_cleanup': {
                'description': 'Clean up after successful deployment',
                'steps': [
                    'Remove compatibility shims if no longer needed',
                    'Update documentation with version requirements',
                    'Plan Django version upgrades if applicable',
                    'Archive old migration compatibility code'
                ]
            }
        }
        
        return deployment_plan
    
    @staticmethod
    def create_version_specific_deployment():
        """Create deployment strategy for specific version combinations"""
        
        def deploy_for_version_range(min_version, max_version):
            """Deploy migrations for specific Django version range"""
            
            deployment_config = {
                'min_django_version': min_version,
                'max_django_version': max_version,
                'compatibility_checks': [],
                'deployment_steps': []
            }
            
            # Add version-specific checks
            if min_version < (3, 1):
                deployment_config['compatibility_checks'].append(
                    'Verify JSONField fallback implementation'
                )
            
            if min_version < (3, 2):
                deployment_config['compatibility_checks'].append(
                    'Check functional index compatibility'
                )
            
            if max_version >= (4, 0):
                deployment_config['compatibility_checks'].append(
                    'Verify new Django 4.0+ features work correctly'
                )
            
            # Add deployment steps
            deployment_config['deployment_steps'].extend([
                f'Verify Django version is between {min_version} and {max_version}',
                'Run migration compatibility tests',
                'Apply migrations with version-specific adaptations',
                'Validate schema matches expected state',
                'Run application smoke tests'
            ])
            
            return deployment_config
        
        return deploy_for_version_range
    
    @staticmethod
    def create_rollback_strategy():
        """Create rollback strategy for multi-version environments"""
        
        rollback_strategy = {
            'immediate_rollback': {
                'description': 'Rollback within same deployment window',
                'conditions': [
                    'Migration fails on any supported Django version',
                    'Application errors detected after migration',
                    'Performance degradation observed'
                ],
                'steps': [
                    'Stop deployment to remaining environments',
                    'Rollback migrations on affected environments',
                    'Restore previous application version',
                    'Verify system stability',
                    'Investigate and fix compatibility issues'
                ]
            },
            
            'version_specific_rollback': {
                'description': 'Rollback specific to Django version',
                'conditions': [
                    'Migration works on some versions but not others',
                    'Version-specific features cause issues',
                    'Database backend compatibility problems'
                ],
                'steps': [
                    'Identify problematic Django version',
                    'Create version-specific rollback migration',
                    'Apply rollback only to affected environments',
                    'Update migration to fix version compatibility',
                    'Redeploy with fixed migration'
                ]
            },
            
            'emergency_rollback': {
                'description': 'Emergency rollback from database backup',
                'conditions': [
                    'Migration causes data corruption',
                    'Irreversible migration fails partway through',
                    'Critical system failure after migration'
                ],
                'steps': [
                    'Stop all application servers immediately',
                    'Restore database from pre-migration backup',
                    'Deploy previous application version',
                    'Verify data integrity',
                    'Investigate migration issues thoroughly'
                ]
            }
        }
        
        return rollback_strategy

# Documentation and maintenance
class VersionDocumentation:
    """Documentation strategies for multi-version support"""
    
    @staticmethod
    def create_version_compatibility_matrix():
        """Create compatibility matrix documentation"""
        
        compatibility_matrix = '''
# Django Version Compatibility Matrix

## Migration Compatibility

| Migration | Django 3.2 | Django 4.0 | Django 4.1 | Django 4.2 | Django 5.0 |
|-----------|-------------|-------------|-------------|-------------|-------------|
| 0001_initial | ✅ | ✅ | ✅ | ✅ | ✅ |
| 0002_add_json_field | ✅* | ✅ | ✅ | ✅ | ✅ |
| 0003_functional_indexes | ❌ | ✅ | ✅ | ✅ | ✅ |
| 0004_covering_indexes | ❌ | ✅ | ✅ | ✅ | ✅ |

*Uses TextField fallback for Django < 3.1

## Feature Support

| Feature | Django 3.2 | Django 4.0 | Django 4.1 | Django 4.2 | Django 5.0 |
|---------|-------------|-------------|-------------|-------------|-------------|
| Native JSONField | ✅ | ✅ | ✅ | ✅ | ✅ |
| Functional Indexes | ✅ | ✅ | ✅ | ✅ | ✅ |
| Covering Indexes | ❌ | ✅ | ✅ | ✅ | ✅ |
| BigAutoField | ✅ | ✅ | ✅ | ✅ | ✅ |

## Database Backend Support

| Backend | Django 3.2 | Django 4.0 | Django 4.1 | Django 4.2 | Django 5.0 |
|---------|-------------|-------------|-------------|-------------|-------------|
| PostgreSQL | ✅ | ✅ | ✅ | ✅ | ✅ |
| MySQL | ✅ | ✅ | ✅ | ✅ | ✅ |
| SQLite | ✅ | ✅ | ✅ | ✅ | ✅ |
| Oracle | ✅ | ✅ | ✅ | ✅ | ✅ |

## Migration Notes

### 0002_add_json_field
- Uses native JSONField for Django 3.1+
- Falls back to TextField with JSON serialization for older versions
- PostgreSQL users can use contrib.postgres.fields.JSONField

### 0003_functional_indexes
- Uses native functional indexes for Django 3.2+
- Falls back to raw SQL for older versions
- May not work on all database backends

### 0004_covering_indexes
- Only available in Django 4.0+
- PostgreSQL specific feature
- Gracefully skipped on unsupported versions
'''
        
        return compatibility_matrix
    
    @staticmethod
    def create_upgrade_guide():
        """Create Django upgrade guide for migrations"""
        
        upgrade_guide = '''
# Django Version Upgrade Guide for Migrations

## Upgrading from Django 3.2 to 4.0

### Before Upgrade
1. Review all migrations for Django 4.0 compatibility
2. Test migrations on Django 4.0 in development
3. Update any deprecated features
4. Ensure all team members are ready for upgrade

### During Upgrade
1. Upgrade Django to 4.0
2. Run `python manage.py check` to identify issues
3. Apply any pending migrations
4. Test application functionality thoroughly

### After Upgrade
1. Update migrations to use new Django 4.0 features
2. Remove compatibility shims if no longer needed
3. Update documentation and deployment scripts

## Common Issues and Solutions

### JSONField Compatibility
- **Issue**: Different JSONField implementations across versions
- **Solution**: Use compatibility layer that detects Django version

### Functional Indexes
- **Issue**: Not available in Django < 3.2
- **Solution**: Use raw SQL fallback for older versions

### BigAutoField
- **Issue**: Default changed in Django 3.2+
- **Solution**: Explicitly set DEFAULT_AUTO_FIELD in settings

## Migration Best Practices for Version Compatibility

1. **Always test on target Django versions**
2. **Use compatibility layers for version-specific features**
3. **Document version requirements clearly**
4. **Plan upgrade paths in advance**
5. **Keep migrations simple and focused**
'''
        
        return upgrade_guide

Supporting multiple Django versions in migrations requires careful planning, compatibility layers, and thorough testing. These strategies ensure your migrations work reliably across different Django releases while maintaining data integrity and application functionality.