sqlobjects 1.2.3__tar.gz → 1.2.4__tar.gz

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 (78) hide show
  1. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/CHANGELOG.md +10 -0
  2. {sqlobjects-1.2.3/sqlobjects.egg-info → sqlobjects-1.2.4}/PKG-INFO +1 -1
  3. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/pyproject.toml +1 -2
  4. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/proxies.py +54 -35
  5. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/relations/descriptors.py +17 -23
  6. sqlobjects-1.2.4/sqlobjects/fields/relations/prefetch.py +159 -0
  7. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/relations/utils.py +115 -172
  8. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/metadata.py +1 -1
  9. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/queryset.py +3 -0
  10. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/session.py +12 -3
  11. {sqlobjects-1.2.3 → sqlobjects-1.2.4/sqlobjects.egg-info}/PKG-INFO +1 -1
  12. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/tests/test_config.py +1 -1
  13. sqlobjects-1.2.3/sqlobjects/fields/relations/prefetch.py +0 -241
  14. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/LICENSE +0 -0
  15. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/README.md +0 -0
  16. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/01-database-session-guide.md +0 -0
  17. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/02-model-definition-guide.md +0 -0
  18. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/03-query-operations-guide.md +0 -0
  19. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/04-crud-operations-guide.md +0 -0
  20. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/05-relationships-guide.md +0 -0
  21. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/06-validation-signals-guide.md +0 -0
  22. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/07-performance-guide.md +0 -0
  23. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/docs/rules/README.md +0 -0
  24. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/setup.cfg +0 -0
  25. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/__init__.py +0 -0
  26. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/_install_rules.py +0 -0
  27. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/cascade.py +0 -0
  28. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/database/__init__.py +0 -0
  29. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/database/config.py +0 -0
  30. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/database/manager.py +0 -0
  31. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/exceptions.py +0 -0
  32. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/__init__.py +0 -0
  33. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/aggregate.py +0 -0
  34. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/base.py +0 -0
  35. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/cte.py +0 -0
  36. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/explain.py +0 -0
  37. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/function.py +0 -0
  38. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/mixins.py +0 -0
  39. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/scalar.py +0 -0
  40. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/subquery.py +0 -0
  41. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/terminal.py +0 -0
  42. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/expressions/window.py +0 -0
  43. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/__init__.py +0 -0
  44. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/core.py +0 -0
  45. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/functions.py +0 -0
  46. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/relations/__init__.py +0 -0
  47. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/relations/managers.py +0 -0
  48. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/relations/strategies.py +0 -0
  49. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/shortcuts.py +0 -0
  50. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/types/__init__.py +0 -0
  51. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/types/base.py +0 -0
  52. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/types/comparators.py +0 -0
  53. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/types/registry.py +0 -0
  54. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/fields/utils.py +0 -0
  55. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/internal/__init__.py +0 -0
  56. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/internal/operations.py +0 -0
  57. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/internal/results.py +0 -0
  58. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/mixins.py +0 -0
  59. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/model.py +0 -0
  60. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/objects/__init__.py +0 -0
  61. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/objects/bulk.py +0 -0
  62. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/objects/core.py +0 -0
  63. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/objects/upsert.py +0 -0
  64. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/queries/__init__.py +0 -0
  65. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/queries/builder.py +0 -0
  66. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/queries/dialect.py +0 -0
  67. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/queries/executor.py +0 -0
  68. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/signals.py +0 -0
  69. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/utils/__init__.py +0 -0
  70. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/utils/inspect.py +0 -0
  71. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/utils/naming.py +0 -0
  72. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/utils/pattern.py +0 -0
  73. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects/validators.py +0 -0
  74. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects.egg-info/SOURCES.txt +0 -0
  75. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects.egg-info/dependency_links.txt +0 -0
  76. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects.egg-info/entry_points.txt +0 -0
  77. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects.egg-info/requires.txt +0 -0
  78. {sqlobjects-1.2.3 → sqlobjects-1.2.4}/sqlobjects.egg-info/top_level.txt +0 -0
@@ -1,3 +1,13 @@
1
+ ## 1.2.4 (2026-02-28)
2
+
3
+ ### Fix
4
+
5
+ - handle nested Q objects in Q._to_sqlalchemy
6
+
7
+ ### Refactor
8
+
9
+ - **relationships**: overhaul relationship resolution and prefetch system
10
+
1
11
  ## 1.2.3 (2026-02-26)
2
12
 
3
13
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.2.3
3
+ Version: 1.2.4
4
4
  Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlobjects"
3
- version = "1.2.3"
3
+ version = "1.2.4"
4
4
  description = "Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -98,7 +98,6 @@ pythonVersion = "3.12"
98
98
  reportMissingImports = "error"
99
99
  reportUnnecessaryTypeIgnoreComment = "warning"
100
100
  reportMissingTypeStubs = false
101
- lang = "en"
102
101
 
103
102
  [tool.ruff]
104
103
  line-length = 120
@@ -96,6 +96,14 @@ class BaseRelated(Generic[T]):
96
96
  self.property = descriptor.property
97
97
  self._cached_value = None
98
98
  self._loaded = False
99
+ self._rel_info = None
100
+
101
+ def _get_relationship_info(self):
102
+ if self._rel_info is None:
103
+ from .relations.utils import RelationshipAnalyzer
104
+
105
+ self._rel_info = RelationshipAnalyzer.analyze_relationship(self.instance.__class__, self.property.name)
106
+ return self._rel_info
99
107
 
100
108
  async def fetch(self) -> T:
101
109
  """Fetch related object(s)."""
@@ -122,23 +130,28 @@ class RelatedObject(BaseRelated[T]):
122
130
 
123
131
  async def _load(self):
124
132
  """Load related object from database."""
125
- if self.property.foreign_keys and self.property.resolved_model:
126
- fk_field = self.property.foreign_keys
127
- if isinstance(fk_field, list):
128
- fk_field = fk_field[0]
133
+ if not self.property.resolved_model:
134
+ self._loaded = True
135
+ return
129
136
 
130
- fk_value = getattr(self.instance, fk_field)
131
- if fk_value is not None:
132
- related_table = self.property.resolved_model.get_table()
133
- pk_col = list(related_table.primary_key.columns)[0]
137
+ info = self._get_relationship_info()
138
+ if not info:
139
+ self._loaded = True
140
+ return
134
141
 
135
- query = select(related_table).where(pk_col == fk_value)
136
- session = self.instance.get_session()
137
- result = await session.execute(query)
138
- row = result.first()
142
+ fk_field = info["foreign_key_fields"][0]
143
+ ref_field = info["ref_fields"][0]
144
+ fk_value = getattr(self.instance, fk_field)
139
145
 
140
- if row:
141
- self._cached_value = self.property.resolved_model.from_dict(dict(row._mapping), validate=False)
146
+ if fk_value is not None:
147
+ related_table = self.property.resolved_model.get_table()
148
+ ref_col = related_table.c[ref_field]
149
+ query = select(related_table).where(ref_col == fk_value)
150
+ session = self.instance.get_session()
151
+ result = await session.execute(query)
152
+ row = result.first()
153
+ if row:
154
+ self._cached_value = self.property.resolved_model.from_dict(dict(row._mapping), validate=False)
142
155
 
143
156
  self._loaded = True
144
157
 
@@ -191,12 +204,18 @@ class OneToManyRelation(RelatedCollection[T]):
191
204
  self._set_empty_result()
192
205
  return
193
206
 
194
- instance_pk = self.instance.id
195
- related_table = self.property.resolved_model.get_table()
207
+ info = self._get_relationship_info()
208
+ if not info:
209
+ self._set_empty_result()
210
+ return
211
+
212
+ fk_field = info["foreign_key_fields"][0]
213
+ ref_field = info["ref_fields"][0]
214
+ ref_value = getattr(self.instance, ref_field)
196
215
 
197
- fk_name = self._get_fk_field()
198
- fk_col = related_table.c[fk_name]
199
- query = select(related_table).where(fk_col == instance_pk)
216
+ related_table = self.property.resolved_model.get_table()
217
+ fk_col = related_table.c[fk_field]
218
+ query = select(related_table).where(fk_col == ref_value)
200
219
  session = self.instance.get_session()
201
220
  result = await session.execute(query)
202
221
 
@@ -207,19 +226,32 @@ class OneToManyRelation(RelatedCollection[T]):
207
226
 
208
227
  async def add(self, *objs: T, session=None) -> None:
209
228
  """Add objects to the relationship."""
229
+ info = self._get_relationship_info()
230
+ if not info:
231
+ raise ValueError(
232
+ f"Cannot resolve relationship '{self.property.name}' on "
233
+ f"'{self.instance.__class__.__name__}'. Define it explicitly with relationship()."
234
+ )
210
235
  session = session or self.instance.get_session()
211
- fk_field = self._get_fk_field()
236
+ fk_field = info["foreign_key_fields"][0]
237
+ ref_value = getattr(self.instance, info["ref_fields"][0])
212
238
 
213
239
  for obj in objs:
214
- setattr(obj, fk_field, self.instance.id)
240
+ setattr(obj, fk_field, ref_value)
215
241
  await obj.using(session).save() # type: ignore
216
242
 
217
243
  self._invalidate_cache()
218
244
 
219
245
  async def remove(self, *objs: T, session=None) -> None:
220
246
  """Remove objects from the relationship."""
247
+ info = self._get_relationship_info()
248
+ if not info:
249
+ raise ValueError(
250
+ f"Cannot resolve relationship '{self.property.name}' on "
251
+ f"'{self.instance.__class__.__name__}'. Define it explicitly with relationship()."
252
+ )
221
253
  session = session or self.instance.get_session()
222
- fk_field = self._get_fk_field()
254
+ fk_field = info["foreign_key_fields"][0]
223
255
 
224
256
  for obj in objs:
225
257
  setattr(obj, fk_field, None)
@@ -227,19 +259,6 @@ class OneToManyRelation(RelatedCollection[T]):
227
259
 
228
260
  self._invalidate_cache()
229
261
 
230
- def _get_fk_field(self):
231
- """Get foreign key field name."""
232
- fk_name = self.property.foreign_keys
233
- if isinstance(fk_name, list):
234
- fk_name = fk_name[0]
235
- elif fk_name is None:
236
- fk_name = (
237
- f"{self.property.back_populates}_id"
238
- if self.property.back_populates
239
- else f"{self.instance.__class__.__name__.lower()}_id"
240
- )
241
- return fk_name
242
-
243
262
  def __str__(self):
244
263
  return f"<OneToManyRelation: {self.property.name}>"
245
264
 
@@ -3,6 +3,12 @@ from typing import TYPE_CHECKING, Generic, TypeVar, overload
3
3
  from ...cascade import OnDelete
4
4
 
5
5
 
6
+ def _normalize_fields(fields: str | list[str] | None) -> list[str] | None:
7
+ if fields is None:
8
+ return None
9
+ return [fields] if isinstance(fields, str) else list(fields)
10
+
11
+
6
12
  if TYPE_CHECKING:
7
13
  from ...model import ObjectModel
8
14
  from ..proxies import BaseRelated
@@ -27,6 +33,7 @@ class RelationshipProperty:
27
33
  self,
28
34
  argument: str | type["ObjectModel"],
29
35
  foreign_keys: str | list[str] | None = None,
36
+ remote_fields: str | list[str] | None = None,
30
37
  back_populates: str | None = None,
31
38
  backref: str | None = None,
32
39
  lazy: str = "select",
@@ -40,32 +47,21 @@ class RelationshipProperty:
40
47
  passive_deletes: bool = False,
41
48
  **kwargs,
42
49
  ):
43
- """Initialize relationship property with cascade and deletion behavior.
44
-
45
- Args:
46
- argument: Target model class or string name
47
- foreign_keys: Foreign key field name(s)
48
- back_populates: Name of reverse relationship attribute
49
- backref: Name for automatic reverse relationship
50
- lazy: Loading strategy ('select', 'dynamic', 'noload', 'raise')
51
- uselist: Whether relationship returns a list
52
- secondary: M2M table name
53
- primaryjoin: Custom primary join condition
54
- secondaryjoin: Custom secondary join condition for M2M
55
- order_by: Default ordering for collections
56
- cascade: Cascade behavior (bool for simple on/off, str for SQLAlchemy cascade options)
57
- on_delete: Behavior when related object is deleted
58
- passive_deletes: Whether to use passive deletes
59
- **kwargs: Additional relationship options
60
- """
50
+ if foreign_keys and remote_fields:
51
+ raise ValueError(
52
+ "Cannot specify both 'foreign_keys' and 'remote_fields'. "
53
+ "Use 'foreign_keys' when the FK is on this model, "
54
+ "'remote_fields' when the FK is on the related model."
55
+ )
61
56
  self.argument = argument
62
- self.foreign_keys = foreign_keys
57
+ self.foreign_keys: list[str] | None = _normalize_fields(foreign_keys)
58
+ self.remote_fields: list[str] | None = _normalize_fields(remote_fields)
63
59
  self.back_populates = back_populates
64
60
  self.backref = backref
65
61
  self.lazy = lazy
66
62
  self.uselist = uselist
67
63
  self.secondary = secondary
68
- self.m2m_definition = None # M2M table definition
64
+ self.m2m_definition = None
69
65
  self.primaryjoin = primaryjoin
70
66
  self.secondaryjoin = secondaryjoin
71
67
  self.order_by = order_by
@@ -75,9 +71,7 @@ class RelationshipProperty:
75
71
  self.name: str | None = None
76
72
  self.resolved_model: type[ObjectModel] | None = None
77
73
  self.relationship_type: str | None = None
78
- self.is_many_to_many: bool = False # M2M relationship flag
79
-
80
- # Store additional relationship configuration parameters
74
+ self.is_many_to_many: bool = False
81
75
  self.extra_kwargs = kwargs
82
76
 
83
77
 
@@ -0,0 +1,159 @@
1
+ from sqlalchemy import tuple_
2
+
3
+ from .utils import RelationshipAnalyzer
4
+
5
+
6
+ class PrefetchHandler:
7
+ """Handle prefetch_related operations for model relationships."""
8
+
9
+ def __init__(self, session):
10
+ self.session = session
11
+
12
+ async def handle_prefetch_relationships(self, instances, prefetch_relationships):
13
+ if not instances or not prefetch_relationships:
14
+ return instances
15
+
16
+ for relationship_name in prefetch_relationships:
17
+ relationship_info = RelationshipAnalyzer.analyze_relationship(instances[0].__class__, relationship_name)
18
+ if relationship_info:
19
+ await self._prefetch_single_relationship(instances, relationship_name, relationship_info)
20
+
21
+ return instances
22
+
23
+ async def _prefetch_single_relationship(self, instances, relationship_name, relationship_info):
24
+ rel_type = relationship_info["type"]
25
+
26
+ if rel_type == "reverse_fk":
27
+ await self._prefetch_by_fields(
28
+ instances,
29
+ relationship_name,
30
+ relationship_info["related_model"],
31
+ relationship_info["ref_fields"],
32
+ relationship_info["foreign_key_fields"],
33
+ [],
34
+ )
35
+ elif rel_type == "one_to_one":
36
+ info = relationship_info
37
+ await self._prefetch_by_fields(
38
+ instances,
39
+ relationship_name,
40
+ info["related_model"],
41
+ info["ref_fields"],
42
+ info["foreign_key_fields"],
43
+ None,
44
+ )
45
+ elif rel_type == "many_to_one":
46
+ info = relationship_info
47
+ await self._prefetch_by_fields(
48
+ instances,
49
+ relationship_name,
50
+ info["related_model"],
51
+ info["foreign_key_fields"],
52
+ info["ref_fields"],
53
+ None,
54
+ )
55
+ elif rel_type == "many_to_many":
56
+ await self._prefetch_many_to_many(instances, relationship_name, relationship_info)
57
+
58
+ async def _prefetch_by_fields(
59
+ self, instances, relationship_name, related_model, lookup_fields, group_fields, empty
60
+ ):
61
+ """Unified prefetch for FK-based relationships."""
62
+ composite = len(lookup_fields) > 1
63
+
64
+ def make_key(obj, fields):
65
+ return tuple(getattr(obj, f) for f in fields) if composite else getattr(obj, fields[0])
66
+
67
+ lookup_values = [
68
+ make_key(inst, lookup_fields)
69
+ for inst in instances
70
+ if all(getattr(inst, f, None) is not None for f in lookup_fields)
71
+ ]
72
+ if not lookup_values:
73
+ for inst in instances:
74
+ inst._update_cache(relationship_name, [] if isinstance(empty, list) else empty)
75
+ return
76
+
77
+ if composite:
78
+ cols = [getattr(related_model, f) for f in group_fields]
79
+ qs = related_model.objects.using(self.session).filter(tuple_(*cols).in_(lookup_values))
80
+ else:
81
+ qs = related_model.objects.using(self.session).filter(
82
+ getattr(related_model, group_fields[0]).in_(lookup_values)
83
+ )
84
+ related_objects = await qs.all()
85
+
86
+ if isinstance(empty, list):
87
+ grouped = {}
88
+ for obj in related_objects:
89
+ grouped.setdefault(make_key(obj, group_fields), []).append(obj)
90
+ for inst in instances:
91
+ inst._update_cache(relationship_name, grouped.get(make_key(inst, lookup_fields), []))
92
+ else:
93
+ related_map = {make_key(obj, group_fields): obj for obj in related_objects}
94
+ for inst in instances:
95
+ inst._update_cache(relationship_name, related_map.get(make_key(inst, lookup_fields)))
96
+
97
+ async def _prefetch_many_to_many(self, instances, relationship_name, relationship_info):
98
+ """Prefetch many-to-many via through table."""
99
+ related_model = relationship_info["related_model"]
100
+ through_table = relationship_info["through_table"]
101
+ left_field = relationship_info["left_field"]
102
+ right_field = relationship_info["right_field"]
103
+ left_ref_field = relationship_info["left_ref_field"]
104
+ right_ref_field = relationship_info["right_ref_field"]
105
+
106
+ instance_values = [
107
+ getattr(inst, left_ref_field) for inst in instances if getattr(inst, left_ref_field, None) is not None
108
+ ]
109
+ if not instance_values:
110
+ for inst in instances:
111
+ inst._update_cache(relationship_name, [])
112
+ return
113
+
114
+ through_model = self._find_through_model(through_table)
115
+ if not through_model:
116
+ return
117
+
118
+ through_objects = await (
119
+ through_model.objects.using(self.session)
120
+ .filter(getattr(through_model, left_field).in_(instance_values))
121
+ .all()
122
+ )
123
+
124
+ related_values = [getattr(obj, right_field) for obj in through_objects]
125
+ if not related_values:
126
+ for inst in instances:
127
+ inst._update_cache(relationship_name, [])
128
+ return
129
+
130
+ related_objects = await (
131
+ related_model.objects.using(self.session)
132
+ .filter(getattr(related_model, right_ref_field).in_(related_values))
133
+ .all()
134
+ )
135
+
136
+ related_map = {getattr(obj, right_ref_field): obj for obj in related_objects}
137
+
138
+ grouped = {}
139
+ for through_obj in through_objects:
140
+ main_val = getattr(through_obj, left_field)
141
+ rel_val = getattr(through_obj, right_field)
142
+ if rel_val in related_map:
143
+ grouped.setdefault(main_val, []).append(related_map[rel_val])
144
+
145
+ for inst in instances:
146
+ key = getattr(inst, left_ref_field, None)
147
+ inst._update_cache(relationship_name, grouped.get(key, []))
148
+
149
+ @staticmethod
150
+ def _find_through_model(through_table):
151
+ from ...model import ObjectModel
152
+
153
+ for subclass in ObjectModel.__subclasses__():
154
+ try:
155
+ if hasattr(subclass, "get_table") and subclass.get_table().name == through_table:
156
+ return subclass
157
+ except Exception: # noqa
158
+ continue
159
+ return None