Search code examples
djangodjango-southdatabase-migration

Django: Migrate CharField enum to SmallIntegerField using South


I have a model with a CharField acting more or less as an enum:

grade = models.CharField(max_length='1', choices=('A', 'B', 'C'))

Unfortunately, for somewhat complicated reasons, I have to migrate it to be a SmallIntegerField, like so:

grade = models.SmallIntegerField(choices=(1, 2, 3))

How would I do this in South? I have a couple general ideas, but am not sure exactly how to execute them. My first thought is a series of migrations:

  1. Add a new grade_new SmallIntegerField and translate the old grades to the new grades in it (during the migration's forward method).
  2. Delete the old grade field while simultaneously renaming grade_new to grade

Is this the right approach? And if so, how would I translate the old grades to new grades in step #1?


Solution

  • While I'd still love to know if this approach was the right one, I was able to figure out how to execute the plan above with only two migrations/commits.

    First, I added a new_grade = models.SmallIntegerField(choices=(1, 2, 3)) field to the model (which required duplicating the enum vars) and updated the references to grade to new_grade in the ordering and unique_together fields of the model's Meta class:

    class Foo(models.Model):
      A, B, C = 'A', 'B', 'C'
      A2, B2, C2, = 1, 2, 3
      grade = models.CharField(max_length='1', choices=((A, 'A'), (B, 'B'), (C, 'C')))
      new_grade = models.SmallIntegerField(choices=((A2, 1), (B2, 2), (C2, 3)))
    
      class Meta:
        ordering = ['x', 'new_grade']
        unique_together = ('x', 'new_grade')
    

    After running manage.py schemamigration app --auto, I opened the migration file and modified the forward method to:

    def forwards(self, orm):
      # For the unique_together...
      db.delete_unique('app_foo', ['x', 'grade'])
    
      db.add_column('app_foo', 'new_grade',
                    self.gf('django.db.models.fields.SmallIntegerField')(default=1),
                    keep_default=False)
      if not db.dry_run:
        mapping = {'A': 1, 'B': 2, 'C': 3}
        for foo in orm.Foo.objects.all():
          foo.new_grade = mapping[foo.grade]
          foo.save()
    
      # For the unique_together...
      db.create_unique('app_foo', ['x', 'new_grade'])
    

    After running manage.py migrate app, all the Foos now had a duplicate new_grade field with the mapped value. At that point I committed my code, since it was in a stable state.

    Second, in models.py, I removed the old grade field, renamed the duplicate enum vars, and updated the references to new_grade in the Meta class again:

    class Foo(models.Model):
      A, B, C, = 1, 2, 3
      grade = models.SmallIntegerField(choices=((A, 1), (B, 2), (C, 3)))
    
      class Meta:
        ordering = ['x', 'grade']
        unique_together = ('x', 'grade')
    

    I once again ran manage.py schemamigration app --auto and opened the migration file to modify the forward method to:

    def forwards(self, orm):
      # For the unique_together...
      db.delete_unique('app_foo', ['x', 'new_grade'])
    
      db.delete_column('app_foo', 'grade')
      db.rename_column('app_foo', 'new_grade', 'grade')
    
      # For the unique_together...
      db.create_unique('app_foo', ['x', 'grade'])
    

    After running manage.py migrate app, all the Foos now had their grade fields replaced with the former new_grade field and the migration was complete!