Fixed #33996 -- Fixed CheckConstraint validation on NULL values.
Bug in 667105877e
.
Thanks James Beith for the report.
This commit is contained in:
parent
b731e88415
commit
e14d08cd89
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,6 +127,9 @@ 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".
|
||||||
|
if connections[using].features.supports_comparing_boolean_expr:
|
||||||
|
query.add_q(Q(Coalesce(self, True, output_field=BooleanField())))
|
||||||
|
else:
|
||||||
query.add_q(self)
|
query.add_q(self)
|
||||||
compiler = query.get_compiler(using=using)
|
compiler = query.get_compiler(using=using)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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`).
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -156,8 +156,6 @@ 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."
|
|
||||||
with self.assertRaisesMessage(ValidationError, msg):
|
|
||||||
constraint.validate(RangesModel, RangesModel())
|
constraint.validate(RangesModel, RangesModel())
|
||||||
|
|
||||||
def test_opclass(self):
|
def test_opclass(self):
|
||||||
|
|
Loading…
Reference in New Issue