django-phone-field icon indicating copy to clipboard operation
django-phone-field copied to clipboard

[Migrations] `null=True, unique=True` Raises `django.db.utils.IntegrityError: UNIQUE constraint failed` when running migration

Open jjorissen52 opened this issue 5 years ago • 8 comments

To reproduce, make sure some rows already exist for MyModel, then add a PhoneField to it.

class MyModel(models.Model):
    -pass
    +phone = PhoneField(null=True, blank=True, unique=True)
python manage.py makemigrations
python manage.py migrate
# django.db.utils.IntegrityError: UNIQUE constraint failed: new__myapp_mymodel.phone

jjorissen52 avatar Oct 01 '20 17:10 jjorissen52

Happening with a SQLite database. When unique=False and the migration succeeds, all of the values for the column are an empty string instead of null.

jjorissen52 avatar Oct 01 '20 17:10 jjorissen52

Can you confirm whether this issue also affects a CharField with the same options and migration process? PhoneField is simply a thin wrapper around CharField, and I'm wondering whether this issue is specific to PhoneField.

va-andrew avatar Oct 04 '20 19:10 va-andrew

Yes, I can confirm that this same issue does not occur with a regular CharField (Django 3.1). The issue seems to be in the current implementation of PhoneField.get_prep_value. An empty PhoneNumber is ultimately stored as an empty string rather than as a SQL NULL. The issue does not occur when I override the method to return None instead of an empty string when not value.

class PhoneField(phone_models.PhoneField):
    def get_prep_value(self, value):
        if not value:
            # return ''
            return None

        if not isinstance(value, PhoneNumber):
            value = PhoneNumber(value)
        return value.cleaned

I can submit a PR if you like.

jjorissen52 avatar Dec 11 '20 22:12 jjorissen52

@va-andrew any interest in this? I will close the issue if not.

jjorissen52 avatar Jun 09 '21 20:06 jjorissen52

Hey, thanks for the notice!

Overall, I wasn't expecting use of null=True with PhoneField, since it's somewhat of an antipattern with the CharField it's based on. However, null=True, unique=True is a valid scenario so I think we should support it.

The only concern I have is to make sure we're not upsetting the behavior of existing use cases. Is there a way we can make get_prep_value() return None only if null=True is set on the field?

va-andrew avatar Jun 10 '21 14:06 va-andrew

I don't know if I agree that it's an anti-pattern. In my typical usage of null=False (the default), I want an IntegrityError to be raised if someone attempts to save the instance without setting a value. I think that's pretty typical. This would also be keeping with the behavior of the underlying CharField, and, in my opinion, follows the "principal of least surprise".

Really what I want a specialty field (as with URLField, and EmailField) to do is if any value (and I consider the empty string to be a value) is going to be saved into a PhoneField column, I can be confident that it's a valid phone number, which of course, the empty string is not. It does seem that specialty fields in Django e.g. EmailField will allow you to save an empty string, so it would be consistent for PhoneField to do the same, but it's not consistent if PhoneField as a whole saves the empty string to the database when you didn't enter a value in on the form.

Regardless, this is how you would "return None only if null=True is set on the field":

class PhoneField(phone_models.PhoneField):
    def get_prep_value(self, value):
        if not value:
            return None if self.null else ''

        if not isinstance(value, PhoneNumber):
            value = PhoneNumber(value)
        return value.cleaned

jjorissen52 avatar Nov 18 '21 22:11 jjorissen52

Just thought I'd add a me too. I think being able to prevent users entering the same phone number for different companies is a valid user case. unique=True is useful and only works with empy fields if they are null=True

the-moog avatar Aug 03 '22 00:08 the-moog

@jjorissen52 Your code snippet fixed the issue for me, thanks.

the-moog avatar Aug 03 '22 00:08 the-moog