plain.models 0.33.1__py3-none-any.whl → 0.34.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/models/CHANGELOG.md +17 -0
- plain/models/README.md +8 -10
- plain/models/__init__.py +2 -6
- plain/models/backends/base/base.py +10 -18
- plain/models/backends/base/creation.py +3 -4
- plain/models/backends/base/introspection.py +2 -3
- plain/models/backends/base/schema.py +3 -9
- plain/models/backends/mysql/validation.py +1 -1
- plain/models/backends/postgresql/base.py +15 -23
- plain/models/backends/postgresql/schema.py +0 -2
- plain/models/backends/sqlite3/base.py +1 -1
- plain/models/backends/sqlite3/creation.py +2 -2
- plain/models/backends/sqlite3/features.py +1 -1
- plain/models/backends/sqlite3/schema.py +1 -1
- plain/models/backends/utils.py +2 -6
- plain/models/backups/core.py +15 -22
- plain/models/base.py +179 -225
- plain/models/cli.py +25 -62
- plain/models/connections.py +48 -165
- plain/models/constraints.py +10 -10
- plain/models/db.py +7 -15
- plain/models/default_settings.py +13 -20
- plain/models/deletion.py +14 -16
- plain/models/expressions.py +7 -10
- plain/models/fields/__init__.py +56 -76
- plain/models/fields/json.py +9 -12
- plain/models/fields/related.py +5 -17
- plain/models/fields/related_descriptors.py +43 -95
- plain/models/forms.py +2 -4
- plain/models/indexes.py +2 -3
- plain/models/lookups.py +0 -7
- plain/models/manager.py +1 -14
- plain/models/migrations/executor.py +0 -16
- plain/models/migrations/loader.py +1 -1
- plain/models/migrations/migration.py +1 -1
- plain/models/migrations/operations/base.py +4 -11
- plain/models/migrations/operations/fields.py +4 -4
- plain/models/migrations/operations/models.py +10 -10
- plain/models/migrations/operations/special.py +6 -14
- plain/models/migrations/recorder.py +1 -1
- plain/models/options.py +4 -7
- plain/models/preflight.py +25 -44
- plain/models/query.py +47 -102
- plain/models/query_utils.py +4 -4
- plain/models/sql/compiler.py +7 -11
- plain/models/sql/query.py +32 -42
- plain/models/sql/subqueries.py +6 -8
- plain/models/sql/where.py +1 -1
- plain/models/test/pytest.py +21 -32
- plain/models/test/utils.py +7 -143
- plain/models/transaction.py +66 -164
- {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/METADATA +9 -11
- {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/RECORD +56 -55
- {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/WHEEL +0 -0
- {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.33.1.dist-info → plain_models-0.34.0.dist-info}/licenses/LICENSE +0 -0
plain/models/deletion.py
CHANGED
@@ -8,7 +8,7 @@ from plain.models import (
|
|
8
8
|
sql,
|
9
9
|
transaction,
|
10
10
|
)
|
11
|
-
from plain.models.db import IntegrityError,
|
11
|
+
from plain.models.db import IntegrityError, db_connection
|
12
12
|
from plain.models.query import QuerySet
|
13
13
|
|
14
14
|
|
@@ -31,7 +31,7 @@ def CASCADE(collector, field, sub_objs, using):
|
|
31
31
|
nullable=field.allow_null,
|
32
32
|
fail_on_restricted=False,
|
33
33
|
)
|
34
|
-
if field.allow_null and not
|
34
|
+
if field.allow_null and not db_connection.features.can_defer_constraint_checks:
|
35
35
|
collector.add_field_update(field, None, sub_objs)
|
36
36
|
|
37
37
|
|
@@ -93,8 +93,7 @@ def get_candidate_relations_to_delete(opts):
|
|
93
93
|
|
94
94
|
|
95
95
|
class Collector:
|
96
|
-
def __init__(self,
|
97
|
-
self.using = using
|
96
|
+
def __init__(self, origin=None):
|
98
97
|
# A Model or QuerySet object.
|
99
98
|
self.origin = origin
|
100
99
|
# Initially, {model: {instances}}, later values become lists.
|
@@ -210,11 +209,12 @@ class Collector:
|
|
210
209
|
|
211
210
|
def get_del_batches(self, objs, fields):
|
212
211
|
"""
|
213
|
-
Return the objs in suitably sized batches for the used
|
212
|
+
Return the objs in suitably sized batches for the used db_connection.
|
214
213
|
"""
|
215
214
|
field_names = [field.name for field in fields]
|
216
215
|
conn_batch_size = max(
|
217
|
-
|
216
|
+
db_connection.ops.bulk_batch_size(field_names, objs),
|
217
|
+
1,
|
218
218
|
)
|
219
219
|
if len(objs) > conn_batch_size:
|
220
220
|
return [
|
@@ -300,7 +300,7 @@ class Collector:
|
|
300
300
|
sub_objs = sub_objs.only(*tuple(referenced_fields))
|
301
301
|
if getattr(on_delete, "lazy_sub_objs", False) or sub_objs:
|
302
302
|
try:
|
303
|
-
on_delete(self, field, sub_objs
|
303
|
+
on_delete(self, field, sub_objs)
|
304
304
|
except ProtectedError as error:
|
305
305
|
key = f"'{field.model.__name__}.{field.name}'"
|
306
306
|
protected_objects[key] += error.protected_objects
|
@@ -352,7 +352,7 @@ class Collector:
|
|
352
352
|
[(f"{related_field.name}__in", objs) for related_field in related_fields],
|
353
353
|
connector=query_utils.Q.OR,
|
354
354
|
)
|
355
|
-
return related_model._base_manager.
|
355
|
+
return related_model._base_manager.filter(predicate)
|
356
356
|
|
357
357
|
def sort(self):
|
358
358
|
sorted_models = []
|
@@ -388,17 +388,15 @@ class Collector:
|
|
388
388
|
if len(self.data) == 1 and len(instances) == 1:
|
389
389
|
instance = list(instances)[0]
|
390
390
|
if self.can_fast_delete(instance):
|
391
|
-
with transaction.mark_for_rollback_on_error(
|
392
|
-
count = sql.DeleteQuery(model).delete_batch(
|
393
|
-
[instance.pk], self.using
|
394
|
-
)
|
391
|
+
with transaction.mark_for_rollback_on_error():
|
392
|
+
count = sql.DeleteQuery(model).delete_batch([instance.pk])
|
395
393
|
setattr(instance, model._meta.pk.attname, None)
|
396
394
|
return count, {model._meta.label: count}
|
397
395
|
|
398
|
-
with transaction.atomic(
|
396
|
+
with transaction.atomic(savepoint=False):
|
399
397
|
# fast deletes
|
400
398
|
for qs in self.fast_deletes:
|
401
|
-
count = qs._raw_delete(
|
399
|
+
count = qs._raw_delete()
|
402
400
|
if count:
|
403
401
|
deleted_counter[qs.model._meta.label] += count
|
404
402
|
|
@@ -421,7 +419,7 @@ class Collector:
|
|
421
419
|
model = objs[0].__class__
|
422
420
|
query = sql.UpdateQuery(model)
|
423
421
|
query.update_batch(
|
424
|
-
list({obj.pk for obj in objs}), {field.name: value}
|
422
|
+
list({obj.pk for obj in objs}), {field.name: value}
|
425
423
|
)
|
426
424
|
|
427
425
|
# reverse instance collections
|
@@ -432,7 +430,7 @@ class Collector:
|
|
432
430
|
for model, instances in self.data.items():
|
433
431
|
query = sql.DeleteQuery(model)
|
434
432
|
pk_list = [obj.pk for obj in instances]
|
435
|
-
count = query.delete_batch(pk_list
|
433
|
+
count = query.delete_batch(pk_list)
|
436
434
|
if count:
|
437
435
|
deleted_counter[model._meta.label] += count
|
438
436
|
|
plain/models/expressions.py
CHANGED
@@ -12,10 +12,9 @@ from plain.exceptions import EmptyResultSet, FieldError, FullResultSet
|
|
12
12
|
from plain.models import fields
|
13
13
|
from plain.models.constants import LOOKUP_SEP
|
14
14
|
from plain.models.db import (
|
15
|
-
DEFAULT_DB_ALIAS,
|
16
15
|
DatabaseError,
|
17
16
|
NotSupportedError,
|
18
|
-
|
17
|
+
db_connection,
|
19
18
|
)
|
20
19
|
from plain.models.query_utils import Q
|
21
20
|
from plain.utils.deconstruct import deconstructible
|
@@ -1782,20 +1781,18 @@ class WindowFrame(Expression):
|
|
1782
1781
|
|
1783
1782
|
def __str__(self):
|
1784
1783
|
if self.start.value is not None and self.start.value < 0:
|
1785
|
-
start = (
|
1786
|
-
f"{abs(self.start.value)} {connections[DEFAULT_DB_ALIAS].ops.PRECEDING}"
|
1787
|
-
)
|
1784
|
+
start = f"{abs(self.start.value)} {db_connection.ops.PRECEDING}"
|
1788
1785
|
elif self.start.value is not None and self.start.value == 0:
|
1789
|
-
start =
|
1786
|
+
start = db_connection.ops.CURRENT_ROW
|
1790
1787
|
else:
|
1791
|
-
start =
|
1788
|
+
start = db_connection.ops.UNBOUNDED_PRECEDING
|
1792
1789
|
|
1793
1790
|
if self.end.value is not None and self.end.value > 0:
|
1794
|
-
end = f"{self.end.value} {
|
1791
|
+
end = f"{self.end.value} {db_connection.ops.FOLLOWING}"
|
1795
1792
|
elif self.end.value is not None and self.end.value == 0:
|
1796
|
-
end =
|
1793
|
+
end = db_connection.ops.CURRENT_ROW
|
1797
1794
|
else:
|
1798
|
-
end =
|
1795
|
+
end = db_connection.ops.UNBOUNDED_FOLLOWING
|
1799
1796
|
return self.template % {
|
1800
1797
|
"frame_type": self.frame_type,
|
1801
1798
|
"start": start,
|
plain/models/fields/__init__.py
CHANGED
@@ -11,7 +11,7 @@ from functools import cached_property, partialmethod, total_ordering
|
|
11
11
|
|
12
12
|
from plain import exceptions, preflight, validators
|
13
13
|
from plain.models.constants import LOOKUP_SEP
|
14
|
-
from plain.models.db import
|
14
|
+
from plain.models.db import db_connection
|
15
15
|
from plain.models.enums import ChoicesMeta
|
16
16
|
from plain.models.query_utils import DeferredAttribute, RegisterLookupMixin
|
17
17
|
from plain.utils import timezone
|
@@ -346,35 +346,29 @@ class Field(RegisterLookupMixin):
|
|
346
346
|
)
|
347
347
|
]
|
348
348
|
|
349
|
-
def _check_db_comment(self,
|
350
|
-
if not self.db_comment or not
|
349
|
+
def _check_db_comment(self, database=False, **kwargs):
|
350
|
+
if not self.db_comment or not database:
|
351
351
|
return []
|
352
352
|
errors = []
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
f"{connection.display_name} does not support comments on "
|
364
|
-
f"columns (db_comment).",
|
365
|
-
obj=self,
|
366
|
-
id="fields.W163",
|
367
|
-
)
|
353
|
+
if not (
|
354
|
+
db_connection.features.supports_comments
|
355
|
+
or "supports_comments" in self.model._meta.required_db_features
|
356
|
+
):
|
357
|
+
errors.append(
|
358
|
+
preflight.Warning(
|
359
|
+
f"{db_connection.display_name} does not support comments on "
|
360
|
+
f"columns (db_comment).",
|
361
|
+
obj=self,
|
362
|
+
id="fields.W163",
|
368
363
|
)
|
364
|
+
)
|
369
365
|
return errors
|
370
366
|
|
371
367
|
def _check_null_allowed_for_primary_keys(self):
|
372
368
|
if (
|
373
369
|
self.primary_key
|
374
370
|
and self.allow_null
|
375
|
-
and not
|
376
|
-
DEFAULT_DB_ALIAS
|
377
|
-
].features.interprets_empty_strings_as_nulls
|
371
|
+
and not db_connection.features.interprets_empty_strings_as_nulls
|
378
372
|
):
|
379
373
|
# We cannot reliably check this for backends like Oracle which
|
380
374
|
# consider NULL and '' to be equal (and thus set up
|
@@ -393,13 +387,11 @@ class Field(RegisterLookupMixin):
|
|
393
387
|
else:
|
394
388
|
return []
|
395
389
|
|
396
|
-
def _check_backend_specific_checks(self,
|
397
|
-
if
|
390
|
+
def _check_backend_specific_checks(self, database=False, **kwargs):
|
391
|
+
if not database:
|
398
392
|
return []
|
399
393
|
errors = []
|
400
|
-
|
401
|
-
if router.allow_migrate_model(alias, self.model):
|
402
|
-
errors.extend(connections[alias].validation.check_field(self, **kwargs))
|
394
|
+
errors.extend(db_connection.validation.check_field(self, **kwargs))
|
403
395
|
return errors
|
404
396
|
|
405
397
|
def _check_validators(self):
|
@@ -896,9 +888,7 @@ class Field(RegisterLookupMixin):
|
|
896
888
|
if (
|
897
889
|
not self.empty_strings_allowed
|
898
890
|
or self.allow_null
|
899
|
-
and not
|
900
|
-
DEFAULT_DB_ALIAS
|
901
|
-
].features.interprets_empty_strings_as_nulls
|
891
|
+
and not db_connection.features.interprets_empty_strings_as_nulls
|
902
892
|
):
|
903
893
|
return return_None
|
904
894
|
return str # return empty string
|
@@ -1015,17 +1005,17 @@ class CharField(Field):
|
|
1015
1005
|
return "String (unlimited)"
|
1016
1006
|
|
1017
1007
|
def check(self, **kwargs):
|
1018
|
-
|
1008
|
+
database = kwargs.get("database", False)
|
1019
1009
|
return [
|
1020
1010
|
*super().check(**kwargs),
|
1021
|
-
*self._check_db_collation(
|
1011
|
+
*self._check_db_collation(database),
|
1022
1012
|
*self._check_max_length_attribute(**kwargs),
|
1023
1013
|
]
|
1024
1014
|
|
1025
1015
|
def _check_max_length_attribute(self, **kwargs):
|
1026
1016
|
if self.max_length is None:
|
1027
1017
|
if (
|
1028
|
-
|
1018
|
+
db_connection.features.supports_unlimited_charfield
|
1029
1019
|
or "supports_unlimited_charfield"
|
1030
1020
|
in self.model._meta.required_db_features
|
1031
1021
|
):
|
@@ -1052,26 +1042,22 @@ class CharField(Field):
|
|
1052
1042
|
else:
|
1053
1043
|
return []
|
1054
1044
|
|
1055
|
-
def _check_db_collation(self,
|
1045
|
+
def _check_db_collation(self, database):
|
1056
1046
|
errors = []
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
obj=self,
|
1072
|
-
id="fields.E190",
|
1073
|
-
),
|
1074
|
-
)
|
1047
|
+
if database and not (
|
1048
|
+
self.db_collation is None
|
1049
|
+
or "supports_collation_on_charfield"
|
1050
|
+
in self.model._meta.required_db_features
|
1051
|
+
or db_connection.features.supports_collation_on_charfield
|
1052
|
+
):
|
1053
|
+
errors.append(
|
1054
|
+
preflight.Error(
|
1055
|
+
f"{db_connection.display_name} does not support a database collation on "
|
1056
|
+
"CharFields.",
|
1057
|
+
obj=self,
|
1058
|
+
id="fields.E190",
|
1059
|
+
),
|
1060
|
+
)
|
1075
1061
|
return errors
|
1076
1062
|
|
1077
1063
|
def cast_db_type(self, connection):
|
@@ -1716,12 +1702,10 @@ class IntegerField(Field):
|
|
1716
1702
|
@cached_property
|
1717
1703
|
def validators(self):
|
1718
1704
|
# These validators can't be added at field initialization time since
|
1719
|
-
# they're based on values retrieved from
|
1705
|
+
# they're based on values retrieved from the database connection.
|
1720
1706
|
validators_ = super().validators
|
1721
1707
|
internal_type = self.get_internal_type()
|
1722
|
-
min_value, max_value =
|
1723
|
-
internal_type
|
1724
|
-
)
|
1708
|
+
min_value, max_value = db_connection.ops.integer_field_range(internal_type)
|
1725
1709
|
if min_value is not None and not any(
|
1726
1710
|
(
|
1727
1711
|
isinstance(validator, validators.MinValueValidator)
|
@@ -1993,32 +1977,28 @@ class TextField(Field):
|
|
1993
1977
|
self.db_collation = db_collation
|
1994
1978
|
|
1995
1979
|
def check(self, **kwargs):
|
1996
|
-
|
1980
|
+
database = kwargs.get("database", False)
|
1997
1981
|
return [
|
1998
1982
|
*super().check(**kwargs),
|
1999
|
-
*self._check_db_collation(
|
1983
|
+
*self._check_db_collation(database),
|
2000
1984
|
]
|
2001
1985
|
|
2002
|
-
def _check_db_collation(self,
|
1986
|
+
def _check_db_collation(self, database):
|
2003
1987
|
errors = []
|
2004
|
-
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2017
|
-
|
2018
|
-
obj=self,
|
2019
|
-
id="fields.E190",
|
2020
|
-
),
|
2021
|
-
)
|
1988
|
+
if database and not (
|
1989
|
+
self.db_collation is None
|
1990
|
+
or "supports_collation_on_textfield"
|
1991
|
+
in self.model._meta.required_db_features
|
1992
|
+
or db_connection.features.supports_collation_on_textfield
|
1993
|
+
):
|
1994
|
+
errors.append(
|
1995
|
+
preflight.Error(
|
1996
|
+
f"{db_connection.display_name} does not support a database collation on "
|
1997
|
+
"TextFields.",
|
1998
|
+
obj=self,
|
1999
|
+
id="fields.E190",
|
2000
|
+
),
|
2001
|
+
)
|
2022
2002
|
return errors
|
2023
2003
|
|
2024
2004
|
def db_parameters(self, connection):
|
plain/models/fields/json.py
CHANGED
@@ -3,7 +3,7 @@ import json
|
|
3
3
|
from plain import exceptions, preflight
|
4
4
|
from plain.models import expressions, lookups
|
5
5
|
from plain.models.constants import LOOKUP_SEP
|
6
|
-
from plain.models.db import NotSupportedError,
|
6
|
+
from plain.models.db import NotSupportedError, db_connection
|
7
7
|
from plain.models.fields import TextField
|
8
8
|
from plain.models.lookups import (
|
9
9
|
FieldGetDbPrepValueMixin,
|
@@ -42,28 +42,25 @@ class JSONField(CheckFieldDefaultMixin, Field):
|
|
42
42
|
|
43
43
|
def check(self, **kwargs):
|
44
44
|
errors = super().check(**kwargs)
|
45
|
-
|
46
|
-
errors.extend(self._check_supported(
|
45
|
+
database = kwargs.get("database", False)
|
46
|
+
errors.extend(self._check_supported(database))
|
47
47
|
return errors
|
48
48
|
|
49
|
-
def _check_supported(self,
|
49
|
+
def _check_supported(self, database):
|
50
50
|
errors = []
|
51
|
-
|
52
|
-
if not router.allow_migrate_model(db, self.model):
|
53
|
-
continue
|
54
|
-
connection = connections[db]
|
51
|
+
if database:
|
55
52
|
if (
|
56
53
|
self.model._meta.required_db_vendor
|
57
|
-
and self.model._meta.required_db_vendor !=
|
54
|
+
and self.model._meta.required_db_vendor != db_connection.vendor
|
58
55
|
):
|
59
|
-
|
56
|
+
return errors
|
60
57
|
if not (
|
61
58
|
"supports_json_field" in self.model._meta.required_db_features
|
62
|
-
or
|
59
|
+
or db_connection.features.supports_json_field
|
63
60
|
):
|
64
61
|
errors.append(
|
65
62
|
preflight.Error(
|
66
|
-
f"{
|
63
|
+
f"{db_connection.display_name} does not support JSONFields.",
|
67
64
|
obj=self.model,
|
68
65
|
id="fields.E180",
|
69
66
|
)
|
plain/models/fields/related.py
CHANGED
@@ -4,11 +4,10 @@ from functools import cached_property, partial
|
|
4
4
|
|
5
5
|
from plain import exceptions, preflight
|
6
6
|
from plain.models.constants import LOOKUP_SEP
|
7
|
-
from plain.models.db import router
|
8
7
|
from plain.models.deletion import SET_DEFAULT, SET_NULL
|
9
8
|
from plain.models.query_utils import PathInfo, Q
|
10
9
|
from plain.models.utils import make_model_tuple
|
11
|
-
from plain.runtime import SettingsReference
|
10
|
+
from plain.runtime import SettingsReference
|
12
11
|
|
13
12
|
from ..registry import models_registry
|
14
13
|
from . import Field
|
@@ -929,8 +928,7 @@ class ForeignKey(ForeignObject):
|
|
929
928
|
if value is None:
|
930
929
|
return
|
931
930
|
|
932
|
-
|
933
|
-
qs = self.remote_field.model._base_manager.using(using).filter(
|
931
|
+
qs = self.remote_field.model._base_manager.filter(
|
934
932
|
**{self.remote_field.field_name: value}
|
935
933
|
)
|
936
934
|
qs = qs.complex_filter(self.get_limit_choices_to())
|
@@ -1373,23 +1371,13 @@ class ManyToManyField(RelatedField):
|
|
1373
1371
|
!= self.remote_field.through._meta.concrete_model
|
1374
1372
|
):
|
1375
1373
|
clashing_obj = model._meta.label
|
1376
|
-
if settings.DATABASE_ROUTERS:
|
1377
|
-
error_class, error_id = preflight.Warning, "fields.W344"
|
1378
|
-
error_hint = (
|
1379
|
-
"You have configured settings.DATABASE_ROUTERS. Verify "
|
1380
|
-
f"that the table of {clashing_obj!r} is correctly routed to a separate "
|
1381
|
-
"database."
|
1382
|
-
)
|
1383
|
-
else:
|
1384
|
-
error_class, error_id = preflight.Error, "fields.E340"
|
1385
|
-
error_hint = None
|
1386
1374
|
return [
|
1387
|
-
|
1375
|
+
preflight.Error(
|
1388
1376
|
f"The field's intermediary table '{m2m_db_table}' clashes with the "
|
1389
1377
|
f"table name of '{clashing_obj}'.",
|
1390
1378
|
obj=self,
|
1391
|
-
hint=
|
1392
|
-
id=
|
1379
|
+
hint=None,
|
1380
|
+
id="fields.E340",
|
1393
1381
|
)
|
1394
1382
|
]
|
1395
1383
|
return []
|