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.
# 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}")
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,
)
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
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.
Serializing Values
Django migrations need to serialize Python values into migration files so they can be recreated when migrations run. Understanding how Django serializes values and how to handle custom serialization is crucial for creating robust migrations with complex data types and custom objects.
Django Serialization Framework
Django's serialization framework provides a mechanism for translating Django models into other formats like JSON, XML, or YAML. This is essential for creating APIs, data exports, fixtures, and data interchange between systems.