Skip to content

Commit 83b48aa

Browse files
committed
fix: issues caused by #620 change of polymorphic_primary_key_name
1 parent 0221c82 commit 83b48aa

4 files changed

Lines changed: 185 additions & 20 deletions

File tree

src/polymorphic/query.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from collections import defaultdict
99

1010
from django.contrib.contenttypes.models import ContentType
11-
from django.core.exceptions import FieldDoesNotExist
11+
from django.core.exceptions import FieldDoesNotExist, FieldError
1212
from django.db import connections, models
1313
from django.db.models import FilteredRelation, Manager
1414
from django.db.models.constants import LOOKUP_SEP
@@ -240,8 +240,19 @@ def pivot_onto_cached_subclass(self, from_obj, obj, model_target_cls):
240240
self.remote_setter(obj, from_obj)
241241
return None, None
242242

243-
pk_name = self.model_cls.polymorphic_primary_key_name
244-
return model_target_cls, (getattr(original, pk_name), self.field.name)
243+
local_pk_name = original.__class__.polymorphic_primary_key_name
244+
target_pk_name = original.__class__.polymorphic_primary_key_name
245+
original_pk = getattr(original, local_pk_name)
246+
247+
# NOTE: We could use a recursive function on model_target_cls._meta.parents
248+
# PolymorphicModel.much _get_inheritance_relation_fields_and_models.like add_all_sub_models
249+
for field in model_target_cls._meta.fields:
250+
if field.is_relation is True:
251+
for rel_field in field.foreign_related_fields:
252+
if rel_field.name is local_pk_name and rel_field.model is original._meta.model:
253+
target_pk_name = field.attname
254+
255+
return model_target_cls, (original_pk, self.field.name, target_pk_name)
245256

246257

247258
def get_related_populators(klass_info, select, db):
@@ -409,8 +420,8 @@ def fetch_polymorphic(self, post_actions, base_result_objects):
409420
for action, populate_fn in post_actions:
410421
target_class, pk_info = action()
411422
if target_class:
412-
pk, name = pk_info
413-
idlist_per_model[target_class].append((pk, name))
423+
pk, name, pk_name = pk_info
424+
idlist_per_model[target_class].append(pk_info)
414425
update_fn_per_model[target_class].append((populate_fn, pk))
415426

416427
# For each model in "idlist_per_model" request its objects (the real model)
@@ -419,16 +430,26 @@ def fetch_polymorphic(self, post_actions, base_result_objects):
419430
# Then we copy the extra() select fields from the base objects to the real objects.
420431
# TODO: defer(), only(): support for these would be around here
421432
for real_concrete_class, data in idlist_per_model.items():
422-
idlist, names = zip(*data)
433+
idlist, names, pk_attr_names = zip(*data)
423434
updates = update_fn_per_model[real_concrete_class]
424-
pk_name = real_concrete_class.polymorphic_primary_key_name
435+
436+
if len(set(pk_attr_names)) != 1:
437+
raise FieldError(
438+
"PolymorphicModel: cannot convert model type as non "
439+
f"upk_namesnique related key names {pk_attr_names}"
440+
)
441+
442+
pk_attr_name = pk_attr_names[0]
443+
# FIXME: this seams to get extra field already fetch in base
444+
# initial query, we may need to add defer?
445+
425446
real_objects = real_concrete_class._base_objects.db_manager(self.queryset.db).filter(
426-
**{("%s__in" % pk_name): idlist}
447+
**{("%s__in" % pk_attr_name): idlist},
427448
)
428449

429450
real_objects = self.apply_select_related(real_objects, set(names))
430451
real_objects_dict = {
431-
getattr(real_object, pk_name): real_object for real_object in real_objects
452+
getattr(real_object, pk_attr_name): real_object for real_object in real_objects
432453
}
433454

434455
for populate_fn, o_pk in updates:

src/polymorphic/tests/migrations/0001_initial.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2 on 2025-12-23 09:40
1+
# Generated by Django 4.2 on 2025-12-23 08:21
22

33
from django.conf import settings
44
from django.db import migrations, models
@@ -1433,6 +1433,30 @@ class Migration(migrations.Migration):
14331433
},
14341434
bases=('tests.mrobase2', 'tests.mrobase3'),
14351435
),
1436+
migrations.CreateModel(
1437+
name='NonAutoPKChild',
1438+
fields=[
1439+
('altchildmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tests.altchildmodel')),
1440+
('uuid_primary_key', models.UUIDField(default=uuid.uuid1, primary_key=True, serialize=False)),
1441+
],
1442+
options={
1443+
'abstract': False,
1444+
'base_manager_name': 'objects',
1445+
},
1446+
bases=('tests.altchildmodel',),
1447+
),
1448+
migrations.CreateModel(
1449+
name='NonUUIDArtProject',
1450+
fields=[
1451+
('uuidresearchproject_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tests.uuidresearchproject')),
1452+
('idkey', models.AutoField(primary_key=True, serialize=False)),
1453+
],
1454+
options={
1455+
'abstract': False,
1456+
'base_manager_name': 'objects',
1457+
},
1458+
bases=('tests.uuidresearchproject',),
1459+
),
14361460
migrations.CreateModel(
14371461
name='PlainC',
14381462
fields=[

src/polymorphic/tests/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ class UUIDResearchProject(UUIDProject):
346346
supervisor = models.CharField(max_length=30)
347347

348348

349+
class NonUUIDArtProject(UUIDResearchProject):
350+
idkey = models.AutoField(primary_key=True)
351+
352+
349353
class UUIDArtProjectA(UUIDArtProject): ...
350354

351355

@@ -833,6 +837,10 @@ class AltChildAsBaseModel(AltChildModel):
833837
more_name = models.CharField(max_length=10)
834838

835839

840+
class NonAutoPKChild(AltChildModel):
841+
uuid_primary_key = models.UUIDField(primary_key=True, default=uuid.uuid1)
842+
843+
836844
class PlainModel(models.Model):
837845
relation = models.ForeignKey(ParentModel, on_delete=models.CASCADE)
838846
objects = models.Manager.from_queryset(PolymorphicRelatedQuerySet)()

src/polymorphic/tests/test_orm.py

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,14 @@
7474
MultiTableDerived,
7575
MyManager,
7676
MyManagerQuerySet,
77+
NonAutoPKChild,
7778
NonPolymorphicParent,
7879
NonProxyChild,
7980
NonSymRelationA,
8081
NonSymRelationB,
8182
NonSymRelationBase,
8283
NonSymRelationBC,
84+
NonUUIDArtProject,
8385
One2OneRelatingModel,
8486
One2OneRelatingModelDerived,
8587
ParentModel,
@@ -112,7 +114,6 @@
112114
SubclassSelectorProxyBaseModel,
113115
SubclassSelectorProxyConcreteModel,
114116
ParentLinkAndRelatedName,
115-
TestParentLinkAndRelatedName,
116117
UUIDArtProject,
117118
UUIDArtProjectA,
118119
UUIDArtProjectB,
@@ -1997,38 +1998,144 @@ def test_infinite_recursion_with_only(self):
19971998
def test_normal_django_to_poly_related_give_poly_type(self):
19981999
obj1 = ParentModel.objects.create(name="m1")
19992000
obj2 = ChildModel.objects.create(name="m2", other_name="m2")
2000-
obj3 = ChildModel.objects.create(name="m1")
2001+
obj3 = ChildModel.objects.create(name="m3")
2002+
obj4 = ChildModel.objects.create(name="m3")
2003+
obj5 = AltChildModel.objects.create(name="m4")
20012004

20022005
PlainModel.objects.create(relation=obj1)
20032006
PlainModel.objects.create(relation=obj2)
20042007
PlainModel.objects.create(relation=obj3)
2008+
PlainModel.objects.create(relation=obj4)
2009+
PlainModel.objects.create(relation=obj5)
20052010

2006-
ContentType.objects.get_for_model(AltChildModel)
2007-
2008-
with self.assertNumQueries(6):
2011+
with self.assertNumQueries(10):
20092012
# Queries will be
20102013
# * 1 for All PlainModels object (1)
2011-
# * 1 for each relations ParentModel (4)
2012-
# * 1 for each relations ChilModel is needed (3)
2014+
# * 1 for each relations ParentModel (5)
2015+
# * 1 for each relations ChildModel is needed (3)
2016+
# * 1 for each relations AltChildModel is needed (1)
20132017
multi_q = [
20142018
# these obj.relation values will have their proper sub type
20152019
obj.relation
20162020
for obj in PlainModel.objects.all()
20172021
]
20182022
multi_q_types = [type(obj) for obj in multi_q]
20192023

2020-
with self.assertNumQueries(2):
2024+
with self.assertNumQueries(3):
20212025
grouped_q = [
20222026
# these obj.relation values will all be ParentModel's
20232027
# unless we fix select related but should be their proper
20242028
# sub type by using PolymorphicRelatedQuerySetMixin
2029+
# 1 query for each relation type
20252030
obj.relation
20262031
for obj in PlainModel.objects.select_related("relation")
20272032
]
20282033
grouped_q_types = [type(obj) for obj in grouped_q]
20292034

20302035
self.assertListEqual(multi_q_types, grouped_q_types)
2031-
self.assertListEqual(grouped_q, [obj1, obj2, obj3])
2036+
self.assertListEqual(grouped_q, [obj1, obj2, obj3, obj4, obj5])
2037+
2038+
def test_normal_django_to_multi_level_poly_related_give_poly_type(self):
2039+
obj1 = ParentModel.objects.create(name="m1")
2040+
obj2 = ChildModel.objects.create(name="m2", other_name="c1")
2041+
obj3 = AltChildModel.objects.create(name="m3")
2042+
obj4 = AltChildAsBaseModel.objects.create(name="m4", more_name="acab1")
2043+
2044+
PlainModel.objects.create(relation=obj1)
2045+
PlainModel.objects.create(relation=obj2)
2046+
PlainModel.objects.create(relation=obj3)
2047+
PlainModel.objects.create(relation=obj4)
2048+
2049+
with self.assertNumQueries(4):
2050+
grouped_q = [
2051+
# these obj.relation values will all be ParentModel's
2052+
# unless we fix select related but should be their proper
2053+
# sub type by using PolymorphicRelatedQuerySetMixin
2054+
obj.relation
2055+
for obj in PlainModel.objects.select_related("relation")
2056+
]
2057+
self.assertListEqual(grouped_q, [obj1, obj2, obj3, obj4])
2058+
2059+
def test_related_fetch_of_different_type_pks(self):
2060+
"pk on child is not same field type as pk on parent and thus prt field"
2061+
obj1 = ChildModel.objects.create(name="m1", other_name="c1")
2062+
obj2 = ParentModel.objects.create(name="m2")
2063+
obj3 = NonAutoPKChild.objects.create(name="m3", other_name="napk1")
2064+
obj4 = AltChildModel.objects.create(name="m4", other_name="acm1")
2065+
obj5 = ChildModel.objects.create(name="m5", other_name="c3")
2066+
obj6 = AltChildAsBaseModel.objects.create(name="m6", more_name="acab1")
2067+
obj7 = NonAutoPKChild.objects.create(name="m7", other_name="napk2")
2068+
2069+
PlainModel.objects.create(relation=obj1)
2070+
PlainModel.objects.create(relation=obj2)
2071+
PlainModel.objects.create(relation=obj3)
2072+
PlainModel.objects.create(relation=obj4)
2073+
PlainModel.objects.create(relation=obj5)
2074+
PlainModel.objects.create(relation=obj6)
2075+
PlainModel.objects.create(relation=obj7)
2076+
2077+
def object_info(obj):
2078+
return {
2079+
"pk": obj.pk,
2080+
"parentmodel_ptr": getattr(obj, "parentmodel_ptr_id", None),
2081+
"altchildmodel_ptr": getattr(obj, "altchildmodel_ptr_id", None),
2082+
}
2083+
2084+
with self.assertNumQueries(5):
2085+
grouped_q = [
2086+
# these obj.relation values will all be ParentModel's
2087+
# unless we fix select related but should be their proper
2088+
# sub type by using PolymorphicRelatedQuerySetMixin
2089+
obj.relation
2090+
for obj in PlainModel.objects.select_related("relation").order_by("pk")
2091+
]
2092+
grouped_info = [object_info(obj) for obj in grouped_q]
2093+
self.assertListEqual(
2094+
grouped_info, [object_info(obj) for obj in [obj1, obj2, obj3, obj4, obj5, obj6, obj7]]
2095+
)
2096+
self.assertListEqual(grouped_q, [obj1, obj2, obj3, obj4, obj5, obj6, obj7])
2097+
2098+
def test_related_fetch_of_non_sequential_pks(self):
2099+
obj1 = ChildModel.objects.create(name="m1", other_name="c1")
2100+
obj2 = ParentModel.objects.create(name="m2")
2101+
2102+
# FIXME use PK from table to get in sequential PKS
2103+
# from django.db import connection
2104+
# with connection.cursor() as cursor:
2105+
# cursor.execute('INSERT INTO "tests_childmodel" ("other_name", "parentmodel_ptr_id") VALUES (%s, %s)', ['fake', 1])
2106+
2107+
obj3 = ChildModel.objects.create(name="m3", other_name="c2")
2108+
obj4 = AltChildModel.objects.create(name="m4", other_name="acm1")
2109+
obj5 = ChildModel.objects.create(name="m5", other_name="c3")
2110+
obj6 = AltChildAsBaseModel.objects.create(name="m6", more_name="acab1")
2111+
2112+
PlainModel.objects.create(relation=obj1)
2113+
PlainModel.objects.create(relation=obj2)
2114+
PlainModel.objects.create(relation=obj3)
2115+
PlainModel.objects.create(relation=obj4)
2116+
PlainModel.objects.create(relation=obj5)
2117+
PlainModel.objects.create(relation=obj6)
2118+
2119+
def object_info(obj):
2120+
return {
2121+
"pk": obj.pk,
2122+
"parentmodel_ptr": getattr(obj, "parentmodel_ptr_id", None),
2123+
"altchildmodel_ptr": getattr(obj, "altchildmodel_ptr_id", None),
2124+
}
2125+
2126+
with self.assertNumQueries(4):
2127+
grouped_q = [
2128+
# these obj.relation values will all be ParentModel's
2129+
# unless we fix select related but should be their proper
2130+
# sub type by using PolymorphicRelatedQuerySetMixin
2131+
obj.relation
2132+
for obj in PlainModel.objects.select_related("relation")
2133+
]
2134+
grouped_info = [object_info(obj) for obj in grouped_q]
2135+
self.assertListEqual(
2136+
grouped_info, [object_info(obj) for obj in [obj1, obj2, obj3, obj4, obj5, obj6]]
2137+
)
2138+
self.assertListEqual(grouped_q, [obj1, obj2, obj3, obj4, obj5, obj6])
20322139

20332140
def test_normal_django_to_poly_related_give_poly_type_using_select_related_true(self):
20342141
obj1 = ParentModel.objects.create(name="m1")
@@ -2335,7 +2442,12 @@ def test_select_related_fecth_all_poly_classes_indirect_related(self):
23352442

23362443
# Prefetch content_types
23372444
ContentType.objects.get_for_models(
2338-
PlainModel, PlainA, ModelExtraExternal, AltChildAsBaseModel, AltChildWithM2MModel
2445+
AltChildAsBaseModel,
2446+
AltChildWithM2MModel,
2447+
ModelExtraExternal,
2448+
NonAutoPKChild,
2449+
PlainA,
2450+
PlainModel,
23392451
)
23402452

23412453
with self.assertNumQueries(1):

0 commit comments

Comments
 (0)