Skip to content

Commit 3a6229c

Browse files
committed
Finalize CompositePrimaryKey support on PostgresPartitionedModel
1 parent b450c69 commit 3a6229c

File tree

2 files changed

+82
-18
lines changed

2 files changed

+82
-18
lines changed

psqlextra/models/partitioned.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from typing import Iterable, List, Optional, Tuple
22

3+
import django
4+
35
from django.core.exceptions import ImproperlyConfigured
46
from django.db import models
57
from django.db.models.base import ModelBase
6-
from django.db.models.fields.composite import CompositePrimaryKey
78
from django.db.models.options import Options
89

910
from 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

162226
class PostgresPartitionedModel(
163227
PostgresModel, metaclass=PostgresPartitionedModelMeta

tests/test_partitioned_model.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ def test_partitioned_model_key_option_none():
8585
def test_partitioned_model_custom_composite_primary_key_with_auto_field():
8686
model = define_fake_partitioned_model(
8787
fields={
88-
"auto_id": models.AutoField(),
88+
"auto_id": models.AutoField(primary_key=True),
8989
"my_custom_pk": models.CompositePrimaryKey("auto_id", "timestamp"),
9090
"timestamp": models.DateTimeField(),
9191
},
92-
partitioning_options=dict(key=["timestamp"], special=True),
92+
partitioning_options=dict(key=["timestamp"]),
9393
)
9494

9595
assert isinstance(model._meta.pk, models.CompositePrimaryKey)
@@ -108,7 +108,7 @@ def test_partitioned_model_custom_composite_primary_key_with_id_field():
108108
"my_custom_pk": models.CompositePrimaryKey("id", "timestamp"),
109109
"timestamp": models.DateTimeField(),
110110
},
111-
partitioning_options=dict(key=["timestamp"], special=True),
111+
partitioning_options=dict(key=["timestamp"]),
112112
)
113113

114114
assert isinstance(model._meta.pk, models.CompositePrimaryKey)
@@ -127,7 +127,7 @@ def test_partitioned_model_custom_composite_primary_key_named_id():
127127
"id": models.CompositePrimaryKey("other_field", "timestamp"),
128128
"timestamp": models.DateTimeField(),
129129
},
130-
partitioning_options=dict(key=["timestamp"], special=True),
130+
partitioning_options=dict(key=["timestamp"]),
131131
)
132132

133133
assert isinstance(model._meta.pk, models.CompositePrimaryKey)
@@ -147,7 +147,7 @@ def test_partitioned_model_field_named_pk_not_composite_not_primary():
147147
"id": models.CompositePrimaryKey("other_field", "timestamp"),
148148
"timestamp": models.DateTimeField(),
149149
},
150-
partitioning_options=dict(key=["timestamp"], special=True),
150+
partitioning_options=dict(key=["timestamp"]),
151151
)
152152

153153

@@ -162,7 +162,7 @@ def test_partitioned_model_field_named_pk_not_composite():
162162
"pk": models.AutoField(primary_key=True),
163163
"timestamp": models.DateTimeField(),
164164
},
165-
partitioning_options=dict(key=["timestamp"], special=True),
165+
partitioning_options=dict(key=["timestamp"]),
166166
)
167167

168168

@@ -179,7 +179,7 @@ def test_partitioned_model_field_multiple_pks():
179179
"timestamp": models.DateTimeField(),
180180
"real_pk": models.CompositePrimaryKey("id", "timestamp"),
181181
},
182-
partitioning_options=dict(key=["timestamp"], special=True),
182+
partitioning_options=dict(key=["timestamp"]),
183183
)
184184

185185

@@ -192,7 +192,7 @@ def test_partitioned_model_no_pk_defined():
192192
fields={
193193
"timestamp": models.DateTimeField(),
194194
},
195-
partitioning_options=dict(key=["timestamp"], special=True),
195+
partitioning_options=dict(key=["timestamp"]),
196196
)
197197

198198
assert isinstance(model._meta.pk, models.CompositePrimaryKey)
@@ -203,7 +203,7 @@ def test_partitioned_model_no_pk_defined():
203203
assert id_field.name == "id"
204204
assert id_field.column == "id"
205205
assert isinstance(id_field, models.AutoField)
206-
assert id_field.primary_key is True
206+
assert id_field.primary_key is False
207207

208208

209209
@pytest.mark.skipif(
@@ -217,7 +217,7 @@ def test_partitioned_model_composite_primary_key():
217217
"pk": models.CompositePrimaryKey("id", "timestamp"),
218218
"timestamp": models.DateTimeField(),
219219
},
220-
partitioning_options=dict(key=["timestamp"], special=True),
220+
partitioning_options=dict(key=["timestamp"]),
221221
)
222222

223223
assert isinstance(model._meta.pk, models.CompositePrimaryKey)
@@ -234,7 +234,7 @@ def test_partitioned_model_composite_primary_key_foreign_key():
234234
fields={
235235
"timestamp": models.DateTimeField(),
236236
},
237-
partitioning_options=dict(key=["timestamp"], special=True),
237+
partitioning_options=dict(key=["timestamp"]),
238238
)
239239

240240
define_fake_model(
@@ -255,7 +255,7 @@ def test_partitioned_model_custom_composite_primary_key_foreign_key():
255255
"timestamp": models.DateTimeField(),
256256
"custom": models.CompositePrimaryKey("id", "timestamp"),
257257
},
258-
partitioning_options=dict(key=["timestamp"], special=True),
258+
partitioning_options=dict(key=["timestamp"]),
259259
)
260260

261261
define_fake_model(

0 commit comments

Comments
 (0)