Skip to content

Instantly share code, notes, and snippets.

@dcwatson
Created December 4, 2017 16:37
Show Gist options
  • Save dcwatson/a1379acd910cabdaf8c5e157d5ba1190 to your computer and use it in GitHub Desktop.
Save dcwatson/a1379acd910cabdaf8c5e157d5ba1190 to your computer and use it in GitHub Desktop.
A hacky script to rewrite old Django migrations to include current on_delete values
#!/usr/bin/env python
# IMPORTANT! USAGE INSTRUCTIONS!
# Drop this file in the top level of your project *after* you've added on_delete to all your ForeignKey and
# OneToOneFields. Running it will rewrite your migration files to add whatever the current on_delete for each field is
# to the past migrations. Make sure you have a clean working copy so you can easily review the differences before
# committing. Assuming there were no pending migrations, running "makemigrations" should yield no new changes.
import os
import re
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
import django
django.setup()
from django.apps import apps
from django.db import models
MIGRATION_TYPE_RE = re.compile(r'\s*migrations.([^(]+)\(')
MODEL_NAME_RE = re.compile(r"\s*model_name='([^']+)',")
NAME_RE = re.compile(r"\s*name='([^']+)',")
FK_NAME_RE = re.compile(r"\s*\('([^']+)', models\.(ForeignKey|OneToOneField)")
FIELD_TYPE_RE = re.compile(r"models\.(ForeignKey|OneToOneField)\(")
top_level = os.path.dirname(os.path.abspath(__file__))
current_on_delete = {}
# Go through all the fields and figure out what the current on_delete values are.
for model_class in apps.get_models():
for field in model_class._meta.fields:
if isinstance(field, (models.ForeignKey, models.OneToOneField)):
current_on_delete['%s.%s' % (model_class.__name__.lower(), field.name)] = field.remote_field.on_delete.__name__
for app_config in apps.get_app_configs():
if app_config.path.startswith(top_level):
migrations_path = os.path.join(app_config.path, 'migrations')
if os.path.isdir(migrations_path):
for filename in os.listdir(migrations_path):
if filename.endswith('.py'):
file_path = os.path.join(migrations_path, filename)
file_lines = []
print(file_path)
# Keep track of the last known migration type, model_name, and name attributes.
migration_type = None
model_name = None
name = None
for lineno, line in enumerate(open(file_path)):
match = FIELD_TYPE_RE.search(line)
if match and 'on_delete' not in line:
field_type = match.group(1)
match = FK_NAME_RE.match(line)
if match:
# For CreateModel, each FK is on it's own line, prefixed by the field name - use the last known "name" as the model name.
key = '%s.%s' % (name.lower(), match.group(1))
else:
# Otherwise, use the last name/model_name attributes to look up the FK.
key = '%s.%s' % (model_name, name)
new_line = line.replace('models.%s(' % field_type, 'models.%s(on_delete=models.%s, ' % (field_type, current_on_delete[key]))
print(' %s: %s\n %s\n %s' % (lineno + 1, key, line.strip(), new_line.strip()))
# TODO: add dry-run flag instead of always replacing
line = new_line
else:
match = MIGRATION_TYPE_RE.match(line)
if match:
migration_type = match.group(1)
match = MODEL_NAME_RE.match(line)
if match:
model_name = match.group(1)
match = NAME_RE.match(line)
if match:
name = match.group(1)
file_lines.append(line)
# Write out the new file with any fixed lines.
new_contents = ''.join(file_lines)
with open(file_path, 'w') as f:
f.write(new_contents)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment