11

I added a new field to one of my models:

class Agency(models.Model):
    email = models.EmailField(unique=True, verbose_name=_("e-mail"))

As this field cannot be blank, django-admin makemigrations requested me to provide one-off default, which I did. Here is the generated migration:

# Generated by Django 1.9.4 on 2016-03-20 10:38
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('accounts', '0008_auto_20160226_1226'),
    ]

    operations = [
        migrations.AddField(
            model_name='agency',
            name='email',
            field=models.EmailField(default='example@example.fr', max_length=254, unique=True, verbose_name='e-mail'),
            preserve_default=False,
        ),
    ]

As expected, django-admin migrate throwed an error:

psycopg2.IntegrityError: could not create unique index "accounts_agency_email_key"
DETAIL:  Key (email)=(example@example.fr) is duplicate.

I thought I could edit the migration to set unique values before making the field unique. So I tried:

# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-03-20 10:38
from __future__ import unicode_literals

from django.db import migrations, models
from django.utils.text import slugify


def set_email(apps, schema_editor):
    Agency = apps.get_model('accounts', 'Agency')
    for agency in Agency.objects.all():
        agency.email = '{}@example.fr'.format(slugify(agency.name))
        agency.save()


class Migration(migrations.Migration):

    dependencies = [
        ('accounts', '0008_auto_20160226_1226'),
    ]

    operations = [
        migrations.AddField(
            model_name='agency',
            name='email',
            field=models.EmailField(default='', max_length=254, blank=True, verbose_name='e-mail'),
            preserve_default=False,
        ),
        migrations.RunPython(set_email),
        migrations.AlterField(
            model_name='agency',
            name='email',
            field=models.EmailField(max_length=254, unique=True, verbose_name='e-mail'),
            preserve_default=False,
        ),
    ]

Unfortunately I get this error when running django-admin migrate:

django.db.utils.OperationalError: cannot ALTER TABLE "accounts_agency" because it has pending trigger events

My guess is that operations are not executed synchronously.

I think I could fix the issue by splitting the migration into two migrations, but I'd like to know if I can do it in only one migration. What is the common way to create migrations when adding a new unique field in a model?

PS: I also tried to use an F expression as default (default=models.F('name') + '@example.fr') but it failed:

django.db.utils.IntegrityError: could not create unique index "accounts_agency_email_key"
DETAIL:  Key (email)=(F(name) + Vallu(@example.fr)) is duplicated.
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
  • 4
    Have you read the section of docs that deals with exactly this problem? https://docs.djangoproject.com/en/1.9/howto/writing-migrations/#migrations-that-add-unique-fields – koniiiik Mar 20 '16 at 12:09
  • @koniiiik, I search on the wrong page https://docs.djangoproject.com/en/1.9/topics/migrations/ So, looks like there is no way to get it work with only one migration. – Antoine Pinsard Mar 20 '16 at 12:16
  • 1
    Is there any reason why you do not want to use two migrations? – koniiiik Mar 20 '16 at 13:23
  • Not especially, it was just about curiosity. – Antoine Pinsard Mar 20 '16 at 13:37
  • 1
    I was surprised that doing all operations in the same migration didn't work. However, having 2 (or 3) migrations means that, by mistake, one could be reverted without the others, which would put the application in an unexpected state. – Antoine Pinsard Mar 20 '16 at 13:43
  • 1
    I wrote a [pull request 9212](https://github.com/django/django/pull/9212) to Django howto, inspired by this question, to simplify it and especially to prevent the "unexpected state". Currently I'm waiting. – hynekcer Oct 07 '17 at 23:42

1 Answers1

5

Maybe it's too late but maybe it could work for someone else

You can do this in one migration via using migrations.RunSQL method

For your example code after you added the new field to your model and run the python manage.py makemigrations command (here if you have existing rows in your table command wants to choice default value you can choice "Provide a one-off default now" option and give some string value it is not important because actually we did not use it) then go to migration file and change operations part with this (Note i use postgresql you can change SQL for your database)

operations = [
        migrations.RunSQL(
        'ALTER TABLE "agency" ADD COLUMN "email" varchar(254) NULL;ALTER TABLE "agency" ALTER COLUMN "email" DROP DEFAULT;COMMIT;',
        ),
        migrations.RunSQL(
        "UPDATE agency SET email= Concat(country_code, '@example.fr');COMMIT;",
        ),
        migrations.RunSQL(
        'ALTER TABLE "agency" ALTER COLUMN "email" SET NOT NULL;ALTER TABLE "agency" ADD CONSTRAINT "agency_email_b551ad2a_uniq" UNIQUE ("email");ALTER TABLE "agency" ALTER COLUMN "email" DROP DEFAULT;CREATE INDEX "agency_email_b551ad2a_like" ON "agency" ("email" varchar_pattern_ops);COMMIT;'
        )
    ]

then run "python manage.py migrate" command that is it.

Selim Yılmaz
  • 596
  • 8
  • 11