11from typing import Iterable , List , Optional , Tuple
22
3+ import django
4+
35from django .core .exceptions import ImproperlyConfigured
46from django .db import models
57from django .db .models .base import ModelBase
6- from django .db .models .fields .composite import CompositePrimaryKey
78from django .db .models .options import Options
89
910from psqlextra .types import PostgresPartitioningMethod
@@ -28,9 +29,11 @@ def __new__(cls, name, bases, attrs, **kwargs):
2829
2930 partitioning_method = getattr (partitioning_meta_class , "method" , None )
3031 partitioning_key = getattr (partitioning_meta_class , "key" , None )
31- special = getattr (partitioning_meta_class , "special" , None )
3232
33- if special :
33+ if django .VERSION >= (5 , 2 ):
34+ for base in bases :
35+ cls ._delete_auto_created_fields (base )
36+
3437 cls ._create_primary_key (attrs , partitioning_key )
3538
3639 patitioning_meta = PostgresPartitionedModelOptions (
@@ -43,21 +46,57 @@ def __new__(cls, name, bases, attrs, **kwargs):
4346 return new_class
4447
4548 @classmethod
46- def _create_primary_key (cls , attrs , partitioning_key : Optional [List [str ]]):
49+ def _create_primary_key (
50+ cls , attrs , partitioning_key : Optional [List [str ]]
51+ ) -> None :
52+ from django .db .models .fields .composite import CompositePrimaryKey
53+
54+ # Find any existing primary key the user might have declared.
55+ #
56+ # If it is a composite primary key, we will do nothing and
57+ # keep it as it is. You're own your own.
4758 pk = cls ._find_primary_key (attrs )
4859 if pk and isinstance (pk [1 ], CompositePrimaryKey ):
4960 return
5061
62+ # Create an `id` field (auto-incrementing) if there is no
63+ # primary key yet.
64+ #
65+ # This matches standard Django behavior.
5166 if not pk :
5267 attrs ["id" ] = attrs .get ("id" ) or cls ._create_auto_field (attrs )
5368 pk_fields = ["id" ]
5469 else :
5570 pk_fields = [pk [0 ]]
5671
57- unique_pk_fields = set (pk_fields + (partitioning_key or []))
72+ partitioning_keys = (
73+ partitioning_key
74+ if isinstance (partitioning_key , list )
75+ else list (filter (None , [partitioning_key ]))
76+ )
77+
78+ unique_pk_fields = set (pk_fields + (partitioning_keys or []))
5879 if len (unique_pk_fields ) <= 1 :
80+ if "id" in attrs :
81+ attrs ["id" ].primary_key = True
5982 return
6083
84+ # You might have done something like this:
85+ #
86+ # id = models.AutoField(primary_key=True)
87+ # pk = CompositePrimaryKey("id", "timestamp")
88+ #
89+ # The `primary_key` attribute has to be removed
90+ # from the `id` field in the example above to
91+ # avoid having two primary keys.
92+ #
93+ # Without this, the generated schema will
94+ # have two primary keys, which is an error.
95+ for field in attrs .values ():
96+ is_pk = getattr (field , "primary_key" , False )
97+ if is_pk :
98+ field .primary_key = False
99+
61100 auto_generated_pk = CompositePrimaryKey (* sorted (unique_pk_fields ))
62101 attrs ["pk" ] = auto_generated_pk
63102
@@ -67,7 +106,7 @@ def _create_auto_field(cls, attrs):
67106 meta_class = attrs .get ("Meta" , None )
68107
69108 pk_class = Options (meta_class , app_label )._get_default_pk_class ()
70- return pk_class (verbose_name = "ID" , primary_key = True , auto_created = True )
109+ return pk_class (verbose_name = "ID" , auto_created = True )
71110
72111 @classmethod
73112 def _find_primary_key (cls , attrs ) -> Optional [Tuple [str , models .Field ]]:
@@ -101,6 +140,8 @@ def _find_primary_key(cls, attrs) -> Optional[Tuple[str, models.Field]]:
101140 3. There is no primary key.
102141 """
103142
143+ from django .db .models .fields .composite import CompositePrimaryKey
144+
104145 fields = {
105146 name : value
106147 for name , value in attrs .items ()
@@ -158,6 +199,29 @@ def _find_primary_key(cls, attrs) -> Optional[Tuple[str, models.Field]]:
158199
159200 return sorted_fields_marked_as_pk [0 ]
160201
202+ @classmethod
203+ def _delete_auto_created_fields (cls , model : models .Model ):
204+ """Base classes might be injecting an auto-generated `id` field before
205+ we even have the chance of doing this ourselves.
206+
207+ Delete any auto generated fields from the base class so that we
208+ can declare our own. If there is no auto-generated field, one
209+ will be added anyways by our own logic
210+ """
211+
212+ fields = model ._meta .local_fields + model ._meta .local_many_to_many
213+ for field in fields :
214+ auto_created = getattr (field , "auto_created" , False )
215+ if auto_created :
216+ if field in model ._meta .local_fields :
217+ model ._meta .local_fields .remove (field )
218+
219+ if field in model ._meta .fields :
220+ model ._meta .fields .remove (field ) # type: ignore [attr-defined]
221+
222+ if hasattr (model , field .name ):
223+ delattr (model , field .name )
224+
161225
162226class PostgresPartitionedModel (
163227 PostgresModel , metaclass = PostgresPartitionedModelMeta
0 commit comments