Fixed #18375 -- Removed dict-ordering dependency for F-expressions
F() expressions reuse joins like any lookup in a .filter() call - reuse multijoins generated in the same .filter() call else generate new joins. Also, lookups can now reuse joins generated by F(). This change is backwards incompatible, but it is required to prevent dict randomization from generating different queries depending on .filter() kwarg ordering. The new way is also more consistent in how joins are reused.
This commit is contained in:
parent
7b9a1fb964
commit
90b86291d0
|
@ -1,14 +1,16 @@
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.fields import FieldDoesNotExist
|
from django.db.models.fields import FieldDoesNotExist
|
||||||
|
from django.db.models.sql.constants import REUSE_ALL
|
||||||
|
|
||||||
class SQLEvaluator(object):
|
class SQLEvaluator(object):
|
||||||
def __init__(self, expression, query, allow_joins=True):
|
def __init__(self, expression, query, allow_joins=True, reuse=REUSE_ALL):
|
||||||
self.expression = expression
|
self.expression = expression
|
||||||
self.opts = query.get_meta()
|
self.opts = query.get_meta()
|
||||||
self.cols = []
|
self.cols = []
|
||||||
|
|
||||||
self.contains_aggregate = False
|
self.contains_aggregate = False
|
||||||
|
self.reuse = reuse
|
||||||
self.expression.prepare(self, query, allow_joins)
|
self.expression.prepare(self, query, allow_joins)
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
|
@ -50,9 +52,10 @@ class SQLEvaluator(object):
|
||||||
try:
|
try:
|
||||||
field, source, opts, join_list, last, _ = query.setup_joins(
|
field, source, opts, join_list, last, _ = query.setup_joins(
|
||||||
field_list, query.get_meta(),
|
field_list, query.get_meta(),
|
||||||
query.get_initial_alias(), False)
|
query.get_initial_alias(), self.reuse)
|
||||||
col, _, join_list = query.trim_joins(source, join_list, last, False)
|
col, _, join_list = query.trim_joins(source, join_list, last, False)
|
||||||
|
if self.reuse is not None and self.reuse != REUSE_ALL:
|
||||||
|
self.reuse.update(join_list)
|
||||||
self.cols.append((node, (join_list[-1], col)))
|
self.cols.append((node, (join_list[-1], col)))
|
||||||
except FieldDoesNotExist:
|
except FieldDoesNotExist:
|
||||||
raise FieldError("Cannot resolve keyword %r into field. "
|
raise FieldError("Cannot resolve keyword %r into field. "
|
||||||
|
|
|
@ -1096,7 +1096,7 @@ class Query(object):
|
||||||
value = value()
|
value = value()
|
||||||
elif isinstance(value, ExpressionNode):
|
elif isinstance(value, ExpressionNode):
|
||||||
# If value is a query expression, evaluate it
|
# If value is a query expression, evaluate it
|
||||||
value = SQLEvaluator(value, self)
|
value = SQLEvaluator(value, self, reuse=can_reuse)
|
||||||
having_clause = value.contains_aggregate
|
having_clause = value.contains_aggregate
|
||||||
|
|
||||||
for alias, aggregate in self.aggregates.items():
|
for alias, aggregate in self.aggregates.items():
|
||||||
|
|
|
@ -589,6 +589,12 @@ Miscellaneous
|
||||||
:ref:`Q() expressions <complex-lookups-with-q>` and ``QuerySet`` combining where
|
:ref:`Q() expressions <complex-lookups-with-q>` and ``QuerySet`` combining where
|
||||||
the operators are used as boolean AND and OR operators.
|
the operators are used as boolean AND and OR operators.
|
||||||
|
|
||||||
|
* In a ``filter()`` call, when :ref:`F() expressions <query-expressions>`
|
||||||
|
contained lookups spanning multi-valued relations, they didn't always reuse
|
||||||
|
the same relations as other lookups along the same chain. This was changed,
|
||||||
|
and now F() expressions will always use the same relations as other lookups
|
||||||
|
within the same ``filter()`` call.
|
||||||
|
|
||||||
* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need
|
* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need
|
||||||
HTML validation against pre-HTML5 Strict DTDs, you should add a div around it
|
HTML validation against pre-HTML5 Strict DTDs, you should add a div around it
|
||||||
in your pages.
|
in your pages.
|
||||||
|
|
|
@ -219,3 +219,42 @@ class ExpressionsTests(TestCase):
|
||||||
)
|
)
|
||||||
acme.num_employees = F("num_employees") + 16
|
acme.num_employees = F("num_employees") + 16
|
||||||
self.assertRaises(TypeError, acme.save)
|
self.assertRaises(TypeError, acme.save)
|
||||||
|
|
||||||
|
def test_ticket_18375_join_reuse(self):
|
||||||
|
# Test that reverse multijoin F() references and the lookup target
|
||||||
|
# the same join. Pre #18375 the F() join was generated first, and the
|
||||||
|
# lookup couldn't reuse that join.
|
||||||
|
qs = Employee.objects.filter(
|
||||||
|
company_ceo_set__num_chairs=F('company_ceo_set__num_employees'))
|
||||||
|
self.assertEqual(str(qs.query).count('JOIN'), 1)
|
||||||
|
|
||||||
|
def test_ticket_18375_kwarg_ordering(self):
|
||||||
|
# The next query was dict-randomization dependent - if the "gte=1"
|
||||||
|
# was seen first, then the F() will reuse the join generated by the
|
||||||
|
# gte lookup, if F() was seen first, then it generated a join the
|
||||||
|
# other lookups could not reuse.
|
||||||
|
qs = Employee.objects.filter(
|
||||||
|
company_ceo_set__num_chairs=F('company_ceo_set__num_employees'),
|
||||||
|
company_ceo_set__num_chairs__gte=1)
|
||||||
|
self.assertEqual(str(qs.query).count('JOIN'), 1)
|
||||||
|
|
||||||
|
def test_ticket_18375_kwarg_ordering_2(self):
|
||||||
|
# Another similar case for F() than above. Now we have the same join
|
||||||
|
# in two filter kwargs, one in the lhs lookup, one in F. Here pre
|
||||||
|
# #18375 the amount of joins generated was random if dict
|
||||||
|
# randomization was enabled, that is the generated query dependend
|
||||||
|
# on which clause was seen first.
|
||||||
|
qs = Employee.objects.filter(
|
||||||
|
company_ceo_set__num_employees=F('pk'),
|
||||||
|
pk=F('company_ceo_set__num_employees')
|
||||||
|
)
|
||||||
|
self.assertEqual(str(qs.query).count('JOIN'), 1)
|
||||||
|
|
||||||
|
def test_ticket_18375_chained_filters(self):
|
||||||
|
# Test that F() expressions do not reuse joins from previous filter.
|
||||||
|
qs = Employee.objects.filter(
|
||||||
|
company_ceo_set__num_employees=F('pk')
|
||||||
|
).filter(
|
||||||
|
company_ceo_set__num_employees=F('company_ceo_set__num_employees')
|
||||||
|
)
|
||||||
|
self.assertEqual(str(qs.query).count('JOIN'), 2)
|
||||||
|
|
Loading…
Reference in New Issue