mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #35275 -- Fixed Meta.constraints validation crash on UniqueConstraint with OpClass().
This also introduces Expression.constraint_validation_compatible that allows specifying that expression should be ignored during a constraint validation.
This commit is contained in:
parent
ceaf1e2848
commit
f030236a86
@ -1,6 +1,5 @@
|
|||||||
from types import NoneType
|
from types import NoneType
|
||||||
|
|
||||||
from django.contrib.postgres.indexes import OpClass
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import DEFAULT_DB_ALIAS, NotSupportedError
|
from django.db import DEFAULT_DB_ALIAS, NotSupportedError
|
||||||
from django.db.backends.ddl_references import Expressions, Statement, Table
|
from django.db.backends.ddl_references import Expressions, Statement, Table
|
||||||
@ -208,12 +207,10 @@ class ExclusionConstraint(BaseConstraint):
|
|||||||
if isinstance(expr, F) and expr.name in exclude:
|
if isinstance(expr, F) and expr.name in exclude:
|
||||||
return
|
return
|
||||||
rhs_expression = expression.replace_expressions(replacements)
|
rhs_expression = expression.replace_expressions(replacements)
|
||||||
# Remove OpClass because it only has sense during the constraint
|
if hasattr(expression, "get_expression_for_validation"):
|
||||||
# creation.
|
expression = expression.get_expression_for_validation()
|
||||||
if isinstance(expression, OpClass):
|
if hasattr(rhs_expression, "get_expression_for_validation"):
|
||||||
expression = expression.get_source_expressions()[0]
|
rhs_expression = rhs_expression.get_expression_for_validation()
|
||||||
if isinstance(rhs_expression, OpClass):
|
|
||||||
rhs_expression = rhs_expression.get_source_expressions()[0]
|
|
||||||
lookup = PostgresOperatorLookup(lhs=expression, rhs=rhs_expression)
|
lookup = PostgresOperatorLookup(lhs=expression, rhs=rhs_expression)
|
||||||
lookup.postgres_operator = operator
|
lookup.postgres_operator = operator
|
||||||
lookups.append(lookup)
|
lookups.append(lookup)
|
||||||
|
@ -244,6 +244,7 @@ class SpGistIndex(PostgresIndex):
|
|||||||
|
|
||||||
class OpClass(Func):
|
class OpClass(Func):
|
||||||
template = "%(expressions)s %(name)s"
|
template = "%(expressions)s %(name)s"
|
||||||
|
constraint_validation_compatible = False
|
||||||
|
|
||||||
def __init__(self, expression, name):
|
def __init__(self, expression, name):
|
||||||
super().__init__(expression, name=name)
|
super().__init__(expression, name=name)
|
||||||
|
@ -6,7 +6,7 @@ from django.core import checks
|
|||||||
from django.core.exceptions import FieldDoesNotExist, FieldError, ValidationError
|
from django.core.exceptions import FieldDoesNotExist, FieldError, ValidationError
|
||||||
from django.db import connections
|
from django.db import connections
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.expressions import Exists, ExpressionList, F, OrderBy, RawSQL
|
from django.db.models.expressions import Exists, ExpressionList, F, RawSQL
|
||||||
from django.db.models.indexes import IndexExpression
|
from django.db.models.indexes import IndexExpression
|
||||||
from django.db.models.lookups import Exact
|
from django.db.models.lookups import Exact
|
||||||
from django.db.models.query_utils import Q
|
from django.db.models.query_utils import Q
|
||||||
@ -644,9 +644,8 @@ class UniqueConstraint(BaseConstraint):
|
|||||||
}
|
}
|
||||||
expressions = []
|
expressions = []
|
||||||
for expr in self.expressions:
|
for expr in self.expressions:
|
||||||
# Ignore ordering.
|
if hasattr(expr, "get_expression_for_validation"):
|
||||||
if isinstance(expr, OrderBy):
|
expr = expr.get_expression_for_validation()
|
||||||
expr = expr.expression
|
|
||||||
expressions.append(Exact(expr, expr.replace_expressions(replacements)))
|
expressions.append(Exact(expr, expr.replace_expressions(replacements)))
|
||||||
queryset = queryset.filter(*expressions)
|
queryset = queryset.filter(*expressions)
|
||||||
model_class_pk = instance._get_pk_val(model._meta)
|
model_class_pk = instance._get_pk_val(model._meta)
|
||||||
|
@ -180,6 +180,8 @@ class BaseExpression:
|
|||||||
window_compatible = False
|
window_compatible = False
|
||||||
# Can the expression be used as a database default value?
|
# Can the expression be used as a database default value?
|
||||||
allowed_default = False
|
allowed_default = False
|
||||||
|
# Can the expression be used during a constraint validation?
|
||||||
|
constraint_validation_compatible = True
|
||||||
|
|
||||||
def __init__(self, output_field=None):
|
def __init__(self, output_field=None):
|
||||||
if output_field is not None:
|
if output_field is not None:
|
||||||
@ -484,6 +486,20 @@ class BaseExpression:
|
|||||||
return self.output_field.select_format(compiler, sql, params)
|
return self.output_field.select_format(compiler, sql, params)
|
||||||
return sql, params
|
return sql, params
|
||||||
|
|
||||||
|
def get_expression_for_validation(self):
|
||||||
|
# Ignore expressions that cannot be used during a constraint validation.
|
||||||
|
if not getattr(self, "constraint_validation_compatible", True):
|
||||||
|
try:
|
||||||
|
(expression,) = self.get_source_expressions()
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(
|
||||||
|
"Expressions with constraint_validation_compatible set to False "
|
||||||
|
"must have only one source expression."
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
return expression
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
@deconstructible
|
@deconstructible
|
||||||
class Expression(BaseExpression, Combinable):
|
class Expression(BaseExpression, Combinable):
|
||||||
@ -1716,6 +1732,7 @@ class Exists(Subquery):
|
|||||||
class OrderBy(Expression):
|
class OrderBy(Expression):
|
||||||
template = "%(expression)s %(ordering)s"
|
template = "%(expression)s %(ordering)s"
|
||||||
conditional = False
|
conditional = False
|
||||||
|
constraint_validation_compatible = False
|
||||||
|
|
||||||
def __init__(self, expression, descending=False, nulls_first=None, nulls_last=None):
|
def __init__(self, expression, descending=False, nulls_first=None, nulls_last=None):
|
||||||
if nulls_first and nulls_last:
|
if nulls_first and nulls_last:
|
||||||
|
@ -1058,6 +1058,15 @@ calling the appropriate methods on the wrapped expression.
|
|||||||
Tells Django that this expression can be used in
|
Tells Django that this expression can be used in
|
||||||
:attr:`Field.db_default`. Defaults to ``False``.
|
:attr:`Field.db_default`. Defaults to ``False``.
|
||||||
|
|
||||||
|
.. attribute:: constraint_validation_compatible
|
||||||
|
|
||||||
|
.. versionadded:: 5.1
|
||||||
|
|
||||||
|
Tells Django that this expression can be used during a constraint
|
||||||
|
validation. Expressions with ``constraint_validation_compatible`` set
|
||||||
|
to ``False`` must have only one source expression. Defaults to
|
||||||
|
``True``.
|
||||||
|
|
||||||
.. attribute:: contains_aggregate
|
.. attribute:: contains_aggregate
|
||||||
|
|
||||||
Tells Django that this expression contains an aggregate and that a
|
Tells Django that this expression contains an aggregate and that a
|
||||||
|
@ -281,6 +281,10 @@ Models
|
|||||||
reload a model's value. This can be used to lock the row before reloading or
|
reload a model's value. This can be used to lock the row before reloading or
|
||||||
to select related objects.
|
to select related objects.
|
||||||
|
|
||||||
|
* The new :attr:`.Expression.constraint_validation_compatible` attribute allows
|
||||||
|
specifying that the expression should be ignored during a constraint
|
||||||
|
validation.
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -1425,6 +1425,16 @@ class SimpleExpressionTests(SimpleTestCase):
|
|||||||
hash(Expression(TestModel._meta.get_field("other_field"))),
|
hash(Expression(TestModel._meta.get_field("other_field"))),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_expression_for_validation_only_one_source_expression(self):
|
||||||
|
expression = Expression()
|
||||||
|
expression.constraint_validation_compatible = False
|
||||||
|
msg = (
|
||||||
|
"Expressions with constraint_validation_compatible set to False must have "
|
||||||
|
"only one source expression."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
expression.get_expression_for_validation()
|
||||||
|
|
||||||
|
|
||||||
class ExpressionsNumericTests(TestCase):
|
class ExpressionsNumericTests(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -266,6 +266,19 @@ class SchemaTests(PostgreSQLTestCase):
|
|||||||
self.assertNotIn(constraint.name, self.get_constraints(Scene._meta.db_table))
|
self.assertNotIn(constraint.name, self.get_constraints(Scene._meta.db_table))
|
||||||
Scene.objects.create(scene="ScEnE 10", setting="Sir Bedemir's Castle")
|
Scene.objects.create(scene="ScEnE 10", setting="Sir Bedemir's Castle")
|
||||||
|
|
||||||
|
def test_opclass_func_validate_constraints(self):
|
||||||
|
constraint_name = "test_opclass_func_validate_constraints"
|
||||||
|
constraint = UniqueConstraint(
|
||||||
|
OpClass(Lower("scene"), name="text_pattern_ops"),
|
||||||
|
name="test_opclass_func_validate_constraints",
|
||||||
|
)
|
||||||
|
Scene.objects.create(scene="First scene")
|
||||||
|
# Non-unique scene.
|
||||||
|
msg = f"Constraint “{constraint_name}” is violated."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(Scene, Scene(scene="first Scene"))
|
||||||
|
constraint.validate(Scene, Scene(scene="second Scene"))
|
||||||
|
|
||||||
|
|
||||||
class ExclusionConstraintTests(PostgreSQLTestCase):
|
class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
def get_constraints(self, table):
|
def get_constraints(self, table):
|
||||||
|
Loading…
Reference in New Issue
Block a user