7

Following what seems like good advice, I migrated from Django's built-in auth.User to my own app.User by doing a migration that renames auth_user to app_user. So far so good, this works fine. The problem comes when I set up a new machine.

In my settings.py I have AUTH_USER_MODEL = 'app.User'. Because of this, when I run syncdb, the auth_user table is not created, so when I migrate, that migration fails.

The only way around this I've found is to modify AUTH_USER_MODEL to point to auth.User, run syncdb and migrations up until the rename migration, then change AUTH_USER_MODEL back, then run the rest of the migrations.

Is there a way around this problem?

Community
  • 1
  • 1
fredley
  • 32,953
  • 42
  • 145
  • 236
  • what is your django version ? – ruddra Aug 22 '14 at 09:09
  • @ruddra 1.6. I'm planning on moving to native migrations with 1.7, so that may make this go away anyway I guess. – fredley Aug 22 '14 at 09:24
  • 2
    What I'd do to fix this is to edit the migration to check whether the database is already in the state it should be after the migration is performed. That is, if the table to be renamed does not exist and the table it should be renamed to exists, then I'd skip performing the actual change. – Louis Aug 22 '14 at 23:19
  • @Louis A simple `try/except` doesn't catch the error (the migration just dumps out without ever reaching the `except` block). How would I check if the table exists? – fredley Aug 27 '14 at 08:37
  • @TomMedley What operation did you put your `try/except` statement around? – Louis Aug 27 '14 at 17:01
  • @Louis db.rename_table – fredley Aug 27 '14 at 19:23

3 Answers3

3

Based on the issues you have mentioned having, the approach I would first try is to modify the migration that performs the table rename to check whether the rename should be performed. Unfortunately South does not readily cooperate with this kind of check. Most higher-level operations completely abort a migration if they fail. However, you can use db.execute and it will raise an exception if it fails. Something like:

from django.db.utils import ProgrammingError
from south.db import db

exists = False
db.start_transaction()
try:
    # Will fail if the destination table does not exist. 
    # Any typo here will yield incorrect results. Be careful.
    db.execute("select count(*) from auth_user")
    # If we get here, the table exists
    exists = True
except ProgrammingError:
    pass

# Always end the transaction we started, rollback or commit shouldn't matter.
db.rollback_transaction()

if exists:
    db.rename_table...
else:
    # The table does not exist, create new one.
    db.create_table...

My tests show that it is always possible to catch the errors raised by South's database calls. However, South does not clean up after an SQL error. (This is what I initially missed in the first version of this answer.) So even if the exception is caught, the next SQL operation that will start will find that there the connection is in an error state. In other words, the operation that occurs after the operation that failed will fail because the previous operation fails. This is the reason for the db.start_transaction() and db.rollback_transaction() calls. This makes the operation end cleanly even if there was an SQL error.

Louis
  • 146,715
  • 28
  • 274
  • 320
  • This still doesn't work, the execute still causes a fatal error and causes the migration to abort. – fredley Aug 28 '14 at 11:40
  • When I tested `db.execute` I was able to catch the errors and thought that was enough but I discovered today that the problem is not that we can't catch SQL errors but that if one South operation fails and we catch the error, the **next operation** will fail too. The revised answer should take care of this. I've tested it by modifying one of my own migrations. You'll notice the new code moves `db.rename_table` out of the `try/catch`. Keeping it inside the `try/catch` makes the code vulnerable to a possible error while `db.rename_table` executes, which would be swallowed by the `try/catch`. – Louis Aug 28 '14 at 22:58
1

I also had problems trying to follow the same instructions like you did, but I chose to fix it another way. I created my model (called UserProfile) like this:

class UserProfile(AbstractUser):
    # Fields
    ...
    class Meta:
        swappable = 'AUTH_USER_MODEL'
        db_table = 'auth_user'

This way, running syncdb will no longer cause problems, because your table is named properly. However, I don't remember exactly all the steps I took when I did this, so it might need a bit more setup.

AdelaN
  • 3,366
  • 2
  • 25
  • 45
  • Hmmm, this doesn't really help me with the situation I find myself in now. I'll keep it in mind for future though! – fredley Aug 22 '14 at 08:11
1

With the ideas from the other answers presented here, this is a solution that works:

def forwards(self, orm):
    if 'auth_user' not in db.execute('SHOW TABLES'):
        db.create_table('app_user', (
            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
            ('password', self.gf('django.db.models.fields.CharField')(max_length=128)),
            ('last_login', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
            ('is_superuser', self.gf('django.db.models.fields.BooleanField')(default=False)),
            ('username', self.gf('django.db.models.fields.CharField')(unique=True, max_length=30)),
            ('first_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)),
            ('last_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)),
            ('email', self.gf('django.db.models.fields.EmailField')(max_length=75, blank=True)),
            ('is_staff', self.gf('django.db.models.fields.BooleanField')(default=False)),
            ('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)),
            ('date_joined', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
        ))
        db.send_create_signal(app', ['User'])
    else:
        db.rename_table('auth_user', 'app_user')
fredley
  • 32,953
  • 42
  • 145
  • 236
  • Just a note, `SHOW TABLES` is specific to MySQL. Also, haven't tested this yet, but I think this might not work if there are foreign key links to the `auth_user` table in previous migrations before this one. This seems like a great idea though! – user193130 Aug 28 '14 at 16:33