plain.postgres 0.84.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/postgres/CHANGELOG.md +1028 -0
- plain/postgres/README.md +925 -0
- plain/postgres/__init__.py +120 -0
- plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
- plain/postgres/aggregates.py +236 -0
- plain/postgres/backups/__init__.py +0 -0
- plain/postgres/backups/cli.py +148 -0
- plain/postgres/backups/clients.py +94 -0
- plain/postgres/backups/core.py +172 -0
- plain/postgres/base.py +1415 -0
- plain/postgres/cli/__init__.py +3 -0
- plain/postgres/cli/db.py +142 -0
- plain/postgres/cli/migrations.py +1085 -0
- plain/postgres/config.py +18 -0
- plain/postgres/connection.py +1331 -0
- plain/postgres/connections.py +77 -0
- plain/postgres/constants.py +13 -0
- plain/postgres/constraints.py +495 -0
- plain/postgres/database_url.py +94 -0
- plain/postgres/db.py +59 -0
- plain/postgres/default_settings.py +38 -0
- plain/postgres/deletion.py +475 -0
- plain/postgres/dialect.py +640 -0
- plain/postgres/entrypoints.py +4 -0
- plain/postgres/enums.py +103 -0
- plain/postgres/exceptions.py +217 -0
- plain/postgres/expressions.py +1912 -0
- plain/postgres/fields/__init__.py +2118 -0
- plain/postgres/fields/encrypted.py +354 -0
- plain/postgres/fields/json.py +413 -0
- plain/postgres/fields/mixins.py +30 -0
- plain/postgres/fields/related.py +1192 -0
- plain/postgres/fields/related_descriptors.py +290 -0
- plain/postgres/fields/related_lookups.py +223 -0
- plain/postgres/fields/related_managers.py +661 -0
- plain/postgres/fields/reverse_descriptors.py +229 -0
- plain/postgres/fields/reverse_related.py +328 -0
- plain/postgres/fields/timezones.py +143 -0
- plain/postgres/forms.py +773 -0
- plain/postgres/functions/__init__.py +189 -0
- plain/postgres/functions/comparison.py +127 -0
- plain/postgres/functions/datetime.py +454 -0
- plain/postgres/functions/math.py +140 -0
- plain/postgres/functions/mixins.py +59 -0
- plain/postgres/functions/text.py +282 -0
- plain/postgres/functions/window.py +125 -0
- plain/postgres/indexes.py +286 -0
- plain/postgres/lookups.py +758 -0
- plain/postgres/meta.py +584 -0
- plain/postgres/migrations/__init__.py +53 -0
- plain/postgres/migrations/autodetector.py +1379 -0
- plain/postgres/migrations/exceptions.py +54 -0
- plain/postgres/migrations/executor.py +188 -0
- plain/postgres/migrations/graph.py +364 -0
- plain/postgres/migrations/loader.py +377 -0
- plain/postgres/migrations/migration.py +180 -0
- plain/postgres/migrations/operations/__init__.py +34 -0
- plain/postgres/migrations/operations/base.py +139 -0
- plain/postgres/migrations/operations/fields.py +373 -0
- plain/postgres/migrations/operations/models.py +798 -0
- plain/postgres/migrations/operations/special.py +184 -0
- plain/postgres/migrations/optimizer.py +74 -0
- plain/postgres/migrations/questioner.py +340 -0
- plain/postgres/migrations/recorder.py +119 -0
- plain/postgres/migrations/serializer.py +378 -0
- plain/postgres/migrations/state.py +882 -0
- plain/postgres/migrations/utils.py +147 -0
- plain/postgres/migrations/writer.py +302 -0
- plain/postgres/options.py +207 -0
- plain/postgres/otel.py +231 -0
- plain/postgres/preflight.py +336 -0
- plain/postgres/query.py +2242 -0
- plain/postgres/query_utils.py +456 -0
- plain/postgres/registry.py +217 -0
- plain/postgres/schema.py +1885 -0
- plain/postgres/sql/__init__.py +40 -0
- plain/postgres/sql/compiler.py +1869 -0
- plain/postgres/sql/constants.py +22 -0
- plain/postgres/sql/datastructures.py +222 -0
- plain/postgres/sql/query.py +2947 -0
- plain/postgres/sql/where.py +374 -0
- plain/postgres/test/__init__.py +0 -0
- plain/postgres/test/pytest.py +117 -0
- plain/postgres/test/utils.py +18 -0
- plain/postgres/transaction.py +222 -0
- plain/postgres/types.py +92 -0
- plain/postgres/types.pyi +751 -0
- plain/postgres/utils.py +345 -0
- plain_postgres-0.84.0.dist-info/METADATA +937 -0
- plain_postgres-0.84.0.dist-info/RECORD +93 -0
- plain_postgres-0.84.0.dist-info/WHEEL +4 -0
- plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
- plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Accessors for related objects.
|
|
3
|
+
|
|
4
|
+
When a field defines a relation between two models, the forward model provides
|
|
5
|
+
an attribute to access related instances. Reverse accessors must be explicitly
|
|
6
|
+
defined using ReverseForeignKey or ReverseManyToMany descriptors.
|
|
7
|
+
|
|
8
|
+
Accessors are implemented as descriptors in order to customize access and
|
|
9
|
+
assignment. This module defines the descriptor classes.
|
|
10
|
+
|
|
11
|
+
Forward accessors follow foreign keys. Reverse accessors trace them back. For
|
|
12
|
+
example, with the following models::
|
|
13
|
+
|
|
14
|
+
class Parent(Model):
|
|
15
|
+
children: ReverseForeignKey[Child] = ReverseForeignKey(to="Child", field="parent")
|
|
16
|
+
|
|
17
|
+
class Child(Model):
|
|
18
|
+
parent: Parent = ForeignKeyField(Parent, on_delete=models.CASCADE)
|
|
19
|
+
|
|
20
|
+
``child.parent`` is a forward foreign key relation. ``parent.children`` is a
|
|
21
|
+
reverse foreign key relation.
|
|
22
|
+
|
|
23
|
+
1. Related instance on the forward side of a foreign key relation:
|
|
24
|
+
``ForwardForeignKeyDescriptor``.
|
|
25
|
+
|
|
26
|
+
Uniqueness of foreign key values is irrelevant to accessing the related
|
|
27
|
+
instance, making the many-to-one and one-to-one cases identical as far as
|
|
28
|
+
the descriptor is concerned. The constraint is checked upstream (unicity
|
|
29
|
+
validation in forms) or downstream (unique indexes in the database).
|
|
30
|
+
|
|
31
|
+
2. Related objects manager for related instances on the forward or reverse
|
|
32
|
+
sides of a many-to-many relation: ``ForwardManyToManyDescriptor``.
|
|
33
|
+
|
|
34
|
+
Many-to-many relations are symmetrical. The syntax of Plain models
|
|
35
|
+
requires declaring them on one side but that's an implementation detail.
|
|
36
|
+
They could be declared on the other side without any change in behavior.
|
|
37
|
+
|
|
38
|
+
Reverse relations must be explicitly defined using ``ReverseForeignKey`` or
|
|
39
|
+
``ReverseManyToMany`` descriptors on the model class.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from functools import cached_property
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
from plain.postgres.query import QuerySet
|
|
48
|
+
from plain.utils.functional import LazyObject
|
|
49
|
+
|
|
50
|
+
from .related_managers import ManyToManyManager
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ForwardForeignKeyDescriptor:
|
|
54
|
+
"""
|
|
55
|
+
Accessor to the related object on the forward side of a foreign key relation.
|
|
56
|
+
|
|
57
|
+
In the example::
|
|
58
|
+
|
|
59
|
+
class Child(Model):
|
|
60
|
+
parent: Parent = ForeignKeyField(Parent, on_delete=models.CASCADE)
|
|
61
|
+
|
|
62
|
+
``Child.parent`` is a ``ForwardForeignKeyDescriptor`` instance.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, field_with_rel: Any) -> None:
|
|
66
|
+
self.field = field_with_rel
|
|
67
|
+
|
|
68
|
+
@cached_property
|
|
69
|
+
def RelatedObjectDoesNotExist(self) -> type:
|
|
70
|
+
# The exception can't be created at initialization time since the
|
|
71
|
+
# related model might not be resolved yet; `self.field.model` might
|
|
72
|
+
# still be a string model reference.
|
|
73
|
+
return type(
|
|
74
|
+
"RelatedObjectDoesNotExist",
|
|
75
|
+
(self.field.remote_field.model.DoesNotExist, AttributeError),
|
|
76
|
+
{
|
|
77
|
+
"__module__": self.field.model.__module__,
|
|
78
|
+
"__qualname__": f"{self.field.model.__qualname__}.{self.field.name}.RelatedObjectDoesNotExist",
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def is_cached(self, instance: Any) -> bool:
|
|
83
|
+
return self.field.is_cached(instance)
|
|
84
|
+
|
|
85
|
+
def get_queryset(self) -> QuerySet:
|
|
86
|
+
qs = self.field.remote_field.model._model_meta.base_queryset
|
|
87
|
+
return qs.all()
|
|
88
|
+
|
|
89
|
+
def get_prefetch_queryset(
|
|
90
|
+
self, instances: list[Any], queryset: QuerySet | None = None
|
|
91
|
+
) -> tuple[QuerySet, Any, Any, bool, str, bool]:
|
|
92
|
+
if queryset is None:
|
|
93
|
+
queryset = self.get_queryset()
|
|
94
|
+
|
|
95
|
+
rel_obj_attr = self.field.get_foreign_related_value
|
|
96
|
+
instance_attr = self.field.get_local_related_value
|
|
97
|
+
instances_dict = {instance_attr(inst): inst for inst in instances}
|
|
98
|
+
related_field = self.field.foreign_related_fields[0]
|
|
99
|
+
remote_field = self.field.remote_field
|
|
100
|
+
|
|
101
|
+
# FIXME: This will need to be revisited when we introduce support for
|
|
102
|
+
# composite fields. In the meantime we take this practical approach.
|
|
103
|
+
# Refs #21410.
|
|
104
|
+
# The check for len(...) == 1 is a special case that allows the query
|
|
105
|
+
# to be join-less and smaller. Refs #21760.
|
|
106
|
+
if len(self.field.foreign_related_fields) == 1:
|
|
107
|
+
query = {
|
|
108
|
+
f"{related_field.name}__in": {
|
|
109
|
+
instance_attr(inst)[0] for inst in instances
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else:
|
|
113
|
+
query = {f"{self.field.related_query_name()}__in": instances}
|
|
114
|
+
queryset = queryset.filter(**query)
|
|
115
|
+
|
|
116
|
+
# Since we're going to assign directly in the cache,
|
|
117
|
+
# we must manage the reverse relation cache manually.
|
|
118
|
+
if not remote_field.multiple:
|
|
119
|
+
for rel_obj in queryset:
|
|
120
|
+
instance = instances_dict[rel_obj_attr(rel_obj)]
|
|
121
|
+
remote_field.set_cached_value(rel_obj, instance)
|
|
122
|
+
return (
|
|
123
|
+
queryset,
|
|
124
|
+
rel_obj_attr,
|
|
125
|
+
instance_attr,
|
|
126
|
+
True,
|
|
127
|
+
self.field.get_cache_name(),
|
|
128
|
+
False,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def get_object(self, instance: Any) -> Any:
|
|
132
|
+
qs = self.get_queryset()
|
|
133
|
+
# Assuming the database enforces foreign keys, this won't fail.
|
|
134
|
+
return qs.get(self.field.get_reverse_related_filter(instance))
|
|
135
|
+
|
|
136
|
+
def __get__(
|
|
137
|
+
self, instance: Any | None, cls: type | None = None
|
|
138
|
+
) -> ForwardForeignKeyDescriptor | Any | None:
|
|
139
|
+
"""
|
|
140
|
+
Get the related instance through the forward relation.
|
|
141
|
+
|
|
142
|
+
With the example above, when getting ``child.parent``:
|
|
143
|
+
|
|
144
|
+
- ``self`` is the descriptor managing the ``parent`` attribute
|
|
145
|
+
- ``instance`` is the ``child`` instance
|
|
146
|
+
- ``cls`` is the ``Child`` class (we don't need it)
|
|
147
|
+
"""
|
|
148
|
+
if instance is None:
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
# The related instance is loaded from the database and then cached
|
|
152
|
+
# by the field on the model instance state. It can also be pre-cached
|
|
153
|
+
# by the reverse accessor.
|
|
154
|
+
try:
|
|
155
|
+
rel_obj = self.field.get_cached_value(instance)
|
|
156
|
+
except KeyError:
|
|
157
|
+
has_value = None not in self.field.get_local_related_value(instance)
|
|
158
|
+
rel_obj = None
|
|
159
|
+
|
|
160
|
+
if rel_obj is None and has_value:
|
|
161
|
+
rel_obj = self.get_object(instance)
|
|
162
|
+
remote_field = self.field.remote_field
|
|
163
|
+
# If this is a one-to-one relation, set the reverse accessor
|
|
164
|
+
# cache on the related object to the current instance to avoid
|
|
165
|
+
# an extra SQL query if it's accessed later on.
|
|
166
|
+
if not remote_field.multiple:
|
|
167
|
+
remote_field.set_cached_value(rel_obj, instance)
|
|
168
|
+
self.field.set_cached_value(instance, rel_obj)
|
|
169
|
+
|
|
170
|
+
if rel_obj is None and not self.field.allow_null:
|
|
171
|
+
raise self.RelatedObjectDoesNotExist(
|
|
172
|
+
f"{self.field.model.__name__} has no {self.field.name}."
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
return rel_obj
|
|
176
|
+
|
|
177
|
+
def __set__(self, instance: Any, value: Any) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Set the related instance through the forward relation.
|
|
180
|
+
|
|
181
|
+
With the example above, when setting ``child.parent = parent``:
|
|
182
|
+
|
|
183
|
+
- ``self`` is the descriptor managing the ``parent`` attribute
|
|
184
|
+
- ``instance`` is the ``child`` instance
|
|
185
|
+
- ``value`` is the ``parent`` instance on the right of the equal sign
|
|
186
|
+
"""
|
|
187
|
+
# If value is a LazyObject, force its evaluation. For ForeignKeyField fields,
|
|
188
|
+
# the value should only be None or a model instance, never a boolean or
|
|
189
|
+
# other type.
|
|
190
|
+
if isinstance(value, LazyObject):
|
|
191
|
+
# This forces evaluation: if it's None, value becomes None;
|
|
192
|
+
# if it's a User instance, value becomes that instance.
|
|
193
|
+
value = value if value else None
|
|
194
|
+
|
|
195
|
+
# An object must be an instance of the related class.
|
|
196
|
+
if value is not None and not isinstance(value, self.field.remote_field.model):
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f'Cannot assign "{value!r}": "{instance.model_options.object_name}.{self.field.name}" must be a "{self.field.remote_field.model.model_options.object_name}" instance.'
|
|
199
|
+
)
|
|
200
|
+
remote_field = self.field.remote_field
|
|
201
|
+
# If we're setting the value of a OneToOneField to None, we need to clear
|
|
202
|
+
# out the cache on any old related object. Otherwise, deleting the
|
|
203
|
+
# previously-related object will also cause this object to be deleted,
|
|
204
|
+
# which is wrong.
|
|
205
|
+
if value is None:
|
|
206
|
+
# Look up the previously-related object, which may still be available
|
|
207
|
+
# since we've not yet cleared out the related field.
|
|
208
|
+
# Use the cache directly, instead of the accessor; if we haven't
|
|
209
|
+
# populated the cache, then we don't care - we're only accessing
|
|
210
|
+
# the object to invalidate the accessor cache, so there's no
|
|
211
|
+
# need to populate the cache just to expire it again.
|
|
212
|
+
related = self.field.get_cached_value(instance, default=None)
|
|
213
|
+
|
|
214
|
+
# If we've got an old related object, we need to clear out its
|
|
215
|
+
# cache. This cache also might not exist if the related object
|
|
216
|
+
# hasn't been accessed yet.
|
|
217
|
+
if related is not None:
|
|
218
|
+
remote_field.set_cached_value(related, None)
|
|
219
|
+
|
|
220
|
+
for lh_field, rh_field in self.field.related_fields:
|
|
221
|
+
setattr(instance, lh_field.attname, None)
|
|
222
|
+
|
|
223
|
+
# Set the values of the related field.
|
|
224
|
+
else:
|
|
225
|
+
for lh_field, rh_field in self.field.related_fields:
|
|
226
|
+
setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
|
|
227
|
+
|
|
228
|
+
# Set the related instance cache used by __get__ to avoid an SQL query
|
|
229
|
+
# when accessing the attribute we just set.
|
|
230
|
+
self.field.set_cached_value(instance, value)
|
|
231
|
+
|
|
232
|
+
# If this is a one-to-one relation, set the reverse accessor cache on
|
|
233
|
+
# the related object to the current instance to avoid an extra SQL
|
|
234
|
+
# query if it's accessed later on.
|
|
235
|
+
if value is not None and not remote_field.multiple:
|
|
236
|
+
remote_field.set_cached_value(value, instance)
|
|
237
|
+
|
|
238
|
+
def __reduce__(self) -> tuple[Any, tuple[Any, str]]:
|
|
239
|
+
"""
|
|
240
|
+
Pickling should return the instance attached by self.field on the
|
|
241
|
+
model, not a new copy of that descriptor. Use getattr() to retrieve
|
|
242
|
+
the instance directly from the model.
|
|
243
|
+
"""
|
|
244
|
+
return getattr, (self.field.model, self.field.name)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ForwardManyToManyDescriptor:
|
|
248
|
+
"""
|
|
249
|
+
Accessor to the related objects manager on the forward side of a
|
|
250
|
+
many-to-many relation.
|
|
251
|
+
|
|
252
|
+
In the example::
|
|
253
|
+
|
|
254
|
+
class Pizza(Model):
|
|
255
|
+
toppings: ManyToManyField[Topping] = ManyToManyField(Topping, through=PizzaTopping)
|
|
256
|
+
|
|
257
|
+
``Pizza.toppings`` is a ``ForwardManyToManyDescriptor`` instance.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(self, rel: Any) -> None:
|
|
261
|
+
self.rel = rel
|
|
262
|
+
self.field = rel.field
|
|
263
|
+
|
|
264
|
+
def __get__(
|
|
265
|
+
self, instance: Any | None, cls: type | None = None
|
|
266
|
+
) -> ForwardManyToManyDescriptor | Any:
|
|
267
|
+
"""Get the related manager when the descriptor is accessed."""
|
|
268
|
+
if instance is None:
|
|
269
|
+
return self
|
|
270
|
+
return ManyToManyManager(
|
|
271
|
+
instance=instance,
|
|
272
|
+
field=self.rel.field,
|
|
273
|
+
through=self.rel.through,
|
|
274
|
+
related_model=self.rel.model,
|
|
275
|
+
is_reverse=False,
|
|
276
|
+
symmetrical=self.rel.symmetrical,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def __set__(self, instance: Any, value: Any) -> None:
|
|
280
|
+
"""Prevent direct assignment to the relation."""
|
|
281
|
+
raise TypeError(
|
|
282
|
+
f"Direct assignment to the forward side of a many-to-many set is prohibited. Use {self.field.name}.set() instead.",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def through(self) -> Any:
|
|
287
|
+
# through is provided so that you have easy access to the through
|
|
288
|
+
# model (Book.authors.through) for inlines, etc. This is done as
|
|
289
|
+
# a property to ensure that the fully resolved value is returned.
|
|
290
|
+
return self.rel.through
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from plain.postgres.lookups import (
|
|
6
|
+
Exact,
|
|
7
|
+
GreaterThan,
|
|
8
|
+
GreaterThanOrEqual,
|
|
9
|
+
In,
|
|
10
|
+
IsNull,
|
|
11
|
+
LessThan,
|
|
12
|
+
LessThanOrEqual,
|
|
13
|
+
Lookup,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from plain.postgres.connection import DatabaseConnection
|
|
18
|
+
from plain.postgres.sql.compiler import SQLCompiler
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MultiColSource:
|
|
22
|
+
contains_aggregate = False
|
|
23
|
+
contains_over_clause = False
|
|
24
|
+
|
|
25
|
+
def __init__(self, alias: str, targets: Any, sources: Any, field: Any) -> None:
|
|
26
|
+
self.targets, self.sources, self.field, self.alias = (
|
|
27
|
+
targets,
|
|
28
|
+
sources,
|
|
29
|
+
field,
|
|
30
|
+
alias,
|
|
31
|
+
)
|
|
32
|
+
self.output_field = self.field
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return f"{self.__class__.__name__}({self.alias}, {self.field})"
|
|
36
|
+
|
|
37
|
+
def relabeled_clone(self, relabels: dict[str, str]) -> MultiColSource:
|
|
38
|
+
return self.__class__(
|
|
39
|
+
relabels.get(self.alias, self.alias), self.targets, self.sources, self.field
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get_lookup(self, lookup: str) -> type[Lookup] | None:
|
|
43
|
+
return self.output_field.get_lookup(lookup)
|
|
44
|
+
|
|
45
|
+
def resolve_expression(self, *args: Any, **kwargs: Any) -> MultiColSource:
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_normalized_value(value: Any, lhs: Any) -> tuple[Any, ...]:
|
|
50
|
+
from plain.postgres import Model
|
|
51
|
+
from plain.postgres.fields.related import RelatedField
|
|
52
|
+
|
|
53
|
+
if isinstance(value, Model):
|
|
54
|
+
if value.id is None:
|
|
55
|
+
raise ValueError("Model instances passed to related filters must be saved.")
|
|
56
|
+
value_list = []
|
|
57
|
+
sources = lhs.output_field.path_infos[-1].target_fields
|
|
58
|
+
for source in sources:
|
|
59
|
+
while not isinstance(value, source.model) and isinstance(
|
|
60
|
+
source, RelatedField
|
|
61
|
+
):
|
|
62
|
+
source = source.remote_field.model._model_meta.get_field(
|
|
63
|
+
source.remote_field.field_name
|
|
64
|
+
)
|
|
65
|
+
try:
|
|
66
|
+
value_list.append(getattr(value, source.attname))
|
|
67
|
+
except AttributeError:
|
|
68
|
+
# A case like Restaurant.query.filter(place=restaurant_instance),
|
|
69
|
+
# where place is a OneToOneField and the primary key of Restaurant.
|
|
70
|
+
return (value.id,)
|
|
71
|
+
return tuple(value_list)
|
|
72
|
+
if not isinstance(value, tuple):
|
|
73
|
+
return (value,)
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RelatedIn(In):
|
|
78
|
+
def get_prep_lookup(self) -> list[Any]:
|
|
79
|
+
if not isinstance(self.lhs, MultiColSource):
|
|
80
|
+
if self.rhs_is_direct_value():
|
|
81
|
+
# If we get here, we are dealing with single-column relations.
|
|
82
|
+
self.rhs = [get_normalized_value(val, self.lhs)[0] for val in self.rhs]
|
|
83
|
+
# We need to run the related field's get_prep_value(). Consider
|
|
84
|
+
# case ForeignKeyField to IntegerField given value 'abc'. The
|
|
85
|
+
# ForeignKeyField itself doesn't have validation for non-integers,
|
|
86
|
+
# so we must run validation using the target field.
|
|
87
|
+
if hasattr(self.lhs.output_field, "path_infos"):
|
|
88
|
+
# Run the target field's get_prep_value. We can safely
|
|
89
|
+
# assume there is only one as we don't get to the direct
|
|
90
|
+
# value branch otherwise.
|
|
91
|
+
target_field = self.lhs.output_field.path_infos[-1].target_fields[
|
|
92
|
+
-1
|
|
93
|
+
]
|
|
94
|
+
self.rhs = [target_field.get_prep_value(v) for v in self.rhs]
|
|
95
|
+
elif not getattr(self.rhs, "has_select_fields", True) and not getattr(
|
|
96
|
+
self.lhs.field.target_field, "primary_key", False
|
|
97
|
+
):
|
|
98
|
+
if (
|
|
99
|
+
getattr(self.lhs.output_field, "primary_key", False)
|
|
100
|
+
and self.lhs.output_field.model == self.rhs.model
|
|
101
|
+
):
|
|
102
|
+
# A case like
|
|
103
|
+
# Restaurant.query.filter(place__in=restaurant_qs), where
|
|
104
|
+
# place is a OneToOneField and the primary key of
|
|
105
|
+
# Restaurant.
|
|
106
|
+
target_field = self.lhs.field.name
|
|
107
|
+
else:
|
|
108
|
+
target_field = self.lhs.field.target_field.name
|
|
109
|
+
self.rhs.set_values([target_field])
|
|
110
|
+
return super().get_prep_lookup()
|
|
111
|
+
|
|
112
|
+
def as_sql(
|
|
113
|
+
self, compiler: SQLCompiler, connection: DatabaseConnection
|
|
114
|
+
) -> tuple[str, list[Any]]:
|
|
115
|
+
if isinstance(self.lhs, MultiColSource):
|
|
116
|
+
# For multicolumn lookups we need to build a multicolumn where clause.
|
|
117
|
+
# This clause is either a SubqueryConstraint (for values that need
|
|
118
|
+
# to be compiled to SQL) or an OR-combined list of
|
|
119
|
+
# (col1 = val1 AND col2 = val2 AND ...) clauses.
|
|
120
|
+
from plain.postgres.sql.where import (
|
|
121
|
+
AND,
|
|
122
|
+
OR,
|
|
123
|
+
SubqueryConstraint,
|
|
124
|
+
WhereNode,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
root_constraint = WhereNode(connector=OR)
|
|
128
|
+
if self.rhs_is_direct_value():
|
|
129
|
+
values = [get_normalized_value(value, self.lhs) for value in self.rhs]
|
|
130
|
+
for value in values:
|
|
131
|
+
value_constraint = WhereNode()
|
|
132
|
+
for source, target, val in zip(
|
|
133
|
+
self.lhs.sources, self.lhs.targets, value
|
|
134
|
+
):
|
|
135
|
+
lookup_class = target.get_lookup("exact")
|
|
136
|
+
lookup = lookup_class(
|
|
137
|
+
target.get_col(self.lhs.alias, source), val
|
|
138
|
+
)
|
|
139
|
+
value_constraint.add(lookup, AND)
|
|
140
|
+
root_constraint.add(value_constraint, OR)
|
|
141
|
+
else:
|
|
142
|
+
root_constraint.add(
|
|
143
|
+
SubqueryConstraint(
|
|
144
|
+
self.lhs.alias,
|
|
145
|
+
[target.column for target in self.lhs.targets],
|
|
146
|
+
[source.name for source in self.lhs.sources],
|
|
147
|
+
self.rhs,
|
|
148
|
+
),
|
|
149
|
+
AND,
|
|
150
|
+
)
|
|
151
|
+
return root_constraint.as_sql(compiler, connection)
|
|
152
|
+
return super().as_sql(compiler, connection)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class RelatedLookupMixin(Lookup):
|
|
156
|
+
# Type hints for attributes/methods expected from Lookup base class
|
|
157
|
+
lhs: Any
|
|
158
|
+
rhs: Any
|
|
159
|
+
prepare_rhs: bool
|
|
160
|
+
lookup_name: str | None
|
|
161
|
+
|
|
162
|
+
def get_prep_lookup(self) -> Any:
|
|
163
|
+
if not isinstance(self.lhs, MultiColSource) and not hasattr(
|
|
164
|
+
self.rhs, "resolve_expression"
|
|
165
|
+
):
|
|
166
|
+
# If we get here, we are dealing with single-column relations.
|
|
167
|
+
self.rhs = get_normalized_value(self.rhs, self.lhs)[0]
|
|
168
|
+
# We need to run the related field's get_prep_value(). Consider case
|
|
169
|
+
# ForeignKeyField to IntegerField given value 'abc'. The ForeignKeyField itself
|
|
170
|
+
# doesn't have validation for non-integers, so we must run validation
|
|
171
|
+
# using the target field.
|
|
172
|
+
if self.prepare_rhs and hasattr(self.lhs.output_field, "path_infos"):
|
|
173
|
+
# Get the target field. We can safely assume there is only one
|
|
174
|
+
# as we don't get to the direct value branch otherwise.
|
|
175
|
+
target_field = self.lhs.output_field.path_infos[-1].target_fields[-1]
|
|
176
|
+
self.rhs = target_field.get_prep_value(self.rhs)
|
|
177
|
+
|
|
178
|
+
return super().get_prep_lookup()
|
|
179
|
+
|
|
180
|
+
def as_sql(
|
|
181
|
+
self, compiler: SQLCompiler, connection: DatabaseConnection
|
|
182
|
+
) -> tuple[str, list[Any]]:
|
|
183
|
+
if isinstance(self.lhs, MultiColSource):
|
|
184
|
+
assert self.rhs_is_direct_value()
|
|
185
|
+
self.rhs = get_normalized_value(self.rhs, self.lhs)
|
|
186
|
+
from plain.postgres.sql.where import AND, WhereNode
|
|
187
|
+
|
|
188
|
+
root_constraint = WhereNode()
|
|
189
|
+
for target, source, val in zip(
|
|
190
|
+
self.lhs.targets, self.lhs.sources, self.rhs
|
|
191
|
+
):
|
|
192
|
+
lookup_class = target.get_lookup(self.lookup_name)
|
|
193
|
+
root_constraint.add(
|
|
194
|
+
lookup_class(target.get_col(self.lhs.alias, source), val), AND
|
|
195
|
+
)
|
|
196
|
+
sql, params = root_constraint.as_sql(compiler, connection)
|
|
197
|
+
return sql, list(params)
|
|
198
|
+
sql, params = super().as_sql(compiler, connection)
|
|
199
|
+
return sql, list(params)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class RelatedExact(RelatedLookupMixin, Exact):
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class RelatedLessThan(RelatedLookupMixin, LessThan):
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class RelatedGreaterThan(RelatedLookupMixin, GreaterThan):
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class RelatedGreaterThanOrEqual(RelatedLookupMixin, GreaterThanOrEqual):
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class RelatedLessThanOrEqual(RelatedLookupMixin, LessThanOrEqual):
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class RelatedIsNull(RelatedLookupMixin, IsNull):
|
|
223
|
+
pass
|