sqlobjects 1.2.3__tar.gz → 1.2.5__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.
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/CHANGELOG.md +17 -0
- {sqlobjects-1.2.3/sqlobjects.egg-info → sqlobjects-1.2.5}/PKG-INFO +1 -1
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/pyproject.toml +1 -2
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/function.py +2 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/mixins.py +19 -3
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/proxies.py +54 -35
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/relations/descriptors.py +17 -23
- sqlobjects-1.2.5/sqlobjects/fields/relations/prefetch.py +159 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/relations/utils.py +115 -172
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/types/comparators.py +54 -5
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/types/registry.py +6 -4
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/metadata.py +1 -1
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/queryset.py +3 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/session.py +12 -3
- {sqlobjects-1.2.3 → sqlobjects-1.2.5/sqlobjects.egg-info}/PKG-INFO +1 -1
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/tests/test_config.py +1 -1
- sqlobjects-1.2.3/sqlobjects/fields/relations/prefetch.py +0 -241
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/LICENSE +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/README.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/01-database-session-guide.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/02-model-definition-guide.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/03-query-operations-guide.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/04-crud-operations-guide.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/05-relationships-guide.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/06-validation-signals-guide.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/07-performance-guide.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/docs/rules/README.md +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/setup.cfg +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/_install_rules.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/cascade.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/database/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/database/config.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/database/manager.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/exceptions.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/aggregate.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/base.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/cte.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/explain.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/scalar.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/subquery.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/terminal.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/expressions/window.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/core.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/functions.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/relations/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/relations/managers.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/relations/strategies.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/shortcuts.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/types/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/types/base.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/fields/utils.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/internal/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/internal/operations.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/internal/results.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/mixins.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/model.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/objects/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/objects/bulk.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/objects/core.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/objects/upsert.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/queries/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/queries/builder.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/queries/dialect.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/queries/executor.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/signals.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/utils/__init__.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/utils/inspect.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/utils/naming.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/utils/pattern.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects/validators.py +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects.egg-info/SOURCES.txt +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects.egg-info/dependency_links.txt +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects.egg-info/entry_points.txt +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects.egg-info/requires.txt +0 -0
- {sqlobjects-1.2.3 → sqlobjects-1.2.5}/sqlobjects.egg-info/top_level.txt +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## 1.2.5 (2026-03-06)
|
|
2
|
+
|
|
3
|
+
### Fix
|
|
4
|
+
|
|
5
|
+
- **examples,docs**: fix PGVECTOR type definition and documentation errors
|
|
6
|
+
- **types**: fix JSON/JSONB field containment query generating wrong SQL
|
|
7
|
+
|
|
8
|
+
## 1.2.4 (2026-02-28)
|
|
9
|
+
|
|
10
|
+
### Fix
|
|
11
|
+
|
|
12
|
+
- handle nested Q objects in Q._to_sqlalchemy
|
|
13
|
+
|
|
14
|
+
### Refactor
|
|
15
|
+
|
|
16
|
+
- **relationships**: overhaul relationship resolution and prefetch system
|
|
17
|
+
|
|
1
18
|
## 1.2.3 (2026-02-26)
|
|
2
19
|
|
|
3
20
|
### Fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
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
|
+
version = "1.2.5"
|
|
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
|
|
@@ -147,6 +147,8 @@ class FunctionExpression:
|
|
|
147
147
|
for building complex database expressions.
|
|
148
148
|
"""
|
|
149
149
|
|
|
150
|
+
is_clause_element = False # prevent __getattr__ from proxying this to expression.is_clause_element
|
|
151
|
+
|
|
150
152
|
if TYPE_CHECKING:
|
|
151
153
|
# Inherit all method hints for IDE support
|
|
152
154
|
def __new__(cls, *args, **kwargs) -> "FunctionExpression & _FunctionMethods": ... # type: ignore
|
|
@@ -247,9 +247,25 @@ class ColumnAttributeFunctionMixin(FunctionMixin):
|
|
|
247
247
|
def extract(self, field: str) -> "FunctionExpression": ...
|
|
248
248
|
def date_trunc(self, precision: str) -> "FunctionExpression": ...
|
|
249
249
|
|
|
250
|
-
# JSON methods
|
|
251
|
-
def
|
|
252
|
-
def
|
|
250
|
+
# JSON methods (MySQL json type)
|
|
251
|
+
def json_extract(self, path: str) -> "FunctionExpression": ...
|
|
252
|
+
def json_unquote(self, path: str) -> "FunctionExpression": ...
|
|
253
|
+
def json_contains(self, other, path: str | None = None) -> "FunctionExpression": ...
|
|
254
|
+
def json_contains_path(self, *paths: str, match: str = "one") -> "FunctionExpression": ...
|
|
255
|
+
def json_length(self, path: str | None = None) -> "FunctionExpression": ...
|
|
256
|
+
def json_keys(self, path: str | None = None) -> "FunctionExpression": ...
|
|
257
|
+
def json_overlaps(self, other) -> "FunctionExpression": ...
|
|
258
|
+
def json_search(self, value: str, match: str = "one", path: str | None = None) -> "FunctionExpression": ...
|
|
259
|
+
def json_type(self) -> "FunctionExpression": ...
|
|
260
|
+
|
|
261
|
+
# JSONB methods (PostgreSQL jsonb type)
|
|
262
|
+
def contained_by(self, other) -> "FunctionExpression": ...
|
|
263
|
+
def has_key(self, other) -> "FunctionExpression": ...
|
|
264
|
+
def has_all(self, other) -> "FunctionExpression": ...
|
|
265
|
+
def has_any(self, other) -> "FunctionExpression": ...
|
|
266
|
+
def path_exists(self, other) -> "FunctionExpression": ...
|
|
267
|
+
def path_match(self, other) -> "FunctionExpression": ...
|
|
268
|
+
def delete_path(self, array) -> "FunctionExpression": ...
|
|
253
269
|
|
|
254
270
|
# Common methods (all types)
|
|
255
271
|
def sum(self) -> "FunctionExpression": ...
|
|
@@ -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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
fk_field = fk_field[0]
|
|
133
|
+
if not self.property.resolved_model:
|
|
134
|
+
self._loaded = True
|
|
135
|
+
return
|
|
129
136
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
info = self._get_relationship_info()
|
|
138
|
+
if not info:
|
|
139
|
+
self._loaded = True
|
|
140
|
+
return
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
fk_col = related_table.c[
|
|
199
|
-
query = select(related_table).where(fk_col ==
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
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
|