dotorm 2.0.8__py3-none-any.whl
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.
- dotorm/__init__.py +87 -0
- dotorm/access.py +151 -0
- dotorm/builder/__init__.py +0 -0
- dotorm/builder/builder.py +72 -0
- dotorm/builder/helpers.py +63 -0
- dotorm/builder/mixins/__init__.py +11 -0
- dotorm/builder/mixins/crud.py +246 -0
- dotorm/builder/mixins/m2m.py +110 -0
- dotorm/builder/mixins/relations.py +96 -0
- dotorm/builder/protocol.py +63 -0
- dotorm/builder/request_builder.py +144 -0
- dotorm/components/__init__.py +18 -0
- dotorm/components/dialect.py +99 -0
- dotorm/components/filter_parser.py +195 -0
- dotorm/databases/__init__.py +13 -0
- dotorm/databases/abstract/__init__.py +25 -0
- dotorm/databases/abstract/dialect.py +134 -0
- dotorm/databases/abstract/pool.py +10 -0
- dotorm/databases/abstract/session.py +67 -0
- dotorm/databases/abstract/types.py +36 -0
- dotorm/databases/clickhouse/__init__.py +8 -0
- dotorm/databases/clickhouse/pool.py +60 -0
- dotorm/databases/clickhouse/session.py +100 -0
- dotorm/databases/mysql/__init__.py +13 -0
- dotorm/databases/mysql/pool.py +69 -0
- dotorm/databases/mysql/session.py +128 -0
- dotorm/databases/mysql/transaction.py +39 -0
- dotorm/databases/postgres/__init__.py +23 -0
- dotorm/databases/postgres/pool.py +133 -0
- dotorm/databases/postgres/session.py +174 -0
- dotorm/databases/postgres/transaction.py +82 -0
- dotorm/decorators.py +379 -0
- dotorm/exceptions.py +9 -0
- dotorm/fields.py +604 -0
- dotorm/integrations/__init__.py +0 -0
- dotorm/integrations/pydantic.py +275 -0
- dotorm/model.py +802 -0
- dotorm/orm/__init__.py +15 -0
- dotorm/orm/mixins/__init__.py +13 -0
- dotorm/orm/mixins/access.py +67 -0
- dotorm/orm/mixins/ddl.py +250 -0
- dotorm/orm/mixins/many2many.py +175 -0
- dotorm/orm/mixins/primary.py +218 -0
- dotorm/orm/mixins/relations.py +513 -0
- dotorm/orm/protocol.py +147 -0
- dotorm/orm/utils.py +39 -0
- dotorm-2.0.8.dist-info/METADATA +1240 -0
- dotorm-2.0.8.dist-info/RECORD +50 -0
- dotorm-2.0.8.dist-info/WHEEL +4 -0
- dotorm-2.0.8.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Relations ORM operations mixin."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar
|
|
4
|
+
|
|
5
|
+
from ...components.filter_parser import FilterExpression
|
|
6
|
+
from ...decorators import hybridmethod
|
|
7
|
+
from ...access import Operation
|
|
8
|
+
from ...builder.request_builder import (
|
|
9
|
+
RequestBuilderForm,
|
|
10
|
+
)
|
|
11
|
+
from ...fields import (
|
|
12
|
+
AttachmentMany2one,
|
|
13
|
+
AttachmentOne2many,
|
|
14
|
+
Many2many,
|
|
15
|
+
Many2one,
|
|
16
|
+
One2many,
|
|
17
|
+
One2one,
|
|
18
|
+
)
|
|
19
|
+
from ..utils import execute_maybe_parallel
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..protocol import DotModelProtocol
|
|
23
|
+
from ...model import DotModel
|
|
24
|
+
|
|
25
|
+
_Base = DotModelProtocol
|
|
26
|
+
else:
|
|
27
|
+
_Base = object
|
|
28
|
+
|
|
29
|
+
# TypeVar for generic payload - accepts any DotModel subclass
|
|
30
|
+
_M = TypeVar("_M", bound="DotModel")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OrmRelationsMixin(_Base):
|
|
34
|
+
"""
|
|
35
|
+
Mixin providing ORM operations for relations.
|
|
36
|
+
|
|
37
|
+
Provides:
|
|
38
|
+
- search - search records with relation loading
|
|
39
|
+
- search_count - count records matching filter
|
|
40
|
+
- exists - have one record or not
|
|
41
|
+
- get_with_relations - get single record with relations
|
|
42
|
+
- update_with_relations - update record with relations
|
|
43
|
+
|
|
44
|
+
Expects DotModel to provide:
|
|
45
|
+
- _get_db_session()
|
|
46
|
+
- _builder
|
|
47
|
+
- _dialect
|
|
48
|
+
- __table__
|
|
49
|
+
- get_fields(), get_store_fields(), get_relation_fields()
|
|
50
|
+
- get_relation_fields_m2m_o2m(), get_relation_fields_attachment()
|
|
51
|
+
- prepare_list_ids()
|
|
52
|
+
- get_many2many(), link_many2many(), unlink_many2many()
|
|
53
|
+
- _records_list_get_relation()
|
|
54
|
+
- update()
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@hybridmethod
|
|
58
|
+
async def search(
|
|
59
|
+
self,
|
|
60
|
+
fields: list[str] = ["id"],
|
|
61
|
+
start: int | None = None,
|
|
62
|
+
end: int | None = None,
|
|
63
|
+
limit: int = 1000,
|
|
64
|
+
order: Literal["DESC", "ASC", "desc", "asc"] = "DESC",
|
|
65
|
+
sort: str = "id",
|
|
66
|
+
filter: FilterExpression | None = None,
|
|
67
|
+
raw: bool = False,
|
|
68
|
+
session=None,
|
|
69
|
+
) -> list[Self]:
|
|
70
|
+
cls = self.__class__
|
|
71
|
+
|
|
72
|
+
# Access check + apply domain filter
|
|
73
|
+
filter = await cls._check_access(Operation.READ, filter=filter)
|
|
74
|
+
|
|
75
|
+
session = cls._get_db_session(session)
|
|
76
|
+
|
|
77
|
+
# Use dialect from class
|
|
78
|
+
dialect = cls._dialect
|
|
79
|
+
|
|
80
|
+
stmt, values = cls._builder.build_search(
|
|
81
|
+
fields, start, end, limit, order, sort, filter
|
|
82
|
+
)
|
|
83
|
+
prepare = cls.prepare_list_ids if not raw else None
|
|
84
|
+
records: list[Self] = await session.execute(
|
|
85
|
+
stmt, values, prepare=prepare
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# если есть хоть одна запись и вообще нужно читать поля связей
|
|
89
|
+
fields_relation = [
|
|
90
|
+
(name, field)
|
|
91
|
+
for name, field in cls.get_relation_fields()
|
|
92
|
+
if name in fields
|
|
93
|
+
]
|
|
94
|
+
if records and fields_relation:
|
|
95
|
+
await cls._records_list_get_relation(
|
|
96
|
+
session, fields_relation, records
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return records
|
|
100
|
+
|
|
101
|
+
@hybridmethod
|
|
102
|
+
async def search_count(
|
|
103
|
+
self,
|
|
104
|
+
filter: FilterExpression | None = None,
|
|
105
|
+
session=None,
|
|
106
|
+
) -> int:
|
|
107
|
+
"""
|
|
108
|
+
Count records matching the filter.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
filter: Filter expression
|
|
112
|
+
session: Database session
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Number of matching records
|
|
116
|
+
"""
|
|
117
|
+
cls = self.__class__
|
|
118
|
+
session = cls._get_db_session(session)
|
|
119
|
+
|
|
120
|
+
stmt, values = cls._builder.build_search_count(filter)
|
|
121
|
+
result = await session.execute(stmt, values)
|
|
122
|
+
|
|
123
|
+
if result and len(result) > 0:
|
|
124
|
+
return result[0].get("count", 0)
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
@hybridmethod
|
|
128
|
+
async def exists(
|
|
129
|
+
self,
|
|
130
|
+
filter: FilterExpression | None = None,
|
|
131
|
+
session=None,
|
|
132
|
+
) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Check if any record matches the filter.
|
|
135
|
+
|
|
136
|
+
More efficient than search_count for existence checks.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
filter: Filter expression
|
|
140
|
+
session: Database session
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if at least one record exists
|
|
144
|
+
"""
|
|
145
|
+
cls = self.__class__
|
|
146
|
+
session = cls._get_db_session(session)
|
|
147
|
+
|
|
148
|
+
stmt, values = cls._builder.build_exists(filter)
|
|
149
|
+
result = await session.execute(stmt, values)
|
|
150
|
+
|
|
151
|
+
return bool(result)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
async def get_with_relations(
|
|
155
|
+
cls,
|
|
156
|
+
id,
|
|
157
|
+
fields=None,
|
|
158
|
+
fields_info={},
|
|
159
|
+
session=None,
|
|
160
|
+
) -> Self:
|
|
161
|
+
"""Get record with relations loaded."""
|
|
162
|
+
if not fields:
|
|
163
|
+
fields = []
|
|
164
|
+
session = cls._get_db_session(session)
|
|
165
|
+
|
|
166
|
+
dialect = cls._dialect
|
|
167
|
+
|
|
168
|
+
# защита, оставить только те поля, которые действительно хранятся в базе
|
|
169
|
+
fields_store = [
|
|
170
|
+
name for name in cls.get_store_fields() if name in fields
|
|
171
|
+
]
|
|
172
|
+
# если вдруг они не заданы, или таких нет, взять все
|
|
173
|
+
if not fields_store:
|
|
174
|
+
fields_store = [name for name in cls.get_store_fields()]
|
|
175
|
+
if "id" not in fields_store:
|
|
176
|
+
fields_store.append("id")
|
|
177
|
+
|
|
178
|
+
stmt, values = cls._builder.build_get(id, fields_store)
|
|
179
|
+
record_raw: list[Any] = await session.execute(stmt, values)
|
|
180
|
+
if not record_raw:
|
|
181
|
+
raise ValueError("Record not found")
|
|
182
|
+
record = cls(**record_raw[0])
|
|
183
|
+
|
|
184
|
+
# защита, оставить только те поля, которые являются отношениями (m2m, o2m, m2o)
|
|
185
|
+
# добавлена информаци о вложенных полях
|
|
186
|
+
fields_relation = [
|
|
187
|
+
(name, field, fields_info.get(name))
|
|
188
|
+
for name, field in cls.get_relation_fields()
|
|
189
|
+
if name in fields
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
# если есть хоть одна запись и вообще нужно читать поля связей
|
|
193
|
+
if record and fields_relation:
|
|
194
|
+
request_list = []
|
|
195
|
+
execute_list = []
|
|
196
|
+
|
|
197
|
+
# добавить запрос на o2m
|
|
198
|
+
for name, field, fields_nested in fields_relation:
|
|
199
|
+
relation_table = field.relation_table
|
|
200
|
+
relation_table_field = field.relation_table_field
|
|
201
|
+
|
|
202
|
+
if not fields_nested and relation_table:
|
|
203
|
+
fields_select = ["id"]
|
|
204
|
+
if relation_table.get_fields().get("name"):
|
|
205
|
+
fields_select.append("name")
|
|
206
|
+
if isinstance(field, AttachmentMany2one):
|
|
207
|
+
fields_select = (
|
|
208
|
+
relation_table.get_store_fields_omit_m2o()
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
fields_select = fields_nested
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
isinstance(field, (Many2one, AttachmentMany2one))
|
|
215
|
+
and relation_table
|
|
216
|
+
):
|
|
217
|
+
m2o_id = getattr(record, name)
|
|
218
|
+
stmt, val = relation_table._builder.build_get(
|
|
219
|
+
m2o_id, fields=fields_select
|
|
220
|
+
)
|
|
221
|
+
req = RequestBuilderForm(
|
|
222
|
+
stmt=stmt,
|
|
223
|
+
value=val,
|
|
224
|
+
field_name=name,
|
|
225
|
+
field=field,
|
|
226
|
+
fields=fields_select,
|
|
227
|
+
)
|
|
228
|
+
request_list.append(req)
|
|
229
|
+
execute_list.append(
|
|
230
|
+
session.execute(
|
|
231
|
+
req.stmt,
|
|
232
|
+
req.value,
|
|
233
|
+
prepare=req.function_prepare,
|
|
234
|
+
cursor=req.function_cursor,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
# если m2m или o2m необходимо посчитать длину, для пагинации
|
|
238
|
+
if isinstance(field, Many2many):
|
|
239
|
+
params = {
|
|
240
|
+
"id": record.id,
|
|
241
|
+
"comodel": relation_table,
|
|
242
|
+
"relation": field.many2many_table,
|
|
243
|
+
"column1": field.column1,
|
|
244
|
+
"column2": field.column2,
|
|
245
|
+
"fields": fields_select,
|
|
246
|
+
"order": "desc",
|
|
247
|
+
"start": 0,
|
|
248
|
+
"end": 40,
|
|
249
|
+
"sort": "id",
|
|
250
|
+
"limit": 40,
|
|
251
|
+
}
|
|
252
|
+
# records
|
|
253
|
+
execute_list.append(cls.get_many2many(**params))
|
|
254
|
+
params["fields"] = ["id"]
|
|
255
|
+
params["start"] = None
|
|
256
|
+
params["end"] = None
|
|
257
|
+
params["limit"] = None
|
|
258
|
+
# len
|
|
259
|
+
execute_list.append(cls.get_many2many(**params))
|
|
260
|
+
req = RequestBuilderForm(
|
|
261
|
+
stmt=None,
|
|
262
|
+
value=None,
|
|
263
|
+
field_name=name,
|
|
264
|
+
field=field,
|
|
265
|
+
fields=fields_select,
|
|
266
|
+
)
|
|
267
|
+
request_list.append(req)
|
|
268
|
+
|
|
269
|
+
if isinstance(field, One2many) and relation_table:
|
|
270
|
+
params = {
|
|
271
|
+
"start": 0,
|
|
272
|
+
"end": 40,
|
|
273
|
+
"limit": 40,
|
|
274
|
+
"fields": fields_select,
|
|
275
|
+
"filter": [(relation_table_field, "=", record.id)],
|
|
276
|
+
}
|
|
277
|
+
execute_list.append(relation_table.search(**params))
|
|
278
|
+
params["fields"] = ["id"]
|
|
279
|
+
params["start"] = None
|
|
280
|
+
params["end"] = None
|
|
281
|
+
params["limit"] = 1000
|
|
282
|
+
execute_list.append(relation_table.search(**params))
|
|
283
|
+
req = RequestBuilderForm(
|
|
284
|
+
stmt=None,
|
|
285
|
+
value=None,
|
|
286
|
+
field_name=name,
|
|
287
|
+
field=field,
|
|
288
|
+
fields=fields_select,
|
|
289
|
+
)
|
|
290
|
+
request_list.append(req)
|
|
291
|
+
|
|
292
|
+
if isinstance(field, AttachmentOne2many) and relation_table:
|
|
293
|
+
params = {
|
|
294
|
+
"start": 0,
|
|
295
|
+
"end": 40,
|
|
296
|
+
"limit": 40,
|
|
297
|
+
"fields": relation_table.get_store_fields_omit_m2o(),
|
|
298
|
+
"filter": [
|
|
299
|
+
("res_id", "=", record.id),
|
|
300
|
+
("res_model", "=", record.__table__),
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
execute_list.append(relation_table.search(**params))
|
|
304
|
+
params["fields"] = ["id"]
|
|
305
|
+
params["start"] = None
|
|
306
|
+
params["end"] = None
|
|
307
|
+
params["limit"] = 1000
|
|
308
|
+
execute_list.append(relation_table.search(**params))
|
|
309
|
+
req = RequestBuilderForm(
|
|
310
|
+
stmt=None,
|
|
311
|
+
value=None,
|
|
312
|
+
field_name=name,
|
|
313
|
+
field=field,
|
|
314
|
+
fields=relation_table.get_store_fields_omit_m2o(),
|
|
315
|
+
)
|
|
316
|
+
request_list.append(req)
|
|
317
|
+
|
|
318
|
+
if isinstance(field, One2one) and relation_table:
|
|
319
|
+
params = {
|
|
320
|
+
"limit": 1,
|
|
321
|
+
"fields": fields_select,
|
|
322
|
+
"filter": [(relation_table_field, "=", record.id)],
|
|
323
|
+
}
|
|
324
|
+
execute_list.append(relation_table.search(**params))
|
|
325
|
+
req = RequestBuilderForm(
|
|
326
|
+
stmt=None,
|
|
327
|
+
value=None,
|
|
328
|
+
field_name=name,
|
|
329
|
+
field=field,
|
|
330
|
+
fields=fields_select,
|
|
331
|
+
)
|
|
332
|
+
request_list.append(req)
|
|
333
|
+
|
|
334
|
+
# выполняем последовательно в транзакции, параллельно вне транзакции
|
|
335
|
+
results = await execute_maybe_parallel(execute_list)
|
|
336
|
+
|
|
337
|
+
# добавляем атрибуты к исходному объекту,
|
|
338
|
+
# получая удобное обращение через дот-нотацию
|
|
339
|
+
i = 0
|
|
340
|
+
for request_builder in request_list:
|
|
341
|
+
result = results[i]
|
|
342
|
+
|
|
343
|
+
if isinstance(
|
|
344
|
+
request_builder.field,
|
|
345
|
+
(Many2one, AttachmentMany2one, One2one),
|
|
346
|
+
):
|
|
347
|
+
# m2o нужно распаковать так как он тоже в списке
|
|
348
|
+
# если пустой список, то установить None
|
|
349
|
+
result = result[0] if result else None
|
|
350
|
+
|
|
351
|
+
if isinstance(
|
|
352
|
+
request_builder.field,
|
|
353
|
+
(Many2many, One2many, AttachmentOne2many),
|
|
354
|
+
):
|
|
355
|
+
# если m2m или o2m необбзодимо взять два результатата
|
|
356
|
+
# так как один из них это число всех строк таблицы
|
|
357
|
+
# для пагинации
|
|
358
|
+
fields_info = request_builder.field.relation_table.get_fields_info_list(
|
|
359
|
+
request_builder.fields
|
|
360
|
+
)
|
|
361
|
+
result = {
|
|
362
|
+
"data": result,
|
|
363
|
+
"fields": fields_info,
|
|
364
|
+
"total": len(results[i + 1]),
|
|
365
|
+
}
|
|
366
|
+
i += 1
|
|
367
|
+
|
|
368
|
+
setattr(record, request_builder.field_name, result)
|
|
369
|
+
i += 1
|
|
370
|
+
|
|
371
|
+
return record
|
|
372
|
+
|
|
373
|
+
async def update_with_relations(
|
|
374
|
+
self, payload: _M, fields=[], session=None
|
|
375
|
+
):
|
|
376
|
+
"""Update record with relations."""
|
|
377
|
+
session = self._get_db_session(session)
|
|
378
|
+
|
|
379
|
+
# Handle attachments
|
|
380
|
+
fields_attachments = [
|
|
381
|
+
(name, field)
|
|
382
|
+
for name, field in self.get_relation_fields_attachment()
|
|
383
|
+
if name in fields
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
if fields_attachments:
|
|
387
|
+
for name, field in fields_attachments:
|
|
388
|
+
if isinstance(field, AttachmentMany2one):
|
|
389
|
+
field_obj = getattr(payload, name)
|
|
390
|
+
if field_obj and field.relation_table:
|
|
391
|
+
# TODO: всегда создавать новую строку аттачмент с файлом
|
|
392
|
+
# также надо продумать механизм обновления уже существующего файла
|
|
393
|
+
# надо ли? или проще удалять
|
|
394
|
+
field_obj["res_id"] = self.id
|
|
395
|
+
# Оборачиваем dict в объект модели
|
|
396
|
+
attachment_payload = field.relation_table(**field_obj)
|
|
397
|
+
attachment_id = await field.relation_table.create(
|
|
398
|
+
attachment_payload, session
|
|
399
|
+
)
|
|
400
|
+
setattr(payload, name, attachment_id)
|
|
401
|
+
|
|
402
|
+
# Update stored fields
|
|
403
|
+
fields_store = [
|
|
404
|
+
name for name in self.get_store_fields() if name in fields
|
|
405
|
+
]
|
|
406
|
+
# Обновление сущности в базе без связей
|
|
407
|
+
if fields_store:
|
|
408
|
+
record_raw = await self.update(payload, fields, session)
|
|
409
|
+
|
|
410
|
+
# защита, оставить только те поля, которые являются отношениями (m2m, o2m)
|
|
411
|
+
# добавлена информаци о вложенных полях
|
|
412
|
+
fields_relation = [
|
|
413
|
+
(name, field)
|
|
414
|
+
for name, field in self.get_relation_fields_m2m_o2m()
|
|
415
|
+
if name in fields
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
if fields_relation:
|
|
419
|
+
request_list = []
|
|
420
|
+
field_list = []
|
|
421
|
+
|
|
422
|
+
for name, field in fields_relation:
|
|
423
|
+
field_obj = getattr(payload, name)
|
|
424
|
+
|
|
425
|
+
if isinstance(field, One2one):
|
|
426
|
+
params = {
|
|
427
|
+
"limit": 1,
|
|
428
|
+
"fields": ["id"],
|
|
429
|
+
"filter": [(field.relation_table_field, "=", self.id)],
|
|
430
|
+
}
|
|
431
|
+
record = await field.relation_table.search(**params)
|
|
432
|
+
if len(record):
|
|
433
|
+
request_list.append(record[0].update(field_obj))
|
|
434
|
+
|
|
435
|
+
if isinstance(field, (One2many, AttachmentOne2many)):
|
|
436
|
+
field_list.append(field)
|
|
437
|
+
# заменить в связанных полях виртуальный ид на вновь созданный
|
|
438
|
+
for obj in field_obj["created"]:
|
|
439
|
+
for k, v in obj.items():
|
|
440
|
+
f = getattr(field.relation_table, k)
|
|
441
|
+
if (
|
|
442
|
+
isinstance(f, (Many2one, AttachmentMany2one))
|
|
443
|
+
and v == "VirtualId"
|
|
444
|
+
):
|
|
445
|
+
obj[k] = self.id
|
|
446
|
+
|
|
447
|
+
data_created = [
|
|
448
|
+
field.relation_table(**obj)
|
|
449
|
+
for obj in field_obj["created"]
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
if isinstance(field, AttachmentOne2many):
|
|
453
|
+
for obj in data_created:
|
|
454
|
+
obj.res_id = self.id
|
|
455
|
+
|
|
456
|
+
if field_obj["created"]:
|
|
457
|
+
request_list.append(
|
|
458
|
+
field.relation_table.create_bulk(data_created)
|
|
459
|
+
)
|
|
460
|
+
if field_obj["deleted"]:
|
|
461
|
+
request_list.append(
|
|
462
|
+
field.relation_table.delete_bulk(
|
|
463
|
+
field_obj["deleted"]
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if isinstance(field, Many2many):
|
|
468
|
+
field_list.append(field)
|
|
469
|
+
|
|
470
|
+
# Replace virtual ID
|
|
471
|
+
for obj in field_obj["created"]:
|
|
472
|
+
for k, v in obj.items():
|
|
473
|
+
f = getattr(field.relation_table, k)
|
|
474
|
+
if (
|
|
475
|
+
isinstance(f, (Many2one, AttachmentMany2one))
|
|
476
|
+
and v == "VirtualId"
|
|
477
|
+
):
|
|
478
|
+
obj[k] = self.id
|
|
479
|
+
|
|
480
|
+
data_created = [
|
|
481
|
+
field.relation_table(**obj)
|
|
482
|
+
for obj in field_obj["created"]
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
if field_obj["created"]:
|
|
486
|
+
created_ids = await field.relation_table.create_bulk(
|
|
487
|
+
data_created
|
|
488
|
+
)
|
|
489
|
+
if "selected" not in field_obj:
|
|
490
|
+
field_obj["selected"] = []
|
|
491
|
+
field_obj["selected"] += [
|
|
492
|
+
rec["id"] for rec in created_ids
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
if field_obj.get("selected"):
|
|
496
|
+
data_selected = [
|
|
497
|
+
(self.id, id) for id in field_obj["selected"]
|
|
498
|
+
]
|
|
499
|
+
request_list.append(
|
|
500
|
+
self.link_many2many(field, data_selected)
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if field_obj.get("unselected"):
|
|
504
|
+
request_list.append(
|
|
505
|
+
self.unlink_many2many(
|
|
506
|
+
field, field_obj["unselected"]
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# выполняем последовательно
|
|
511
|
+
results = await execute_maybe_parallel(request_list)
|
|
512
|
+
|
|
513
|
+
return record_raw
|
dotorm/orm/protocol.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Protocols defining what ORM mixins expect from the model class."""
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
TYPE_CHECKING,
|
|
5
|
+
Any,
|
|
6
|
+
ClassVar,
|
|
7
|
+
Protocol,
|
|
8
|
+
Self,
|
|
9
|
+
Type,
|
|
10
|
+
Union,
|
|
11
|
+
runtime_checkable,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..builder.builder import Builder
|
|
16
|
+
from ..components.dialect import Dialect
|
|
17
|
+
from ..fields import Field
|
|
18
|
+
from ..access import Operation
|
|
19
|
+
import aiomysql
|
|
20
|
+
import asyncpg
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class DotModelProtocol(Protocol):
|
|
25
|
+
"""
|
|
26
|
+
Base protocol that all ORM mixins expect.
|
|
27
|
+
|
|
28
|
+
Defines the full interface that DotModel provides.
|
|
29
|
+
All mixins inherit from this protocol for type checking.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__table__: ClassVar[str]
|
|
33
|
+
__auto_create__: ClassVar[bool] = True
|
|
34
|
+
_pool: ClassVar[Union["aiomysql.Pool", "asyncpg.Pool"]]
|
|
35
|
+
_no_transaction: ClassVar[Type]
|
|
36
|
+
_dialect: ClassVar["Dialect"]
|
|
37
|
+
_builder: ClassVar["Builder"]
|
|
38
|
+
|
|
39
|
+
id: int
|
|
40
|
+
|
|
41
|
+
# Session
|
|
42
|
+
@classmethod
|
|
43
|
+
def _get_db_session(cls, session=None) -> Any: ...
|
|
44
|
+
|
|
45
|
+
# Access control (from AccessMixin)
|
|
46
|
+
@classmethod
|
|
47
|
+
async def _check_access(
|
|
48
|
+
cls,
|
|
49
|
+
operation: "Operation",
|
|
50
|
+
record_ids: list[int] | None = None,
|
|
51
|
+
filter: list | None = None,
|
|
52
|
+
) -> list | None: ...
|
|
53
|
+
|
|
54
|
+
# Field introspection
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_fields(cls) -> dict[str, "Field"]: ...
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_store_fields(cls) -> list[str]: ...
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def get_store_fields_omit_m2o(cls) -> list[str]: ...
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_relation_fields(cls) -> list[tuple[str, "Field"]]: ...
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_relation_fields_m2m_o2m(cls) -> list[tuple[str, "Field"]]: ...
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_relation_fields_attachment(cls) -> list[tuple[str, "Field"]]: ...
|
|
72
|
+
|
|
73
|
+
# Serialization
|
|
74
|
+
@classmethod
|
|
75
|
+
def prepare_list_ids(cls, rows: list[dict]) -> list[Self]: ...
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def prepare_form_id(cls, r: list) -> Self | None: ...
|
|
79
|
+
|
|
80
|
+
def json(
|
|
81
|
+
self,
|
|
82
|
+
include: Any = ...,
|
|
83
|
+
exclude: Any = ...,
|
|
84
|
+
exclude_none: bool = ...,
|
|
85
|
+
exclude_unset: bool = ...,
|
|
86
|
+
only_store: Any = ...,
|
|
87
|
+
mode: Any = ...,
|
|
88
|
+
) -> dict[str, Any]: ...
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_none_update_fields_set(cls) -> set[str]: ...
|
|
92
|
+
|
|
93
|
+
def __init__(self, **kwargs: Any) -> None: ...
|
|
94
|
+
|
|
95
|
+
# From OrmPrimaryMixin
|
|
96
|
+
async def update(
|
|
97
|
+
self,
|
|
98
|
+
payload: Self | None = None,
|
|
99
|
+
fields: Any = None,
|
|
100
|
+
session: Any = None,
|
|
101
|
+
) -> Any: ...
|
|
102
|
+
|
|
103
|
+
# From OrmMany2manyMixin
|
|
104
|
+
@classmethod
|
|
105
|
+
async def get_many2many(
|
|
106
|
+
cls,
|
|
107
|
+
id: int,
|
|
108
|
+
comodel: Any,
|
|
109
|
+
relation: str,
|
|
110
|
+
column1: str,
|
|
111
|
+
column2: str,
|
|
112
|
+
fields: list[str] | None = None,
|
|
113
|
+
order: str = "desc",
|
|
114
|
+
start: int | None = None,
|
|
115
|
+
end: int | None = None,
|
|
116
|
+
sort: str = "id",
|
|
117
|
+
limit: int | None = 10,
|
|
118
|
+
session: Any = None,
|
|
119
|
+
) -> list[Any]: ...
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
async def link_many2many(
|
|
123
|
+
cls,
|
|
124
|
+
field: Any,
|
|
125
|
+
values: list,
|
|
126
|
+
session: Any = None,
|
|
127
|
+
) -> Any: ...
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
async def unlink_many2many(
|
|
131
|
+
cls,
|
|
132
|
+
field: Any,
|
|
133
|
+
ids: list,
|
|
134
|
+
session: Any = None,
|
|
135
|
+
) -> Any: ...
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
async def _records_list_get_relation(
|
|
139
|
+
cls,
|
|
140
|
+
session: Any,
|
|
141
|
+
fields_relation: list[tuple[str, "Field"]],
|
|
142
|
+
records: list[Any],
|
|
143
|
+
) -> None: ...
|
|
144
|
+
|
|
145
|
+
# From DDLMixin
|
|
146
|
+
@staticmethod
|
|
147
|
+
def format_default_value(value: Any) -> str: ...
|
dotorm/orm/utils.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Utility functions for dotorm."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Coroutine, Sequence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def execute_maybe_parallel(
|
|
8
|
+
coroutines: Sequence[Coroutine[Any, Any, Any]],
|
|
9
|
+
) -> list[Any]:
|
|
10
|
+
"""
|
|
11
|
+
Execute coroutines in parallel or sequentially depending on transaction context.
|
|
12
|
+
|
|
13
|
+
If inside a transaction (single connection), executes sequentially to avoid
|
|
14
|
+
asyncpg "another operation is in progress" error.
|
|
15
|
+
|
|
16
|
+
If outside transaction (pool), executes in parallel for better performance.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
coroutines: List of coroutines to execute
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of results in the same order as input coroutines
|
|
23
|
+
"""
|
|
24
|
+
from ..databases.postgres.transaction import get_current_session
|
|
25
|
+
|
|
26
|
+
if not coroutines:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
# Check if we're inside a transaction
|
|
30
|
+
if get_current_session() is not None:
|
|
31
|
+
# Inside transaction - execute sequentially
|
|
32
|
+
results = []
|
|
33
|
+
for coro in coroutines:
|
|
34
|
+
result = await coro
|
|
35
|
+
results.append(result)
|
|
36
|
+
return results
|
|
37
|
+
else:
|
|
38
|
+
# Outside transaction - execute in parallel
|
|
39
|
+
return list(await asyncio.gather(*coroutines))
|