plain.models 0.38.0__py3-none-any.whl → 0.39.1__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 (40) hide show
  1. plain/models/CHANGELOG.md +31 -0
  2. plain/models/README.md +31 -0
  3. plain/models/__init__.py +2 -2
  4. plain/models/backends/base/creation.py +1 -1
  5. plain/models/backends/base/operations.py +1 -3
  6. plain/models/backends/base/schema.py +4 -8
  7. plain/models/backends/mysql/base.py +1 -3
  8. plain/models/backends/mysql/introspection.py +2 -6
  9. plain/models/backends/mysql/operations.py +2 -4
  10. plain/models/backends/postgresql/base.py +2 -6
  11. plain/models/backends/postgresql/introspection.py +2 -6
  12. plain/models/backends/postgresql/operations.py +1 -3
  13. plain/models/backends/postgresql/schema.py +2 -10
  14. plain/models/backends/sqlite3/base.py +2 -6
  15. plain/models/backends/sqlite3/introspection.py +2 -8
  16. plain/models/base.py +46 -74
  17. plain/models/constraints.py +3 -3
  18. plain/models/deletion.py +9 -9
  19. plain/models/fields/__init__.py +30 -104
  20. plain/models/fields/related.py +90 -343
  21. plain/models/fields/related_descriptors.py +14 -14
  22. plain/models/fields/related_lookups.py +2 -2
  23. plain/models/fields/reverse_related.py +6 -14
  24. plain/models/forms.py +14 -76
  25. plain/models/lookups.py +2 -2
  26. plain/models/migrations/autodetector.py +2 -25
  27. plain/models/migrations/operations/fields.py +0 -6
  28. plain/models/migrations/state.py +2 -26
  29. plain/models/migrations/utils.py +4 -14
  30. plain/models/options.py +4 -12
  31. plain/models/query.py +46 -54
  32. plain/models/query_utils.py +3 -5
  33. plain/models/sql/compiler.py +16 -18
  34. plain/models/sql/query.py +12 -11
  35. plain/models/sql/subqueries.py +10 -10
  36. {plain_models-0.38.0.dist-info → plain_models-0.39.1.dist-info}/METADATA +32 -1
  37. {plain_models-0.38.0.dist-info → plain_models-0.39.1.dist-info}/RECORD +40 -40
  38. {plain_models-0.38.0.dist-info → plain_models-0.39.1.dist-info}/WHEEL +0 -0
  39. {plain_models-0.38.0.dist-info → plain_models-0.39.1.dist-info}/entry_points.txt +0 -0
  40. {plain_models-0.38.0.dist-info → plain_models-0.39.1.dist-info}/licenses/LICENSE +0 -0
plain/models/query.py CHANGED
@@ -24,10 +24,10 @@ from plain.models.db import (
24
24
  )
25
25
  from plain.models.expressions import Case, F, Value, When
26
26
  from plain.models.fields import (
27
- AutoField,
28
27
  DateField,
29
28
  DateTimeField,
30
29
  Field,
30
+ PrimaryKeyField,
31
31
  )
32
32
  from plain.models.functions import Cast, Trunc
33
33
  from plain.models.query_utils import FilteredRelation, Q
@@ -82,14 +82,7 @@ class ModelIterable(BaseIterable):
82
82
  (
83
83
  field,
84
84
  related_objs,
85
- operator.attrgetter(
86
- *[
87
- field.attname
88
- if from_field == "self"
89
- else queryset.model._meta.get_field(from_field).attname
90
- for from_field in field.from_fields
91
- ]
92
- ),
85
+ operator.attrgetter(field.attname),
93
86
  )
94
87
  for field, related_objs in queryset._known_related_objects.items()
95
88
  ]
@@ -135,7 +128,7 @@ class RawModelIterable(BaseIterable):
135
128
  annotation_fields,
136
129
  ) = self.queryset.resolve_model_init_order()
137
130
  model_cls = self.queryset.model
138
- if model_cls._meta.pk.attname not in model_init_names:
131
+ if "id" not in model_init_names:
139
132
  raise exceptions.FieldDoesNotExist(
140
133
  "Raw query must include the primary key"
141
134
  )
@@ -271,7 +264,7 @@ class QuerySet:
271
264
  self._for_write = False
272
265
  self._prefetch_related_lookups = ()
273
266
  self._prefetch_done = False
274
- self._known_related_objects = {} # {rel_field: {pk: rel_obj}}
267
+ self._known_related_objects = {} # {rel_field: {id: rel_obj}}
275
268
  self._iterable_class = ModelIterable
276
269
  self._fields = None
277
270
  self._defer_next_filter = False
@@ -432,12 +425,12 @@ class QuerySet:
432
425
  query = (
433
426
  self
434
427
  if self.query.can_filter()
435
- else self.model._base_manager.filter(pk__in=self.values("pk"))
428
+ else self.model._base_manager.filter(id__in=self.values("id"))
436
429
  )
437
430
  combined = query._chain()
438
431
  combined._merge_known_related_objects(other)
439
432
  if not other.query.can_filter():
440
- other = other.model._base_manager.filter(pk__in=other.values("pk"))
433
+ other = other.model._base_manager.filter(id__in=other.values("id"))
441
434
  combined.query.combine(other.query, sql.OR)
442
435
  return combined
443
436
 
@@ -451,12 +444,12 @@ class QuerySet:
451
444
  query = (
452
445
  self
453
446
  if self.query.can_filter()
454
- else self.model._base_manager.filter(pk__in=self.values("pk"))
447
+ else self.model._base_manager.filter(id__in=self.values("id"))
455
448
  )
456
449
  combined = query._chain()
457
450
  combined._merge_known_related_objects(other)
458
451
  if not other.query.can_filter():
459
- other = other.model._base_manager.filter(pk__in=other.values("pk"))
452
+ other = other.model._base_manager.filter(id__in=other.values("id"))
460
453
  combined.query.combine(other.query, sql.XOR)
461
454
  return combined
462
455
 
@@ -591,10 +584,11 @@ class QuerySet:
591
584
  return obj
592
585
 
593
586
  def _prepare_for_bulk_create(self, objs):
587
+ id_field = self.model._meta.get_field("id")
594
588
  for obj in objs:
595
- if obj.pk is None:
596
- # Populate new PK values.
597
- obj.pk = obj._meta.pk.get_pk_value_on_save(obj)
589
+ if obj.id is None:
590
+ # Populate new primary key values.
591
+ obj.id = id_field.get_id_value_on_save(obj)
598
592
  obj._prepare_related_fields_for_save(operation_name="bulk_create")
599
593
 
600
594
  def _check_bulk_create_options(
@@ -673,11 +667,7 @@ class QuerySet:
673
667
  return objs
674
668
  opts = self.model._meta
675
669
  if unique_fields:
676
- # Primary key is allowed in unique_fields.
677
- unique_fields = [
678
- self.model._meta.get_field(opts.pk.name if name == "pk" else name)
679
- for name in unique_fields
680
- ]
670
+ unique_fields = [self.model._meta.get_field(name) for name in unique_fields]
681
671
  if update_fields:
682
672
  update_fields = [self.model._meta.get_field(name) for name in update_fields]
683
673
  on_conflict = self._check_bulk_create_options(
@@ -690,26 +680,27 @@ class QuerySet:
690
680
  objs = list(objs)
691
681
  self._prepare_for_bulk_create(objs)
692
682
  with transaction.atomic(savepoint=False):
693
- objs_with_pk, objs_without_pk = partition(lambda o: o.pk is None, objs)
694
- if objs_with_pk:
683
+ objs_with_id, objs_without_id = partition(lambda o: o.id is None, objs)
684
+ if objs_with_id:
695
685
  returned_columns = self._batched_insert(
696
- objs_with_pk,
686
+ objs_with_id,
697
687
  fields,
698
688
  batch_size,
699
689
  on_conflict=on_conflict,
700
690
  update_fields=update_fields,
701
691
  unique_fields=unique_fields,
702
692
  )
703
- for obj_with_pk, results in zip(objs_with_pk, returned_columns):
693
+ id_field = opts.get_field("id")
694
+ for obj_with_id, results in zip(objs_with_id, returned_columns):
704
695
  for result, field in zip(results, opts.db_returning_fields):
705
- if field != opts.pk:
706
- setattr(obj_with_pk, field.attname, result)
707
- for obj_with_pk in objs_with_pk:
708
- obj_with_pk._state.adding = False
709
- if objs_without_pk:
710
- fields = [f for f in fields if not isinstance(f, AutoField)]
696
+ if field != id_field:
697
+ setattr(obj_with_id, field.attname, result)
698
+ for obj_with_id in objs_with_id:
699
+ obj_with_id._state.adding = False
700
+ if objs_without_id:
701
+ fields = [f for f in fields if not isinstance(f, PrimaryKeyField)]
711
702
  returned_columns = self._batched_insert(
712
- objs_without_pk,
703
+ objs_without_id,
713
704
  fields,
714
705
  batch_size,
715
706
  on_conflict=on_conflict,
@@ -720,11 +711,11 @@ class QuerySet:
720
711
  db_connection.features.can_return_rows_from_bulk_insert
721
712
  and on_conflict is None
722
713
  ):
723
- assert len(returned_columns) == len(objs_without_pk)
724
- for obj_without_pk, results in zip(objs_without_pk, returned_columns):
714
+ assert len(returned_columns) == len(objs_without_id)
715
+ for obj_without_id, results in zip(objs_without_id, returned_columns):
725
716
  for result, field in zip(results, opts.db_returning_fields):
726
- setattr(obj_without_pk, field.attname, result)
727
- obj_without_pk._state.adding = False
717
+ setattr(obj_without_id, field.attname, result)
718
+ obj_without_id._state.adding = False
728
719
 
729
720
  return objs
730
721
 
@@ -737,7 +728,7 @@ class QuerySet:
737
728
  if not fields:
738
729
  raise ValueError("Field names must be given to bulk_update().")
739
730
  objs = tuple(objs)
740
- if any(obj.pk is None for obj in objs):
731
+ if any(obj.id is None for obj in objs):
741
732
  raise ValueError("All bulk_update() objects must have a primary key set.")
742
733
  fields = [self.model._meta.get_field(name) for name in fields]
743
734
  if any(not f.concrete or f.many_to_many for f in fields):
@@ -753,7 +744,7 @@ class QuerySet:
753
744
  # PK is used twice in the resulting update query, once in the filter
754
745
  # and once in the WHEN. Each field will also have one CAST.
755
746
  self._for_write = True
756
- max_batch_size = db_connection.ops.bulk_batch_size(["pk", "pk"] + fields, objs)
747
+ max_batch_size = db_connection.ops.bulk_batch_size(["id", "id"] + fields, objs)
757
748
  batch_size = min(batch_size, max_batch_size) if batch_size else max_batch_size
758
749
  requires_casting = db_connection.features.requires_casted_case_in_updates
759
750
  batches = (objs[i : i + batch_size] for i in range(0, len(objs), batch_size))
@@ -766,17 +757,17 @@ class QuerySet:
766
757
  attr = getattr(obj, field.attname)
767
758
  if not hasattr(attr, "resolve_expression"):
768
759
  attr = Value(attr, output_field=field)
769
- when_statements.append(When(pk=obj.pk, then=attr))
760
+ when_statements.append(When(id=obj.id, then=attr))
770
761
  case_statement = Case(*when_statements, output_field=field)
771
762
  if requires_casting:
772
763
  case_statement = Cast(case_statement, output_field=field)
773
764
  update_kwargs[field.attname] = case_statement
774
- updates.append(([obj.pk for obj in batch_objs], update_kwargs))
765
+ updates.append(([obj.id for obj in batch_objs], update_kwargs))
775
766
  rows_updated = 0
776
767
  queryset = self._chain()
777
768
  with transaction.atomic(savepoint=False):
778
- for pks, update_kwargs in updates:
779
- rows_updated += queryset.filter(pk__in=pks).update(**update_kwargs)
769
+ for ids, update_kwargs in updates:
770
+ rows_updated += queryset.filter(id__in=ids).update(**update_kwargs)
780
771
  return rows_updated
781
772
 
782
773
  def get_or_create(self, defaults=None, **kwargs):
@@ -924,7 +915,7 @@ class QuerySet:
924
915
  queryset = self
925
916
  else:
926
917
  self._check_ordering_first_last_queryset_aggregation(method="first")
927
- queryset = self.order_by("pk")
918
+ queryset = self.order_by("id")
928
919
  for obj in queryset[:1]:
929
920
  return obj
930
921
 
@@ -934,11 +925,11 @@ class QuerySet:
934
925
  queryset = self.reverse()
935
926
  else:
936
927
  self._check_ordering_first_last_queryset_aggregation(method="last")
937
- queryset = self.order_by("-pk")
928
+ queryset = self.order_by("-id")
938
929
  for obj in queryset[:1]:
939
930
  return obj
940
931
 
941
- def in_bulk(self, id_list=None, *, field_name="pk"):
932
+ def in_bulk(self, id_list=None, *, field_name="id"):
942
933
  """
943
934
  Return a dictionary mapping each of the given IDs to the object with
944
935
  that ID. If `id_list` isn't provided, evaluate the entire QuerySet.
@@ -952,7 +943,7 @@ class QuerySet:
952
943
  if len(constraint.fields) == 1
953
944
  ]
954
945
  if (
955
- field_name != "pk"
946
+ field_name != "id"
956
947
  and not opts.get_field(field_name).primary_key
957
948
  and field_name not in unique_fields
958
949
  and self.query.distinct_fields != (field_name,)
@@ -1106,11 +1097,11 @@ class QuerySet:
1106
1097
  return False
1107
1098
  except AttributeError:
1108
1099
  raise TypeError("'obj' must be a model instance.")
1109
- if obj.pk is None:
1100
+ if obj.id is None:
1110
1101
  raise ValueError("QuerySet.contains() cannot be used on unsaved objects.")
1111
1102
  if self._result_cache is not None:
1112
1103
  return obj in self._result_cache
1113
- return self.filter(pk=obj.pk).exists()
1104
+ return self.filter(id=obj.id).exists()
1114
1105
 
1115
1106
  def _prefetch_related_objects(self):
1116
1107
  # This method can only be called once the result cache has been filled.
@@ -1774,7 +1765,8 @@ class QuerySet:
1774
1765
 
1775
1766
  def _check_ordering_first_last_queryset_aggregation(self, method):
1776
1767
  if isinstance(self.query.group_by, tuple) and not any(
1777
- col.output_field is self.model._meta.pk for col in self.query.group_by
1768
+ col.output_field is self.model._meta.get_field("id")
1769
+ for col in self.query.group_by
1778
1770
  ):
1779
1771
  raise TypeError(
1780
1772
  f"Cannot use QuerySet.{method}() on an unordered queryset performing "
@@ -2337,7 +2329,7 @@ class RelatedPopulator:
2337
2329
  # we have to reorder the parent data. The reorder_for_init
2338
2330
  # attribute contains a function used to reorder the field data
2339
2331
  # in the order __init__ expects it.
2340
- # - pk_idx: the index of the primary key field in the reordered
2332
+ # - id_idx: the index of the primary key field in the reordered
2341
2333
  # model data. Used to check if a related object exists at all.
2342
2334
  # - init_list: the field attnames fetched from the database. For
2343
2335
  # deferred models this isn't the same as all attnames of the
@@ -2357,7 +2349,7 @@ class RelatedPopulator:
2357
2349
  self.reorder_for_init = None
2358
2350
 
2359
2351
  self.model_cls = klass_info["model"]
2360
- self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
2352
+ self.id_idx = self.init_list.index("id")
2361
2353
  self.related_populators = get_related_populators(klass_info, select)
2362
2354
  self.local_setter = klass_info["local_setter"]
2363
2355
  self.remote_setter = klass_info["remote_setter"]
@@ -2367,7 +2359,7 @@ class RelatedPopulator:
2367
2359
  obj_data = self.reorder_for_init(row)
2368
2360
  else:
2369
2361
  obj_data = row[self.cols_start : self.cols_end]
2370
- if obj_data[self.pk_idx] is None:
2362
+ if obj_data[self.id_idx] is None:
2371
2363
  obj = None
2372
2364
  else:
2373
2365
  obj = self.model_cls.from_db(self.init_list, obj_data)
@@ -313,8 +313,6 @@ def select_related_descend(field, restricted, requested, select_mask, reverse=Fa
313
313
  """
314
314
  if not field.remote_field:
315
315
  return False
316
- if field.remote_field.parent_link and not reverse:
317
- return False
318
316
  if restricted:
319
317
  if reverse and field.related_query_name() not in requested:
320
318
  return False
@@ -363,11 +361,11 @@ def check_rel_lookup_compatibility(model, target_opts, field):
363
361
  # model is ok, too. Consider the case:
364
362
  # class Restaurant(models.Model):
365
363
  # place = OneToOneField(Place, primary_key=True):
366
- # Restaurant.objects.filter(pk__in=Restaurant.objects.all()).
367
- # If we didn't have the primary key check, then pk__in (== place__in) would
364
+ # Restaurant.objects.filter(id__in=Restaurant.objects.all()).
365
+ # If we didn't have the primary key check, then id__in (== place__in) would
368
366
  # give Place's opts as the target opts, but Restaurant isn't compatible
369
367
  # with that. This logic applies only to primary keys, as when doing __in=qs,
370
- # we are going to turn this into __in=qs.values('pk') later on.
368
+ # we are going to turn this into __in=qs.values('id') later on.
371
369
  return check(target_opts) or (
372
370
  getattr(field, "primary_key", False) and check(field.model._meta)
373
371
  )
@@ -107,12 +107,12 @@ class SQLCompiler:
107
107
  # SomeModel.objects.annotate(Count('somecol')).values('name')
108
108
  # GROUP BY: all cols of the model
109
109
  #
110
- # SomeModel.objects.values('name', 'pk')
111
- # .annotate(Count('somecol')).values('pk')
112
- # GROUP BY: name, pk
110
+ # SomeModel.objects.values('name', 'id')
111
+ # .annotate(Count('somecol')).values('id')
112
+ # GROUP BY: name, id
113
113
  #
114
- # SomeModel.objects.values('name').annotate(Count('somecol')).values('pk')
115
- # GROUP BY: name, pk
114
+ # SomeModel.objects.values('name').annotate(Count('somecol')).values('id')
115
+ # GROUP BY: name, id
116
116
  #
117
117
  # In fact, the self.query.group_by is the minimal set to GROUP BY. It
118
118
  # can't be ever restricted to a smaller set, but additional columns in
@@ -1000,14 +1000,13 @@ class SQLCompiler:
1000
1000
  ) = self._setup_joins(pieces, opts, alias)
1001
1001
 
1002
1002
  # If we get to this point and the field is a relation to another model,
1003
- # append the default ordering for that model unless it is the pk
1004
- # shortcut or the attribute name of the field that is specified or
1003
+ # append the default ordering for that model unless it is the
1004
+ # attribute name of the field that is specified or
1005
1005
  # there are transforms to process.
1006
1006
  if (
1007
1007
  field.is_relation
1008
1008
  and opts.ordering
1009
1009
  and getattr(field, "attname", None) != pieces[-1]
1010
- and name != "pk"
1011
1010
  and not getattr(transform_function, "has_transforms", False)
1012
1011
  ):
1013
1012
  # Firstly, avoid infinite loops.
@@ -1654,7 +1653,7 @@ class SQLInsertCompiler(SQLCompiler):
1654
1653
  on_conflict=self.query.on_conflict,
1655
1654
  )
1656
1655
  result = [f"{insert_statement} {qn(opts.db_table)}"]
1657
- fields = self.query.fields or [opts.pk]
1656
+ fields = self.query.fields or [opts.get_field("id")]
1658
1657
  result.append("({})".format(", ".join(qn(f.column) for f in fields)))
1659
1658
 
1660
1659
  if self.query.fields:
@@ -1757,7 +1756,7 @@ class SQLInsertCompiler(SQLCompiler):
1757
1756
  self.connection.ops.last_insert_id(
1758
1757
  cursor,
1759
1758
  opts.db_table,
1760
- opts.pk.column,
1759
+ opts.get_field("id").column,
1761
1760
  ),
1762
1761
  )
1763
1762
  ]
@@ -1813,15 +1812,15 @@ class SQLDeleteCompiler(SQLCompiler):
1813
1812
  innerq = self.query.clone()
1814
1813
  innerq.__class__ = Query
1815
1814
  innerq.clear_select_clause()
1816
- pk = self.query.model._meta.pk
1817
- innerq.select = [pk.get_col(self.query.get_initial_alias())]
1815
+ id_field = self.query.model._meta.get_field("id")
1816
+ innerq.select = [id_field.get_col(self.query.get_initial_alias())]
1818
1817
  outerq = Query(self.query.model)
1819
1818
  if not self.connection.features.update_can_self_select:
1820
1819
  # Force the materialization of the inner query to allow reference
1821
1820
  # to the target table on MySQL.
1822
1821
  sql, params = innerq.get_compiler().as_sql()
1823
1822
  innerq = RawSQL(f"SELECT * FROM ({sql}) subquery", params)
1824
- outerq.add_filter("pk__in", innerq)
1823
+ outerq.add_filter("id__in", innerq)
1825
1824
  return self._as_sql(outerq)
1826
1825
 
1827
1826
 
@@ -1930,12 +1929,11 @@ class SQLUpdateCompiler(SQLCompiler):
1930
1929
  query.clear_ordering(force=True)
1931
1930
  query.extra = {}
1932
1931
  query.select = []
1933
- meta = query.get_meta()
1934
- fields = [meta.pk.name]
1932
+ fields = ["id"]
1935
1933
  related_ids_index = []
1936
1934
  for related in self.query.related_updates:
1937
1935
  # If a primary key chain exists to the targeted related update,
1938
- # then the meta.pk value can be used for it.
1936
+ # then the primary key value can be used for it.
1939
1937
  related_ids_index.append((related, 0))
1940
1938
 
1941
1939
  query.add_fields(fields)
@@ -1958,11 +1956,11 @@ class SQLUpdateCompiler(SQLCompiler):
1958
1956
  idents.extend(r[0] for r in rows)
1959
1957
  for parent, index in related_ids_index:
1960
1958
  related_ids[parent].extend(r[index] for r in rows)
1961
- self.query.add_filter("pk__in", idents)
1959
+ self.query.add_filter("id__in", idents)
1962
1960
  self.query.related_ids = related_ids
1963
1961
  else:
1964
1962
  # The fast path. Filters and updates in one query.
1965
- self.query.add_filter("pk__in", query)
1963
+ self.query.add_filter("id__in", query)
1966
1964
  self.query.reset_refcounts(refcounts_before)
1967
1965
 
1968
1966
 
plain/models/sql/query.py CHANGED
@@ -438,7 +438,9 @@ class Query(BaseExpression):
438
438
  # used.
439
439
  if inner_query.default_cols and has_existing_aggregation:
440
440
  inner_query.group_by = (
441
- self.model._meta.pk.get_col(inner_query.get_initial_alias()),
441
+ self.model._meta.get_field("id").get_col(
442
+ inner_query.get_initial_alias()
443
+ ),
442
444
  )
443
445
  inner_query.default_cols = False
444
446
  if not qualify:
@@ -480,7 +482,9 @@ class Query(BaseExpression):
480
482
  # field selected in the inner query, yet we must use a subquery.
481
483
  # So, make sure at least one field is selected.
482
484
  inner_query.select = (
483
- self.model._meta.pk.get_col(inner_query.get_initial_alias()),
485
+ self.model._meta.get_field("id").get_col(
486
+ inner_query.get_initial_alias()
487
+ ),
484
488
  )
485
489
  else:
486
490
  outer_query = self
@@ -689,7 +693,7 @@ class Query(BaseExpression):
689
693
  def _get_defer_select_mask(self, opts, mask, select_mask=None):
690
694
  if select_mask is None:
691
695
  select_mask = {}
692
- select_mask[opts.pk] = {}
696
+ select_mask[opts.get_field("id")] = {}
693
697
  # All concrete fields that are not part of the defer mask must be
694
698
  # loaded. If a relational field is encountered it gets added to the
695
699
  # mask for it be considered if `select_related` and the cycle continues
@@ -726,7 +730,7 @@ class Query(BaseExpression):
726
730
  def _get_only_select_mask(self, opts, mask, select_mask=None):
727
731
  if select_mask is None:
728
732
  select_mask = {}
729
- select_mask[opts.pk] = {}
733
+ select_mask[opts.get_field("id")] = {}
730
734
  # Only include fields mentioned in the mask.
731
735
  for field_name, field_mask in mask.items():
732
736
  field = opts.get_field(field_name)
@@ -1567,8 +1571,6 @@ class Query(BaseExpression):
1567
1571
  path, names_with_path = [], []
1568
1572
  for pos, name in enumerate(names):
1569
1573
  cur_names_with_path = (name, [])
1570
- if name == "pk":
1571
- name = opts.pk.name
1572
1574
 
1573
1575
  field = None
1574
1576
  filtered_relation = None
@@ -1917,14 +1919,16 @@ class Query(BaseExpression):
1917
1919
  select_field = col.target
1918
1920
  alias = col.alias
1919
1921
  if alias in can_reuse:
1920
- pk = select_field.model._meta.pk
1922
+ id_field = select_field.model._meta.get_field("id")
1921
1923
  # Need to add a restriction so that outer query's filters are in effect for
1922
1924
  # the subquery, too.
1923
1925
  query.bump_prefix(self)
1924
1926
  lookup_class = select_field.get_lookup("exact")
1925
1927
  # Note that the query.select[0].alias is different from alias
1926
1928
  # due to bump_prefix above.
1927
- lookup = lookup_class(pk.get_col(query.select[0].alias), pk.get_col(alias))
1929
+ lookup = lookup_class(
1930
+ id_field.get_col(query.select[0].alias), id_field.get_col(alias)
1931
+ )
1928
1932
  query.where.add(lookup, AND)
1929
1933
  query.external_aliases[alias] = True
1930
1934
 
@@ -2257,9 +2261,6 @@ class Query(BaseExpression):
2257
2261
  """
2258
2262
  existing, defer = self.deferred_loading
2259
2263
  field_names = set(field_names)
2260
- if "pk" in field_names:
2261
- field_names.remove("pk")
2262
- field_names.add(self.get_meta().pk.name)
2263
2264
 
2264
2265
  if defer:
2265
2266
  # Remove any existing deferred names from the current set before
@@ -23,21 +23,21 @@ class DeleteQuery(Query):
23
23
  return cursor.rowcount
24
24
  return 0
25
25
 
26
- def delete_batch(self, pk_list):
26
+ def delete_batch(self, id_list):
27
27
  """
28
- Set up and execute delete queries for all the objects in pk_list.
28
+ Set up and execute delete queries for all the objects in id_list.
29
29
 
30
30
  More than one physical query may be executed if there are a
31
- lot of values in pk_list.
31
+ lot of values in id_list.
32
32
  """
33
33
  # number of objects deleted
34
34
  num_deleted = 0
35
- field = self.get_meta().pk
36
- for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
35
+ field = self.get_meta().get_field("id")
36
+ for offset in range(0, len(id_list), GET_ITERATOR_CHUNK_SIZE):
37
37
  self.clear_where()
38
38
  self.add_filter(
39
39
  f"{field.attname}__in",
40
- pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE],
40
+ id_list[offset : offset + GET_ITERATOR_CHUNK_SIZE],
41
41
  )
42
42
  num_deleted += self.do_query(self.get_meta().db_table, self.where)
43
43
  return num_deleted
@@ -66,12 +66,12 @@ class UpdateQuery(Query):
66
66
  obj.related_updates = self.related_updates.copy()
67
67
  return obj
68
68
 
69
- def update_batch(self, pk_list, values):
69
+ def update_batch(self, id_list, values):
70
70
  self.add_update_values(values)
71
- for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
71
+ for offset in range(0, len(id_list), GET_ITERATOR_CHUNK_SIZE):
72
72
  self.clear_where()
73
73
  self.add_filter(
74
- "pk__in", pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]
74
+ "id__in", id_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]
75
75
  )
76
76
  self.get_compiler().execute_sql(NO_RESULTS)
77
77
 
@@ -132,7 +132,7 @@ class UpdateQuery(Query):
132
132
  query = UpdateQuery(model)
133
133
  query.values = values
134
134
  if self.related_ids is not None:
135
- query.add_filter("pk__in", self.related_ids[model])
135
+ query.add_filter("id__in", self.related_ids[model])
136
136
  result.append(query)
137
137
  return result
138
138
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.38.0
3
+ Version: 0.39.1
4
4
  Summary: Database models for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -30,6 +30,9 @@ class User(models.Model):
30
30
  return self.email
31
31
  ```
32
32
 
33
+ Every model automatically includes an `id` field which serves as the primary
34
+ key. The name `id` is reserved and can't be used for other fields.
35
+
33
36
  Create, update, and delete instances of your models:
34
37
 
35
38
  ```python
@@ -114,3 +117,31 @@ TODO
114
117
  ## Forms
115
118
 
116
119
  TODO
120
+
121
+ ## Sharing fields across models
122
+
123
+ To share common fields across multiple models, use Python classes as mixins. The final, registered model must inherit directly from `models.Model` and the mixins should not.
124
+
125
+ ```python
126
+ from plain import models
127
+
128
+
129
+ # Regular Python class for shared fields
130
+ class TimestampedMixin:
131
+ created_at = models.DateTimeField(auto_now_add=True)
132
+ updated_at = models.DateTimeField(auto_now=True)
133
+
134
+
135
+ # Models inherit from the mixin AND models.Model
136
+ @models.register_model
137
+ class User(TimestampedMixin, models.Model):
138
+ email = models.EmailField()
139
+ password = PasswordField()
140
+ is_admin = models.BooleanField(default=False)
141
+
142
+
143
+ @models.register_model
144
+ class Note(TimestampedMixin, models.Model):
145
+ content = models.TextField(max_length=1024)
146
+ liked = models.BooleanField(default=False)
147
+ ```