Skip to content

Commit 044d507

Browse files
committed
Add support for IN (VALUES (a), (b), (c)) lookups
This is a bit experimental so I am not adding any documentation yet.
1 parent a00751f commit 044d507

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

psqlextra/apps.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
class PostgresExtraAppConfig(AppConfig):
55
name = "psqlextra"
66
verbose_name = "PostgreSQL Extra"
7+
8+
def ready(self) -> None:
9+
from .lookups import InValuesLookup # noqa

psqlextra/lookups.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django.db.models import lookups
2+
from django.db.models.fields import Field, related_lookups
3+
from django.db.models.fields.related import ForeignObject
4+
5+
6+
class InValuesLookupMixin:
7+
"""Performs a `lhs IN VALUES ((a), (b), (c))` lookup.
8+
9+
This can be significantly faster then a normal `IN (a, b, c)`. The
10+
latter sometimes causes the Postgres query planner do a sequential
11+
scan.
12+
"""
13+
14+
def as_sql(self, compiler, connection):
15+
16+
if not self.rhs_is_direct_value():
17+
return super().as_sql(compiler, connection)
18+
19+
lhs, lhs_params = self.process_lhs(compiler, connection)
20+
21+
_, rhs_params = self.process_rhs(compiler, connection)
22+
rhs = ",".join([f"(%s)" for _ in rhs_params]) # noqa: F541
23+
24+
return f"{lhs} IN (VALUES {rhs})", lhs_params + list(rhs_params)
25+
26+
27+
@Field.register_lookup
28+
class InValuesLookup(InValuesLookupMixin, lookups.In):
29+
lookup_name = "invalues"
30+
31+
32+
@ForeignObject.register_lookup
33+
class InValuesRelatedLookup(InValuesLookupMixin, related_lookups.RelatedIn):
34+
lookup_name = "invalues"

tests/test_lookups.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from django.db import models
2+
3+
from .fake_model import get_fake_model
4+
5+
6+
def test_invalues_lookup_text_field():
7+
model = get_fake_model({"name": models.TextField()})
8+
[a, b] = model.objects.bulk_create(
9+
[
10+
model(name="a"),
11+
model(name="b"),
12+
]
13+
)
14+
15+
results = list(model.objects.filter(name__invalues=[a.name, b.name, "c"]))
16+
assert results == [a, b]
17+
18+
19+
def test_invalues_lookup_integer_field():
20+
model = get_fake_model({"number": models.IntegerField()})
21+
[a, b] = model.objects.bulk_create(
22+
[
23+
model(number=1),
24+
model(number=2),
25+
]
26+
)
27+
28+
results = list(
29+
model.objects.filter(number__invalues=[a.number, b.number, 3])
30+
)
31+
assert results == [a, b]
32+
33+
34+
def test_invalues_lookup_uuid_field():
35+
model = get_fake_model({"value": models.UUIDField()})
36+
[a, b] = model.objects.bulk_create(
37+
[
38+
model(value="f8fe0431-29f8-4c4c-839c-8a6bf29f95d5"),
39+
model(value="2fb0f45b-afaf-4e24-8637-2d81ded997bb"),
40+
]
41+
)
42+
43+
results = list(
44+
model.objects.filter(
45+
value__invalues=[
46+
a.value,
47+
b.value,
48+
"d7a8df83-f3f8-487b-b982-547c8f22b0bb",
49+
]
50+
)
51+
)
52+
assert results == [a, b]
53+
54+
55+
def test_invalues_lookup_related_field():
56+
model_1 = get_fake_model({"name": models.TextField()})
57+
model_2 = get_fake_model(
58+
{"relation": models.ForeignKey(model_1, on_delete=models.CASCADE)}
59+
)
60+
61+
[a_relation, b_relation] = model_1.objects.bulk_create(
62+
[
63+
model_1(name="a"),
64+
model_1(name="b"),
65+
]
66+
)
67+
68+
[a, b] = model_2.objects.bulk_create(
69+
[model_2(relation=a_relation), model_2(relation=b_relation)]
70+
)
71+
72+
results = list(
73+
model_2.objects.filter(relation__invalues=[a_relation, b_relation])
74+
)
75+
assert results == [a, b]
76+
77+
78+
def test_invalues_lookup_related_field_subquery():
79+
model_1 = get_fake_model({"name": models.TextField()})
80+
model_2 = get_fake_model(
81+
{"relation": models.ForeignKey(model_1, on_delete=models.CASCADE)}
82+
)
83+
84+
[a_relation, b_relation] = model_1.objects.bulk_create(
85+
[
86+
model_1(name="a"),
87+
model_1(name="b"),
88+
]
89+
)
90+
91+
[a, b] = model_2.objects.bulk_create(
92+
[model_2(relation=a_relation), model_2(relation=b_relation)]
93+
)
94+
95+
results = list(
96+
model_2.objects.filter(
97+
relation__invalues=model_1.objects.all().values_list(
98+
"id", flat=True
99+
)
100+
)
101+
)
102+
assert results == [a, b]

0 commit comments

Comments
 (0)