#37057: UniqueConstraint incorrectly raises ValidationError on nullable fields 
in
condition
-------------------------------------+-------------------------------------
     Reporter:  Drews                |                    Owner:  (none)
         Type:  Bug                  |                   Status:  new
    Component:  Database layer       |                  Version:  6.0
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:
     Keywords:  UniqueConstraint     |             Triage Stage:
                                     |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
Description changed by Drews:

Old description:

> == Summary
> When a `UniqueConstraint` has a `condition` that references a nullable
> field (e.g., `condition=Q(cash_register_type=10)` on a nullable
> `PositiveSmallIntegerField`), Django's `UniqueConstraint.validate()`
> incorrectly reports a constraint violation if the instance being saved
> has `cash_register_type=None` and another record matching the condition
> already exists.
>
> ''Tested in Django==5.2.4 (with Postgres 17) and 6.0.4 (with Postgres
> 18)''
>

> == Steps to Reproduce
>
> {{{
> from django.db import models
>
> class Location(models.Model):
>     name = models.CharField(max_length=100)
>
> class Device(models.Model):
>     class CashRegisterType(models.IntegerChoices):
>         MASTER = 10, "Master"
>         SLAVE  = 20, "Slave"
>     name = models.CharField(max_length=100)
>     pos_location = models.ForeignKey(Location, on_delete=models.CASCADE)
>     cash_register_type = models.PositiveSmallIntegerField(
>         choices=CashRegisterType,
>         blank=True, null=True, default=None,
>     )
>
>     class Meta:
>         constraints = [
>             models.UniqueConstraint(
>                 fields=["pos_location"],
>                 condition=models.Q(cash_register_type=10),
>                 name="unique_master_cash_register",
>                 violation_error_message="Only one Master per
> POSLocation.",
>             )
>         ]
> }}}
>
> Create one Device with `cash_register_type` Master and creating another
> one for the same Location with `cash_register_type=None` will raise
> ValidationError
> {{{
> from myapp.models import Location, Device
>
> loc = Location.objects.create(name="Store 1")
>
> # 1. Create a Master -- works
> master = Device.objects.create(
>     name="CashRegister A", pos_location=loc, cash_register_type=10
> )
>
> # 2. Create a second Device with cash_register_type=None
> # while a Master already exists -- BUG
> register_b = Device.objects.create(
>     name="Device B", pos_location=loc, cash_register_type=None
> )
> }}}
>
> == Workaround
> Add an explicit `isnull=False` check to the condition:
> `condition=models.Q(cash_register_type=10) &
> models.Q(cash_register_type__isnull=False)`
> Although `10` should already be `isnull=False`.

New description:

 == Summary
 When a `UniqueConstraint` has a `condition` that references a nullable
 field (e.g., `condition=Q(cash_register_type=10)` on a nullable
 `PositiveSmallIntegerField`), Django's `UniqueConstraint.validate()`
 incorrectly reports a constraint violation if the instance being saved has
 `cash_register_type=None` and another record matching the condition
 already exists.

 ''Tested in Django==5.2.4 (with Postgres 17) and 6.0.4 (with Postgres
 18)''


 == Steps to Reproduce

 {{{
 from django.db import models

 class Location(models.Model):
     name = models.CharField(max_length=100)

 class Device(models.Model):
     class CashRegisterType(models.IntegerChoices):
         MASTER = 10, "Master"
         SLAVE  = 20, "Slave"
     name = models.CharField(max_length=100)
     pos_location = models.ForeignKey(Location, on_delete=models.CASCADE)
     cash_register_type = models.PositiveSmallIntegerField(
         choices=CashRegisterType,
         blank=True, null=True, default=None,
     )

     class Meta:
         constraints = [
             models.UniqueConstraint(
                 fields=["pos_location"],
                 condition=models.Q(cash_register_type=10),
                 name="unique_master_cash_register",
                 violation_error_message="Only one Master per
 POSLocation.",
             )
         ]
 }}}

 Create one Device with `cash_register_type` Master and creating another
 one for the same Location with `cash_register_type=None` will raise
 ValidationError
 {{{
 from myapp.models import Location, Device

 loc = Location.objects.create(name="Store 1")

 # 1. Create a Master -- works
 master = Device.objects.create(
     name="CashRegister A", pos_location=loc, cash_register_type=10
 )

 # 2. Create a second Device with cash_register_type=None
 # while a Master already exists -- BUG
 register_b = Device(
     name="Device B", pos_location=loc, cash_register_type=None
 )
 register_b.full_clean()    # raises ValidationError
 }}}

 == Workaround
 Add an explicit `isnull=False` check to the condition:
 `condition=models.Q(cash_register_type=10) &
 models.Q(cash_register_type__isnull=False)`
 Although `10` should already be `isnull=False`.

--
-- 
Ticket URL: <https://code.djangoproject.com/ticket/37057#comment:4>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019db5565f37-b7b9963e-d93c-4a94-8458-f9fb1fb9f16f-000000%40eu-central-1.amazonses.com.

Reply via email to