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,413 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from plain import exceptions, preflight
8
+ from plain.postgres import expressions, lookups
9
+ from plain.postgres.constants import LOOKUP_SEP
10
+ from plain.postgres.dialect import adapt_json_value
11
+ from plain.postgres.fields import TextField
12
+ from plain.postgres.lookups import (
13
+ FieldGetDbPrepValueMixin,
14
+ Lookup,
15
+ OperatorLookup,
16
+ Transform,
17
+ )
18
+
19
+ from . import Field
20
+
21
+ if TYPE_CHECKING:
22
+ from plain.postgres.connection import DatabaseConnection
23
+ from plain.postgres.sql.compiler import SQLCompiler
24
+ from plain.preflight.results import PreflightResult
25
+
26
+ __all__ = ["JSONField"]
27
+
28
+
29
+ class JSONField(Field):
30
+ empty_strings_allowed = False
31
+ description = "A JSON object"
32
+ default_error_messages = {
33
+ "invalid": "Value must be valid JSON.",
34
+ }
35
+ _default_fix = ("dict", "{}")
36
+
37
+ def __init__(
38
+ self,
39
+ *,
40
+ encoder: type[json.JSONEncoder] | None = None,
41
+ decoder: type[json.JSONDecoder] | None = None,
42
+ **kwargs: Any,
43
+ ):
44
+ if encoder and not callable(encoder):
45
+ raise ValueError("The encoder parameter must be a callable object.")
46
+ if decoder and not callable(decoder):
47
+ raise ValueError("The decoder parameter must be a callable object.")
48
+ self.encoder = encoder
49
+ self.decoder = decoder
50
+ super().__init__(**kwargs)
51
+
52
+ def _check_default(self) -> list[PreflightResult]:
53
+ if (
54
+ self.has_default()
55
+ and self.default is not None
56
+ and not callable(self.default)
57
+ ):
58
+ return [
59
+ preflight.PreflightResult(
60
+ fix=(
61
+ f"{self.__class__.__name__} default should be a callable instead of an instance "
62
+ "so that it's not shared between all field instances. "
63
+ "Use a callable instead, e.g., use `{}` instead of "
64
+ "`{}`.".format(*self._default_fix)
65
+ ),
66
+ obj=self,
67
+ id="fields.invalid_choice_mixin_default",
68
+ warning=True,
69
+ )
70
+ ]
71
+ else:
72
+ return []
73
+
74
+ def preflight(self, **kwargs: Any) -> list[PreflightResult]:
75
+ errors = super().preflight(**kwargs)
76
+ errors.extend(self._check_default())
77
+ errors.extend(self._check_supported())
78
+ return errors
79
+
80
+ def _check_supported(self) -> list[PreflightResult]:
81
+ # PostgreSQL always supports JSONField (native JSONB type).
82
+ return []
83
+
84
+ def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
85
+ name, path, args, kwargs = super().deconstruct()
86
+ if self.encoder is not None:
87
+ kwargs["encoder"] = self.encoder
88
+ if self.decoder is not None:
89
+ kwargs["decoder"] = self.decoder
90
+ return name, path, args, kwargs
91
+
92
+ def from_db_value(
93
+ self, value: Any, expression: Any, connection: DatabaseConnection
94
+ ) -> Any:
95
+ if value is None:
96
+ return value
97
+ # KeyTransform may extract non-string values directly.
98
+ if isinstance(expression, KeyTransform) and not isinstance(value, str):
99
+ return value
100
+ try:
101
+ return json.loads(value, cls=self.decoder)
102
+ except json.JSONDecodeError:
103
+ return value
104
+
105
+ def get_internal_type(self) -> str:
106
+ return "JSONField"
107
+
108
+ def get_db_prep_value(
109
+ self, value: Any, connection: DatabaseConnection, prepared: bool = False
110
+ ) -> Any:
111
+ if isinstance(value, expressions.Value) and isinstance(
112
+ value.output_field, JSONField
113
+ ):
114
+ value = value.value
115
+ elif hasattr(value, "as_sql"):
116
+ return value
117
+ return adapt_json_value(value, self.encoder)
118
+
119
+ def get_db_prep_save(self, value: Any, connection: DatabaseConnection) -> Any:
120
+ if value is None:
121
+ return value
122
+ return self.get_db_prep_value(value, connection)
123
+
124
+ def get_transform(
125
+ self, lookup_name: str
126
+ ) -> type[Transform] | Callable[..., Any] | None:
127
+ # Always returns a transform (never None in practice)
128
+ transform = super().get_transform(lookup_name)
129
+ if transform:
130
+ return transform
131
+ return KeyTransformFactory(lookup_name)
132
+
133
+ def validate(self, value: Any, model_instance: Any) -> None:
134
+ super().validate(value, model_instance)
135
+ try:
136
+ json.dumps(value, cls=self.encoder)
137
+ except TypeError:
138
+ raise exceptions.ValidationError(
139
+ self.error_messages["invalid"],
140
+ code="invalid",
141
+ params={"value": value},
142
+ )
143
+
144
+ def value_to_string(self, obj: Any) -> Any:
145
+ return self.value_from_object(obj)
146
+
147
+
148
+ class DataContains(FieldGetDbPrepValueMixin, OperatorLookup):
149
+ lookup_name = "contains"
150
+ # PostgreSQL @> operator checks if left JSON contains right JSON.
151
+ operator = "@>"
152
+
153
+
154
+ class ContainedBy(FieldGetDbPrepValueMixin, OperatorLookup):
155
+ lookup_name = "contained_by"
156
+ # PostgreSQL <@ operator checks if left JSON is contained by right JSON.
157
+ operator = "<@"
158
+
159
+
160
+ class HasKeyLookup(OperatorLookup):
161
+ """Lookup for checking if a JSON field has a key."""
162
+
163
+ logical_operator: str | None = None
164
+
165
+ def as_sql(
166
+ self, compiler: SQLCompiler, connection: DatabaseConnection
167
+ ) -> tuple[str, tuple[Any, ...]]:
168
+ # Handle KeyTransform on RHS by expanding it into LHS chain.
169
+ if isinstance(self.rhs, KeyTransform):
170
+ *_, rhs_key_transforms = self.rhs.preprocess_lhs(compiler, connection)
171
+ for key in rhs_key_transforms[:-1]:
172
+ self.lhs = KeyTransform(key, self.lhs)
173
+ self.rhs = rhs_key_transforms[-1]
174
+ return super().as_sql(compiler, connection)
175
+
176
+
177
+ class HasKey(HasKeyLookup):
178
+ lookup_name = "has_key"
179
+ # PostgreSQL ? operator checks if key exists.
180
+ operator = "?"
181
+ prepare_rhs = False
182
+
183
+
184
+ class HasKeys(HasKeyLookup):
185
+ lookup_name = "has_keys"
186
+ # PostgreSQL ?& operator checks if all keys exist.
187
+ operator = "?&"
188
+ logical_operator = " AND "
189
+
190
+ def get_prep_lookup(self) -> list[str]:
191
+ return [str(item) for item in self.rhs]
192
+
193
+
194
+ class HasAnyKeys(HasKeys):
195
+ lookup_name = "has_any_keys"
196
+ # PostgreSQL ?| operator checks if any key exists.
197
+ operator = "?|"
198
+ logical_operator = " OR "
199
+
200
+
201
+ class JSONExact(lookups.Exact):
202
+ can_use_none_as_rhs = True
203
+
204
+ def process_rhs(
205
+ self, compiler: SQLCompiler, connection: DatabaseConnection
206
+ ) -> tuple[str, list[Any]] | tuple[list[str], list[Any]]:
207
+ rhs, rhs_params = super().process_rhs(compiler, connection)
208
+ if isinstance(rhs, str):
209
+ # Treat None lookup values as null.
210
+ if rhs == "%s" and rhs_params == [None]:
211
+ rhs_params = ["null"]
212
+ return rhs, rhs_params
213
+ else:
214
+ return rhs, rhs_params
215
+
216
+
217
+ class JSONIContains(lookups.IContains):
218
+ pass
219
+
220
+
221
+ JSONField.register_lookup(DataContains)
222
+ JSONField.register_lookup(ContainedBy)
223
+ JSONField.register_lookup(HasKey)
224
+ JSONField.register_lookup(HasKeys)
225
+ JSONField.register_lookup(HasAnyKeys)
226
+ JSONField.register_lookup(JSONExact)
227
+ JSONField.register_lookup(JSONIContains)
228
+
229
+
230
+ class KeyTransform(Transform):
231
+ # PostgreSQL -> operator extracts JSON object field as JSON.
232
+ operator = "->"
233
+ # PostgreSQL #> operator extracts nested JSON path as JSON.
234
+ nested_operator = "#>"
235
+
236
+ def __init__(self, key_name: str, *args: Any, **kwargs: Any):
237
+ super().__init__(*args, **kwargs)
238
+ self.key_name = str(key_name)
239
+
240
+ def preprocess_lhs(
241
+ self, compiler: SQLCompiler, connection: DatabaseConnection
242
+ ) -> tuple[str, tuple[Any, ...], list[str]]:
243
+ key_transforms = [self.key_name]
244
+ previous = self.lhs
245
+ while isinstance(previous, KeyTransform):
246
+ key_transforms.insert(0, previous.key_name)
247
+ previous = previous.lhs
248
+ lhs, params = compiler.compile(previous)
249
+ return lhs, params, key_transforms
250
+
251
+ def as_sql(
252
+ self,
253
+ compiler: SQLCompiler,
254
+ connection: DatabaseConnection,
255
+ function: str | None = None,
256
+ template: str | None = None,
257
+ arg_joiner: str | None = None,
258
+ **extra_context: Any,
259
+ ) -> tuple[str, list[Any]]:
260
+ lhs, params, key_transforms = self.preprocess_lhs(compiler, connection)
261
+ if len(key_transforms) > 1:
262
+ sql = f"({lhs} {self.nested_operator} %s)"
263
+ return sql, list(params) + [key_transforms]
264
+ try:
265
+ lookup = int(self.key_name)
266
+ except ValueError:
267
+ lookup = self.key_name
268
+ return f"({lhs} {self.operator} %s)", list(params) + [lookup]
269
+
270
+
271
+ class KeyTextTransform(KeyTransform):
272
+ # PostgreSQL ->> operator extracts JSON object field as text.
273
+ operator = "->>"
274
+ # PostgreSQL #>> operator extracts nested JSON path as text.
275
+ nested_operator = "#>>"
276
+ output_field = TextField()
277
+
278
+ @classmethod
279
+ def from_lookup(cls, lookup: str) -> Any:
280
+ transform, *keys = lookup.split(LOOKUP_SEP)
281
+ if not keys:
282
+ raise ValueError("Lookup must contain key or index transforms.")
283
+ for key in keys:
284
+ transform = cls(key, transform)
285
+ return transform
286
+
287
+
288
+ KT = KeyTextTransform.from_lookup
289
+
290
+
291
+ class KeyTransformTextLookupMixin(Lookup):
292
+ """
293
+ Mixin for lookups expecting text LHS from a JSONField key lookup.
294
+ Uses the ->> operator to extract JSON values as text.
295
+ """
296
+
297
+ def __init__(self, key_transform: Any, *args: Any, **kwargs: Any):
298
+ if not isinstance(key_transform, KeyTransform):
299
+ raise TypeError(
300
+ "Transform should be an instance of KeyTransform in order to "
301
+ "use this lookup."
302
+ )
303
+ key_text_transform = KeyTextTransform(
304
+ key_transform.key_name,
305
+ *key_transform.source_expressions,
306
+ **key_transform.extra,
307
+ )
308
+ super().__init__(key_text_transform, *args, **kwargs)
309
+
310
+
311
+ class KeyTransformIsNull(lookups.IsNull):
312
+ # key__isnull=False is the same as has_key='key'
313
+ pass
314
+
315
+
316
+ class KeyTransformIn(lookups.In):
317
+ def resolve_expression_parameter(
318
+ self,
319
+ compiler: SQLCompiler,
320
+ connection: DatabaseConnection,
321
+ sql: str,
322
+ param: Any,
323
+ ) -> tuple[str, list[Any]]:
324
+ sql, params = super().resolve_expression_parameter(
325
+ compiler,
326
+ connection,
327
+ sql,
328
+ param,
329
+ )
330
+ return sql, list(params)
331
+
332
+
333
+ class KeyTransformExact(JSONExact):
334
+ def process_rhs(
335
+ self, compiler: SQLCompiler, connection: DatabaseConnection
336
+ ) -> tuple[str, list[Any]] | tuple[list[str], list[Any]]:
337
+ if isinstance(self.rhs, KeyTransform):
338
+ return super(lookups.Exact, self).process_rhs(compiler, connection)
339
+ return super().process_rhs(compiler, connection)
340
+
341
+
342
+ class KeyTransformIExact(KeyTransformTextLookupMixin, lookups.IExact):
343
+ pass
344
+
345
+
346
+ class KeyTransformIContains(KeyTransformTextLookupMixin, lookups.IContains):
347
+ pass
348
+
349
+
350
+ class KeyTransformStartsWith(KeyTransformTextLookupMixin, lookups.StartsWith):
351
+ pass
352
+
353
+
354
+ class KeyTransformIStartsWith(KeyTransformTextLookupMixin, lookups.IStartsWith):
355
+ pass
356
+
357
+
358
+ class KeyTransformEndsWith(KeyTransformTextLookupMixin, lookups.EndsWith):
359
+ pass
360
+
361
+
362
+ class KeyTransformIEndsWith(KeyTransformTextLookupMixin, lookups.IEndsWith):
363
+ pass
364
+
365
+
366
+ class KeyTransformRegex(KeyTransformTextLookupMixin, lookups.Regex):
367
+ pass
368
+
369
+
370
+ class KeyTransformIRegex(KeyTransformTextLookupMixin, lookups.IRegex):
371
+ pass
372
+
373
+
374
+ class KeyTransformLt(lookups.LessThan):
375
+ pass
376
+
377
+
378
+ class KeyTransformLte(lookups.LessThanOrEqual):
379
+ pass
380
+
381
+
382
+ class KeyTransformGt(lookups.GreaterThan):
383
+ pass
384
+
385
+
386
+ class KeyTransformGte(lookups.GreaterThanOrEqual):
387
+ pass
388
+
389
+
390
+ KeyTransform.register_lookup(KeyTransformIn)
391
+ KeyTransform.register_lookup(KeyTransformExact)
392
+ KeyTransform.register_lookup(KeyTransformIExact)
393
+ KeyTransform.register_lookup(KeyTransformIsNull)
394
+ KeyTransform.register_lookup(KeyTransformIContains)
395
+ KeyTransform.register_lookup(KeyTransformStartsWith)
396
+ KeyTransform.register_lookup(KeyTransformIStartsWith)
397
+ KeyTransform.register_lookup(KeyTransformEndsWith)
398
+ KeyTransform.register_lookup(KeyTransformIEndsWith)
399
+ KeyTransform.register_lookup(KeyTransformRegex)
400
+ KeyTransform.register_lookup(KeyTransformIRegex)
401
+
402
+ KeyTransform.register_lookup(KeyTransformLt)
403
+ KeyTransform.register_lookup(KeyTransformLte)
404
+ KeyTransform.register_lookup(KeyTransformGt)
405
+ KeyTransform.register_lookup(KeyTransformGte)
406
+
407
+
408
+ class KeyTransformFactory:
409
+ def __init__(self, key_name: str):
410
+ self.key_name = key_name
411
+
412
+ def __call__(self, *args: Any, **kwargs: Any) -> KeyTransform:
413
+ return KeyTransform(self.key_name, *args, **kwargs)
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ NOT_PROVIDED = object()
6
+
7
+
8
+ class FieldCacheMixin:
9
+ """Provide an API for working with the model's fields value cache."""
10
+
11
+ def get_cache_name(self) -> str:
12
+ raise NotImplementedError
13
+
14
+ def get_cached_value(self, instance: Any, default: Any = NOT_PROVIDED) -> Any:
15
+ cache_name = self.get_cache_name()
16
+ try:
17
+ return instance._state.fields_cache[cache_name]
18
+ except KeyError:
19
+ if default is NOT_PROVIDED:
20
+ raise
21
+ return default
22
+
23
+ def is_cached(self, instance: Any) -> bool:
24
+ return self.get_cache_name() in instance._state.fields_cache
25
+
26
+ def set_cached_value(self, instance: Any, value: Any) -> None:
27
+ instance._state.fields_cache[self.get_cache_name()] = value
28
+
29
+ def delete_cached_value(self, instance: Any) -> None:
30
+ del instance._state.fields_cache[self.get_cache_name()]