mirror of
https://github.com/django/django.git
synced 2024-12-27 11:35:53 +00:00
20bab2cf9d
Allowed users to specify which lookups or transforms ("nested lookus") are available for fields. The implementation is now class based. Squashed commit of the following: commitfa7a7195f1
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 18 10:53:24 2014 +0200 Added lookup registration API docs commiteb1c8ce164
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Tue Jan 14 18:59:36 2014 +0200 Release notes and other minor docs changes commit11501c29c9
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Jan 12 20:53:03 2014 +0200 Forgot to add custom_lookups tests in prev commit commit83173b960e
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Jan 12 19:59:12 2014 +0200 Renamed Extract -> Transform commit3b18d9f3a1
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Jan 12 19:51:53 2014 +0200 Removed suggestion of temporary lookup registration from docs commit21d0c7631c
Merge:2509006
f2dc442
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Jan 12 09:38:23 2014 -0800 Merge pull request #2 from mjtamlyn/lookups_3 Reworked custom lookups docs. commitf2dc4429a1
Author: Marc Tamlyn <marc.tamlyn@gmail.com> Date: Sun Jan 12 13:15:05 2014 +0000 Reworked custom lookups docs. Mostly just formatting and rewording, but also replaced the example using ``YearExtract`` to use an example which is unlikely to ever be possible directly in the ORM. commit2509006506
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Jan 12 13:19:13 2014 +0200 Removed unused import commit4fba5dfaa0
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 11 22:34:41 2014 +0200 Added docs to index commit6d53963f37
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 11 22:10:24 2014 +0200 Dead code removal commitf9cc039007
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 11 19:00:43 2014 +0200 A new try for docs commit33aa18a6e3
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 11 14:57:12 2014 +0200 Renamed get_cols to get_group_by_cols commitc7d5f8661b
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 11 14:45:53 2014 +0200 Altered query string customization for backends vendors The new way is trying to call first method 'as_' + connection.vendor. If that doesn't exist, then call as_sql(). Also altered how lookup registration is done. There is now RegisterLookupMixin class that is used by Field, Extract and sql.Aggregate. This allows one to register lookups for extracts and aggregates in the same way lookup registration is done for fields. commit90e7004ec1
Merge:66649ff
f7c2c0a
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 11 13:21:01 2014 +0200 Merge branch 'master' into lookups_3 commit66649ff891
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Jan 11 13:16:01 2014 +0200 Some rewording in docs commit31b8faa627
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Dec 29 15:52:29 2013 +0200 Cleanup based on review comments commit1016159f34
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Dec 28 18:37:04 2013 +0200 Proof-of-concept fix for #16731 Implemented only for SQLite and PostgreSQL, and only for startswith and istartswith lookups. commit193cd097ca
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Dec 28 17:57:58 2013 +0200 Fixed #11722 -- iexact=F() produced invalid SQL commit08ed3c3b49
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Dec 21 23:59:52 2013 +0200 Made Lookup and Extract available from django.db.models commitb99c8d83c9
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Dec 21 23:06:29 2013 +0200 Fixed review notes by Loic commit049eebc070
Merge:ed8fab7
b80a835
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Dec 21 22:53:10 2013 +0200 Merge branch 'master' into lookups_3 Conflicts: django/db/models/fields/__init__.py django/db/models/sql/compiler.py django/db/models/sql/query.py tests/null_queries/tests.py commited8fab7fe8
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Dec 21 22:47:23 2013 +0200 Made Extracts aware of full lookup path commit27a57b7aed
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Dec 1 21:10:11 2013 +0200 Removed debugger import commit074e0f5aca
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Dec 1 21:02:16 2013 +0200 GIS lookup support added commit760e28e72b
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Dec 1 20:04:31 2013 +0200 Removed usage of Constraint, used Lookup instead commiteac4776684
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Dec 1 02:22:30 2013 +0200 Minor cleanup of Lookup API commit2adf50428d
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sun Dec 1 02:14:19 2013 +0200 Added documentation, polished implementation commit32c04357a8
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Nov 30 23:10:15 2013 +0200 Avoid OrderedDict creation on lookup aggregate check commit7c8b3a32cc
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Sat Nov 30 23:04:34 2013 +0200 Implemented nested lookups But there is no support of using lookups outside filtering yet. commit4d219d4cde
Author: Anssi Kääriäinen <akaariai@gmail.com> Date: Wed Nov 27 22:07:30 2013 +0200 Initial implementation of custom lookups
284 lines
10 KiB
Python
284 lines
10 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from django.core.exceptions import FieldError
|
|
from django.db.models import F
|
|
from django.db import transaction
|
|
from django.test import TestCase, skipIfDBFeature
|
|
from django.utils import six
|
|
|
|
from .models import Company, Employee
|
|
|
|
|
|
class ExpressionsTests(TestCase):
|
|
def test_filter(self):
|
|
Company.objects.create(
|
|
name="Example Inc.", num_employees=2300, num_chairs=5,
|
|
ceo=Employee.objects.create(firstname="Joe", lastname="Smith")
|
|
)
|
|
Company.objects.create(
|
|
name="Foobar Ltd.", num_employees=3, num_chairs=4,
|
|
ceo=Employee.objects.create(firstname="Frank", lastname="Meyer")
|
|
)
|
|
Company.objects.create(
|
|
name="Test GmbH", num_employees=32, num_chairs=1,
|
|
ceo=Employee.objects.create(firstname="Max", lastname="Mustermann")
|
|
)
|
|
|
|
company_query = Company.objects.values(
|
|
"name", "num_employees", "num_chairs"
|
|
).order_by(
|
|
"name", "num_employees", "num_chairs"
|
|
)
|
|
|
|
# We can filter for companies where the number of employees is greater
|
|
# than the number of chairs.
|
|
self.assertQuerysetEqual(
|
|
company_query.filter(num_employees__gt=F("num_chairs")), [
|
|
{
|
|
"num_chairs": 5,
|
|
"name": "Example Inc.",
|
|
"num_employees": 2300,
|
|
},
|
|
{
|
|
"num_chairs": 1,
|
|
"name": "Test GmbH",
|
|
"num_employees": 32
|
|
},
|
|
],
|
|
lambda o: o
|
|
)
|
|
|
|
# We can set one field to have the value of another field
|
|
# Make sure we have enough chairs
|
|
company_query.update(num_chairs=F("num_employees"))
|
|
self.assertQuerysetEqual(
|
|
company_query, [
|
|
{
|
|
"num_chairs": 2300,
|
|
"name": "Example Inc.",
|
|
"num_employees": 2300
|
|
},
|
|
{
|
|
"num_chairs": 3,
|
|
"name": "Foobar Ltd.",
|
|
"num_employees": 3
|
|
},
|
|
{
|
|
"num_chairs": 32,
|
|
"name": "Test GmbH",
|
|
"num_employees": 32
|
|
}
|
|
],
|
|
lambda o: o
|
|
)
|
|
|
|
# We can perform arithmetic operations in expressions
|
|
# Make sure we have 2 spare chairs
|
|
company_query.update(num_chairs=F("num_employees") + 2)
|
|
self.assertQuerysetEqual(
|
|
company_query, [
|
|
{
|
|
'num_chairs': 2302,
|
|
'name': 'Example Inc.',
|
|
'num_employees': 2300
|
|
},
|
|
{
|
|
'num_chairs': 5,
|
|
'name': 'Foobar Ltd.',
|
|
'num_employees': 3
|
|
},
|
|
{
|
|
'num_chairs': 34,
|
|
'name': 'Test GmbH',
|
|
'num_employees': 32
|
|
}
|
|
],
|
|
lambda o: o,
|
|
)
|
|
|
|
# Law of order of operations is followed
|
|
company_query.update(
|
|
num_chairs=F('num_employees') + 2 * F('num_employees')
|
|
)
|
|
self.assertQuerysetEqual(
|
|
company_query, [
|
|
{
|
|
'num_chairs': 6900,
|
|
'name': 'Example Inc.',
|
|
'num_employees': 2300
|
|
},
|
|
{
|
|
'num_chairs': 9,
|
|
'name': 'Foobar Ltd.',
|
|
'num_employees': 3
|
|
},
|
|
{
|
|
'num_chairs': 96,
|
|
'name': 'Test GmbH',
|
|
'num_employees': 32
|
|
}
|
|
],
|
|
lambda o: o,
|
|
)
|
|
|
|
# Law of order of operations can be overridden by parentheses
|
|
company_query.update(
|
|
num_chairs=((F('num_employees') + 2) * F('num_employees'))
|
|
)
|
|
self.assertQuerysetEqual(
|
|
company_query, [
|
|
{
|
|
'num_chairs': 5294600,
|
|
'name': 'Example Inc.',
|
|
'num_employees': 2300
|
|
},
|
|
{
|
|
'num_chairs': 15,
|
|
'name': 'Foobar Ltd.',
|
|
'num_employees': 3
|
|
},
|
|
{
|
|
'num_chairs': 1088,
|
|
'name': 'Test GmbH',
|
|
'num_employees': 32
|
|
}
|
|
],
|
|
lambda o: o,
|
|
)
|
|
|
|
# The relation of a foreign key can become copied over to an other
|
|
# foreign key.
|
|
self.assertEqual(
|
|
Company.objects.update(point_of_contact=F('ceo')),
|
|
3
|
|
)
|
|
self.assertQuerysetEqual(
|
|
Company.objects.all(), [
|
|
"Joe Smith",
|
|
"Frank Meyer",
|
|
"Max Mustermann",
|
|
],
|
|
lambda c: six.text_type(c.point_of_contact),
|
|
ordered=False
|
|
)
|
|
|
|
c = Company.objects.all()[0]
|
|
c.point_of_contact = Employee.objects.create(firstname="Guido", lastname="van Rossum")
|
|
c.save()
|
|
|
|
# F Expressions can also span joins
|
|
self.assertQuerysetEqual(
|
|
Company.objects.filter(ceo__firstname=F("point_of_contact__firstname")), [
|
|
"Foobar Ltd.",
|
|
"Test GmbH",
|
|
],
|
|
lambda c: c.name,
|
|
ordered=False
|
|
)
|
|
|
|
Company.objects.exclude(
|
|
ceo__firstname=F("point_of_contact__firstname")
|
|
).update(name="foo")
|
|
self.assertEqual(
|
|
Company.objects.exclude(
|
|
ceo__firstname=F('point_of_contact__firstname')
|
|
).get().name,
|
|
"foo",
|
|
)
|
|
|
|
with transaction.atomic():
|
|
with self.assertRaises(FieldError):
|
|
Company.objects.exclude(
|
|
ceo__firstname=F('point_of_contact__firstname')
|
|
).update(name=F('point_of_contact__lastname'))
|
|
|
|
# F expressions can be used to update attributes on single objects
|
|
test_gmbh = Company.objects.get(name="Test GmbH")
|
|
self.assertEqual(test_gmbh.num_employees, 32)
|
|
test_gmbh.num_employees = F("num_employees") + 4
|
|
test_gmbh.save()
|
|
test_gmbh = Company.objects.get(pk=test_gmbh.pk)
|
|
self.assertEqual(test_gmbh.num_employees, 36)
|
|
|
|
# F expressions cannot be used to update attributes which are foreign
|
|
# keys, or attributes which involve joins.
|
|
test_gmbh.point_of_contact = None
|
|
test_gmbh.save()
|
|
self.assertTrue(test_gmbh.point_of_contact is None)
|
|
|
|
def test():
|
|
test_gmbh.point_of_contact = F("ceo")
|
|
self.assertRaises(ValueError, test)
|
|
|
|
test_gmbh.point_of_contact = test_gmbh.ceo
|
|
test_gmbh.save()
|
|
test_gmbh.name = F("ceo__last_name")
|
|
self.assertRaises(FieldError, test_gmbh.save)
|
|
|
|
# F expressions cannot be used to update attributes on objects which do
|
|
# not yet exist in the database
|
|
acme = Company(
|
|
name="The Acme Widget Co.", num_employees=12, num_chairs=5,
|
|
ceo=test_gmbh.ceo
|
|
)
|
|
acme.num_employees = F("num_employees") + 16
|
|
self.assertRaises(TypeError, acme.save)
|
|
|
|
def test_ticket_11722_iexact_lookup(self):
|
|
Employee.objects.create(firstname="John", lastname="Doe")
|
|
Employee.objects.create(firstname="Test", lastname="test")
|
|
|
|
queryset = Employee.objects.filter(firstname__iexact=F('lastname'))
|
|
self.assertQuerysetEqual(queryset, ["<Employee: Test test>"])
|
|
|
|
@skipIfDBFeature('has_case_insensitive_like')
|
|
def test_ticket_16731_startswith_lookup(self):
|
|
Employee.objects.create(firstname="John", lastname="Doe")
|
|
e2 = Employee.objects.create(firstname="Jack", lastname="Jackson")
|
|
e3 = Employee.objects.create(firstname="Jack", lastname="jackson")
|
|
self.assertQuerysetEqual(
|
|
Employee.objects.filter(lastname__startswith=F('firstname')),
|
|
[e2], lambda x: x)
|
|
self.assertQuerysetEqual(
|
|
Employee.objects.filter(lastname__istartswith=F('firstname')).order_by('pk'),
|
|
[e2, e3], lambda x: x)
|
|
|
|
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)
|