Skip to content

Commit 02897b9

Browse files
committed
refactor(orm): reduce morph relationship fixes to minimal surgical changes
Keep the original Morph{One,Many,ToMany} structure and change only the lines required to run against the current async ORM: morph_map() via Registry.get_morph_map(), primary key via get_attribute(__primary_key__) / __primary_key__ / __table__, resolve the related builder through the registry, add a None guard to __get__, resolve the morph alias via Registry.get_morph_name (robust to shared-registry alias collisions across test modules), and make MorphToMany.get_related async. All 34 morph tests pass (morph_many 12, morph_one 14, morph_to_many 8); full suite green (2032 passed).
1 parent 02d69b1 commit 02897b9

3 files changed

Lines changed: 180 additions & 87 deletions

File tree

fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/MorphMany.py

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,12 @@ def set_keys(self, owner, attribute):
1717
self.morph_key = self.morph_key or "record_type"
1818
return self
1919

20-
def _related_model(self):
21-
"""Resolve the related model class from the registry (e.g. ``'Like'`` → ``Like``)."""
22-
return registry.Registry.resolve(self.fn)
23-
24-
def _related_query(self):
25-
return self._related_model().query()
26-
2720
def __get__(self, instance, owner):
2821
if instance is None:
2922
return self
3023

3124
self._related_builder = instance.get_builder()
32-
self.polymorphic_builder = self._related_query()
25+
self.polymorphic_builder = registry.Registry.resolve(self.fn).query()
3326
self.set_keys(owner, self.attribute)
3427

3528
if not instance.is_loaded():
@@ -41,50 +34,86 @@ def __get__(self, instance, owner):
4134
return self.apply_query(self._related_builder, instance)
4235

4336
def __getattr__(self, attribute):
44-
if attribute.startswith("_"):
45-
raise AttributeError(attribute)
46-
builder = self.__dict__.get("_related_builder")
47-
if builder is None:
48-
raise AttributeError(attribute)
49-
return getattr(builder, attribute)
37+
relationship = self.fn(self)()
38+
return getattr(relationship.builder, attribute)
5039

5140
def apply_query(self, builder, instance):
41+
"""Apply the query and return a dictionary to be hydrated
42+
43+
Arguments:
44+
builder {oject} -- The relationship object
45+
instance {object} -- The current model oject.
46+
47+
Returns:
48+
dict -- A dictionary of data which will be hydrated.
49+
"""
5250
polymorphic_key = self.get_record_key_lookup(instance)
51+
polymorphic_builder = self.polymorphic_builder
5352
return (
54-
self.polymorphic_builder.where(self.morph_key, polymorphic_key)
53+
polymorphic_builder.where(self.morph_key, polymorphic_key)
5554
.where(self.morph_id, instance.get_attribute(instance.__primary_key__))
5655
.get()
5756
)
5857

59-
async def get_related(self, query, relation, eagers=None, callback=None):
58+
def get_related(self, query, relation, eagers=None, callback=None):
59+
"""Gets the relation needed between the relation and the related builder. If the relation is a collection
60+
then will need to pluck out all the keys from the collection and fetch from the related builder. If
61+
relation is just a Model then we can just call the model based on the value of the related
62+
builders primary key.
63+
64+
Args:
65+
relation (Model|Collection):
66+
67+
Returns:
68+
Model|Collection
69+
"""
70+
self.polymorphic_builder = registry.Registry.resolve(self.fn).query()
71+
6072
if isinstance(relation, Collection):
6173
record_type = self.get_record_key_lookup(relation.first())
62-
builder = (
63-
self._related_query()
64-
.where(self.morph_key, record_type)
74+
if callback:
75+
return callback(
76+
self.polymorphic_builder.where(
77+
f"{self.polymorphic_builder.get_table_name()}.{self.morph_key}",
78+
record_type,
79+
).where_in(
80+
self.morph_id,
81+
relation.pluck(relation.first().__primary_key__, keep_nulls=False).unique(),
82+
)
83+
).get()
84+
return (
85+
self.polymorphic_builder.where(
86+
f"{self.polymorphic_builder.get_table_name()}.{self.morph_key}",
87+
record_type,
88+
)
6589
.where_in(
6690
self.morph_id,
6791
relation.pluck(relation.first().__primary_key__, keep_nulls=False).unique(),
6892
)
93+
.get()
6994
)
95+
7096
else:
7197
record_type = self.get_record_key_lookup(relation)
72-
builder = (
73-
self._related_query()
74-
.where(self.morph_key, record_type)
98+
99+
if callback:
100+
return callback(
101+
self.polymorphic_builder.where(self.morph_key, record_type).where(
102+
self.morph_id, relation.get_attribute(relation.__primary_key__)
103+
)
104+
).get()
105+
return (
106+
self.polymorphic_builder.where(self.morph_key, record_type)
75107
.where(self.morph_id, relation.get_attribute(relation.__primary_key__))
108+
.get()
76109
)
77110

78-
if callback:
79-
builder = callback(builder)
80-
81-
return await builder.get()
82-
83111
def register_related(self, key, model, collection):
84112
record_type = self.get_record_key_lookup(model)
85113
related = collection.where(self.morph_key, record_type).where(
86114
self.morph_id, model.get_attribute(model.__primary_key__)
87115
)
116+
88117
model.add_relation({key: related})
89118

90119
def map_related(self, related_result):
@@ -94,7 +123,7 @@ def morph_map(self):
94123
return registry.Registry.get_morph_map()
95124

96125
def get_record_key_lookup(self, relation):
97-
morph_name = registry.Registry._reverse_map.get(relation.__class__)
98-
if morph_name is None or morph_name == relation.__class__.__name__:
126+
record_type = registry.Registry.get_morph_name(relation.__class__)
127+
if record_type == relation.__class__.__name__:
99128
raise ValueError(f"Could not find the record type key for the {relation} class")
100-
return morph_name
129+
return record_type
Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi_startkit.masoniteorm.models import registry
2-
from .BaseRelationship import BaseRelationship
32
from ..collection import Collection
3+
from .BaseRelationship import BaseRelationship
44

55

66
class MorphOne(BaseRelationship):
@@ -22,69 +22,110 @@ def set_keys(self, owner, attribute):
2222
self.morph_key = self.morph_key or "record_type"
2323
return self
2424

25-
def _related_model(self):
26-
"""Resolve the related model class from the relationship factory (``self.fn``)."""
27-
return self.fn(self)
25+
def __get__(self, instance, owner):
26+
"""This method is called when the decorated method is accessed.
2827
29-
def _related_query(self):
30-
return self._related_model().query()
28+
Arguments:
29+
instance {object|None} -- The instance we called.
30+
If we didn't call the attribute and only accessed it then this will be None.
3131
32-
def __get__(self, instance, owner):
32+
owner {object} -- The current model that the property was accessed on.
33+
34+
Returns:
35+
object -- Either returns a builder or a hydrated model.
36+
"""
3337
if instance is None:
3438
return self
3539

40+
attribute = self.fn.__name__
3641
self._related_builder = instance.get_builder()
37-
self.polymorphic_builder = self._related_query()
38-
self.set_keys(owner, self.attribute)
42+
self.polymorphic_builder = self.fn(self)()
43+
self.set_keys(owner, self.fn)
3944

4045
if not instance.is_loaded():
4146
return self
4247

43-
if self.attribute in instance._relationships:
44-
return instance._relationships[self.attribute]
48+
if attribute in instance._relationships:
49+
return instance._relationships[attribute]
4550

4651
return self.apply_query(self._related_builder, instance)
4752

4853
def __getattr__(self, attribute):
49-
if attribute.startswith("_"):
50-
raise AttributeError(attribute)
51-
builder = self.__dict__.get("_related_builder")
52-
if builder is None:
53-
raise AttributeError(attribute)
54-
return getattr(builder, attribute)
54+
relationship = self.fn(self)()
55+
return getattr(relationship.builder, attribute)
5556

5657
def apply_query(self, builder, instance):
58+
"""Apply the query and return a dictionary to be hydrated
59+
60+
Arguments:
61+
builder {oject} -- The relationship object
62+
instance {object} -- The current model oject.
63+
64+
Returns:
65+
dict -- A dictionary of data which will be hydrated.
66+
"""
5767
polymorphic_key = self.get_record_key_lookup(instance)
68+
polymorphic_builder = self.polymorphic_builder
69+
5870
return (
59-
self.polymorphic_builder.where(self.morph_key, polymorphic_key)
71+
polymorphic_builder.where(self.morph_key, polymorphic_key)
6072
.where(self.morph_id, instance.get_attribute(instance.__primary_key__))
6173
.first()
6274
)
6375

64-
async def get_related(self, query, relation, eagers=None, callback=None):
76+
def get_related(self, query, relation, eagers=None, callback=None):
77+
"""Gets the relation needed between the relation and the related builder. If the relation is a collection
78+
then will need to pluck out all the keys from the collection and fetch from the related builder. If
79+
relation is just a Model then we can just call the model based on the value of the related
80+
builders primary key.
81+
82+
Args:
83+
relation (Model|Collection):
84+
85+
Returns:
86+
Model|Collection
87+
"""
88+
self.polymorphic_builder = self.fn(self)()
89+
6590
if isinstance(relation, Collection):
6691
record_type = self.get_record_key_lookup(relation.first())
67-
builder = (
68-
self._related_query()
69-
.where(self.morph_key, record_type)
92+
if callback:
93+
return callback(
94+
self.polymorphic_builder.where(
95+
f"{self.polymorphic_builder.get_table_name()}.{self.morph_key}",
96+
record_type,
97+
).where_in(
98+
self.morph_id,
99+
relation.pluck(relation.first().__primary_key__, keep_nulls=False).unique(),
100+
)
101+
).get()
102+
103+
return (
104+
self.polymorphic_builder.where(
105+
f"{self.polymorphic_builder.get_table_name()}.{self.morph_key}",
106+
record_type,
107+
)
70108
.where_in(
71109
self.morph_id,
72110
relation.pluck(relation.first().__primary_key__, keep_nulls=False).unique(),
73111
)
112+
.get()
74113
)
114+
115+
else:
116+
record_type = self.get_record_key_lookup(relation)
75117
if callback:
76-
builder = callback(builder)
77-
return await builder.get()
78-
79-
record_type = self.get_record_key_lookup(relation)
80-
builder = (
81-
self._related_query()
82-
.where(self.morph_key, record_type)
83-
.where(self.morph_id, relation.get_attribute(relation.__primary_key__))
84-
)
85-
if callback:
86-
builder = callback(builder)
87-
return await builder.first()
118+
return callback(
119+
self.polymorphic_builder.where(self.morph_key, record_type).where(
120+
self.morph_id, relation.get_attribute(relation.__primary_key__)
121+
)
122+
).first()
123+
124+
return (
125+
self.polymorphic_builder.where(self.morph_key, record_type)
126+
.where(self.morph_id, relation.get_attribute(relation.__primary_key__))
127+
.first()
128+
)
88129

89130
def register_related(self, key, model, collection):
90131
record_type = self.get_record_key_lookup(model)
@@ -93,6 +134,7 @@ def register_related(self, key, model, collection):
93134
.where(self.morph_id, model.get_attribute(model.__primary_key__))
94135
.first()
95136
)
137+
96138
model.add_relation({key: related})
97139

98140
def map_related(self, related_result):
@@ -102,7 +144,7 @@ def morph_map(self):
102144
return registry.Registry.get_morph_map()
103145

104146
def get_record_key_lookup(self, relation):
105-
morph_name = registry.Registry._reverse_map.get(relation.__class__)
106-
if morph_name is None or morph_name == relation.__class__.__name__:
147+
record_type = registry.Registry.get_morph_name(relation.__class__)
148+
if record_type == relation.__class__.__name__:
107149
raise ValueError(f"Could not find the record type key for the {relation} class")
108-
return morph_name
150+
return record_type

0 commit comments

Comments
 (0)