Failed Check Constraint raises database error, not ValidationError?

Hi there,

Just got my project upgraded to 4.1, where we now have model validation of unique and check constraints. I was testing out a create form, and when I failed to satisfy the check constraint, I got a 500 and the database threw an IntegrityError. Shouldn’t this have been invalidated before trying to save to the database? I was expecting an invalid form back…

Thanks for any insight.

Hi! Can you share your view, form and model?

Sure, I’ll have to trim it up but here are the relevant bits:

The model:

class Client(models.Model):
    name = models.CharField(max_length=100)
    ...
    phone_service = models.BooleanField()
    phone_number = models.CharField(
        max_length=10,
        blank=True,
        validators=[
            PhoneValidator(),
        ],
    )  
    ...

    class Meta:
        ordering = ["name"]
        constraints = [
            models.CheckConstraint(
                name="if_phone_require_number",
                check=(models.Q(phone_service=False) | ~models.Q(phone_number="")),
            ),
            models.UniqueConstraint(
                fields=["name", "client_type"], name="unique_clients_per_organization"
            ),
        ]

The view:

class CreateClient(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
    template_name = "clients/create_client.html"
    model = models.Client
    form_class = forms.ClientForm
    permission_required = "clients.add_client"
    raise_exception = True
    success_message = "Your new client has been created. Make any changes below, \
        or click the Back button below to return to the client list."

    def form_valid(self, form):
        response = super().form_valid(form)
        if template := form.cleaned_data["template"]:
            self.copy_template(template)
        return response

The form:

class PhoneField(forms.CharField):
    def clean(self, value):
        number = re.sub(r"[^0-9]", "", value)
        return super().clean(number)

class ClientForm(forms.ModelForm):
    phone_number = PhoneField(required=False)

    class Meta:
        model = models.Client
        fields = "__all__"
        widgets = {"systems": FilteredSelectMultiple("Systems", is_stacked=False)}

The idea, as you can see, is the make sure that if you check the box for phone service, we require that you provide a phone number, otherwise phone can be blank. What happens in older versions of Django–and what is happening to me currently–is that unless you write custom validation, check constraints are enforced when writing to the database. So if you fail one, the database throws an error, and your user gets a 500. But my understanding of this new 4.1 feature is that these check constraints should now be validated during model validation, which means a ModelForm should raise a ValidationError if the constraint is invalidated, and I would assume an invalid form is returned to the user showing some kind of error message. So far, that’s not at all what’s happening for me, it’s like nothing’s changed. Have I somehow misunderstood what this feature does?

The only part of this that might interfere with validation is the fact that I’ve overwritten form_valid() in the view so I can run a custom method if necessary, but I call the super() method, so all the validation should run as normal.

Does Client have anything overriding the clean function? Does it actually import from models.Model?

Have you tried walking through the form’s validation logic to confirm it’s calling full_clean on the model with breakpoint() or a debugger?

Nope, I don’t touch clean(). Client is based directly on django.db.models.Model.

Where would be best to place a breakpoint()? I don’t override or write out any validation logic for the form (see above), so I wouldn’t know where to begin.

I would put it in the ModelForm’s _post_clean() function: django/django/forms/models.py at stable/4.1.x · django/django · GitHub

To whoever may find this in the future with a similar issue, I just wanted to come back and say that I think the problem is that I’m using an MS SQL database, for which we’re using mssql-django, and I don’t think this is implemented there yet.