datatracker/south/migration.py
2010-07-21 12:48:05 +00:00

613 lines
23 KiB
Python

"""
Main migration logic.
"""
import datetime
import os
import sys
import traceback
import inspect
from django.conf import settings
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from django.core.management import call_command
from south.models import MigrationHistory
from south.db import db
from south.orm import LazyFakeORM, FakeORM
from south.signals import *
def get_app(app):
"""
Returns the migrations module for the given app model name/module, or None
if it does not use migrations.
"""
if isinstance(app, (str, unicode)):
# If it's a string, use the models module
app = models.get_app(app)
mod = __import__(app.__name__[:-7], {}, {}, ['migrations'])
if hasattr(mod, 'migrations'):
return getattr(mod, 'migrations')
def get_migrated_apps():
"""
Returns all apps with migrations.
"""
for mapp in models.get_apps():
app = get_app(mapp)
if app:
yield app
def get_app_name(app):
"""
Returns the _internal_ app name for the given app module.
i.e. for <module django.contrib.auth.models> will return 'auth'
"""
return app.__name__.split('.')[-2]
def get_app_fullname(app):
"""
Returns the full python name of an app - e.g. django.contrib.auth
"""
return app.__name__[:-11]
def short_from_long(app_name):
return app_name.split(".")[-1]
def get_migration_names(app):
"""
Returns a list of migration file names for the given app.
"""
if getattr(settings, "SOUTH_USE_PYC", False):
allowed_extensions = (".pyc", ".py")
ignored_files = ("__init__.pyc", "__init__.py")
else:
allowed_extensions = (".py",)
ignored_files = ("__init__.py",)
return sorted(set([
os.path.splitext(filename)[0]
for filename in os.listdir(os.path.dirname(app.__file__))
if os.path.splitext(filename)[1] in allowed_extensions and filename not in ignored_files and not filename.startswith(".")
]))
def get_migration_classes(app):
"""
Returns a list of migration classes (one for each migration) for the app.
"""
for name in get_migration_names(app):
yield get_migration(app, name)
def get_migration(app, name):
"""
Returns the migration class implied by 'name'.
"""
try:
module = __import__(app.__name__ + "." + name, '', '', ['Migration'])
migclass = module.Migration
migclass.orm = LazyFakeORM(migclass, get_app_name(app))
module._ = lambda x: x # Fake i18n
module.datetime = datetime
return migclass
except ImportError:
print " ! Migration %s:%s probably doesn't exist." % (get_app_name(app), name)
print " - Traceback:"
raise
except Exception:
print "While loading migration '%s.%s':" % (get_app_name(app), name)
raise
def all_migrations():
return dict([
(app, dict([(name, get_migration(app, name)) for name in get_migration_names(app)]))
for app in get_migrated_apps()
])
def dependency_tree():
tree = all_migrations()
# Annotate tree with 'backwards edges'
for app, classes in tree.items():
for name, cls in classes.items():
if not hasattr(cls, "_dependency_parents"):
cls._dependency_parents = []
if not hasattr(cls, "_dependency_children"):
cls._dependency_children = []
# Get forwards dependencies
if hasattr(cls, "depends_on"):
for dapp, dname in cls.depends_on:
dapp = get_app(dapp)
if dapp not in tree:
print "Migration %s in app %s depends on unmigrated app %s." % (
name,
get_app_name(app),
dapp,
)
sys.exit(1)
if dname not in tree[dapp]:
print "Migration %s in app %s depends on nonexistent migration %s in app %s." % (
name,
get_app_name(app),
dname,
get_app_name(dapp),
)
sys.exit(1)
cls._dependency_parents.append((dapp, dname))
if not hasattr(tree[dapp][dname], "_dependency_children"):
tree[dapp][dname]._dependency_children = []
tree[dapp][dname]._dependency_children.append((app, name))
# Get backwards dependencies
if hasattr(cls, "needed_by"):
for dapp, dname in cls.needed_by:
dapp = get_app(dapp)
if dapp not in tree:
print "Migration %s in app %s claims to be needed by unmigrated app %s." % (
name,
get_app_name(app),
dapp,
)
sys.exit(1)
if dname not in tree[dapp]:
print "Migration %s in app %s claims to be needed by nonexistent migration %s in app %s." % (
name,
get_app_name(app),
dname,
get_app_name(dapp),
)
sys.exit(1)
cls._dependency_children.append((dapp, dname))
if not hasattr(tree[dapp][dname], "_dependency_parents"):
tree[dapp][dname]._dependency_parents = []
tree[dapp][dname]._dependency_parents.append((app, name))
# Sanity check whole tree
for app, classes in tree.items():
for name, cls in classes.items():
cls.dependencies = dependencies(tree, app, name)
return tree
def nice_trace(trace):
return " -> ".join([str((get_app_name(a), n)) for a, n in trace])
def dependencies(tree, app, name, trace=[]):
# Copy trace to stop pass-by-ref problems
trace = trace[:]
# Sanity check
for papp, pname in trace:
if app == papp:
if pname == name:
print "Found circular dependency: %s" % nice_trace(trace + [(app,name)])
sys.exit(1)
else:
# See if they depend in the same app the wrong way
migrations = get_migration_names(app)
if migrations.index(name) > migrations.index(pname):
print "Found a lower migration (%s) depending on a higher migration (%s) in the same app (%s)." % (pname, name, get_app_name(app))
print "Path: %s" % nice_trace(trace + [(app,name)])
sys.exit(1)
# Get the dependencies of a migration
deps = []
migration = tree[app][name]
for dapp, dname in migration._dependency_parents:
deps.extend(
dependencies(tree, dapp, dname, trace+[(app,name)])
)
return deps
def remove_duplicates(l):
m = []
for x in l:
if x not in m:
m.append(x)
return m
def needed_before_forwards(tree, app, name, sameapp=True):
"""
Returns a list of migrations that must be applied before (app, name),
in the order they should be applied.
Used to make sure a migration can be applied (and to help apply up to it).
"""
app_migrations = get_migration_names(app)
needed = []
if sameapp:
for aname in app_migrations[:app_migrations.index(name)]:
needed += needed_before_forwards(tree, app, aname, False)
needed += [(app, aname)]
for dapp, dname in tree[app][name]._dependency_parents:
needed += needed_before_forwards(tree, dapp, dname)
needed += [(dapp, dname)]
return remove_duplicates(needed)
def needed_before_backwards(tree, app, name, sameapp=True):
"""
Returns a list of migrations that must be unapplied before (app, name) is,
in the order they should be unapplied.
Used to make sure a migration can be unapplied (and to help unapply up to it).
"""
app_migrations = get_migration_names(app)
needed = []
if sameapp:
for aname in reversed(app_migrations[app_migrations.index(name)+1:]):
needed += needed_before_backwards(tree, app, aname, False)
needed += [(app, aname)]
for dapp, dname in tree[app][name]._dependency_children:
needed += needed_before_backwards(tree, dapp, dname)
needed += [(dapp, dname)]
return remove_duplicates(needed)
def run_migrations(toprint, torun, recorder, app, migrations, fake=False, db_dry_run=False, verbosity=0):
"""
Runs the specified migrations forwards/backwards, in order.
"""
for migration in migrations:
app_name = get_app_name(app)
if verbosity:
print toprint % (app_name, migration)
# Get migration class
klass = get_migration(app, migration)
# Find its predecessor, and attach the ORM from that as prev_orm.
all_names = get_migration_names(app)
idx = all_names.index(migration)
# First migration? The 'previous ORM' is empty.
if idx == 0:
klass.prev_orm = FakeORM(None, app)
else:
klass.prev_orm = get_migration(app, all_names[idx-1]).orm
# If this is a 'fake' migration, do nothing.
if fake:
if verbosity:
print " (faked)"
# OK, we should probably do something then.
else:
runfunc = getattr(klass(), torun)
args = inspect.getargspec(runfunc)
# Get the correct ORM.
if torun == "forwards":
orm = klass.orm
else:
orm = klass.prev_orm
db.current_orm = orm
# If the database doesn't support running DDL inside a transaction
# *cough*MySQL*cough* then do a dry run first.
if not db.has_ddl_transactions or db_dry_run:
if not (hasattr(klass, "no_dry_run") and klass.no_dry_run):
db.dry_run = True
# Only hide SQL if this is an automatic dry run.
if not db.has_ddl_transactions:
db.debug, old_debug = False, db.debug
pending_creates = db.get_pending_creates()
db.start_transaction()
try:
if len(args[0]) == 1: # They don't want an ORM param
runfunc()
else:
runfunc(orm)
db.rollback_transactions_dry_run()
except:
traceback.print_exc()
print " ! Error found during dry run of migration! Aborting."
return False
if not db.has_ddl_transactions:
db.debug = old_debug
db.clear_run_data(pending_creates)
db.dry_run = False
elif db_dry_run:
print " - Migration '%s' is marked for no-dry-run." % migration
# If they really wanted to dry-run, then quit!
if db_dry_run:
return
if db.has_ddl_transactions:
db.start_transaction()
try:
if len(args[0]) == 1: # They don't want an ORM param
runfunc()
else:
runfunc(orm)
db.execute_deferred_sql()
except:
if db.has_ddl_transactions:
db.rollback_transaction()
raise
else:
traceback.print_exc()
print " ! Error found during real run of migration! Aborting."
print
print " ! Since you have a database that does not support running"
print " ! schema-altering statements in transactions, we have had to"
print " ! leave it in an interim state between migrations."
if torun == "forwards":
print
print " ! You *might* be able to recover with:"
db.debug = db.dry_run = True
if len(args[0]) == 1:
klass().backwards()
else:
klass().backwards(klass.prev_orm)
print
print " ! The South developers regret this has happened, and would"
print " ! like to gently persuade you to consider a slightly"
print " ! easier-to-deal-with DBMS."
return False
else:
if db.has_ddl_transactions:
db.commit_transaction()
if not db_dry_run:
# Record us as having done this
recorder(app_name, migration)
if not fake:
# Send a signal saying it ran
# Actually, don't - we're implementing this properly in 0.7
#ran_migration.send(None, app=app_name, migration=migration, method=torun)
pass
def run_forwards(app, migrations, fake=False, db_dry_run=False, verbosity=0):
"""
Runs the specified migrations forwards, in order.
"""
def record(app_name, migration):
# Record us as having done this
record = MigrationHistory.for_migration(app_name, migration)
record.applied = datetime.datetime.utcnow()
record.save()
return run_migrations(
toprint = " > %s: %s",
torun = "forwards",
recorder = record,
app = app,
migrations = migrations,
fake = fake,
db_dry_run = db_dry_run,
verbosity = verbosity,
)
def run_backwards(app, migrations, ignore=[], fake=False, db_dry_run=False, verbosity=0):
"""
Runs the specified migrations backwards, in order, skipping those
migrations in 'ignore'.
"""
def record(app_name, migration):
# Record us as having not done this
record = MigrationHistory.for_migration(app_name, migration)
record.delete()
return run_migrations(
toprint = " < %s: %s",
torun = "backwards",
recorder = record,
app = app,
migrations = [x for x in migrations if x not in ignore],
fake = fake,
db_dry_run = db_dry_run,
verbosity = verbosity,
)
def right_side_of(x, y):
return left_side_of(reversed(x), reversed(y))
def left_side_of(x, y):
return list(y)[:len(x)] == list(x)
def forwards_problems(tree, forwards, done, verbosity=0):
problems = []
for app, name in forwards:
if (app, name) not in done:
for dapp, dname in needed_before_backwards(tree, app, name):
if (dapp, dname) in done:
print " ! Migration (%s, %s) should not have been applied before (%s, %s) but was." % (get_app_name(dapp), dname, get_app_name(app), name)
problems.append(((app, name), (dapp, dname)))
return problems
def backwards_problems(tree, backwards, done, verbosity=0):
problems = []
for app, name in backwards:
if (app, name) in done:
for dapp, dname in needed_before_forwards(tree, app, name):
if (dapp, dname) not in done:
print " ! Migration (%s, %s) should have been applied before (%s, %s) but wasn't." % (get_app_name(dapp), dname, get_app_name(app), name)
problems.append(((app, name), (dapp, dname)))
return problems
def migrate_app(app, tree, target_name=None, resolve_mode=None, fake=False, db_dry_run=False, yes=False, verbosity=0, load_inital_data=False, skip=False):
app_name = get_app_name(app)
verbosity = int(verbosity)
db.debug = (verbosity > 1)
# Fire off the pre-migrate signal
pre_migrate.send(None, app=app_name)
# Find out what delightful migrations we have
migrations = get_migration_names(app)
# If there aren't any, quit quizically
if not migrations:
print "? You have no migrations for the '%s' app. You might want some." % app_name
return
if target_name not in migrations and target_name not in ["zero", None]:
matches = [x for x in migrations if x.startswith(target_name)]
if len(matches) == 1:
target = migrations.index(matches[0]) + 1
if verbosity:
print " - Soft matched migration %s to %s." % (
target_name,
matches[0]
)
target_name = matches[0]
elif len(matches) > 1:
if verbosity:
print " - Prefix %s matches more than one migration:" % target_name
print " " + "\n ".join(matches)
return
else:
print " ! '%s' is not a migration." % target_name
return
# Check there's no strange ones in the database
ghost_migrations = []
for m in MigrationHistory.objects.filter(applied__isnull = False):
try:
if get_app(m.app_name) not in tree or m.migration not in tree[get_app(m.app_name)]:
ghost_migrations.append(m)
except ImproperlyConfigured:
pass
if ghost_migrations:
print " ! These migrations are in the database but not on disk:"
print " - " + "\n - ".join(["%s: %s" % (x.app_name, x.migration) for x in ghost_migrations])
print " ! I'm not trusting myself; fix this yourself by fiddling"
print " ! with the south_migrationhistory table."
return
# Say what we're doing
if verbosity:
print "Running migrations for %s:" % app_name
# Get the forwards and reverse dependencies for this target
if target_name == None:
target_name = migrations[-1]
if target_name == "zero":
forwards = []
backwards = needed_before_backwards(tree, app, migrations[0]) + [(app, migrations[0])]
else:
forwards = needed_before_forwards(tree, app, target_name) + [(app, target_name)]
# When migrating backwards we want to remove up to and including
# the next migration up in this app (not the next one, that includes other apps)
try:
migration_before_here = migrations[migrations.index(target_name)+1]
backwards = needed_before_backwards(tree, app, migration_before_here) + [(app, migration_before_here)]
except IndexError:
backwards = []
# Get the list of currently applied migrations from the db
current_migrations = []
for m in MigrationHistory.objects.filter(applied__isnull = False):
try:
current_migrations.append((get_app(m.app_name), m.migration))
except ImproperlyConfigured:
pass
direction = None
bad = False
# Work out the direction
applied_for_this_app = list(MigrationHistory.objects.filter(app_name=app_name, applied__isnull=False).order_by("migration"))
if target_name == "zero":
direction = -1
elif not applied_for_this_app:
direction = 1
elif migrations.index(target_name) > migrations.index(applied_for_this_app[-1].migration):
direction = 1
elif migrations.index(target_name) < migrations.index(applied_for_this_app[-1].migration):
direction = -1
else:
direction = None
# Is the whole forward branch applied?
missing = [step for step in forwards if step not in current_migrations]
# If they're all applied, we only know it's not backwards
if not missing:
direction = None
# If the remaining migrations are strictly a right segment of the forwards
# trace, we just need to go forwards to our target (and check for badness)
else:
problems = forwards_problems(tree, forwards, current_migrations, verbosity=verbosity)
if problems:
bad = True
direction = 1
# What about the whole backward trace then?
if not bad:
missing = [step for step in backwards if step not in current_migrations]
# If they're all missing, stick with the forwards decision
if missing == backwards:
pass
# If what's missing is a strict left segment of backwards (i.e.
# all the higher migrations) then we need to go backwards
else:
problems = backwards_problems(tree, backwards, current_migrations, verbosity=verbosity)
if problems:
bad = True
direction = -1
if bad and resolve_mode not in ['merge'] and not skip:
print " ! Inconsistent migration history"
print " ! The following options are available:"
print " --merge: will just attempt the migration ignoring any potential dependency conflicts."
sys.exit(1)
if direction == 1:
if verbosity:
print " - Migrating forwards to %s." % target_name
try:
for mapp, mname in forwards:
if (mapp, mname) not in current_migrations:
result = run_forwards(mapp, [mname], fake=fake, db_dry_run=db_dry_run, verbosity=verbosity)
if result is False: # The migrations errored, but nicely.
return False
finally:
# Call any pending post_syncdb signals
db.send_pending_create_signals()
# Now load initial data, only if we're really doing things and ended up at current
if not fake and not db_dry_run and load_inital_data and target_name == migrations[-1]:
if verbosity:
print " - Loading initial data for %s." % app_name
# Override Django's get_apps call temporarily to only load from the
# current app
old_get_apps, models.get_apps = (
models.get_apps,
lambda: [models.get_app(get_app_name(app))],
)
# Load the initial fixture
call_command('loaddata', 'initial_data', verbosity=verbosity)
# Un-override
models.get_apps = old_get_apps
elif direction == -1:
if verbosity:
print " - Migrating backwards to just after %s." % target_name
for mapp, mname in backwards:
if (mapp, mname) in current_migrations:
run_backwards(mapp, [mname], fake=fake, db_dry_run=db_dry_run, verbosity=verbosity)
else:
if verbosity:
print "- Nothing to migrate."
# Finally, fire off the post-migrate signal
post_migrate.send(None, app=app_name)