summaryrefslogtreecommitdiff
path: root/django/db/models/query_utils.py
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2025-02-19 03:04:16 -0500
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-08-04 09:42:32 +0200
commitb3bb7230e1225861b5c1f08931f2d82c2b04133a (patch)
tree0832739f5f549254b63f1c4fbeb42040e241de2b /django/db/models/query_utils.py
parente5ccb69bc3da407ab2b0477c0cc5db27c7207225 (diff)
[5.2.x] Fixed #34871, #36518 -- Implemented unresolved lookups expression replacement.
This allows the proper resolving of lookups when performing constraint validation involving Q and Case objects. Thanks Andrew Roberts for the report and Sarah for the tests and review. Backport of 079d31e698fa08dd92e2bc4f3fe9b4817a214419 from main.
Diffstat (limited to 'django/db/models/query_utils.py')
-rw-r--r--django/db/models/query_utils.py41
1 files changed, 40 insertions, 1 deletions
diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
index f0ae810f47..d219c5fb0e 100644
--- a/django/db/models/query_utils.py
+++ b/django/db/models/query_utils.py
@@ -13,7 +13,7 @@ from collections import namedtuple
from contextlib import nullcontext
from django.core.exceptions import FieldError
-from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections, transaction
+from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections, models, transaction
from django.db.models.constants import LOOKUP_SEP
from django.utils import tree
from django.utils.functional import cached_property
@@ -99,6 +99,45 @@ class Q(tree.Node):
query.promote_joins(joins)
return clause
+ def replace_expressions(self, replacements):
+ if not replacements:
+ return self
+ clone = self.create(connector=self.connector, negated=self.negated)
+ for child in self.children:
+ child_replacement = child
+ if isinstance(child, tuple):
+ lhs, rhs = child
+ if LOOKUP_SEP in lhs:
+ path, lookup = lhs.rsplit(LOOKUP_SEP, 1)
+ else:
+ path = lhs
+ lookup = None
+ field = models.F(path)
+ if (
+ field_replacement := field.replace_expressions(replacements)
+ ) is not field:
+ # Handle the implicit __exact case by falling back to an
+ # extra transform when get_lookup returns no match for the
+ # last component of the path.
+ if lookup is None:
+ lookup = "exact"
+ if (lookup_class := field_replacement.get_lookup(lookup)) is None:
+ if (
+ transform_class := field_replacement.get_transform(lookup)
+ ) is not None:
+ field_replacement = transform_class(field_replacement)
+ lookup = "exact"
+ lookup_class = field_replacement.get_lookup(lookup)
+ if rhs is None and lookup == "exact":
+ lookup_class = field_replacement.get_lookup("isnull")
+ rhs = True
+ if lookup_class is not None:
+ child_replacement = lookup_class(field_replacement, rhs)
+ else:
+ child_replacement = child.replace_expressions(replacements)
+ clone.children.append(child_replacement)
+ return clone
+
def flatten(self):
"""
Recursively yield this Q object and all subexpressions, in depth-first