diff --git a/django/contrib/postgres/constraints.py b/django/contrib/postgres/constraints.py index c61072b5a5..ff702c53b0 100644 --- a/django/contrib/postgres/constraints.py +++ b/django/contrib/postgres/constraints.py @@ -77,6 +77,14 @@ class ExclusionConstraint(BaseConstraint): expressions.append(expression) return ExpressionList(*expressions).resolve_expression(query) + def _check(self, model, connection): + references = set() + for expr, _ in self.expressions: + if isinstance(expr, str): + expr = F(expr) + references.update(model._get_expr_references(expr)) + return self._check_references(model, references) + def _get_condition_sql(self, compiler, schema_editor, query): if self.condition is None: return None diff --git a/tests/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py index e5a8e9dbe9..ee0be3cbb3 100644 --- a/tests/postgres_tests/test_constraints.py +++ b/tests/postgres_tests/test_constraints.py @@ -2,12 +2,17 @@ import datetime from unittest import mock from django.contrib.postgres.indexes import OpClass +from django.core.checks import Error from django.core.exceptions import ValidationError from django.db import IntegrityError, NotSupportedError, connection, transaction from django.db.models import ( + CASCADE, + CharField, CheckConstraint, + DateField, Deferrable, F, + ForeignKey, Func, IntegerField, Model, @@ -328,6 +333,57 @@ class ExclusionConstraintTests(PostgreSQLTestCase): include="invalid", ) + @isolate_apps("postgres_tests") + def test_check(self): + class Author(Model): + name = CharField(max_length=255) + alias = CharField(max_length=255) + + class Meta: + app_label = "postgres_tests" + + class Book(Model): + title = CharField(max_length=255) + published_date = DateField() + author = ForeignKey(Author, CASCADE) + + class Meta: + app_label = "postgres_tests" + constraints = [ + ExclusionConstraint( + name="exclude_check", + expressions=[ + (F("title"), RangeOperators.EQUAL), + (F("published_date__year"), RangeOperators.EQUAL), + ("published_date__month", RangeOperators.EQUAL), + (F("author__name"), RangeOperators.EQUAL), + ("author__alias", RangeOperators.EQUAL), + ("nonexistent", RangeOperators.EQUAL), + ], + ) + ] + + self.assertCountEqual( + Book.check(databases=self.databases), + [ + Error( + "'constraints' refers to the nonexistent field 'nonexistent'.", + obj=Book, + id="models.E012", + ), + Error( + "'constraints' refers to the joined field 'author__alias'.", + obj=Book, + id="models.E041", + ), + Error( + "'constraints' refers to the joined field 'author__name'.", + obj=Book, + id="models.E041", + ), + ], + ) + def test_repr(self): constraint = ExclusionConstraint( name="exclude_overlapping",