mirror of https://github.com/django/django.git
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
|
||||
# clauses?
|
||||
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?
|
||||
supports_json_field = True
|
||||
|
|
|
@ -71,6 +71,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
supports_slicing_ordering_in_compound = True
|
||||
allows_multiple_constraints_on_same_fields = False
|
||||
supports_boolean_expr_in_select_clause = False
|
||||
supports_comparing_boolean_expr = False
|
||||
supports_primitives_in_json_field = False
|
||||
supports_json_field_contains = False
|
||||
supports_collation_on_textfield = False
|
||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from collections import namedtuple
|
||||
|
||||
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.utils import tree
|
||||
|
||||
|
@ -115,7 +115,8 @@ class Q(tree.Node):
|
|||
matches against the expressions.
|
||||
"""
|
||||
# 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.constants import SINGLE
|
||||
|
||||
|
@ -126,7 +127,10 @@ class Q(tree.Node):
|
|||
query.add_annotation(value, name, select=False)
|
||||
query.add_annotation(Value(1), "_check")
|
||||
# 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)
|
||||
try:
|
||||
return compiler.execute_sql(SINGLE) is not None
|
||||
|
|
|
@ -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')``
|
||||
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
|
||||
|
||||
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
|
||||
contained an ``Exists`` annotation to crash due to too many selected columns
|
||||
(: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.functions import Lower
|
||||
from django.db.transaction import atomic
|
||||
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
|
||||
from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||
|
||||
from .models import (
|
||||
ChildModel,
|
||||
|
@ -234,6 +234,23 @@ class CheckConstraintTests(TestCase):
|
|||
constraint.validate(Product, Product(price=501, 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):
|
||||
@classmethod
|
||||
|
|
|
@ -156,9 +156,7 @@ class SchemaTests(PostgreSQLTestCase):
|
|||
check=Q(ints__startswith__gte=0),
|
||||
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):
|
||||
constraint = UniqueConstraint(
|
||||
|
|
Loading…
Reference in New Issue