plain.models 0.50.0__py3-none-any.whl → 0.51.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 +14 -0
- plain/models/README.md +26 -42
- plain/models/__init__.py +2 -0
- plain/models/backends/base/creation.py +2 -2
- plain/models/backends/base/introspection.py +8 -4
- plain/models/backends/base/schema.py +89 -71
- plain/models/backends/base/validation.py +1 -1
- plain/models/backends/mysql/compiler.py +1 -1
- plain/models/backends/mysql/operations.py +1 -1
- plain/models/backends/mysql/schema.py +4 -4
- plain/models/backends/postgresql/operations.py +1 -1
- plain/models/backends/postgresql/schema.py +3 -3
- plain/models/backends/sqlite3/operations.py +1 -1
- plain/models/backends/sqlite3/schema.py +61 -50
- plain/models/base.py +116 -163
- plain/models/cli.py +4 -4
- plain/models/constraints.py +14 -9
- plain/models/deletion.py +15 -14
- plain/models/expressions.py +1 -1
- plain/models/fields/__init__.py +20 -16
- plain/models/fields/json.py +3 -3
- plain/models/fields/related.py +73 -71
- plain/models/fields/related_descriptors.py +2 -2
- plain/models/fields/related_lookups.py +1 -1
- plain/models/fields/related_managers.py +21 -32
- plain/models/fields/reverse_related.py +8 -8
- plain/models/forms.py +12 -12
- plain/models/indexes.py +5 -4
- plain/models/meta.py +505 -0
- plain/models/migrations/operations/base.py +1 -1
- plain/models/migrations/operations/fields.py +6 -6
- plain/models/migrations/operations/models.py +18 -16
- plain/models/migrations/recorder.py +9 -5
- plain/models/migrations/state.py +35 -46
- plain/models/migrations/utils.py +1 -1
- plain/models/options.py +182 -518
- plain/models/preflight.py +7 -5
- plain/models/query.py +119 -65
- plain/models/query_utils.py +18 -13
- plain/models/registry.py +6 -5
- plain/models/sql/compiler.py +51 -37
- plain/models/sql/query.py +77 -68
- plain/models/sql/subqueries.py +4 -4
- plain/models/utils.py +4 -1
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/RECORD +49 -48
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
plain/models/sql/query.py
CHANGED
@@ -52,7 +52,7 @@ from plain.utils.tree import Node
|
|
52
52
|
if TYPE_CHECKING:
|
53
53
|
from plain.models import Model
|
54
54
|
from plain.models.backends.base.base import BaseDatabaseWrapper
|
55
|
-
from plain.models.
|
55
|
+
from plain.models.meta import Meta
|
56
56
|
from plain.models.sql.compiler import SQLCompiler
|
57
57
|
|
58
58
|
|
@@ -67,12 +67,12 @@ FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|--|/\*|\*/")
|
|
67
67
|
EXPLAIN_OPTIONS_PATTERN = _lazy_re_compile(r"[\w\-]+")
|
68
68
|
|
69
69
|
|
70
|
-
def get_field_names_from_opts(
|
71
|
-
if
|
70
|
+
def get_field_names_from_opts(meta: Meta | None) -> set[str]:
|
71
|
+
if meta is None:
|
72
72
|
return set()
|
73
73
|
return set(
|
74
74
|
chain.from_iterable(
|
75
|
-
(f.name, f.attname) if f.concrete else (f.name,) for f in
|
75
|
+
(f.name, f.attname) if f.concrete else (f.name,) for f in meta.get_fields()
|
76
76
|
)
|
77
77
|
)
|
78
78
|
|
@@ -87,7 +87,7 @@ def get_children_from_q(q: Q) -> TypingIterator[tuple[str, Any]]:
|
|
87
87
|
|
88
88
|
JoinInfo = namedtuple(
|
89
89
|
"JoinInfo",
|
90
|
-
("final_field", "targets", "
|
90
|
+
("final_field", "targets", "meta", "joins", "path", "transform_function"),
|
91
91
|
)
|
92
92
|
|
93
93
|
|
@@ -115,7 +115,10 @@ class RawQuery:
|
|
115
115
|
if self.cursor is None:
|
116
116
|
self._execute_query()
|
117
117
|
converter = db_connection.introspection.identifier_converter
|
118
|
-
return [
|
118
|
+
return [
|
119
|
+
converter(column_model_meta[0])
|
120
|
+
for column_model_meta in self.cursor.description
|
121
|
+
]
|
119
122
|
|
120
123
|
def __iter__(self) -> TypingIterator[Any]:
|
121
124
|
# Always execute a new query for a new iterator.
|
@@ -307,14 +310,13 @@ class Query(BaseExpression):
|
|
307
310
|
self, db_connection, elide_empty
|
308
311
|
)
|
309
312
|
|
310
|
-
def
|
313
|
+
def get_model_meta(self) -> Meta:
|
311
314
|
"""
|
312
|
-
Return the
|
313
|
-
processing. Normally, this is self.model.
|
315
|
+
Return the Meta instance (the model._model_meta) from which to start
|
316
|
+
processing. Normally, this is self.model._model_meta, but it can be changed
|
314
317
|
by subclasses.
|
315
318
|
"""
|
316
|
-
|
317
|
-
return self.model._meta # type: ignore[attr-defined,return-value]
|
319
|
+
return self.model._model_meta
|
318
320
|
|
319
321
|
def clone(self) -> Query:
|
320
322
|
"""
|
@@ -449,7 +451,7 @@ class Query(BaseExpression):
|
|
449
451
|
# used.
|
450
452
|
if inner_query.default_cols and has_existing_aggregation:
|
451
453
|
inner_query.group_by = (
|
452
|
-
self.model.
|
454
|
+
self.model._model_meta.get_field("id").get_col(
|
453
455
|
inner_query.get_initial_alias()
|
454
456
|
),
|
455
457
|
)
|
@@ -493,7 +495,7 @@ class Query(BaseExpression):
|
|
493
495
|
# field selected in the inner query, yet we must use a subquery.
|
494
496
|
# So, make sure at least one field is selected.
|
495
497
|
inner_query.select = (
|
496
|
-
self.model.
|
498
|
+
self.model._model_meta.get_field("id").get_col(
|
497
499
|
inner_query.get_initial_alias()
|
498
500
|
),
|
499
501
|
)
|
@@ -552,7 +554,7 @@ class Query(BaseExpression):
|
|
552
554
|
if not (q.distinct and q.is_sliced):
|
553
555
|
if q.group_by is True:
|
554
556
|
q.add_fields(
|
555
|
-
(f.attname for f in self.model.
|
557
|
+
(f.attname for f in self.model._model_meta.concrete_fields), False
|
556
558
|
)
|
557
559
|
# Disable GROUP BY aliases to avoid orphaning references to the
|
558
560
|
# SELECT clause which is about to be cleared.
|
@@ -703,18 +705,18 @@ class Query(BaseExpression):
|
|
703
705
|
|
704
706
|
def _get_defer_select_mask(
|
705
707
|
self,
|
706
|
-
|
708
|
+
meta: Meta,
|
707
709
|
mask: dict[str, Any],
|
708
710
|
select_mask: dict[Any, Any] | None = None,
|
709
711
|
) -> dict[Any, Any]:
|
710
712
|
if select_mask is None:
|
711
713
|
select_mask = {}
|
712
|
-
select_mask[
|
714
|
+
select_mask[meta.get_field("id")] = {}
|
713
715
|
# All concrete fields that are not part of the defer mask must be
|
714
716
|
# loaded. If a relational field is encountered it gets added to the
|
715
717
|
# mask for it be considered if `select_related` and the cycle continues
|
716
718
|
# by recursively calling this function.
|
717
|
-
for field in
|
719
|
+
for field in meta.concrete_fields:
|
718
720
|
field_mask = mask.pop(field.name, None)
|
719
721
|
if field_mask is None:
|
720
722
|
select_mask.setdefault(field, {})
|
@@ -724,44 +726,44 @@ class Query(BaseExpression):
|
|
724
726
|
field_select_mask = select_mask.setdefault(field, {})
|
725
727
|
related_model = field.remote_field.model
|
726
728
|
self._get_defer_select_mask(
|
727
|
-
related_model.
|
729
|
+
related_model._model_meta, field_mask, field_select_mask
|
728
730
|
)
|
729
731
|
# Remaining defer entries must be references to reverse relationships.
|
730
732
|
# The following code is expected to raise FieldError if it encounters
|
731
733
|
# a malformed defer entry.
|
732
734
|
for field_name, field_mask in mask.items():
|
733
735
|
if filtered_relation := self._filtered_relations.get(field_name):
|
734
|
-
relation =
|
736
|
+
relation = meta.get_field(filtered_relation.relation_name)
|
735
737
|
field_select_mask = select_mask.setdefault((field_name, relation), {})
|
736
738
|
field = relation.field
|
737
739
|
else:
|
738
|
-
field =
|
740
|
+
field = meta.get_field(field_name).field
|
739
741
|
field_select_mask = select_mask.setdefault(field, {})
|
740
742
|
related_model = field.model
|
741
743
|
self._get_defer_select_mask(
|
742
|
-
related_model.
|
744
|
+
related_model._model_meta, field_mask, field_select_mask
|
743
745
|
)
|
744
746
|
return select_mask
|
745
747
|
|
746
748
|
def _get_only_select_mask(
|
747
749
|
self,
|
748
|
-
|
750
|
+
meta: Meta,
|
749
751
|
mask: dict[str, Any],
|
750
752
|
select_mask: dict[Any, Any] | None = None,
|
751
753
|
) -> dict[Any, Any]:
|
752
754
|
if select_mask is None:
|
753
755
|
select_mask = {}
|
754
|
-
select_mask[
|
756
|
+
select_mask[meta.get_field("id")] = {}
|
755
757
|
# Only include fields mentioned in the mask.
|
756
758
|
for field_name, field_mask in mask.items():
|
757
|
-
field =
|
759
|
+
field = meta.get_field(field_name)
|
758
760
|
field_select_mask = select_mask.setdefault(field, {})
|
759
761
|
if field_mask:
|
760
762
|
if not field.is_relation:
|
761
763
|
raise FieldError(next(iter(field_mask)))
|
762
764
|
related_model = field.remote_field.model
|
763
765
|
self._get_only_select_mask(
|
764
|
-
related_model.
|
766
|
+
related_model._model_meta, field_mask, field_select_mask
|
765
767
|
)
|
766
768
|
return select_mask
|
767
769
|
|
@@ -782,10 +784,10 @@ class Query(BaseExpression):
|
|
782
784
|
part_mask = mask
|
783
785
|
for part in field_name.split(LOOKUP_SEP):
|
784
786
|
part_mask = part_mask.setdefault(part, {})
|
785
|
-
|
787
|
+
meta = self.get_model_meta()
|
786
788
|
if defer:
|
787
|
-
return self._get_defer_select_mask(
|
788
|
-
return self._get_only_select_mask(
|
789
|
+
return self._get_defer_select_mask(meta, mask)
|
790
|
+
return self._get_only_select_mask(meta, mask)
|
789
791
|
|
790
792
|
def table_alias(
|
791
793
|
self, table_name: str, create: bool = False, filtered_relation: Any = None
|
@@ -1000,7 +1002,9 @@ class Query(BaseExpression):
|
|
1000
1002
|
alias = self.base_table
|
1001
1003
|
self.ref_alias(alias)
|
1002
1004
|
elif self.model:
|
1003
|
-
alias = self.join(
|
1005
|
+
alias = self.join(
|
1006
|
+
self.base_table_class(self.model.model_options.db_table, None)
|
1007
|
+
)
|
1004
1008
|
else:
|
1005
1009
|
alias = None
|
1006
1010
|
return alias
|
@@ -1110,7 +1114,7 @@ class Query(BaseExpression):
|
|
1110
1114
|
for alias, table in query.alias_map.items():
|
1111
1115
|
clone.external_aliases[alias] = (
|
1112
1116
|
isinstance(table, Join)
|
1113
|
-
and table.join_field.related_model.
|
1117
|
+
and table.join_field.related_model.model_options.db_table != alias
|
1114
1118
|
) or (
|
1115
1119
|
isinstance(table, BaseTable) and table.table_name != table.table_alias
|
1116
1120
|
)
|
@@ -1191,46 +1195,48 @@ class Query(BaseExpression):
|
|
1191
1195
|
if summarize:
|
1192
1196
|
expression = Ref(annotation, expression)
|
1193
1197
|
return expression_lookups, (), expression
|
1194
|
-
_, field, _, lookup_parts = self.names_to_path(
|
1198
|
+
_, field, _, lookup_parts = self.names_to_path(
|
1199
|
+
lookup_splitted, self.get_model_meta()
|
1200
|
+
)
|
1195
1201
|
field_parts = lookup_splitted[0 : len(lookup_splitted) - len(lookup_parts)]
|
1196
1202
|
if len(lookup_parts) > 1 and not field_parts:
|
1197
1203
|
raise FieldError(
|
1198
|
-
f'Invalid lookup "{lookup}" for model {self.
|
1204
|
+
f'Invalid lookup "{lookup}" for model {self.get_model_meta().model.__name__}".'
|
1199
1205
|
)
|
1200
1206
|
return lookup_parts, field_parts, False # type: ignore[return-value]
|
1201
1207
|
|
1202
|
-
def check_query_object_type(self, value: Any,
|
1208
|
+
def check_query_object_type(self, value: Any, meta: Meta, field: Field) -> None:
|
1203
1209
|
"""
|
1204
1210
|
Check whether the object passed while querying is of the correct type.
|
1205
1211
|
If not, raise a ValueError specifying the wrong object.
|
1206
1212
|
"""
|
1207
|
-
if hasattr(value, "
|
1208
|
-
if not check_rel_lookup_compatibility(value.
|
1213
|
+
if hasattr(value, "_model_meta"):
|
1214
|
+
if not check_rel_lookup_compatibility(value._model_meta.model, meta, field):
|
1209
1215
|
raise ValueError(
|
1210
|
-
f'Cannot query "{value}": Must be "{
|
1216
|
+
f'Cannot query "{value}": Must be "{meta.model.model_options.object_name}" instance.'
|
1211
1217
|
)
|
1212
1218
|
|
1213
|
-
def check_related_objects(self, field: Field, value: Any,
|
1219
|
+
def check_related_objects(self, field: Field, value: Any, meta: Meta) -> None:
|
1214
1220
|
"""Check the type of object passed to query relations."""
|
1215
1221
|
if field.is_relation:
|
1216
1222
|
# Check that the field and the queryset use the same model in a
|
1217
1223
|
# query like .filter(author=Author.query.all()). For example, the
|
1218
|
-
#
|
1224
|
+
# meta would be Author's (from the author field) and value.model
|
1219
1225
|
# would be Author.query.all() queryset's .model (Author also).
|
1220
1226
|
# The field is the related field on the lhs side.
|
1221
1227
|
if (
|
1222
1228
|
isinstance(value, Query)
|
1223
1229
|
and not value.has_select_fields
|
1224
|
-
and not check_rel_lookup_compatibility(value.model,
|
1230
|
+
and not check_rel_lookup_compatibility(value.model, meta, field)
|
1225
1231
|
):
|
1226
1232
|
raise ValueError(
|
1227
|
-
f'Cannot use QuerySet for "{value.model.
|
1233
|
+
f'Cannot use QuerySet for "{value.model.model_options.object_name}": Use a QuerySet for "{meta.model.model_options.object_name}".'
|
1228
1234
|
)
|
1229
|
-
elif hasattr(value, "
|
1230
|
-
self.check_query_object_type(value,
|
1235
|
+
elif hasattr(value, "_model_meta"):
|
1236
|
+
self.check_query_object_type(value, meta, field)
|
1231
1237
|
elif hasattr(value, "__iter__"):
|
1232
1238
|
for v in value:
|
1233
|
-
self.check_query_object_type(v,
|
1239
|
+
self.check_query_object_type(v, meta, field)
|
1234
1240
|
|
1235
1241
|
def check_filterable(self, expression: Any) -> None:
|
1236
1242
|
"""Raise an error if expression cannot be used in a WHERE clause."""
|
@@ -1391,14 +1397,14 @@ class Query(BaseExpression):
|
|
1391
1397
|
condition = self.build_lookup(lookups, reffed_expression, value)
|
1392
1398
|
return WhereNode([condition], connector=AND), [] # type: ignore[return-value]
|
1393
1399
|
|
1394
|
-
|
1400
|
+
meta = self.get_model_meta()
|
1395
1401
|
alias = self.get_initial_alias()
|
1396
1402
|
allow_many = not branch_negated or not split_subq
|
1397
1403
|
|
1398
1404
|
try:
|
1399
1405
|
join_info = self.setup_joins(
|
1400
1406
|
parts,
|
1401
|
-
|
1407
|
+
meta,
|
1402
1408
|
alias,
|
1403
1409
|
can_reuse=can_reuse,
|
1404
1410
|
allow_many=allow_many,
|
@@ -1408,7 +1414,7 @@ class Query(BaseExpression):
|
|
1408
1414
|
# Prevent iterator from being consumed by check_related_objects()
|
1409
1415
|
if isinstance(value, Iterator):
|
1410
1416
|
value = list(value)
|
1411
|
-
self.check_related_objects(join_info.final_field, value, join_info.
|
1417
|
+
self.check_related_objects(join_info.final_field, value, join_info.meta)
|
1412
1418
|
|
1413
1419
|
# split_exclude() needs to know which joins were generated for the
|
1414
1420
|
# lookup parts
|
@@ -1602,7 +1608,7 @@ class Query(BaseExpression):
|
|
1602
1608
|
def names_to_path(
|
1603
1609
|
self,
|
1604
1610
|
names: list[str],
|
1605
|
-
|
1611
|
+
meta: Meta,
|
1606
1612
|
allow_many: bool = True,
|
1607
1613
|
fail_on_missing: bool = False,
|
1608
1614
|
) -> tuple[list[Any], Field, tuple[Field, ...], list[str]]:
|
@@ -1610,7 +1616,7 @@ class Query(BaseExpression):
|
|
1610
1616
|
Walk the list of names and turns them into PathInfo tuples. A single
|
1611
1617
|
name in 'names' can generate multiple PathInfos (m2m, for example).
|
1612
1618
|
|
1613
|
-
'names' is the path of names to travel, '
|
1619
|
+
'names' is the path of names to travel, 'meta' is the Meta we
|
1614
1620
|
start the name resolving from, 'allow_many' is as for setup_joins().
|
1615
1621
|
If fail_on_missing is set to True, then a name that can't be resolved
|
1616
1622
|
will generate a FieldError.
|
@@ -1627,9 +1633,9 @@ class Query(BaseExpression):
|
|
1627
1633
|
field = None
|
1628
1634
|
filtered_relation = None
|
1629
1635
|
try:
|
1630
|
-
if
|
1636
|
+
if meta is None:
|
1631
1637
|
raise FieldDoesNotExist
|
1632
|
-
field =
|
1638
|
+
field = meta.get_field(name)
|
1633
1639
|
except FieldDoesNotExist:
|
1634
1640
|
if name in self.annotation_select:
|
1635
1641
|
field = self.annotation_select[name].output_field
|
@@ -1639,13 +1645,13 @@ class Query(BaseExpression):
|
|
1639
1645
|
parts = filtered_relation.relation_name.split(LOOKUP_SEP)
|
1640
1646
|
filtered_relation_path, field, _, _ = self.names_to_path(
|
1641
1647
|
parts,
|
1642
|
-
|
1648
|
+
meta,
|
1643
1649
|
allow_many,
|
1644
1650
|
fail_on_missing,
|
1645
1651
|
)
|
1646
1652
|
path.extend(filtered_relation_path[:-1])
|
1647
1653
|
else:
|
1648
|
-
field =
|
1654
|
+
field = meta.get_field(filtered_relation.relation_name) # type: ignore[attr-defined]
|
1649
1655
|
if field is not None:
|
1650
1656
|
# Fields that contain one-to-many relations with a generic
|
1651
1657
|
# model (like a GenericForeignKey) cannot generate reverse
|
@@ -1664,7 +1670,7 @@ class Query(BaseExpression):
|
|
1664
1670
|
if pos == -1 or fail_on_missing:
|
1665
1671
|
available = sorted(
|
1666
1672
|
[
|
1667
|
-
*get_field_names_from_opts(
|
1673
|
+
*get_field_names_from_opts(meta),
|
1668
1674
|
*self.annotation_select,
|
1669
1675
|
*self._filtered_relations,
|
1670
1676
|
]
|
@@ -1689,7 +1695,7 @@ class Query(BaseExpression):
|
|
1689
1695
|
last = pathinfos[-1]
|
1690
1696
|
path.extend(pathinfos)
|
1691
1697
|
final_field = last.join_field
|
1692
|
-
|
1698
|
+
meta = last.to_meta
|
1693
1699
|
targets = last.target_fields
|
1694
1700
|
cur_names_with_path[1].extend(pathinfos)
|
1695
1701
|
names_with_path.append(cur_names_with_path)
|
@@ -1708,7 +1714,7 @@ class Query(BaseExpression):
|
|
1708
1714
|
def setup_joins(
|
1709
1715
|
self,
|
1710
1716
|
names: list[str],
|
1711
|
-
|
1717
|
+
meta: Meta,
|
1712
1718
|
alias: str,
|
1713
1719
|
can_reuse: set[str] | None = None,
|
1714
1720
|
allow_many: bool = True,
|
@@ -1716,7 +1722,7 @@ class Query(BaseExpression):
|
|
1716
1722
|
) -> JoinInfo:
|
1717
1723
|
"""
|
1718
1724
|
Compute the necessary table joins for the passage through the fields
|
1719
|
-
given in 'names'. '
|
1725
|
+
given in 'names'. 'meta' is the Meta for the current model
|
1720
1726
|
(which gives the table we are starting from), 'alias' is the alias for
|
1721
1727
|
the table to start the joining from.
|
1722
1728
|
|
@@ -1762,7 +1768,7 @@ class Query(BaseExpression):
|
|
1762
1768
|
try:
|
1763
1769
|
path, final_field, targets, rest = self.names_to_path(
|
1764
1770
|
names[:pivot],
|
1765
|
-
|
1771
|
+
meta,
|
1766
1772
|
allow_many,
|
1767
1773
|
fail_on_missing=True,
|
1768
1774
|
)
|
@@ -1807,13 +1813,13 @@ class Query(BaseExpression):
|
|
1807
1813
|
else:
|
1808
1814
|
filtered_relation = None
|
1809
1815
|
table_alias = None
|
1810
|
-
|
1816
|
+
meta = join.to_meta
|
1811
1817
|
if join.direct:
|
1812
1818
|
nullable = self.is_nullable(join.join_field)
|
1813
1819
|
else:
|
1814
1820
|
nullable = True
|
1815
1821
|
connection = self.join_class(
|
1816
|
-
|
1822
|
+
meta.model.model_options.db_table,
|
1817
1823
|
alias,
|
1818
1824
|
table_alias,
|
1819
1825
|
INNER,
|
@@ -1830,7 +1836,7 @@ class Query(BaseExpression):
|
|
1830
1836
|
joins.append(alias)
|
1831
1837
|
if filtered_relation:
|
1832
1838
|
filtered_relation.path = joins[:]
|
1833
|
-
return JoinInfo(final_field, targets,
|
1839
|
+
return JoinInfo(final_field, targets, meta, joins, path, final_transformer)
|
1834
1840
|
|
1835
1841
|
def trim_joins(
|
1836
1842
|
self, targets: tuple[Field, ...], joins: list[str], path: list[Any]
|
@@ -1929,7 +1935,10 @@ class Query(BaseExpression):
|
|
1929
1935
|
annotation = self.try_transform(annotation, transform)
|
1930
1936
|
return annotation
|
1931
1937
|
join_info = self.setup_joins(
|
1932
|
-
field_list,
|
1938
|
+
field_list,
|
1939
|
+
self.get_model_meta(),
|
1940
|
+
self.get_initial_alias(),
|
1941
|
+
can_reuse=reuse,
|
1933
1942
|
)
|
1934
1943
|
targets, final_alias, join_list = self.trim_joins(
|
1935
1944
|
join_info.targets, join_info.joins, join_info.path
|
@@ -1991,7 +2000,7 @@ class Query(BaseExpression):
|
|
1991
2000
|
select_field = col.target
|
1992
2001
|
alias = col.alias
|
1993
2002
|
if alias in can_reuse:
|
1994
|
-
id_field = select_field.model.
|
2003
|
+
id_field = select_field.model._model_meta.get_field("id")
|
1995
2004
|
# Need to add a restriction so that outer query's filters are in effect for
|
1996
2005
|
# the subquery, too.
|
1997
2006
|
query.bump_prefix(self)
|
@@ -2115,7 +2124,7 @@ class Query(BaseExpression):
|
|
2115
2124
|
the order specified.
|
2116
2125
|
"""
|
2117
2126
|
alias = self.get_initial_alias()
|
2118
|
-
|
2127
|
+
meta = self.get_model_meta()
|
2119
2128
|
|
2120
2129
|
try:
|
2121
2130
|
cols = []
|
@@ -2123,7 +2132,7 @@ class Query(BaseExpression):
|
|
2123
2132
|
# Join promotion note - we must not remove any rows here, so
|
2124
2133
|
# if there is no existing joins, use outer join.
|
2125
2134
|
join_info = self.setup_joins(
|
2126
|
-
name.split(LOOKUP_SEP),
|
2135
|
+
name.split(LOOKUP_SEP), meta, alias, allow_many=allow_m2m
|
2127
2136
|
)
|
2128
2137
|
targets, final_alias, joins = self.trim_joins(
|
2129
2138
|
join_info.targets,
|
@@ -2148,7 +2157,7 @@ class Query(BaseExpression):
|
|
2148
2157
|
else:
|
2149
2158
|
names = sorted(
|
2150
2159
|
[
|
2151
|
-
*get_field_names_from_opts(
|
2160
|
+
*get_field_names_from_opts(meta),
|
2152
2161
|
*self.extra,
|
2153
2162
|
*self.annotation_select,
|
2154
2163
|
*self._filtered_relations,
|
@@ -2181,7 +2190,7 @@ class Query(BaseExpression):
|
|
2181
2190
|
continue
|
2182
2191
|
# names_to_path() validates the lookup. A descriptive
|
2183
2192
|
# FieldError will be raise if it's not.
|
2184
|
-
self.names_to_path(item.split(LOOKUP_SEP), self.model.
|
2193
|
+
self.names_to_path(item.split(LOOKUP_SEP), self.model._model_meta)
|
2185
2194
|
elif not hasattr(item, "resolve_expression"):
|
2186
2195
|
errors.append(item)
|
2187
2196
|
if getattr(item, "contains_aggregate", False):
|
@@ -2404,13 +2413,13 @@ class Query(BaseExpression):
|
|
2404
2413
|
self.set_annotation_mask(annotation_names)
|
2405
2414
|
selected = frozenset(field_names + extra_names + annotation_names)
|
2406
2415
|
else:
|
2407
|
-
field_names = [f.attname for f in self.model.
|
2416
|
+
field_names = [f.attname for f in self.model._model_meta.concrete_fields]
|
2408
2417
|
selected = frozenset(field_names)
|
2409
2418
|
# Selected annotations must be known before setting the GROUP BY
|
2410
2419
|
# clause.
|
2411
2420
|
if self.group_by is True:
|
2412
2421
|
self.add_fields(
|
2413
|
-
(f.attname for f in self.model.
|
2422
|
+
(f.attname for f in self.model._model_meta.concrete_fields), False
|
2414
2423
|
)
|
2415
2424
|
# Disable GROUP BY aliases to avoid orphaning references to the
|
2416
2425
|
# SELECT clause which is about to be cleared.
|
plain/models/sql/subqueries.py
CHANGED
@@ -36,14 +36,14 @@ class DeleteQuery(Query):
|
|
36
36
|
"""
|
37
37
|
# number of objects deleted
|
38
38
|
num_deleted = 0
|
39
|
-
field = self.
|
39
|
+
field = self.get_model_meta().get_field("id")
|
40
40
|
for offset in range(0, len(id_list), GET_ITERATOR_CHUNK_SIZE):
|
41
41
|
self.clear_where()
|
42
42
|
self.add_filter(
|
43
43
|
f"{field.attname}__in",
|
44
44
|
id_list[offset : offset + GET_ITERATOR_CHUNK_SIZE],
|
45
45
|
)
|
46
|
-
num_deleted += self.do_query(self.
|
46
|
+
num_deleted += self.do_query(self.model.model_options.db_table, self.where)
|
47
47
|
return num_deleted
|
48
48
|
|
49
49
|
|
@@ -87,7 +87,7 @@ class UpdateQuery(Query):
|
|
87
87
|
"""
|
88
88
|
values_seq = []
|
89
89
|
for name, val in values.items():
|
90
|
-
field = self.
|
90
|
+
field = self.get_model_meta().get_field(name)
|
91
91
|
direct = (
|
92
92
|
not (field.auto_created and not field.concrete) or not field.concrete
|
93
93
|
)
|
@@ -97,7 +97,7 @@ class UpdateQuery(Query):
|
|
97
97
|
f"Cannot update model field {field!r} (only non-relations and "
|
98
98
|
"foreign keys permitted)."
|
99
99
|
)
|
100
|
-
if model is not self.
|
100
|
+
if model is not self.get_model_meta().model:
|
101
101
|
self.add_related_update(model, field, val)
|
102
102
|
continue
|
103
103
|
values_seq.append((field, model, val))
|
plain/models/utils.py
CHANGED
@@ -19,7 +19,10 @@ def make_model_tuple(model: Any) -> tuple[str, str]:
|
|
19
19
|
package_label, model_name = model.split(".")
|
20
20
|
model_tuple = package_label, model_name.lower()
|
21
21
|
else:
|
22
|
-
model_tuple =
|
22
|
+
model_tuple = (
|
23
|
+
model.model_options.package_label,
|
24
|
+
model.model_options.model_name,
|
25
|
+
)
|
23
26
|
assert len(model_tuple) == 2
|
24
27
|
return model_tuple
|
25
28
|
except (ValueError, AssertionError):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: plain.models
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.51.0
|
4
4
|
Summary: Model your data and store it in a database.
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
6
6
|
License-File: LICENSE
|
@@ -211,31 +211,32 @@ class User(models.Model):
|
|
211
211
|
username = models.CharField(max_length=150)
|
212
212
|
age = models.IntegerField()
|
213
213
|
|
214
|
-
|
215
|
-
indexes
|
214
|
+
model_options = models.Options(
|
215
|
+
indexes=[
|
216
216
|
models.Index(fields=["email"]),
|
217
217
|
models.Index(fields=["-created_at"], name="user_created_idx"),
|
218
|
-
]
|
219
|
-
constraints
|
218
|
+
],
|
219
|
+
constraints=[
|
220
220
|
models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
|
221
221
|
models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
|
222
|
-
]
|
222
|
+
],
|
223
|
+
)
|
223
224
|
```
|
224
225
|
|
225
226
|
## Custom QuerySets
|
226
227
|
|
227
|
-
With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
|
228
|
-
|
229
|
-
### Setting a default QuerySet for a model
|
228
|
+
With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
|
230
229
|
|
231
|
-
|
230
|
+
Define a custom QuerySet and assign it to your model's `query` attribute:
|
232
231
|
|
233
232
|
```python
|
234
|
-
|
235
|
-
|
233
|
+
from typing import Self
|
234
|
+
|
235
|
+
class PublishedQuerySet(models.QuerySet["Article"]):
|
236
|
+
def published_only(self) -> Self:
|
236
237
|
return self.filter(status="published")
|
237
238
|
|
238
|
-
def draft_only(self):
|
239
|
+
def draft_only(self) -> Self:
|
239
240
|
return self.filter(status="draft")
|
240
241
|
|
241
242
|
@models.register_model
|
@@ -243,50 +244,33 @@ class Article(models.Model):
|
|
243
244
|
title = models.CharField(max_length=200)
|
244
245
|
status = models.CharField(max_length=20)
|
245
246
|
|
246
|
-
|
247
|
-
queryset_class = PublishedQuerySet
|
247
|
+
query = PublishedQuerySet()
|
248
248
|
|
249
|
-
# Usage - all methods available on Article.
|
249
|
+
# Usage - all methods available on Article.query
|
250
250
|
all_articles = Article.query.all()
|
251
251
|
published_articles = Article.query.published_only()
|
252
252
|
draft_articles = Article.query.draft_only()
|
253
253
|
```
|
254
254
|
|
255
|
-
|
256
|
-
|
257
|
-
You can also use custom QuerySets manually without setting them as the default:
|
255
|
+
Custom methods can be chained with built-in QuerySet methods:
|
258
256
|
|
259
257
|
```python
|
260
|
-
|
261
|
-
|
262
|
-
return self.filter(special=True)
|
263
|
-
|
264
|
-
# Create and use the QuerySet manually
|
265
|
-
special_qs = SpecialQuerySet(model=Article)
|
266
|
-
special_articles = special_qs.special_filter()
|
258
|
+
# Chaining works naturally
|
259
|
+
recent_published = Article.query.published_only().order_by("-created_at")[:10]
|
267
260
|
```
|
268
261
|
|
269
|
-
###
|
262
|
+
### Programmatic QuerySet usage
|
270
263
|
|
271
|
-
For
|
264
|
+
For internal code that needs to create QuerySet instances programmatically, use `from_model()`:
|
272
265
|
|
273
266
|
```python
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
status = models.CharField(max_length=20)
|
278
|
-
|
279
|
-
@classmethod
|
280
|
-
def published(cls):
|
281
|
-
return PublishedQuerySet(model=cls).published_only()
|
282
|
-
|
283
|
-
@classmethod
|
284
|
-
def drafts(cls):
|
285
|
-
return PublishedQuerySet(model=cls).draft_only()
|
267
|
+
class SpecialQuerySet(models.QuerySet["Article"]):
|
268
|
+
def special_filter(self) -> Self:
|
269
|
+
return self.filter(special=True)
|
286
270
|
|
287
|
-
#
|
288
|
-
|
289
|
-
|
271
|
+
# Create and use the QuerySet programmatically
|
272
|
+
special_qs = SpecialQuerySet.from_model(Article)
|
273
|
+
special_articles = special_qs.special_filter()
|
290
274
|
```
|
291
275
|
|
292
276
|
## Forms
|