From 76c3e310dd37a1d77642a8744db636a3a4337af2 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Thu, 27 Jul 2023 17:07:48 +1000 Subject: [PATCH] Fixed #34744 -- Prevented recreation of migration for constraints with a dict_keys. Co-authored-by: Mariusz Felisiak --- django/db/models/query_utils.py | 23 ++++++++++++++++ tests/migrations/test_autodetector.py | 37 ++++++++++++++++++++++++++ tests/queries/test_q.py | 38 +++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 78148f76b0d..fcda30b3a7c 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -14,6 +14,8 @@ from django.core.exceptions import FieldError from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections from django.db.models.constants import LOOKUP_SEP from django.utils import tree +from django.utils.functional import cached_property +from django.utils.hashable import make_hashable logger = logging.getLogger("django.db.models") @@ -151,6 +153,27 @@ class Q(tree.Node): kwargs["_negated"] = True return path, args, kwargs + @cached_property + def identity(self): + path, args, kwargs = self.deconstruct() + identity = [path, *kwargs.items()] + for child in args: + if isinstance(child, tuple): + arg, value = child + value = make_hashable(value) + identity.append((arg, value)) + else: + identity.append(child) + return tuple(identity) + + def __eq__(self, other): + if not isinstance(other, Q): + return NotImplemented + return other.identity == self.identity + + def __hash__(self): + return hash(self.identity) + class DeferredAttribute: """ diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 74892bbf3da..4c91659ca87 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -2793,6 +2793,43 @@ class AutodetectorTests(BaseAutodetectorTests): ["CreateModel", "AddField", "AddConstraint"], ) + def test_add_constraints_with_dict_keys(self): + book_types = {"F": "Fantasy", "M": "Mystery"} + book_with_type = ModelState( + "testapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("type", models.CharField(max_length=1)), + ], + { + "constraints": [ + models.CheckConstraint( + check=models.Q(type__in=book_types.keys()), + name="book_type_check", + ), + ], + }, + ) + book_with_resolved_type = ModelState( + "testapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("type", models.CharField(max_length=1)), + ], + { + "constraints": [ + models.CheckConstraint( + check=models.Q(("type__in", tuple(book_types))), + name="book_type_check", + ), + ], + }, + ) + changes = self.get_changes([book_with_type], [book_with_resolved_type]) + self.assertEqual(len(changes), 0) + def test_add_index_with_new_model(self): book_with_index_title_and_pony = ModelState( "otherapp", diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py index cdf40292b06..d3bab1f2a06 100644 --- a/tests/queries/test_q.py +++ b/tests/queries/test_q.py @@ -200,6 +200,44 @@ class QTests(SimpleTestCase): path, args, kwargs = q.deconstruct() self.assertEqual(Q(*args, **kwargs), q) + def test_equal(self): + self.assertEqual(Q(), Q()) + self.assertEqual( + Q(("pk__in", (1, 2))), + Q(("pk__in", [1, 2])), + ) + self.assertEqual( + Q(("pk__in", (1, 2))), + Q(pk__in=[1, 2]), + ) + self.assertEqual( + Q(("pk__in", (1, 2))), + Q(("pk__in", {1: "first", 2: "second"}.keys())), + ) + self.assertNotEqual( + Q(name__iexact=F("other_name")), + Q(name=Lower(F("other_name"))), + ) + + def test_hash(self): + self.assertEqual(hash(Q()), hash(Q())) + self.assertEqual( + hash(Q(("pk__in", (1, 2)))), + hash(Q(("pk__in", [1, 2]))), + ) + self.assertEqual( + hash(Q(("pk__in", (1, 2)))), + hash(Q(pk__in=[1, 2])), + ) + self.assertEqual( + hash(Q(("pk__in", (1, 2)))), + hash(Q(("pk__in", {1: "first", 2: "second"}.keys()))), + ) + self.assertNotEqual( + hash(Q(name__iexact=F("other_name"))), + hash(Q(name=Lower(F("other_name")))), + ) + def test_flatten(self): q = Q() self.assertEqual(list(q.flatten()), [q])