Skip to content

Commit 61a62be

Browse files
charettesjacobtylerwalls
authored andcommitted
Fixed #37057 -- Adjusted UniqueConstraint handling of UNKNOWN condition.
When we adjusted UNKNOWN handling for CheckConstraint in refs #33996 we assumed that all usage of Q.check would benefit from this approach. However while CHECK constraints enforcement do ignore conditions involving NULL that resolve to UNKNOWN it's not the case for other type of constraints such as UNIQUE ones. Given how UNKNOWN should be treated depends on the callers context it appears that a better strategy for COALESCE wrapping is to force them to apply it if necessary. Thanks Drew Shapiro for the report.
1 parent 63c56cd commit 61a62be

3 files changed

Lines changed: 22 additions & 7 deletions

File tree

django/db/models/constraints.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from django.db import connections
77
from django.db.models.constants import LOOKUP_SEP
88
from django.db.models.expressions import Exists, ExpressionList, F, RawSQL
9+
from django.db.models.fields import BooleanField
10+
from django.db.models.functions import Coalesce
911
from django.db.models.indexes import IndexExpression
1012
from django.db.models.lookups import Exact, IsNull
1113
from django.db.models.query_utils import Q
@@ -212,7 +214,11 @@ def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
212214
# Ignore constraints with excluded fields in condition.
213215
if exclude and self._expression_refs_exclude(model, self.condition, exclude):
214216
return
215-
if not Q(self.condition).check(against, using=using):
217+
condition = self.condition
218+
if connections[using].features.supports_comparing_boolean_expr:
219+
# Checks constraints do not fail when they evaluate to UNKNOWN.
220+
condition = Coalesce(condition, True, output_field=BooleanField())
221+
if not Q(condition).check(against, using=using):
216222
raise ValidationError(
217223
self.get_violation_error_message(), code=self.violation_error_code
218224
)

django/db/models/query_utils.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,7 @@ def check(self, against, using=DEFAULT_DB_ALIAS):
165165
matches against the expressions.
166166
"""
167167
# Avoid circular imports.
168-
from django.db.models import BooleanField, Value
169-
from django.db.models.functions import Coalesce
168+
from django.db.models import Value
170169
from django.db.models.sql import Query
171170
from django.db.models.sql.constants import SINGLE
172171

@@ -178,10 +177,7 @@ def check(self, against, using=DEFAULT_DB_ALIAS):
178177
query.add_annotation(Value(1), "_check")
179178
connection = connections[using]
180179
# This will raise a FieldError if a field is missing in "against".
181-
if connection.features.supports_comparing_boolean_expr:
182-
query.add_q(Q(Coalesce(self, True, output_field=BooleanField())))
183-
else:
184-
query.add_q(self)
180+
query.add_q(self)
185181
compiler = query.get_compiler(using=using)
186182
context_manager = (
187183
transaction.atomic(using=using)

tests/constraints/tests.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,19 @@ def test_validate_nulls_distinct_expressions(self):
12971297
with self.assertRaisesMessage(ValidationError, msg):
12981298
constraint.validate(Product, Product(price=None))
12991299

1300+
@skipUnlessDBFeature("supports_comparing_boolean_expr")
1301+
def test_validate_nullable_condition(self):
1302+
GeneratedFieldStoredProduct.objects.create(name="Product", price=42)
1303+
constraint = models.UniqueConstraint(
1304+
fields=["name"],
1305+
name="uniq_name_for_positive_price",
1306+
condition=models.Q(price__gt=0),
1307+
)
1308+
constraint.validate(
1309+
GeneratedFieldStoredProduct,
1310+
GeneratedFieldStoredProduct(name="Product", price=None),
1311+
)
1312+
13001313
def test_name(self):
13011314
constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
13021315
expected_name = "name_color_uniq"

0 commit comments

Comments
 (0)