Fixed #33996 -- Fixed CheckConstraint validation on NULL values.

Bug in 667105877e.

Thanks James Beith for the report.
This commit is contained in:
David Sanders 2022-09-09 00:02:58 +10:00 committed by Mariusz Felisiak
parent b731e88415
commit e14d08cd89
7 changed files with 42 additions and 7 deletions

View File

@ -302,6 +302,9 @@ class BaseDatabaseFeatures:
# Does the backend support boolean expressions in SELECT and GROUP BY # Does the backend support boolean expressions in SELECT and GROUP BY
# clauses? # clauses?
supports_boolean_expr_in_select_clause = True supports_boolean_expr_in_select_clause = True
# Does the backend support comparing boolean expressions in WHERE clauses?
# Eg: WHERE (price > 0) IS NOT NULL
supports_comparing_boolean_expr = True
# Does the backend support JSONField? # Does the backend support JSONField?
supports_json_field = True supports_json_field = True

View File

@ -71,6 +71,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_slicing_ordering_in_compound = True supports_slicing_ordering_in_compound = True
allows_multiple_constraints_on_same_fields = False allows_multiple_constraints_on_same_fields = False
supports_boolean_expr_in_select_clause = False supports_boolean_expr_in_select_clause = False
supports_comparing_boolean_expr = False
supports_primitives_in_json_field = False supports_primitives_in_json_field = False
supports_json_field_contains = False supports_json_field_contains = False
supports_collation_on_textfield = False supports_collation_on_textfield = False

View File

@ -11,7 +11,7 @@ import logging
from collections import namedtuple from collections import namedtuple
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db import DEFAULT_DB_ALIAS, DatabaseError from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.utils import tree from django.utils import tree
@ -115,7 +115,8 @@ class Q(tree.Node):
matches against the expressions. matches against the expressions.
""" """
# Avoid circular imports. # Avoid circular imports.
from django.db.models import Value from django.db.models import BooleanField, Value
from django.db.models.functions import Coalesce
from django.db.models.sql import Query from django.db.models.sql import Query
from django.db.models.sql.constants import SINGLE from django.db.models.sql.constants import SINGLE
@ -126,7 +127,10 @@ class Q(tree.Node):
query.add_annotation(value, name, select=False) query.add_annotation(value, name, select=False)
query.add_annotation(Value(1), "_check") query.add_annotation(Value(1), "_check")
# This will raise a FieldError if a field is missing in "against". # This will raise a FieldError if a field is missing in "against".
query.add_q(self) if connections[using].features.supports_comparing_boolean_expr:
query.add_q(Q(Coalesce(self, True, output_field=BooleanField())))
else:
query.add_q(self)
compiler = query.get_compiler(using=using) compiler = query.get_compiler(using=using)
try: try:
return compiler.execute_sql(SINGLE) is not None return compiler.execute_sql(SINGLE) is not None

View File

@ -102,6 +102,15 @@ specifies the check you want the constraint to enforce.
For example, ``CheckConstraint(check=Q(age__gte=18), name='age_gte_18')`` For example, ``CheckConstraint(check=Q(age__gte=18), name='age_gte_18')``
ensures the age field is never less than 18. ensures the age field is never less than 18.
.. admonition:: Oracle
Checks with nullable fields on Oracle must include a condition allowing for
``NULL`` values in order for :meth:`validate() <BaseConstraint.validate>`
to behave the same as check constraints validation. For example, if ``age``
is a nullable field::
CheckConstraint(check=Q(age__gte=18) | Q(age__isnull=True), name='age_gte_18')
.. versionchanged:: 4.1 .. versionchanged:: 4.1
The ``violation_error_message`` argument was added. The ``violation_error_message`` argument was added.

View File

@ -15,3 +15,6 @@ Bugfixes
* Fixed a regression in Django 4.1 that caused aggregation over a queryset that * Fixed a regression in Django 4.1 that caused aggregation over a queryset that
contained an ``Exists`` annotation to crash due to too many selected columns contained an ``Exists`` annotation to crash due to too many selected columns
(:ticket:`33992`). (:ticket:`33992`).
* Fixed a bug in Django 4.1 that caused an incorrect validation of
``CheckConstraint`` on ``NULL`` values (:ticket:`33996`).

View File

@ -6,7 +6,7 @@ from django.db.models import F
from django.db.models.constraints import BaseConstraint from django.db.models.constraints import BaseConstraint
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.db.transaction import atomic from django.db.transaction import atomic
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
from .models import ( from .models import (
ChildModel, ChildModel,
@ -234,6 +234,23 @@ class CheckConstraintTests(TestCase):
constraint.validate(Product, Product(price=501, discounted_price=5)) constraint.validate(Product, Product(price=501, discounted_price=5))
constraint.validate(Product, Product(price=499, discounted_price=5)) constraint.validate(Product, Product(price=499, discounted_price=5))
@skipUnlessDBFeature("supports_comparing_boolean_expr")
def test_validate_nullable_field_with_none(self):
# Nullable fields should be considered valid on None values.
constraint = models.CheckConstraint(
check=models.Q(price__gte=0),
name="positive_price",
)
constraint.validate(Product, Product())
@skipIfDBFeature("supports_comparing_boolean_expr")
def test_validate_nullable_field_with_isnull(self):
constraint = models.CheckConstraint(
check=models.Q(price__gte=0) | models.Q(price__isnull=True),
name="positive_price",
)
constraint.validate(Product, Product())
class UniqueConstraintTests(TestCase): class UniqueConstraintTests(TestCase):
@classmethod @classmethod

View File

@ -156,9 +156,7 @@ class SchemaTests(PostgreSQLTestCase):
check=Q(ints__startswith__gte=0), check=Q(ints__startswith__gte=0),
name="ints_positive_range", name="ints_positive_range",
) )
msg = f"Constraint “{constraint.name}” is violated." constraint.validate(RangesModel, RangesModel())
with self.assertRaisesMessage(ValidationError, msg):
constraint.validate(RangesModel, RangesModel())
def test_opclass(self): def test_opclass(self):
constraint = UniqueConstraint( constraint = UniqueConstraint(