From 6b0193146d3818d17d231b8e3d9fb38f06f55757 Mon Sep 17 00:00:00 2001
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Date: Tue, 9 Aug 2022 06:08:48 +0200
Subject: [PATCH] [4.1.x] Fixed #33902 -- Fixed Meta.constraints validation
 crash with F() expressions.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Thanks Adam Zahradník for the report.

Bug in 667105877e6723c6985399803a364848891513cc.
Backport of 63884829acd207404f2a5c3cc1d6b4cd0a822b70 from main
---
 django/db/models/constraints.py |  9 ++++++---
 django/db/models/expressions.py |  7 ++++---
 docs/releases/4.1.1.txt         |  3 +++
 tests/constraints/tests.py      | 14 ++++++++++++++
 4 files changed, 27 insertions(+), 6 deletions(-)

diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py
index 86f015465a..8cf1f0ff20 100644
--- a/django/db/models/constraints.py
+++ b/django/db/models/constraints.py
@@ -326,9 +326,12 @@ class UniqueConstraint(BaseConstraint):
             # Ignore constraints with excluded fields.
             if exclude:
                 for expression in self.expressions:
-                    for expr in expression.flatten():
-                        if isinstance(expr, F) and expr.name in exclude:
-                            return
+                    if hasattr(expression, "flatten"):
+                        for expr in expression.flatten():
+                            if isinstance(expr, F) and expr.name in exclude:
+                                return
+                    elif isinstance(expression, F) and expression.name in exclude:
+                        return
             replacement_map = instance._get_field_value_map(
                 meta=model._meta, exclude=exclude
             )
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 5d23c1572f..822968ef56 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -393,9 +393,7 @@ class BaseExpression:
         clone = self.copy()
         clone.set_source_expressions(
             [
-                references_map.get(expr.name, expr)
-                if isinstance(expr, F)
-                else expr.replace_references(references_map)
+                expr.replace_references(references_map)
                 for expr in self.get_source_expressions()
             ]
         )
@@ -810,6 +808,9 @@ class F(Combinable):
     ):
         return query.resolve_ref(self.name, allow_joins, reuse, summarize)
 
+    def replace_references(self, references_map):
+        return references_map.get(self.name, self)
+
     def asc(self, **kwargs):
         return OrderBy(self, **kwargs)
 
diff --git a/docs/releases/4.1.1.txt b/docs/releases/4.1.1.txt
index 2e61e3877d..dbac1ff926 100644
--- a/docs/releases/4.1.1.txt
+++ b/docs/releases/4.1.1.txt
@@ -29,3 +29,6 @@ Bugfixes
 
 * Fixed a regression in Django 4.1 that caused a migration crash on SQLite
   3.35.5+ when removing an indexed field (:ticket:`33899`).
+
+* Fixed a bug in Django 4.1 that caused a crash of model validation on
+  ``UniqueConstraint()`` with field names in ``expressions`` (:ticket:`33902`).
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 4032b418b4..d4054dfd77 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -685,6 +685,20 @@ class UniqueConstraintTests(TestCase):
             exclude={"color"},
         )
 
+    def test_validate_expression_str(self):
+        constraint = models.UniqueConstraint("name", name="name_uniq")
+        msg = "Constraint “name_uniq” is violated."
+        with self.assertRaisesMessage(ValidationError, msg):
+            constraint.validate(
+                UniqueConstraintProduct,
+                UniqueConstraintProduct(name=self.p1.name),
+            )
+        constraint.validate(
+            UniqueConstraintProduct,
+            UniqueConstraintProduct(name=self.p1.name),
+            exclude={"name"},
+        )
+
     def test_name(self):
         constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
         expected_name = "name_color_uniq"