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,139 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from plain.postgres.migrations.state import ProjectState
7
+ from plain.postgres.schema import DatabaseSchemaEditor
8
+
9
+
10
+ class Operation:
11
+ """
12
+ Base class for migration operations.
13
+
14
+ It's responsible for both mutating the in-memory model state
15
+ (see db/migrations/state.py) to represent what it performs, as well
16
+ as actually performing it against a live database.
17
+
18
+ Note that some operations won't modify memory state at all (e.g. data
19
+ copying operations), and some will need their modifications to be
20
+ optionally specified by the user (e.g. custom Python code snippets)
21
+
22
+ Due to the way this class deals with deconstruction, it should be
23
+ considered immutable.
24
+ """
25
+
26
+ # Set by __new__ to capture constructor arguments for deconstruction
27
+ _constructor_args: tuple[tuple[Any, ...], dict[str, Any]]
28
+ # Set by autodetector to track operation dependencies
29
+ # Each dependency is a 4-tuple: (package_label, model_name, field_name, create/delete/alter)
30
+ _auto_deps: list[tuple[str, str, str | None, bool | str]]
31
+
32
+ # Can this migration be represented as SQL? (things like RunPython cannot)
33
+ reduces_to_sql = True
34
+
35
+ # Should this operation be forced as atomic (i.e., does it have no DDL, like RunPython)
36
+ atomic = False
37
+
38
+ # Should this operation be considered safe to elide and optimize across?
39
+ elidable = False
40
+
41
+ serialization_expand_args: list[str] = []
42
+
43
+ def __new__(cls, *args: Any, **kwargs: Any) -> Operation:
44
+ # We capture the arguments to make returning them trivial
45
+ self = object.__new__(cls)
46
+ self._constructor_args = (args, kwargs)
47
+ return self
48
+
49
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
50
+ """
51
+ Return a 3-tuple of class import path (or just name if it lives
52
+ under plain.postgres.migrations), positional arguments, and keyword
53
+ arguments.
54
+ """
55
+ return (
56
+ self.__class__.__name__,
57
+ self._constructor_args[0],
58
+ self._constructor_args[1],
59
+ )
60
+
61
+ def state_forwards(self, package_label: str, state: ProjectState) -> None:
62
+ """
63
+ Take the state from the previous migration, and mutate it
64
+ so that it matches what this migration would perform.
65
+ """
66
+ raise NotImplementedError(
67
+ "subclasses of Operation must provide a state_forwards() method"
68
+ )
69
+
70
+ def database_forwards(
71
+ self,
72
+ package_label: str,
73
+ schema_editor: DatabaseSchemaEditor,
74
+ from_state: ProjectState,
75
+ to_state: ProjectState,
76
+ ) -> None:
77
+ """
78
+ Perform the mutation on the database schema in the normal
79
+ (forwards) direction.
80
+ """
81
+ raise NotImplementedError(
82
+ "subclasses of Operation must provide a database_forwards() method"
83
+ )
84
+
85
+ def describe(self) -> str:
86
+ """
87
+ Output a brief summary of what the action does.
88
+ """
89
+ return f"{self.__class__.__name__}: {self._constructor_args}"
90
+
91
+ @property
92
+ def migration_name_fragment(self) -> str | None:
93
+ """
94
+ A filename part suitable for automatically naming a migration
95
+ containing this operation, or None if not applicable.
96
+ """
97
+ return None
98
+
99
+ def references_model(self, name: str, package_label: str) -> bool:
100
+ """
101
+ Return True if there is a chance this operation references the given
102
+ model name (as a string), with an app label for accuracy.
103
+
104
+ Used for optimization. If in doubt, return True;
105
+ returning a false positive will merely make the optimizer a little
106
+ less efficient, while returning a false negative may result in an
107
+ unusable optimized migration.
108
+ """
109
+ return True
110
+
111
+ def references_field(self, model_name: str, name: str, package_label: str) -> bool:
112
+ """
113
+ Return True if there is a chance this operation references the given
114
+ field name, with an app label for accuracy.
115
+
116
+ Used for optimization. If in doubt, return True.
117
+ """
118
+ return self.references_model(model_name, package_label)
119
+
120
+ def reduce(
121
+ self, operation: Operation, package_label: str
122
+ ) -> list[Operation] | bool:
123
+ """
124
+ Return either a list of operations the actual operation should be
125
+ replaced with or a boolean that indicates whether or not the specified
126
+ operation can be optimized across.
127
+ """
128
+ if self.elidable:
129
+ return [operation]
130
+ elif operation.elidable:
131
+ return [self]
132
+ return False
133
+
134
+ def __repr__(self) -> str:
135
+ return "<{} {}{}>".format(
136
+ self.__class__.__name__,
137
+ ", ".join(map(repr, self._constructor_args[0])),
138
+ ",".join(" {}={!r}".format(*x) for x in self._constructor_args[1].items()),
139
+ )
@@ -0,0 +1,373 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cached_property
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from plain.postgres.fields import NOT_PROVIDED
7
+ from plain.postgres.migrations.utils import field_references
8
+
9
+ from .base import Operation
10
+
11
+ if TYPE_CHECKING:
12
+ from plain.postgres.fields import Field
13
+ from plain.postgres.migrations.state import ProjectState
14
+ from plain.postgres.schema import DatabaseSchemaEditor
15
+
16
+
17
+ class FieldOperation(Operation):
18
+ def __init__(self, model_name: str, name: str, field: Field | None = None) -> None:
19
+ self.model_name = model_name
20
+ self.name = name
21
+ self.field = field
22
+
23
+ @cached_property
24
+ def model_name_lower(self) -> str:
25
+ return self.model_name.lower()
26
+
27
+ @cached_property
28
+ def name_lower(self) -> str:
29
+ return self.name.lower()
30
+
31
+ def is_same_model_operation(self, operation: FieldOperation) -> bool:
32
+ return self.model_name_lower == operation.model_name_lower
33
+
34
+ def is_same_field_operation(self, operation: FieldOperation) -> bool:
35
+ return (
36
+ self.is_same_model_operation(operation)
37
+ and self.name_lower == operation.name_lower
38
+ )
39
+
40
+ def references_model(self, name: str, package_label: str) -> bool:
41
+ name_lower = name.lower()
42
+ if name_lower == self.model_name_lower:
43
+ return True
44
+ if self.field:
45
+ return bool(
46
+ field_references(
47
+ (package_label, self.model_name_lower),
48
+ self.field,
49
+ (package_label, name_lower),
50
+ )
51
+ )
52
+ return False
53
+
54
+ def references_field(self, model_name: str, name: str, package_label: str) -> bool:
55
+ model_name_lower = model_name.lower()
56
+ # Check if this operation locally references the field.
57
+ if model_name_lower == self.model_name_lower:
58
+ if name == self.name:
59
+ return True
60
+ # Check if this operation remotely references the field.
61
+ if self.field is None:
62
+ return False
63
+ return bool(
64
+ field_references(
65
+ (package_label, self.model_name_lower),
66
+ self.field,
67
+ (package_label, model_name_lower),
68
+ name,
69
+ )
70
+ )
71
+
72
+ def reduce(
73
+ self, operation: Operation, package_label: str
74
+ ) -> list[Operation] | bool:
75
+ return super().reduce(
76
+ operation, package_label
77
+ ) or not operation.references_field(self.model_name, self.name, package_label)
78
+
79
+
80
+ class AddField(FieldOperation):
81
+ """Add a field to a model."""
82
+
83
+ def __init__(
84
+ self, model_name: str, name: str, field: Field, preserve_default: bool = True
85
+ ) -> None:
86
+ self.preserve_default = preserve_default
87
+ super().__init__(model_name, name, field)
88
+
89
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
90
+ kwargs: dict[str, Any] = {
91
+ "model_name": self.model_name,
92
+ "name": self.name,
93
+ "field": self.field,
94
+ }
95
+ if self.preserve_default is not True:
96
+ kwargs["preserve_default"] = self.preserve_default
97
+ return (self.__class__.__name__, (), kwargs)
98
+
99
+ def state_forwards(self, package_label: str, state: Any) -> None:
100
+ state.add_field(
101
+ package_label,
102
+ self.model_name_lower,
103
+ self.name,
104
+ self.field,
105
+ self.preserve_default,
106
+ )
107
+
108
+ def database_forwards(
109
+ self,
110
+ package_label: str,
111
+ schema_editor: DatabaseSchemaEditor,
112
+ from_state: ProjectState,
113
+ to_state: ProjectState,
114
+ ) -> None:
115
+ to_model = to_state.models_registry.get_model(package_label, self.model_name)
116
+ from_model = from_state.models_registry.get_model(
117
+ package_label, self.model_name
118
+ )
119
+ field = to_model._model_meta.get_forward_field(self.name)
120
+ assert self.field is not None
121
+ if not self.preserve_default:
122
+ field.default = self.field.default
123
+ schema_editor.add_field(
124
+ from_model,
125
+ field,
126
+ )
127
+ if not self.preserve_default:
128
+ field.default = NOT_PROVIDED
129
+
130
+ def describe(self) -> str:
131
+ return f"Add field {self.name} to {self.model_name}"
132
+
133
+ @property
134
+ def migration_name_fragment(self) -> str:
135
+ return f"{self.model_name_lower}_{self.name_lower}"
136
+
137
+ def reduce(
138
+ self, operation: Operation, package_label: str
139
+ ) -> list[Operation] | bool:
140
+ if isinstance(operation, FieldOperation) and self.is_same_field_operation(
141
+ operation
142
+ ):
143
+ if isinstance(operation, AlterField):
144
+ assert operation.field is not None
145
+ return [
146
+ AddField(
147
+ model_name=self.model_name,
148
+ name=operation.name,
149
+ field=operation.field,
150
+ ),
151
+ ]
152
+ elif isinstance(operation, RemoveField):
153
+ return []
154
+ elif isinstance(operation, RenameField):
155
+ assert self.field is not None # AddField always has a field
156
+ return [
157
+ AddField(
158
+ model_name=self.model_name,
159
+ name=operation.new_name,
160
+ field=self.field,
161
+ ),
162
+ ]
163
+ return super().reduce(operation, package_label)
164
+
165
+
166
+ class RemoveField(FieldOperation):
167
+ """Remove a field from a model."""
168
+
169
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
170
+ kwargs: dict[str, Any] = {
171
+ "model_name": self.model_name,
172
+ "name": self.name,
173
+ }
174
+ return (self.__class__.__name__, (), kwargs)
175
+
176
+ def state_forwards(self, package_label: str, state: Any) -> None:
177
+ state.remove_field(package_label, self.model_name_lower, self.name)
178
+
179
+ def database_forwards(
180
+ self,
181
+ package_label: str,
182
+ schema_editor: DatabaseSchemaEditor,
183
+ from_state: ProjectState,
184
+ to_state: ProjectState,
185
+ ) -> None:
186
+ from_model = from_state.models_registry.get_model(
187
+ package_label, self.model_name
188
+ )
189
+ schema_editor.remove_field(
190
+ from_model,
191
+ from_model._model_meta.get_forward_field(self.name),
192
+ )
193
+
194
+ def describe(self) -> str:
195
+ return f"Remove field {self.name} from {self.model_name}"
196
+
197
+ @property
198
+ def migration_name_fragment(self) -> str:
199
+ return f"remove_{self.model_name_lower}_{self.name_lower}"
200
+
201
+ def reduce(
202
+ self, operation: Operation, package_label: str
203
+ ) -> list[Operation] | bool:
204
+ from .models import DeleteModel
205
+
206
+ if (
207
+ isinstance(operation, DeleteModel)
208
+ and operation.name_lower == self.model_name_lower
209
+ ):
210
+ return [operation]
211
+ return super().reduce(operation, package_label)
212
+
213
+
214
+ class AlterField(FieldOperation):
215
+ """
216
+ Alter a field's database column (e.g. null, max_length) to the provided
217
+ new field.
218
+ """
219
+
220
+ def __init__(
221
+ self, model_name: str, name: str, field: Field, preserve_default: bool = True
222
+ ) -> None:
223
+ self.preserve_default = preserve_default
224
+ super().__init__(model_name, name, field)
225
+
226
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
227
+ kwargs: dict[str, Any] = {
228
+ "model_name": self.model_name,
229
+ "name": self.name,
230
+ "field": self.field,
231
+ }
232
+ if self.preserve_default is not True:
233
+ kwargs["preserve_default"] = self.preserve_default
234
+ return (self.__class__.__name__, (), kwargs)
235
+
236
+ def state_forwards(self, package_label: str, state: Any) -> None:
237
+ state.alter_field(
238
+ package_label,
239
+ self.model_name_lower,
240
+ self.name,
241
+ self.field,
242
+ self.preserve_default,
243
+ )
244
+
245
+ def database_forwards(
246
+ self,
247
+ package_label: str,
248
+ schema_editor: DatabaseSchemaEditor,
249
+ from_state: ProjectState,
250
+ to_state: ProjectState,
251
+ ) -> None:
252
+ to_model = to_state.models_registry.get_model(package_label, self.model_name)
253
+ from_model = from_state.models_registry.get_model(
254
+ package_label, self.model_name
255
+ )
256
+ from_field = from_model._model_meta.get_forward_field(self.name)
257
+ to_field = to_model._model_meta.get_forward_field(self.name)
258
+ assert self.field is not None
259
+ if not self.preserve_default:
260
+ to_field.default = self.field.default
261
+ schema_editor.alter_field(from_model, from_field, to_field)
262
+ if not self.preserve_default:
263
+ to_field.default = NOT_PROVIDED
264
+
265
+ def describe(self) -> str:
266
+ return f"Alter field {self.name} on {self.model_name}"
267
+
268
+ @property
269
+ def migration_name_fragment(self) -> str:
270
+ return f"alter_{self.model_name_lower}_{self.name_lower}"
271
+
272
+ def reduce(
273
+ self, operation: Operation, package_label: str
274
+ ) -> list[Operation] | bool:
275
+ if isinstance(
276
+ operation, AlterField | RemoveField
277
+ ) and self.is_same_field_operation(operation):
278
+ return [operation]
279
+ elif (
280
+ isinstance(operation, RenameField)
281
+ and self.is_same_field_operation(operation)
282
+ and self.field is not None
283
+ ):
284
+ return [
285
+ operation,
286
+ AlterField(
287
+ model_name=self.model_name,
288
+ name=operation.new_name,
289
+ field=self.field,
290
+ ),
291
+ ]
292
+ return super().reduce(operation, package_label)
293
+
294
+
295
+ class RenameField(FieldOperation):
296
+ """Rename a field on the model."""
297
+
298
+ def __init__(self, model_name: str, old_name: str, new_name: str) -> None:
299
+ self.old_name = old_name
300
+ self.new_name = new_name
301
+ super().__init__(model_name, old_name)
302
+
303
+ @cached_property
304
+ def old_name_lower(self) -> str:
305
+ return self.old_name.lower()
306
+
307
+ @cached_property
308
+ def new_name_lower(self) -> str:
309
+ return self.new_name.lower()
310
+
311
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
312
+ kwargs: dict[str, Any] = {
313
+ "model_name": self.model_name,
314
+ "old_name": self.old_name,
315
+ "new_name": self.new_name,
316
+ }
317
+ return (self.__class__.__name__, (), kwargs)
318
+
319
+ def state_forwards(self, package_label: str, state: Any) -> None:
320
+ state.rename_field(
321
+ package_label, self.model_name_lower, self.old_name, self.new_name
322
+ )
323
+
324
+ def database_forwards(
325
+ self,
326
+ package_label: str,
327
+ schema_editor: DatabaseSchemaEditor,
328
+ from_state: ProjectState,
329
+ to_state: ProjectState,
330
+ ) -> None:
331
+ to_model = to_state.models_registry.get_model(package_label, self.model_name)
332
+ from_model = from_state.models_registry.get_model(
333
+ package_label, self.model_name
334
+ )
335
+ schema_editor.alter_field(
336
+ from_model,
337
+ from_model._model_meta.get_forward_field(self.old_name),
338
+ to_model._model_meta.get_forward_field(self.new_name),
339
+ )
340
+
341
+ def describe(self) -> str:
342
+ return f"Rename field {self.old_name} on {self.model_name} to {self.new_name}"
343
+
344
+ @property
345
+ def migration_name_fragment(self) -> str:
346
+ return f"rename_{self.old_name_lower}_{self.model_name_lower}_{self.new_name_lower}"
347
+
348
+ def references_field(self, model_name: str, name: str, package_label: str) -> bool:
349
+ return self.references_model(model_name, package_label) and (
350
+ name.lower() == self.old_name_lower or name.lower() == self.new_name_lower
351
+ )
352
+
353
+ def reduce(
354
+ self, operation: Operation, package_label: str
355
+ ) -> list[Operation] | bool:
356
+ if (
357
+ isinstance(operation, RenameField)
358
+ and self.is_same_model_operation(operation)
359
+ and self.new_name_lower == operation.old_name_lower
360
+ ):
361
+ return [
362
+ RenameField(
363
+ self.model_name,
364
+ self.old_name,
365
+ operation.new_name,
366
+ ),
367
+ ]
368
+ # Skip `FieldOperation.reduce` as we want to run `references_field`
369
+ # against self.old_name and self.new_name.
370
+ return super(FieldOperation, self).reduce(operation, package_label) or not (
371
+ operation.references_field(self.model_name, self.old_name, package_label)
372
+ or operation.references_field(self.model_name, self.new_name, package_label)
373
+ )