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.
Files changed (93) hide show
  1. plain/postgres/CHANGELOG.md +1028 -0
  2. plain/postgres/README.md +925 -0
  3. plain/postgres/__init__.py +120 -0
  4. plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
  5. plain/postgres/aggregates.py +236 -0
  6. plain/postgres/backups/__init__.py +0 -0
  7. plain/postgres/backups/cli.py +148 -0
  8. plain/postgres/backups/clients.py +94 -0
  9. plain/postgres/backups/core.py +172 -0
  10. plain/postgres/base.py +1415 -0
  11. plain/postgres/cli/__init__.py +3 -0
  12. plain/postgres/cli/db.py +142 -0
  13. plain/postgres/cli/migrations.py +1085 -0
  14. plain/postgres/config.py +18 -0
  15. plain/postgres/connection.py +1331 -0
  16. plain/postgres/connections.py +77 -0
  17. plain/postgres/constants.py +13 -0
  18. plain/postgres/constraints.py +495 -0
  19. plain/postgres/database_url.py +94 -0
  20. plain/postgres/db.py +59 -0
  21. plain/postgres/default_settings.py +38 -0
  22. plain/postgres/deletion.py +475 -0
  23. plain/postgres/dialect.py +640 -0
  24. plain/postgres/entrypoints.py +4 -0
  25. plain/postgres/enums.py +103 -0
  26. plain/postgres/exceptions.py +217 -0
  27. plain/postgres/expressions.py +1912 -0
  28. plain/postgres/fields/__init__.py +2118 -0
  29. plain/postgres/fields/encrypted.py +354 -0
  30. plain/postgres/fields/json.py +413 -0
  31. plain/postgres/fields/mixins.py +30 -0
  32. plain/postgres/fields/related.py +1192 -0
  33. plain/postgres/fields/related_descriptors.py +290 -0
  34. plain/postgres/fields/related_lookups.py +223 -0
  35. plain/postgres/fields/related_managers.py +661 -0
  36. plain/postgres/fields/reverse_descriptors.py +229 -0
  37. plain/postgres/fields/reverse_related.py +328 -0
  38. plain/postgres/fields/timezones.py +143 -0
  39. plain/postgres/forms.py +773 -0
  40. plain/postgres/functions/__init__.py +189 -0
  41. plain/postgres/functions/comparison.py +127 -0
  42. plain/postgres/functions/datetime.py +454 -0
  43. plain/postgres/functions/math.py +140 -0
  44. plain/postgres/functions/mixins.py +59 -0
  45. plain/postgres/functions/text.py +282 -0
  46. plain/postgres/functions/window.py +125 -0
  47. plain/postgres/indexes.py +286 -0
  48. plain/postgres/lookups.py +758 -0
  49. plain/postgres/meta.py +584 -0
  50. plain/postgres/migrations/__init__.py +53 -0
  51. plain/postgres/migrations/autodetector.py +1379 -0
  52. plain/postgres/migrations/exceptions.py +54 -0
  53. plain/postgres/migrations/executor.py +188 -0
  54. plain/postgres/migrations/graph.py +364 -0
  55. plain/postgres/migrations/loader.py +377 -0
  56. plain/postgres/migrations/migration.py +180 -0
  57. plain/postgres/migrations/operations/__init__.py +34 -0
  58. plain/postgres/migrations/operations/base.py +139 -0
  59. plain/postgres/migrations/operations/fields.py +373 -0
  60. plain/postgres/migrations/operations/models.py +798 -0
  61. plain/postgres/migrations/operations/special.py +184 -0
  62. plain/postgres/migrations/optimizer.py +74 -0
  63. plain/postgres/migrations/questioner.py +340 -0
  64. plain/postgres/migrations/recorder.py +119 -0
  65. plain/postgres/migrations/serializer.py +378 -0
  66. plain/postgres/migrations/state.py +882 -0
  67. plain/postgres/migrations/utils.py +147 -0
  68. plain/postgres/migrations/writer.py +302 -0
  69. plain/postgres/options.py +207 -0
  70. plain/postgres/otel.py +231 -0
  71. plain/postgres/preflight.py +336 -0
  72. plain/postgres/query.py +2242 -0
  73. plain/postgres/query_utils.py +456 -0
  74. plain/postgres/registry.py +217 -0
  75. plain/postgres/schema.py +1885 -0
  76. plain/postgres/sql/__init__.py +40 -0
  77. plain/postgres/sql/compiler.py +1869 -0
  78. plain/postgres/sql/constants.py +22 -0
  79. plain/postgres/sql/datastructures.py +222 -0
  80. plain/postgres/sql/query.py +2947 -0
  81. plain/postgres/sql/where.py +374 -0
  82. plain/postgres/test/__init__.py +0 -0
  83. plain/postgres/test/pytest.py +117 -0
  84. plain/postgres/test/utils.py +18 -0
  85. plain/postgres/transaction.py +222 -0
  86. plain/postgres/types.py +92 -0
  87. plain/postgres/types.pyi +751 -0
  88. plain/postgres/utils.py +345 -0
  89. plain_postgres-0.84.0.dist-info/METADATA +937 -0
  90. plain_postgres-0.84.0.dist-info/RECORD +93 -0
  91. plain_postgres-0.84.0.dist-info/WHEEL +4 -0
  92. plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
  93. 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