Hello there,

Partial unique constraints are currently not supported during validation 
for reasons described in this ticket[0].

For example (inspired by this Github comment[1]), if you define the 
following model

class Article(models.Model):
    slug = models.CharField(max_length=100)
    deleted_at = models.DateTimeField(null=True)

    class Meta:
        constraints = [
            UniqueConstraint('slug', condition=Q(deleted_at=None), 
name='unique_slug'),
        ]

Then validate_unique must perform the following query to determine if the 
constraint is violated

SELECT NOT (%(deleted_at)s IS NULL) OR NOT EXISTS(SELECT 1 FROM article 
WHERE NOT id = %(id)s AND slug = %(slug)s AND deleted_at IS NULL)

In other words, the validation of a partial unique constraint must check 
that either of these conditions are true
1. The provided instance doesn't match the condition
2. There's no existing rows matching the unique constraint (excluding the 
current instance if it already exists)

This is not something Django supports right now.

In order to add proper support for this feature I believe (personal opinion 
here feedback is welcome) we should follow these steps:

1. Add support for Expression.check(using: str) -> bool that would 
translate IsNull(deleted_at, True).check('alias') into a backend compatible 
'SELECT %(deleted_at)s IS NULL' query and return whether or not it passed. 
That would also allow the constructions of forms like

(~Q(IsNull(deleted_at, True)) | 
~Exists(Article.objects.exclude(pk=pk).filter(slug=slug, 
deleted_at=None)).check(using)

2. Add support for Constraint.validate(instance, excluded_fields) as 
described in [0] that would build on top of Expression.check to implement 
proper UniqueConstraint, CheckConstraint, and ExclusionConstraint 
validation and allow for third-party app (e.g. django-rest-framework which 
doesn't use model level validation[2]) to take advantage of this feature. 
For example the unique_for_(date|month|year) feature of Date(Time)?Field 
could be deprecated in favour of Constraint subclasses that implement 
as_sql to enforce SQL level constraint if available by the current backend 
and implement .validate to replace the special case logic we have currently 
in place for these options[3].

I hope this clarify the current situation.

Cheers,
Simon

[0] https://code.djangoproject.com/ticket/30581#comment:7
[1] https://github.com/django/django/pull/10796#discussion_r244216763
[2] https://github.com/encode/django-rest-framework/issues/7173
[3] 
https://github.com/django/django/blob/e703b152c6148ddda1b072a4353e9a41dca87f90/django/db/models/base.py#L1062-L1084

Le mardi 1 juin 2021 à 11:18:23 UTC-4, gaga...@gmail.com a écrit :

> Hi,
>
> I changed several models from fields using `unique=True` to using 
> `UniqueConstraint` with a condition in the Meta.
>
> As a side-effect, the uniqueness are no longer validated during cleaning 
> of a Form and an integrity error is raised. This is because partial unique 
> indexes are excluded :
>
> https://github.com/django/django/blob/e703b152c6148ddda1b072a4353e9a41dca87f90/django/db/models/options.py#L865-L874
>
> It seems that `total_unique_constraints` is also used to check for fields 
> that should be unique (related fields and USERNAME_FIELD specifically).
>
> I tried modifying `total_unique_constraints` and the only tests which 
> failed were related to the above concern and 
> `test_total_ordering_optimization_meta_constraints` which also uses `
> total_unique_constraints`. My application works fine and the validation 
> error are correctly raised in my forms.
>
> The current behaviour of `Model.validate_unique` is also not the one I 
> expected as my conditional `UniqueConstraint` were not used (which caused 
> the integrity error).
>
> Am I missing something? Or should we use all constraints (including 
> partial) in `Model.validate_unique`?
>
> If this is indeed what should be done, adding an `all_unique_constraints` 
> next to `total_unique_constraints` and using it in `Model.validate_unique` 
> instead of `total_unique_constraints` would do the trick. I don't mind 
> opening a ticket and doing the PR if needed.
>
> Thanks.
>

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers  (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-developers+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-developers/4972f6d0-c590-473d-8571-063738baf2ccn%40googlegroups.com.

Reply via email to