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,882 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from functools import cached_property, partial
|
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
|
|
9
|
+
from plain import postgres
|
|
10
|
+
from plain.packages import packages_registry
|
|
11
|
+
from plain.postgres.exceptions import FieldDoesNotExist
|
|
12
|
+
from plain.postgres.fields import NOT_PROVIDED
|
|
13
|
+
from plain.postgres.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT, RelatedField
|
|
14
|
+
from plain.postgres.meta import Meta
|
|
15
|
+
from plain.postgres.migrations.utils import field_is_referenced, get_references
|
|
16
|
+
from plain.postgres.registry import ModelsRegistry
|
|
17
|
+
from plain.postgres.registry import models_registry as global_models
|
|
18
|
+
|
|
19
|
+
from .exceptions import InvalidBasesError
|
|
20
|
+
from .utils import resolve_relation
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Generator, Iterable
|
|
24
|
+
|
|
25
|
+
from plain.postgres.fields import Field
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_package_label_and_model_name(
|
|
29
|
+
model: str | type[postgres.Model], package_label: str = ""
|
|
30
|
+
) -> tuple[str, str]:
|
|
31
|
+
if isinstance(model, str):
|
|
32
|
+
split = model.split(".", 1)
|
|
33
|
+
return (
|
|
34
|
+
cast(tuple[str, str], tuple(split))
|
|
35
|
+
if len(split) == 2
|
|
36
|
+
else (package_label, split[0])
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
return model.model_options.package_label, model.model_options.model_name
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_related_models(m: type[postgres.Model]) -> list[type[postgres.Model]]:
|
|
43
|
+
"""Return all models that have a direct relationship to the given model."""
|
|
44
|
+
related_models = [
|
|
45
|
+
subclass
|
|
46
|
+
for subclass in m.__subclasses__()
|
|
47
|
+
if issubclass(subclass, postgres.Model)
|
|
48
|
+
]
|
|
49
|
+
from plain.postgres.fields.reverse_related import ForeignObjectRel
|
|
50
|
+
|
|
51
|
+
related_fields_models = set()
|
|
52
|
+
for f in m._model_meta.get_fields(include_reverse=True):
|
|
53
|
+
if isinstance(f, RelatedField | ForeignObjectRel) and not isinstance(
|
|
54
|
+
f.related_model, str
|
|
55
|
+
):
|
|
56
|
+
related_fields_models.add(f.model)
|
|
57
|
+
related_models.append(f.related_model)
|
|
58
|
+
return related_models
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_related_models_tuples(model: type[postgres.Model]) -> set[tuple[str, str]]:
|
|
62
|
+
"""
|
|
63
|
+
Return a list of typical (package_label, model_name) tuples for all related
|
|
64
|
+
models for the given model.
|
|
65
|
+
"""
|
|
66
|
+
return {
|
|
67
|
+
(rel_mod.model_options.package_label, rel_mod.model_options.model_name)
|
|
68
|
+
for rel_mod in _get_related_models(model)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_related_models_recursive(model: type[postgres.Model]) -> set[tuple[str, str]]:
|
|
73
|
+
"""
|
|
74
|
+
Return all models that have a direct or indirect relationship
|
|
75
|
+
to the given model.
|
|
76
|
+
|
|
77
|
+
Relationships are either defined by explicit relational fields, like
|
|
78
|
+
ForeignKeyField or ManyToManyField, or by inheriting from another
|
|
79
|
+
model (a superclass is related to its subclasses, but not vice versa).
|
|
80
|
+
"""
|
|
81
|
+
seen = set()
|
|
82
|
+
queue = _get_related_models(model)
|
|
83
|
+
for rel_mod in queue:
|
|
84
|
+
rel_package_label, rel_model_name = (
|
|
85
|
+
rel_mod.model_options.package_label,
|
|
86
|
+
rel_mod.model_options.model_name,
|
|
87
|
+
)
|
|
88
|
+
if (rel_package_label, rel_model_name) in seen:
|
|
89
|
+
continue
|
|
90
|
+
seen.add((rel_package_label, rel_model_name))
|
|
91
|
+
queue.extend(_get_related_models(rel_mod))
|
|
92
|
+
return seen - {(model.model_options.package_label, model.model_options.model_name)}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ProjectState:
|
|
96
|
+
"""
|
|
97
|
+
Represent the entire project's overall state. This is the item that is
|
|
98
|
+
passed around - do it here rather than at the app level so that cross-app
|
|
99
|
+
FKs/etc. resolve properly.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
models: dict[tuple[str, str], ModelState] | None = None,
|
|
105
|
+
real_packages: set[str] | None = None,
|
|
106
|
+
):
|
|
107
|
+
self.models = models or {}
|
|
108
|
+
# Packages to include from main registry, usually unmigrated ones
|
|
109
|
+
if real_packages is None:
|
|
110
|
+
real_packages = set()
|
|
111
|
+
else:
|
|
112
|
+
assert isinstance(real_packages, set)
|
|
113
|
+
self.real_packages = real_packages
|
|
114
|
+
self.is_delayed = False
|
|
115
|
+
# {remote_model_key: {model_key: {field_name: field}}}
|
|
116
|
+
self._relations: (
|
|
117
|
+
dict[tuple[str, str], dict[tuple[str, str], dict[str, Field]]] | None
|
|
118
|
+
) = None
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def relations(
|
|
122
|
+
self,
|
|
123
|
+
) -> dict[tuple[str, str], dict[tuple[str, str], dict[str, Field]]]:
|
|
124
|
+
if self._relations is None:
|
|
125
|
+
self.resolve_fields_and_relations()
|
|
126
|
+
assert self._relations is not None
|
|
127
|
+
return self._relations
|
|
128
|
+
|
|
129
|
+
def add_model(self, model_state: ModelState) -> None:
|
|
130
|
+
model_key = model_state.package_label, model_state.name_lower
|
|
131
|
+
self.models[model_key] = model_state
|
|
132
|
+
if self._relations is not None:
|
|
133
|
+
self.resolve_model_relations(model_key)
|
|
134
|
+
if "models_registry" in self.__dict__: # hasattr would cache the property
|
|
135
|
+
self.reload_model(*model_key)
|
|
136
|
+
|
|
137
|
+
def remove_model(self, package_label: str, model_name: str) -> None:
|
|
138
|
+
model_key = package_label, model_name
|
|
139
|
+
del self.models[model_key]
|
|
140
|
+
if self._relations is not None:
|
|
141
|
+
self._relations.pop(model_key, None)
|
|
142
|
+
# Call list() since _relations can change size during iteration.
|
|
143
|
+
for related_model_key, model_relations in list(self._relations.items()):
|
|
144
|
+
model_relations.pop(model_key, None)
|
|
145
|
+
if not model_relations:
|
|
146
|
+
del self._relations[related_model_key]
|
|
147
|
+
if "models_registry" in self.__dict__: # hasattr would cache the property
|
|
148
|
+
self.models_registry.unregister_model(*model_key)
|
|
149
|
+
# Need to do this explicitly since unregister_model() doesn't clear
|
|
150
|
+
# the cache automatically (#24513)
|
|
151
|
+
self.models_registry.clear_cache()
|
|
152
|
+
|
|
153
|
+
def rename_model(self, package_label: str, old_name: str, new_name: str) -> None:
|
|
154
|
+
# Add a new model.
|
|
155
|
+
old_name_lower = old_name.lower()
|
|
156
|
+
new_name_lower = new_name.lower()
|
|
157
|
+
renamed_model = self.models[package_label, old_name_lower].clone()
|
|
158
|
+
renamed_model.name = new_name
|
|
159
|
+
self.models[package_label, new_name_lower] = renamed_model
|
|
160
|
+
# Repoint all fields pointing to the old model to the new one.
|
|
161
|
+
old_model_tuple = (package_label, old_name_lower)
|
|
162
|
+
new_remote_model = f"{package_label}.{new_name}"
|
|
163
|
+
to_reload = set()
|
|
164
|
+
for model_state, name, field, reference in get_references(
|
|
165
|
+
self, old_model_tuple
|
|
166
|
+
):
|
|
167
|
+
if not isinstance(field, RelatedField):
|
|
168
|
+
continue
|
|
169
|
+
changed_field = None
|
|
170
|
+
if reference.to:
|
|
171
|
+
changed_field = field.clone()
|
|
172
|
+
assert changed_field.remote_field is not None
|
|
173
|
+
changed_field.remote_field.model = new_remote_model # type: ignore[assignment]
|
|
174
|
+
if reference.through:
|
|
175
|
+
if changed_field is None:
|
|
176
|
+
changed_field = field.clone()
|
|
177
|
+
assert changed_field.remote_field is not None
|
|
178
|
+
changed_field.remote_field.through = new_remote_model # type: ignore[attr-defined]
|
|
179
|
+
if changed_field:
|
|
180
|
+
model_state.fields[name] = changed_field
|
|
181
|
+
to_reload.add((model_state.package_label, model_state.name_lower))
|
|
182
|
+
if self._relations is not None:
|
|
183
|
+
old_name_key = package_label, old_name_lower
|
|
184
|
+
new_name_key = package_label, new_name_lower
|
|
185
|
+
if old_name_key in self._relations:
|
|
186
|
+
self._relations[new_name_key] = self._relations.pop(old_name_key)
|
|
187
|
+
for model_relations in self._relations.values():
|
|
188
|
+
if old_name_key in model_relations:
|
|
189
|
+
model_relations[new_name_key] = model_relations.pop(old_name_key)
|
|
190
|
+
# Reload models related to old model before removing the old model.
|
|
191
|
+
self.reload_models(to_reload, delay=True)
|
|
192
|
+
# Remove the old model.
|
|
193
|
+
self.remove_model(package_label, old_name_lower)
|
|
194
|
+
self.reload_model(package_label, new_name_lower, delay=True)
|
|
195
|
+
|
|
196
|
+
def alter_model_options(
|
|
197
|
+
self,
|
|
198
|
+
package_label: str,
|
|
199
|
+
model_name: str,
|
|
200
|
+
options: dict[str, Any],
|
|
201
|
+
option_keys: Iterable[str] | None = None,
|
|
202
|
+
) -> None:
|
|
203
|
+
model_state = self.models[package_label, model_name]
|
|
204
|
+
model_state.options = {**model_state.options, **options}
|
|
205
|
+
if option_keys:
|
|
206
|
+
for key in option_keys:
|
|
207
|
+
if key not in options:
|
|
208
|
+
model_state.options.pop(key, False)
|
|
209
|
+
self.reload_model(package_label, model_name, delay=True)
|
|
210
|
+
|
|
211
|
+
def _append_option(
|
|
212
|
+
self, package_label: str, model_name: str, option_name: str, obj: Any
|
|
213
|
+
) -> None:
|
|
214
|
+
model_state = self.models[package_label, model_name]
|
|
215
|
+
model_state.options[option_name] = [*model_state.options[option_name], obj]
|
|
216
|
+
self.reload_model(package_label, model_name, delay=True)
|
|
217
|
+
|
|
218
|
+
def _remove_option(
|
|
219
|
+
self, package_label: str, model_name: str, option_name: str, obj_name: str
|
|
220
|
+
) -> None:
|
|
221
|
+
model_state = self.models[package_label, model_name]
|
|
222
|
+
objs = model_state.options[option_name]
|
|
223
|
+
model_state.options[option_name] = [obj for obj in objs if obj.name != obj_name]
|
|
224
|
+
self.reload_model(package_label, model_name, delay=True)
|
|
225
|
+
|
|
226
|
+
def add_index(self, package_label: str, model_name: str, index: Any) -> None:
|
|
227
|
+
self._append_option(package_label, model_name, "indexes", index)
|
|
228
|
+
|
|
229
|
+
def remove_index(
|
|
230
|
+
self, package_label: str, model_name: str, index_name: str
|
|
231
|
+
) -> None:
|
|
232
|
+
self._remove_option(package_label, model_name, "indexes", index_name)
|
|
233
|
+
|
|
234
|
+
def rename_index(
|
|
235
|
+
self,
|
|
236
|
+
package_label: str,
|
|
237
|
+
model_name: str,
|
|
238
|
+
old_index_name: str,
|
|
239
|
+
new_index_name: str,
|
|
240
|
+
) -> None:
|
|
241
|
+
model_state = self.models[package_label, model_name]
|
|
242
|
+
objs = model_state.options["indexes"]
|
|
243
|
+
|
|
244
|
+
new_indexes = []
|
|
245
|
+
for obj in objs:
|
|
246
|
+
if obj.name == old_index_name:
|
|
247
|
+
obj = obj.clone()
|
|
248
|
+
obj.name = new_index_name
|
|
249
|
+
new_indexes.append(obj)
|
|
250
|
+
|
|
251
|
+
model_state.options["indexes"] = new_indexes
|
|
252
|
+
self.reload_model(package_label, model_name, delay=True)
|
|
253
|
+
|
|
254
|
+
def add_constraint(
|
|
255
|
+
self, package_label: str, model_name: str, constraint: Any
|
|
256
|
+
) -> None:
|
|
257
|
+
self._append_option(package_label, model_name, "constraints", constraint)
|
|
258
|
+
|
|
259
|
+
def remove_constraint(
|
|
260
|
+
self, package_label: str, model_name: str, constraint_name: str
|
|
261
|
+
) -> None:
|
|
262
|
+
self._remove_option(package_label, model_name, "constraints", constraint_name)
|
|
263
|
+
|
|
264
|
+
def add_field(
|
|
265
|
+
self,
|
|
266
|
+
package_label: str,
|
|
267
|
+
model_name: str,
|
|
268
|
+
name: str,
|
|
269
|
+
field: Field,
|
|
270
|
+
preserve_default: bool,
|
|
271
|
+
) -> None:
|
|
272
|
+
# If preserve default is off, don't use the default for future state.
|
|
273
|
+
if not preserve_default:
|
|
274
|
+
field = field.clone()
|
|
275
|
+
field.default = NOT_PROVIDED
|
|
276
|
+
else:
|
|
277
|
+
field = field
|
|
278
|
+
model_key = package_label, model_name
|
|
279
|
+
self.models[model_key].fields[name] = field
|
|
280
|
+
if self._relations is not None:
|
|
281
|
+
self.resolve_model_field_relations(model_key, name, field)
|
|
282
|
+
# Delay rendering of relationships if it's not a relational field.
|
|
283
|
+
delay = not isinstance(field, RelatedField)
|
|
284
|
+
self.reload_model(*model_key, delay=delay)
|
|
285
|
+
|
|
286
|
+
def remove_field(self, package_label: str, model_name: str, name: str) -> None:
|
|
287
|
+
model_key = package_label, model_name
|
|
288
|
+
model_state = self.models[model_key]
|
|
289
|
+
old_field = model_state.fields.pop(name)
|
|
290
|
+
if self._relations is not None:
|
|
291
|
+
self.resolve_model_field_relations(model_key, name, old_field)
|
|
292
|
+
# Delay rendering of relationships if it's not a relational field.
|
|
293
|
+
delay = not isinstance(old_field, RelatedField)
|
|
294
|
+
self.reload_model(*model_key, delay=delay)
|
|
295
|
+
|
|
296
|
+
def alter_field(
|
|
297
|
+
self,
|
|
298
|
+
package_label: str,
|
|
299
|
+
model_name: str,
|
|
300
|
+
name: str,
|
|
301
|
+
field: Field,
|
|
302
|
+
preserve_default: bool,
|
|
303
|
+
) -> None:
|
|
304
|
+
if not preserve_default:
|
|
305
|
+
field = field.clone()
|
|
306
|
+
field.default = NOT_PROVIDED
|
|
307
|
+
else:
|
|
308
|
+
field = field
|
|
309
|
+
model_key = package_label, model_name
|
|
310
|
+
fields = self.models[model_key].fields
|
|
311
|
+
if self._relations is not None:
|
|
312
|
+
old_field = fields.pop(name)
|
|
313
|
+
if isinstance(old_field, RelatedField):
|
|
314
|
+
self.resolve_model_field_relations(model_key, name, old_field)
|
|
315
|
+
fields[name] = field
|
|
316
|
+
if isinstance(field, RelatedField):
|
|
317
|
+
self.resolve_model_field_relations(model_key, name, field)
|
|
318
|
+
else:
|
|
319
|
+
fields[name] = field
|
|
320
|
+
# TODO: investigate if old relational fields must be reloaded or if
|
|
321
|
+
# it's sufficient if the new field is (#27737).
|
|
322
|
+
# Delay rendering of relationships if it's not a relational field and
|
|
323
|
+
# not referenced by a foreign key.
|
|
324
|
+
delay = not isinstance(field, RelatedField) and not field_is_referenced(
|
|
325
|
+
self, model_key, (name, field)
|
|
326
|
+
)
|
|
327
|
+
self.reload_model(*model_key, delay=delay)
|
|
328
|
+
|
|
329
|
+
def rename_field(
|
|
330
|
+
self, package_label: str, model_name: str, old_name: str, new_name: str
|
|
331
|
+
) -> None:
|
|
332
|
+
model_key = package_label, model_name
|
|
333
|
+
model_state = self.models[model_key]
|
|
334
|
+
# Rename the field.
|
|
335
|
+
fields = model_state.fields
|
|
336
|
+
try:
|
|
337
|
+
found = fields.pop(old_name)
|
|
338
|
+
except KeyError:
|
|
339
|
+
raise FieldDoesNotExist(
|
|
340
|
+
f"{package_label}.{model_name} has no field named '{old_name}'"
|
|
341
|
+
)
|
|
342
|
+
fields[new_name] = found
|
|
343
|
+
# Check if there are any references to this field
|
|
344
|
+
references = get_references(self, model_key, (old_name, found))
|
|
345
|
+
delay = not bool(references)
|
|
346
|
+
if self._relations is not None:
|
|
347
|
+
old_name_lower = old_name.lower()
|
|
348
|
+
new_name_lower = new_name.lower()
|
|
349
|
+
for to_model in self._relations.values():
|
|
350
|
+
if old_name_lower in to_model[model_key]:
|
|
351
|
+
field = to_model[model_key].pop(old_name_lower)
|
|
352
|
+
field.name = new_name_lower
|
|
353
|
+
to_model[model_key][new_name_lower] = field
|
|
354
|
+
self.reload_model(*model_key, delay=delay)
|
|
355
|
+
|
|
356
|
+
def _find_reload_model(
|
|
357
|
+
self, package_label: str, model_name: str, delay: bool = False
|
|
358
|
+
) -> set[tuple[str, str]]:
|
|
359
|
+
if delay:
|
|
360
|
+
self.is_delayed = True
|
|
361
|
+
|
|
362
|
+
related_models: set[tuple[str, str]] = set()
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
old_model = self.models_registry.get_model(package_label, model_name)
|
|
366
|
+
except LookupError:
|
|
367
|
+
pass
|
|
368
|
+
else:
|
|
369
|
+
# Get all relations to and from the old model before reloading,
|
|
370
|
+
# as _model_meta.models_registry may change
|
|
371
|
+
if delay:
|
|
372
|
+
related_models = get_related_models_tuples(old_model)
|
|
373
|
+
else:
|
|
374
|
+
related_models = get_related_models_recursive(old_model)
|
|
375
|
+
|
|
376
|
+
# Get all outgoing references from the model to be rendered
|
|
377
|
+
model_state = self.models[(package_label, model_name)]
|
|
378
|
+
# Directly related models are the models pointed to by ForeignKeys and ManyToManyFields.
|
|
379
|
+
direct_related_models = set()
|
|
380
|
+
for field in model_state.fields.values():
|
|
381
|
+
if isinstance(field, RelatedField):
|
|
382
|
+
if field.remote_field.model == RECURSIVE_RELATIONSHIP_CONSTANT:
|
|
383
|
+
continue
|
|
384
|
+
rel_package_label, rel_model_name = _get_package_label_and_model_name(
|
|
385
|
+
field.related_model,
|
|
386
|
+
package_label,
|
|
387
|
+
)
|
|
388
|
+
direct_related_models.add((rel_package_label, rel_model_name.lower()))
|
|
389
|
+
|
|
390
|
+
# For all direct related models recursively get all related models.
|
|
391
|
+
related_models.update(direct_related_models)
|
|
392
|
+
for rel_package_label, rel_model_name in direct_related_models:
|
|
393
|
+
try:
|
|
394
|
+
rel_model = self.models_registry.get_model(
|
|
395
|
+
rel_package_label, rel_model_name
|
|
396
|
+
)
|
|
397
|
+
except LookupError:
|
|
398
|
+
pass
|
|
399
|
+
else:
|
|
400
|
+
if delay:
|
|
401
|
+
related_models.update(get_related_models_tuples(rel_model))
|
|
402
|
+
else:
|
|
403
|
+
related_models.update(get_related_models_recursive(rel_model))
|
|
404
|
+
|
|
405
|
+
# Include the model itself
|
|
406
|
+
related_models.add((package_label, model_name))
|
|
407
|
+
|
|
408
|
+
return related_models
|
|
409
|
+
|
|
410
|
+
def reload_model(
|
|
411
|
+
self, package_label: str, model_name: str, delay: bool = False
|
|
412
|
+
) -> None:
|
|
413
|
+
if "models_registry" in self.__dict__: # hasattr would cache the property
|
|
414
|
+
related_models = self._find_reload_model(package_label, model_name, delay)
|
|
415
|
+
self._reload(related_models)
|
|
416
|
+
|
|
417
|
+
def reload_models(self, models: set[tuple[str, str]], delay: bool = True) -> None:
|
|
418
|
+
if "models_registry" in self.__dict__: # hasattr would cache the property
|
|
419
|
+
related_models = set()
|
|
420
|
+
for package_label, model_name in models:
|
|
421
|
+
related_models.update(
|
|
422
|
+
self._find_reload_model(package_label, model_name, delay)
|
|
423
|
+
)
|
|
424
|
+
self._reload(related_models)
|
|
425
|
+
|
|
426
|
+
def _reload(self, related_models: set[tuple[str, str]]) -> None:
|
|
427
|
+
# Unregister all related models
|
|
428
|
+
with self.models_registry.bulk_update():
|
|
429
|
+
for rel_package_label, rel_model_name in related_models:
|
|
430
|
+
self.models_registry.unregister_model(rel_package_label, rel_model_name)
|
|
431
|
+
|
|
432
|
+
states_to_be_rendered = []
|
|
433
|
+
# Gather all models states of those models that will be rerendered.
|
|
434
|
+
# This includes:
|
|
435
|
+
# 1. All related models of unmigrated packages
|
|
436
|
+
for model_state in self.models_registry.real_models:
|
|
437
|
+
if (model_state.package_label, model_state.name_lower) in related_models:
|
|
438
|
+
states_to_be_rendered.append(model_state)
|
|
439
|
+
|
|
440
|
+
# 2. All related models of migrated packages
|
|
441
|
+
for rel_package_label, rel_model_name in related_models:
|
|
442
|
+
try:
|
|
443
|
+
model_state = self.models[rel_package_label, rel_model_name]
|
|
444
|
+
except KeyError:
|
|
445
|
+
pass
|
|
446
|
+
else:
|
|
447
|
+
states_to_be_rendered.append(model_state)
|
|
448
|
+
|
|
449
|
+
# Render all models
|
|
450
|
+
self.models_registry.render_multiple(states_to_be_rendered)
|
|
451
|
+
|
|
452
|
+
def update_model_field_relation(
|
|
453
|
+
self,
|
|
454
|
+
model: str | type[postgres.Model],
|
|
455
|
+
model_key: tuple[str, str],
|
|
456
|
+
field_name: str,
|
|
457
|
+
field: Field,
|
|
458
|
+
concretes: dict[tuple[str, str], tuple[str, str]],
|
|
459
|
+
) -> None:
|
|
460
|
+
assert self._relations is not None
|
|
461
|
+
remote_model_key = resolve_relation(model, *model_key)
|
|
462
|
+
if (
|
|
463
|
+
remote_model_key[0] not in self.real_packages
|
|
464
|
+
and remote_model_key in concretes
|
|
465
|
+
):
|
|
466
|
+
remote_model_key = concretes[remote_model_key]
|
|
467
|
+
relations_to_remote_model = self._relations[remote_model_key]
|
|
468
|
+
if field_name in self.models[model_key].fields:
|
|
469
|
+
# The assert holds because it's a new relation, or an altered
|
|
470
|
+
# relation, in which case references have been removed by
|
|
471
|
+
# alter_field().
|
|
472
|
+
assert field_name not in relations_to_remote_model[model_key]
|
|
473
|
+
relations_to_remote_model[model_key][field_name] = field
|
|
474
|
+
else:
|
|
475
|
+
del relations_to_remote_model[model_key][field_name]
|
|
476
|
+
if not relations_to_remote_model[model_key]:
|
|
477
|
+
del relations_to_remote_model[model_key]
|
|
478
|
+
|
|
479
|
+
def resolve_model_field_relations(
|
|
480
|
+
self,
|
|
481
|
+
model_key: tuple[str, str],
|
|
482
|
+
field_name: str,
|
|
483
|
+
field: Field,
|
|
484
|
+
concretes: dict[tuple[str, str], tuple[str, str]] | None = None,
|
|
485
|
+
) -> None:
|
|
486
|
+
# Only process fields that have relations
|
|
487
|
+
if not isinstance(field, RelatedField):
|
|
488
|
+
return None
|
|
489
|
+
remote_field = field.remote_field
|
|
490
|
+
if not remote_field:
|
|
491
|
+
return None
|
|
492
|
+
if concretes is None:
|
|
493
|
+
concretes = self._get_concrete_models_mapping()
|
|
494
|
+
|
|
495
|
+
self.update_model_field_relation(
|
|
496
|
+
remote_field.model,
|
|
497
|
+
model_key,
|
|
498
|
+
field_name,
|
|
499
|
+
field,
|
|
500
|
+
concretes,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
through = getattr(remote_field, "through", None)
|
|
504
|
+
if not through:
|
|
505
|
+
return None
|
|
506
|
+
self.update_model_field_relation(
|
|
507
|
+
through, model_key, field_name, field, concretes
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def resolve_model_relations(
|
|
511
|
+
self,
|
|
512
|
+
model_key: tuple[str, str],
|
|
513
|
+
concretes: dict[tuple[str, str], tuple[str, str]] | None = None,
|
|
514
|
+
) -> None:
|
|
515
|
+
if concretes is None:
|
|
516
|
+
concretes = self._get_concrete_models_mapping()
|
|
517
|
+
|
|
518
|
+
model_state = self.models[model_key]
|
|
519
|
+
for field_name, field in model_state.fields.items():
|
|
520
|
+
self.resolve_model_field_relations(model_key, field_name, field, concretes)
|
|
521
|
+
|
|
522
|
+
def resolve_fields_and_relations(self) -> None:
|
|
523
|
+
# Resolve fields.
|
|
524
|
+
for model_state in self.models.values():
|
|
525
|
+
for field_name, field in model_state.fields.items():
|
|
526
|
+
field.name = field_name
|
|
527
|
+
# Resolve relations.
|
|
528
|
+
# {remote_model_key: {model_key: {field_name: field}}}
|
|
529
|
+
self._relations = defaultdict(partial(defaultdict, dict))
|
|
530
|
+
concretes = self._get_concrete_models_mapping()
|
|
531
|
+
|
|
532
|
+
for model_key in concretes:
|
|
533
|
+
self.resolve_model_relations(model_key, concretes)
|
|
534
|
+
|
|
535
|
+
def _get_concrete_models_mapping(self) -> dict[tuple[str, str], tuple[str, str]]:
|
|
536
|
+
concrete_models_mapping = {}
|
|
537
|
+
for model_key, model_state in self.models.items():
|
|
538
|
+
concrete_models_mapping[model_key] = model_key
|
|
539
|
+
return concrete_models_mapping
|
|
540
|
+
|
|
541
|
+
def clone(self) -> ProjectState:
|
|
542
|
+
"""Return an exact copy of this ProjectState."""
|
|
543
|
+
new_state = ProjectState(
|
|
544
|
+
models={k: v.clone() for k, v in self.models.items()},
|
|
545
|
+
real_packages=self.real_packages,
|
|
546
|
+
)
|
|
547
|
+
if "models_registry" in self.__dict__:
|
|
548
|
+
new_state.models_registry = self.models_registry.clone()
|
|
549
|
+
new_state.is_delayed = self.is_delayed
|
|
550
|
+
return new_state
|
|
551
|
+
|
|
552
|
+
def clear_delayed_models_cache(self) -> None:
|
|
553
|
+
if self.is_delayed and "models_registry" in self.__dict__:
|
|
554
|
+
del self.__dict__["models_registry"]
|
|
555
|
+
|
|
556
|
+
@cached_property
|
|
557
|
+
def models_registry(self) -> StateModelsRegistry:
|
|
558
|
+
return StateModelsRegistry(self.real_packages, self.models)
|
|
559
|
+
|
|
560
|
+
@classmethod
|
|
561
|
+
def from_models_registry(cls, models_registry: ModelsRegistry) -> ProjectState:
|
|
562
|
+
"""Take an Packages and return a ProjectState matching it."""
|
|
563
|
+
app_models = {}
|
|
564
|
+
for model in models_registry.get_models():
|
|
565
|
+
model_state = ModelState.from_model(model)
|
|
566
|
+
app_models[(model_state.package_label, model_state.name_lower)] = (
|
|
567
|
+
model_state
|
|
568
|
+
)
|
|
569
|
+
return cls(app_models)
|
|
570
|
+
|
|
571
|
+
def __eq__(self, other: object) -> bool:
|
|
572
|
+
if not isinstance(other, ProjectState):
|
|
573
|
+
return NotImplemented
|
|
574
|
+
return self.models == other.models and self.real_packages == other.real_packages
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class StateModelsRegistry(ModelsRegistry):
|
|
578
|
+
"""
|
|
579
|
+
Subclass of the global Packages registry class to better handle dynamic model
|
|
580
|
+
additions and removals.
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
def __init__(
|
|
584
|
+
self,
|
|
585
|
+
real_packages: set[str],
|
|
586
|
+
models: dict[tuple[str, str], ModelState],
|
|
587
|
+
):
|
|
588
|
+
# Any packages in self.real_packages should have all their models included
|
|
589
|
+
# in the render. We don't use the original model instances as there
|
|
590
|
+
# are some variables that refer to the Packages object.
|
|
591
|
+
# FKs/M2Ms from real packages are also not included as they just
|
|
592
|
+
# mess things up with partial states (due to lack of dependencies)
|
|
593
|
+
self.real_models: list[ModelState] = []
|
|
594
|
+
for package_label in real_packages:
|
|
595
|
+
for model in global_models.get_models(package_label=package_label):
|
|
596
|
+
self.real_models.append(ModelState.from_model(model, exclude_rels=True))
|
|
597
|
+
|
|
598
|
+
super().__init__()
|
|
599
|
+
|
|
600
|
+
self.render_multiple([*models.values(), *self.real_models])
|
|
601
|
+
|
|
602
|
+
self.ready = True
|
|
603
|
+
|
|
604
|
+
# There shouldn't be any operations pending at this point.
|
|
605
|
+
from plain.postgres.preflight import _check_lazy_references
|
|
606
|
+
|
|
607
|
+
if errors := _check_lazy_references(self, packages_registry):
|
|
608
|
+
raise ValueError("\n".join(error.fix for error in errors))
|
|
609
|
+
|
|
610
|
+
@contextmanager
|
|
611
|
+
def bulk_update(self) -> Generator[None]:
|
|
612
|
+
# Avoid clearing each model's cache for each change. Instead, clear
|
|
613
|
+
# all caches when we're finished updating the model instances.
|
|
614
|
+
ready = self.ready
|
|
615
|
+
self.ready = False
|
|
616
|
+
try:
|
|
617
|
+
yield
|
|
618
|
+
finally:
|
|
619
|
+
self.ready = ready
|
|
620
|
+
self.clear_cache()
|
|
621
|
+
|
|
622
|
+
def render_multiple(self, model_states: list[ModelState]) -> None:
|
|
623
|
+
# We keep trying to render the models in a loop, ignoring invalid
|
|
624
|
+
# base errors, until the size of the unrendered models doesn't
|
|
625
|
+
# decrease by at least one, meaning there's a base dependency loop/
|
|
626
|
+
# missing base.
|
|
627
|
+
if not model_states:
|
|
628
|
+
return None
|
|
629
|
+
# Prevent that all model caches are expired for each render.
|
|
630
|
+
with self.bulk_update():
|
|
631
|
+
unrendered_models = model_states
|
|
632
|
+
while unrendered_models:
|
|
633
|
+
new_unrendered_models = []
|
|
634
|
+
for model in unrendered_models:
|
|
635
|
+
try:
|
|
636
|
+
model.render(self)
|
|
637
|
+
except InvalidBasesError:
|
|
638
|
+
new_unrendered_models.append(model)
|
|
639
|
+
if len(new_unrendered_models) == len(unrendered_models):
|
|
640
|
+
raise InvalidBasesError(
|
|
641
|
+
f"Cannot resolve bases for {new_unrendered_models!r}\nThis can happen if you are "
|
|
642
|
+
"inheriting models from an app with migrations (e.g. "
|
|
643
|
+
"contrib.auth)\n in an app with no migrations"
|
|
644
|
+
)
|
|
645
|
+
unrendered_models = new_unrendered_models
|
|
646
|
+
|
|
647
|
+
def clone(self) -> StateModelsRegistry:
|
|
648
|
+
"""Return a clone of this registry."""
|
|
649
|
+
clone = StateModelsRegistry(set(), {})
|
|
650
|
+
clone.all_models = copy.deepcopy(self.all_models)
|
|
651
|
+
|
|
652
|
+
# No need to actually clone them, they'll never change
|
|
653
|
+
clone.real_models = self.real_models
|
|
654
|
+
return clone
|
|
655
|
+
|
|
656
|
+
def register_model(self, package_label: str, model: type[postgres.Model]) -> None:
|
|
657
|
+
self.all_models[package_label][model.model_options.model_name] = model
|
|
658
|
+
self.do_pending_operations(model)
|
|
659
|
+
self.clear_cache()
|
|
660
|
+
|
|
661
|
+
def unregister_model(self, package_label: str, model_name: str) -> None:
|
|
662
|
+
try:
|
|
663
|
+
del self.all_models[package_label][model_name]
|
|
664
|
+
except KeyError:
|
|
665
|
+
pass
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class ModelState:
|
|
669
|
+
"""
|
|
670
|
+
Represent a Plain Model. Don't use the actual Model class as it's not
|
|
671
|
+
designed to have its options changed - instead, mutate this one and then
|
|
672
|
+
render it into a Model as required.
|
|
673
|
+
|
|
674
|
+
Note that while you are allowed to mutate .fields, you are not allowed
|
|
675
|
+
to mutate the Field instances inside there themselves - you must instead
|
|
676
|
+
assign new ones, as these are not detached during a clone.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
def __init__(
|
|
680
|
+
self,
|
|
681
|
+
package_label: str,
|
|
682
|
+
name: str,
|
|
683
|
+
fields: Iterable[tuple[str, Field]],
|
|
684
|
+
options: dict[str, Any] | None = None,
|
|
685
|
+
bases: tuple[str | type[postgres.Model], ...] | None = None,
|
|
686
|
+
):
|
|
687
|
+
self.package_label = package_label
|
|
688
|
+
self.name = name
|
|
689
|
+
self.fields: dict[str, Field] = dict(fields)
|
|
690
|
+
self.options = options or {}
|
|
691
|
+
self.options.setdefault("indexes", [])
|
|
692
|
+
self.options.setdefault("constraints", [])
|
|
693
|
+
self.bases = bases or (postgres.Model,)
|
|
694
|
+
for name, field in self.fields.items():
|
|
695
|
+
# Sanity-check that fields are NOT already bound to a model.
|
|
696
|
+
if hasattr(field, "model"):
|
|
697
|
+
raise ValueError(
|
|
698
|
+
f'ModelState.fields cannot be bound to a model - "{name}" is.'
|
|
699
|
+
)
|
|
700
|
+
# Sanity-check that relation fields are NOT referring to a model class.
|
|
701
|
+
if isinstance(field, RelatedField) and hasattr(
|
|
702
|
+
field.related_model, "_model_meta"
|
|
703
|
+
):
|
|
704
|
+
raise ValueError(
|
|
705
|
+
f'ModelState.fields cannot refer to a model class - "{name}.to" does. '
|
|
706
|
+
"Use a string reference instead."
|
|
707
|
+
)
|
|
708
|
+
from plain.postgres.fields.related import ManyToManyField
|
|
709
|
+
|
|
710
|
+
if isinstance(field, ManyToManyField) and hasattr(
|
|
711
|
+
field.remote_field.through, "_model_meta"
|
|
712
|
+
):
|
|
713
|
+
raise ValueError(
|
|
714
|
+
f'ModelState.fields cannot refer to a model class - "{name}.through" '
|
|
715
|
+
"does. Use a string reference instead."
|
|
716
|
+
)
|
|
717
|
+
# Sanity-check that indexes have their name set.
|
|
718
|
+
for index in self.options["indexes"]:
|
|
719
|
+
if not index.name:
|
|
720
|
+
raise ValueError(
|
|
721
|
+
"Indexes passed to ModelState require a name attribute. "
|
|
722
|
+
f"{index!r} doesn't have one."
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
@cached_property
|
|
726
|
+
def name_lower(self) -> str:
|
|
727
|
+
return self.name.lower()
|
|
728
|
+
|
|
729
|
+
def get_field(self, field_name: str) -> Field:
|
|
730
|
+
return self.fields[field_name]
|
|
731
|
+
|
|
732
|
+
@classmethod
|
|
733
|
+
def from_model(
|
|
734
|
+
cls, model: type[postgres.Model], exclude_rels: bool = False
|
|
735
|
+
) -> ModelState:
|
|
736
|
+
"""Given a model, return a ModelState representing it."""
|
|
737
|
+
# Deconstruct the fields
|
|
738
|
+
fields = []
|
|
739
|
+
for field in model._model_meta.local_fields:
|
|
740
|
+
if getattr(field, "remote_field", None) and exclude_rels:
|
|
741
|
+
continue
|
|
742
|
+
name = field.name
|
|
743
|
+
try:
|
|
744
|
+
fields.append((name, field.clone()))
|
|
745
|
+
except TypeError as e:
|
|
746
|
+
raise TypeError(
|
|
747
|
+
f"Couldn't reconstruct field {name} on {model.model_options.label}: {e}"
|
|
748
|
+
)
|
|
749
|
+
if not exclude_rels:
|
|
750
|
+
for field in model._model_meta.local_many_to_many:
|
|
751
|
+
name = field.name
|
|
752
|
+
try:
|
|
753
|
+
fields.append((name, field.clone()))
|
|
754
|
+
except TypeError as e:
|
|
755
|
+
raise TypeError(
|
|
756
|
+
f"Couldn't reconstruct m2m field {name} on {model.model_options.object_name}: {e}"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
def flatten_bases(model: type[postgres.Model]) -> list[type[postgres.Model]]:
|
|
760
|
+
bases = []
|
|
761
|
+
for base in model.__bases__:
|
|
762
|
+
bases.append(base)
|
|
763
|
+
return bases
|
|
764
|
+
|
|
765
|
+
# We can't rely on __mro__ directly because we only want to flatten
|
|
766
|
+
# abstract models and not the whole tree. However by recursing on
|
|
767
|
+
# __bases__ we may end up with duplicates and ordering issues, we
|
|
768
|
+
# therefore discard any duplicates and reorder the bases according
|
|
769
|
+
# to their index in the MRO.
|
|
770
|
+
flattened_bases = sorted(
|
|
771
|
+
set(flatten_bases(model)), key=lambda x: model.__mro__.index(x)
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# Make our record
|
|
775
|
+
bases = tuple(
|
|
776
|
+
(
|
|
777
|
+
base.model_options.label_lower
|
|
778
|
+
if not isinstance(base, str)
|
|
779
|
+
and base is not postgres.Model
|
|
780
|
+
and hasattr(base, "_model_meta")
|
|
781
|
+
else base
|
|
782
|
+
)
|
|
783
|
+
for base in flattened_bases
|
|
784
|
+
)
|
|
785
|
+
# Ensure at least one base inherits from postgres.Model
|
|
786
|
+
if not any(
|
|
787
|
+
(isinstance(base, str) or issubclass(base, postgres.Model))
|
|
788
|
+
for base in bases
|
|
789
|
+
):
|
|
790
|
+
bases = (postgres.Model,)
|
|
791
|
+
|
|
792
|
+
# Construct the new ModelState
|
|
793
|
+
return cls(
|
|
794
|
+
model.model_options.package_label,
|
|
795
|
+
model.model_options.object_name,
|
|
796
|
+
fields,
|
|
797
|
+
model.model_options.export_for_migrations(),
|
|
798
|
+
bases,
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
def clone(self) -> ModelState:
|
|
802
|
+
"""Return an exact copy of this ModelState."""
|
|
803
|
+
return self.__class__(
|
|
804
|
+
package_label=self.package_label,
|
|
805
|
+
name=self.name,
|
|
806
|
+
fields=list(self.fields.items()),
|
|
807
|
+
# Since options are shallow-copied here, operations such as
|
|
808
|
+
# AddIndex must replace their option (e.g 'indexes') rather
|
|
809
|
+
# than mutating it.
|
|
810
|
+
options=dict(self.options),
|
|
811
|
+
bases=self.bases,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
def render(self, models_registry: ModelsRegistry) -> type[postgres.Model]:
|
|
815
|
+
"""Create a Model object from our current state into the given packages."""
|
|
816
|
+
# Create Options instance with metadata
|
|
817
|
+
meta_options = postgres.Options(
|
|
818
|
+
package_label=self.package_label,
|
|
819
|
+
**self.options,
|
|
820
|
+
)
|
|
821
|
+
# Then, work out our bases
|
|
822
|
+
try:
|
|
823
|
+
bases = tuple(
|
|
824
|
+
(models_registry.get_model(base) if isinstance(base, str) else base)
|
|
825
|
+
for base in self.bases
|
|
826
|
+
)
|
|
827
|
+
except LookupError:
|
|
828
|
+
raise InvalidBasesError(
|
|
829
|
+
f"Cannot resolve one or more bases from {self.bases!r}"
|
|
830
|
+
)
|
|
831
|
+
# Clone fields for the body, add other bits.
|
|
832
|
+
body: dict[str, Any] = {
|
|
833
|
+
name: field.clone() for name, field in self.fields.items()
|
|
834
|
+
}
|
|
835
|
+
body["model_options"] = meta_options
|
|
836
|
+
body["_model_meta"] = Meta(
|
|
837
|
+
models_registry=models_registry
|
|
838
|
+
) # Use custom registry
|
|
839
|
+
body["__module__"] = "__fake__"
|
|
840
|
+
|
|
841
|
+
# Then, make a Model object (models_registry.register_model is called in __new__)
|
|
842
|
+
model_class = cast(type[postgres.Model], type(self.name, bases, body))
|
|
843
|
+
from plain.postgres import register_model
|
|
844
|
+
|
|
845
|
+
# Register it to the models_registry associated with the model meta
|
|
846
|
+
# (could probably do this directly right here too...)
|
|
847
|
+
register_model(model_class)
|
|
848
|
+
|
|
849
|
+
return model_class
|
|
850
|
+
|
|
851
|
+
def get_index_by_name(self, name: str) -> Any:
|
|
852
|
+
for index in self.options["indexes"]:
|
|
853
|
+
if index.name == name:
|
|
854
|
+
return index
|
|
855
|
+
raise ValueError(f"No index named {name} on model {self.name}")
|
|
856
|
+
|
|
857
|
+
def get_constraint_by_name(self, name: str) -> Any:
|
|
858
|
+
for constraint in self.options["constraints"]:
|
|
859
|
+
if constraint.name == name:
|
|
860
|
+
return constraint
|
|
861
|
+
raise ValueError(f"No constraint named {name} on model {self.name}")
|
|
862
|
+
|
|
863
|
+
def __repr__(self) -> str:
|
|
864
|
+
return f"<{self.__class__.__name__}: '{self.package_label}.{self.name}'>"
|
|
865
|
+
|
|
866
|
+
def __eq__(self, other: object) -> bool:
|
|
867
|
+
if not isinstance(other, ModelState):
|
|
868
|
+
return NotImplemented
|
|
869
|
+
return (
|
|
870
|
+
(self.package_label == other.package_label)
|
|
871
|
+
and (self.name == other.name)
|
|
872
|
+
and (len(self.fields) == len(other.fields))
|
|
873
|
+
and all(
|
|
874
|
+
k1 == k2 and f1.deconstruct()[1:] == f2.deconstruct()[1:]
|
|
875
|
+
for (k1, f1), (k2, f2) in zip(
|
|
876
|
+
sorted(self.fields.items()),
|
|
877
|
+
sorted(other.fields.items()),
|
|
878
|
+
)
|
|
879
|
+
)
|
|
880
|
+
and (self.options == other.options)
|
|
881
|
+
and (self.bases == other.bases)
|
|
882
|
+
)
|