lush-sqlalchemyx 0.2.1__tar.gz → 0.3.0__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 (30) hide show
  1. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/PKG-INFO +1 -1
  2. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/pyproject.toml +21 -1
  3. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/_compat.py +1 -1
  4. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/__init__.py +72 -24
  5. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/_async.py +164 -28
  6. lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_async_v2.py +184 -0
  7. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/_common.py +37 -19
  8. lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_pagination.py +90 -0
  9. lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_params.py +32 -0
  10. lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_repository.py +311 -0
  11. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/_sync.py +164 -63
  12. lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_sync_v2.py +183 -0
  13. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/flask/ext.py +2 -2
  14. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/README.md +0 -0
  15. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/__init__.py +0 -0
  16. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/__init__.py +0 -0
  17. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/__init__.py +0 -0
  18. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/fastapi/__init__.py +0 -0
  19. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/fastapi/depends/__init__.py +0 -0
  20. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/flask/__init__.py +0 -0
  21. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/__init__.py +0 -0
  22. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/__init__.py +0 -0
  23. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/manager.py +0 -0
  24. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/mapper.py +0 -0
  25. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/sync_manager.py +0 -0
  26. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/sync_mapper.py +0 -0
  27. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/py.typed +0 -0
  28. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/same_impl_just_warn_wrapper.py +0 -0
  29. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/shortcuts/__init__.py +0 -0
  30. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/shortcuts/meta.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lush-sqlalchemyx
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: SQLAlchemy helpers (DAL) and async MySQL managers, with some web frameworks integrations
5
5
  Author: straydragon
6
6
  Author-email: straydragon <straydragonl@foxmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lush-sqlalchemyx"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "SQLAlchemy helpers (DAL) and async MySQL managers, with some web frameworks integrations"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -47,6 +47,8 @@ dev = [
47
47
  "sqlalchemy[asyncio]>=2.0.43",
48
48
  "flask-sqlalchemy>=3.1.1",
49
49
  "flask>=3.0.0",
50
+ "pytest-benchmark>=5.2.3",
51
+ "pymysql>=1.1.2",
50
52
  ]
51
53
 
52
54
  [tool.pytest.ini_options]
@@ -147,6 +149,9 @@ ignore = [
147
149
  ]
148
150
 
149
151
  [tool.ruff.lint.per-file-ignores]
152
+ "**/dal/_*_v2.py" = [
153
+ "ARG003", # extra 参数是 ABC 合规所必需但在部分方法中未使用
154
+ ]
150
155
  "tests/**/*.py" = [
151
156
  # === 测试代码质量 ===
152
157
  "B011", # assert False应改为raise AssertionError
@@ -249,6 +254,7 @@ reportAny = "none"
249
254
  reportExplicitAny = "none"
250
255
  reportConstantRedefinition = "none"
251
256
  reportUnnecessaryComparison = "none"
257
+ reportImplicitOverride = false
252
258
 
253
259
  [[tool.basedpyright.executionEnvironments]]
254
260
  root = "tests"
@@ -277,10 +283,24 @@ reportMissingTypeArgument = false
277
283
  reportOptionalMemberAccess = false
278
284
  reportAttributeAccessIssue = "none"
279
285
  reportGeneralTypeIssues = false
286
+ reportAssignmentType = false
280
287
  reportOperatorIssue = false
281
288
  reportIndexIssue = false
282
289
  reportInvalidTypeArguments = "none"
290
+ reportUnknownLambdaType = false
291
+ reportImplicitOverride = false
292
+ reportUnusedClass = false
293
+
283
294
 
295
+ [[tool.basedpyright.executionEnvironments]]
296
+ root = "src/lush_sqlalchemyx/integrations"
297
+ reportAttributeAccessIssue = "none"
298
+ reportAssignmentType = false
299
+ reportImportCycles = false
300
+ reportUnknownVariableType = false
301
+ reportUnknownParameterType = false
302
+ reportUnknownMemberType = false
303
+ reportUnknownArgumentType = false
284
304
 
285
305
  [[tool.basedpyright.executionEnvironments]]
286
306
  root = "src"
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  _HAS_ASYNC: bool
6
6
  try:
7
- from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession # noqa: F401
7
+ from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession # noqa: F401 # pyright: ignore[reportUnusedImport]
8
8
 
9
9
  _HAS_ASYNC = True
10
10
  except ImportError: # pragma: no cover
@@ -4,14 +4,22 @@
4
4
  """
5
5
 
6
6
  # --- shared (sync/async agnostic) ---
7
- # --- lush-dal-protocol protocols (ORM 无关的抽象层) ---
7
+ # --- lush-dal-protocol ABCs (ORM 无关的抽象层) ---
8
8
  from lush_dal_protocol import (
9
- AsyncBaseDALProtocol,
10
- AsyncReadDALProtocol,
11
- AsyncWriteDALProtocol,
12
- SyncBaseDALProtocol,
13
- SyncReadDALProtocol,
14
- SyncWriteDALProtocol,
9
+ AbstractAsyncAdvancedWriteDAL,
10
+ AbstractAsyncBaseDAL,
11
+ AbstractAsyncBatchFieldDAL,
12
+ AbstractAsyncLockDAL,
13
+ AbstractAsyncRawSQLDAL,
14
+ AbstractAsyncReadDAL,
15
+ AbstractAsyncWriteDAL,
16
+ AbstractSyncAdvancedWriteDAL,
17
+ AbstractSyncBaseDAL,
18
+ AbstractSyncBatchFieldDAL,
19
+ AbstractSyncLockDAL,
20
+ AbstractSyncRawSQLDAL,
21
+ AbstractSyncReadDAL,
22
+ AbstractSyncWriteDAL,
15
23
  )
16
24
 
17
25
  # --- async (requires sqlalchemy[asyncio]) ---
@@ -32,6 +40,9 @@ from ._async import (
32
40
  async_temp_set_lock_wait_timeout,
33
41
  async_with_retry,
34
42
  )
43
+
44
+ # --- V2 (ABC-compliant, options-based) ---
45
+ from ._async_v2 import AsyncBaseDALV2, AsyncReadDALV2, AsyncWriteDALV2
35
46
  from ._common import (
36
47
  DEFAULT_RETRY_CONFIG,
37
48
  OPTIMISTIC_LOCK_ERROR_MSG_TRAIT,
@@ -39,7 +50,6 @@ from ._common import (
39
50
  READONLY_SESSION_FLAG,
40
51
  BaseCU,
41
52
  BaseDTO,
42
- BaseModelT,
43
53
  CUModelT,
44
54
  DBRetryableError,
45
55
  DTOModelT,
@@ -50,16 +60,27 @@ from ._common import (
50
60
  SQLATableT,
51
61
  StdBaseCU,
52
62
  StdBaseDTO,
53
- T,
54
- V,
55
- _ensure_strict_fields,
56
63
  escape_like,
57
64
  filtered_in_sql_values,
58
65
  )
59
66
 
60
67
  # Event listener references — accessed by tests via getattr(module, name).
61
- from ._common import __prevent_readonly_write as __prevent_readonly_write
62
- from ._common import __receive_before_flush as __receive_before_flush
68
+ from ._common import __prevent_readonly_write as __prevent_readonly_write # pyright: ignore[reportPrivateUsage]
69
+ from ._common import __receive_before_flush as __receive_before_flush # pyright: ignore[reportPrivateUsage]
70
+ from ._pagination import (
71
+ CursorPagination,
72
+ CursorResult,
73
+ OffsetPagination,
74
+ PageResult,
75
+ build_cursor_stmt,
76
+ build_offset_stmt,
77
+ decode_cursor,
78
+ encode_cursor,
79
+ make_cursor_result,
80
+ make_page_result,
81
+ )
82
+ from ._params import SQLAExtra
83
+ from ._repository import AsyncSQLAlchemyRepository, SyncSQLAlchemyRepository
63
84
 
64
85
  # --- sync ---
65
86
  from ._sync import (
@@ -79,6 +100,7 @@ from ._sync import (
79
100
  sync_temp_set_lock_wait_timeout,
80
101
  sync_with_retry,
81
102
  )
103
+ from ._sync_v2 import SyncBaseDALV2, SyncReadDALV2, SyncWriteDALV2
82
104
 
83
105
  __all__ = (
84
106
  # common
@@ -88,7 +110,6 @@ __all__ = (
88
110
  "READONLY_SESSION_FLAG",
89
111
  "BaseCU",
90
112
  "BaseDTO",
91
- "BaseModelT",
92
113
  "CUModelT",
93
114
  "DBRetryableError",
94
115
  "DTOModelT",
@@ -99,9 +120,6 @@ __all__ = (
99
120
  "SoftDeleteTableMixin",
100
121
  "StdBaseCU",
101
122
  "StdBaseDTO",
102
- "T",
103
- "V",
104
- "_ensure_strict_fields",
105
123
  "escape_like",
106
124
  "filtered_in_sql_values",
107
125
  # async
@@ -136,11 +154,41 @@ __all__ = (
136
154
  "SyncXDALOp",
137
155
  "sync_temp_set_lock_wait_timeout",
138
156
  "sync_with_retry",
139
- # lush-dal-protocol protocols
140
- "AsyncBaseDALProtocol",
141
- "AsyncReadDALProtocol",
142
- "AsyncWriteDALProtocol",
143
- "SyncBaseDALProtocol",
144
- "SyncReadDALProtocol",
145
- "SyncWriteDALProtocol",
157
+ # pagination
158
+ "CursorPagination",
159
+ "CursorResult",
160
+ "OffsetPagination",
161
+ "PageResult",
162
+ "build_cursor_stmt",
163
+ "build_offset_stmt",
164
+ "decode_cursor",
165
+ "encode_cursor",
166
+ "make_cursor_result",
167
+ "make_page_result",
168
+ # V2
169
+ "AsyncBaseDALV2",
170
+ "AsyncReadDALV2",
171
+ "AsyncWriteDALV2",
172
+ "SQLAExtra",
173
+ "SyncBaseDALV2",
174
+ "SyncReadDALV2",
175
+ "SyncWriteDALV2",
176
+ # repository
177
+ "AsyncSQLAlchemyRepository",
178
+ "SyncSQLAlchemyRepository",
179
+ # lush-dal-protocol ABCs
180
+ "AbstractAsyncAdvancedWriteDAL",
181
+ "AbstractAsyncBaseDAL",
182
+ "AbstractAsyncBatchFieldDAL",
183
+ "AbstractAsyncLockDAL",
184
+ "AbstractAsyncRawSQLDAL",
185
+ "AbstractAsyncReadDAL",
186
+ "AbstractAsyncWriteDAL",
187
+ "AbstractSyncAdvancedWriteDAL",
188
+ "AbstractSyncBaseDAL",
189
+ "AbstractSyncBatchFieldDAL",
190
+ "AbstractSyncLockDAL",
191
+ "AbstractSyncRawSQLDAL",
192
+ "AbstractSyncReadDAL",
193
+ "AbstractSyncWriteDAL",
146
194
  )
@@ -9,7 +9,8 @@ from __future__ import annotations
9
9
  import asyncio
10
10
  import datetime
11
11
  import logging
12
- from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
12
+ import warnings
13
+ from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
13
14
  from contextlib import asynccontextmanager, suppress
14
15
  from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, ParamSpec, TypeVar, cast
15
16
 
@@ -101,7 +102,7 @@ def async_with_retry(
101
102
  async def async_temp_set_lock_wait_timeout(
102
103
  session: AsyncSession,
103
104
  timeout_seconds: int | None,
104
- ) -> AsyncIterator[None]:
105
+ ) -> AsyncGenerator[None, None]:
105
106
  """临时设置锁等待超时时间的上下文管理器"""
106
107
  if timeout_seconds is None:
107
108
  yield
@@ -140,10 +141,23 @@ class ReadOnlyBasicAsyncBaseTable(AsyncSqlATableBase, ReadOnlyMixin):
140
141
 
141
142
 
142
143
  class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
143
- """标准异步表类:符合新规范的表应该使用这个."""
144
+ """标准异步表类: 包含 id/时间戳/操作人/软删除等标准字段.
145
+
146
+ .. deprecated::
147
+ 此类预设了特定业务字段, 下游应自行继承 ``BasicAsyncBaseTable`` 定义所需字段.
148
+ """
144
149
 
145
150
  __abstract__ = True
146
151
 
152
+ def __init_subclass__(cls, **kwargs: Any) -> None:
153
+ super().__init_subclass__(**kwargs)
154
+ if not cls.__dict__.get("__abstract__", False):
155
+ warnings.warn(
156
+ f"{cls.__name__} 继承了已废弃的 StdAsyncBaseTable, 请改为直接继承 BasicAsyncBaseTable 并自行定义所需字段",
157
+ DeprecationWarning,
158
+ stacklevel=2,
159
+ )
160
+
147
161
  id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
148
162
 
149
163
  create_datetime: Mapped[datetime.datetime] = mapped_column(
@@ -177,10 +191,23 @@ class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
177
191
 
178
192
 
179
193
  class StdReadOnlyBasicAsyncBaseTable(ReadOnlyBasicAsyncBaseTable):
180
- """标准只读表基类."""
194
+ """标准只读异步表基类: 包含 id/时间戳/操作人等标准字段.
195
+
196
+ .. deprecated::
197
+ 此类预设了特定业务字段, 下游应自行继承 ``ReadOnlyBasicAsyncBaseTable`` 定义所需字段.
198
+ """
181
199
 
182
200
  __abstract__ = True
183
201
 
202
+ def __init_subclass__(cls, **kwargs: Any) -> None:
203
+ super().__init_subclass__(**kwargs)
204
+ if not cls.__dict__.get("__abstract__", False):
205
+ warnings.warn(
206
+ f"{cls.__name__} 继承了已废弃的 StdReadOnlyBasicAsyncBaseTable, 请改为直接继承 ReadOnlyBasicAsyncBaseTable 并自行定义所需字段",
207
+ DeprecationWarning,
208
+ stacklevel=2,
209
+ )
210
+
184
211
  id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
185
212
 
186
213
  create_datetime: Mapped[datetime.datetime] = mapped_column(
@@ -252,7 +279,7 @@ class AsyncRawReadDAL:
252
279
  where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
253
280
  with_deleted: bool = False,
254
281
  batch_size: int = 500,
255
- ) -> AsyncIterator[AsyncSQLATableT]:
282
+ ) -> AsyncGenerator[AsyncSQLATableT, None]:
256
283
  if not hasattr(table_class, "id") or not isinstance(getattr(table_class, "id", None), InstrumentedAttribute):
257
284
  raise ValueError(f"表 {table_class.__name__} 必须有 id 字段才能使用迭代方法")
258
285
 
@@ -398,15 +425,15 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
398
425
  return entity is not None
399
426
 
400
427
  @classmethod
401
- async def get_by_id_for_update(
428
+ async def _get_by_id_for_update_core(
402
429
  cls,
403
430
  session: AsyncSession,
404
431
  entity_id: int,
405
432
  *,
406
- lock_wait_timeout: int | None = None,
433
+ timeout: int | None = None,
407
434
  ) -> AsyncSQLATableT | None:
408
435
  try:
409
- async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
436
+ async with async_temp_set_lock_wait_timeout(session, timeout):
410
437
  stmt = (
411
438
  sa.select(cls._Table)
412
439
  .where(cls._Table.id == entity_id) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
@@ -421,19 +448,29 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
421
448
  raise
422
449
 
423
450
  @classmethod
424
- async def batch_get_for_update(
451
+ async def get_by_id_for_update(
425
452
  cls,
426
453
  session: AsyncSession,
427
- entity_ids: Iterable[int],
454
+ entity_id: int,
428
455
  *,
429
456
  lock_wait_timeout: int | None = None,
457
+ ) -> AsyncSQLATableT | None:
458
+ return await cls._get_by_id_for_update_core(session, entity_id, timeout=lock_wait_timeout)
459
+
460
+ @classmethod
461
+ async def _batch_get_for_update_core(
462
+ cls,
463
+ session: AsyncSession,
464
+ entity_ids: Iterable[int],
465
+ *,
466
+ timeout: int | None = None,
430
467
  ) -> list[AsyncSQLATableT]:
431
468
  filtered_ids = filtered_in_sql_values(entity_ids, int)
432
469
  if not filtered_ids:
433
470
  return []
434
471
 
435
472
  try:
436
- async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
473
+ async with async_temp_set_lock_wait_timeout(session, timeout):
437
474
  stmt = (
438
475
  sa.select(cls._Table)
439
476
  .where(cls._Table.id.in_(filtered_ids)) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
@@ -448,15 +485,25 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
448
485
  raise
449
486
 
450
487
  @classmethod
451
- async def get_one_for_update(
488
+ async def batch_get_for_update(
452
489
  cls,
453
490
  session: AsyncSession,
491
+ entity_ids: Iterable[int],
454
492
  *,
455
- where_clauses: list[ColumnExpressionArgument[bool]],
456
493
  lock_wait_timeout: int | None = None,
494
+ ) -> list[AsyncSQLATableT]:
495
+ return await cls._batch_get_for_update_core(session, entity_ids, timeout=lock_wait_timeout)
496
+
497
+ @classmethod
498
+ async def _get_one_for_update_core(
499
+ cls,
500
+ session: AsyncSession,
501
+ *,
502
+ where_clauses: list[ColumnExpressionArgument[bool]],
503
+ timeout: int | None = None,
457
504
  ) -> AsyncSQLATableT | None:
458
505
  try:
459
- async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
506
+ async with async_temp_set_lock_wait_timeout(session, timeout):
460
507
  stmt = sa.select(cls._Table).with_for_update()
461
508
 
462
509
  for clause in where_clauses:
@@ -470,6 +517,16 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
470
517
  raise DBRetryableError(f"{PESSIMISTIC_LOCK_ERROR_MSG_TRAIT}-条件锁等待超时: {error_msg}") from e
471
518
  raise
472
519
 
520
+ @classmethod
521
+ async def get_one_for_update(
522
+ cls,
523
+ session: AsyncSession,
524
+ *,
525
+ where_clauses: list[ColumnExpressionArgument[bool]],
526
+ lock_wait_timeout: int | None = None,
527
+ ) -> AsyncSQLATableT | None:
528
+ return await cls._get_one_for_update_core(session, where_clauses=where_clauses, timeout=lock_wait_timeout)
529
+
473
530
  @classmethod
474
531
  async def iter_record_dtos(
475
532
  cls,
@@ -478,7 +535,7 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
478
535
  where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
479
536
  with_deleted: bool = False,
480
537
  batch_size: int = 500,
481
- ) -> AsyncIterator[DTOModelT]:
538
+ ) -> AsyncGenerator[DTOModelT, None]:
482
539
  async for entity in cls._iter_records(
483
540
  session,
484
541
  cls._Table,
@@ -522,7 +579,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
522
579
  ) -> AsyncSQLATableT:
523
580
  if session.info.get(READONLY_SESSION_FLAG):
524
581
  raise TypeError("当前会话被标记为只读, 不允许执行写入操作")
525
- entity = cu.to_sqla_model()
582
+ entity = cu.to_orm_model()
526
583
  session.add(entity)
527
584
  await session.flush()
528
585
  if need_refresh:
@@ -568,10 +625,10 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
568
625
  cls,
569
626
  session: AsyncSession,
570
627
  entity_id: int,
571
- vo: CUModelT,
628
+ cu: CUModelT,
572
629
  need_refresh: bool = True,
573
630
  ) -> DTOModelT | None:
574
- entity = await cls.update_only_set_by_id(session, entity_id, vo, need_refresh)
631
+ entity = await cls.update_only_set_by_id(session, entity_id, cu, need_refresh)
575
632
  if entity:
576
633
  return cls._DTO.model_validate(entity)
577
634
  return None
@@ -586,7 +643,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
586
643
  _ensure_strict_fields(provided_keys=provided_keys, allowed_names=allowed_names, strict=strict)
587
644
 
588
645
  @classmethod
589
- async def update_full_by_id(
646
+ async def _update_full_by_id_core(
590
647
  cls,
591
648
  session: AsyncSession,
592
649
  entity_id: int,
@@ -620,7 +677,19 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
620
677
  return entity
621
678
 
622
679
  @classmethod
623
- async def update_partial_by_id(
680
+ async def update_full_by_id(
681
+ cls,
682
+ session: AsyncSession,
683
+ entity_id: int,
684
+ cu: CUModelT,
685
+ *,
686
+ need_refresh: bool = False,
687
+ strict_missing: bool = True,
688
+ ) -> AsyncSQLATableT | None:
689
+ return await cls._update_full_by_id_core(session, entity_id, cu, need_refresh=need_refresh, strict_missing=strict_missing)
690
+
691
+ @classmethod
692
+ async def _update_partial_by_id_core(
624
693
  cls,
625
694
  session: AsyncSession,
626
695
  entity_id: int,
@@ -687,6 +756,30 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
687
756
  await session.refresh(entity)
688
757
  return entity
689
758
 
759
+ @classmethod
760
+ async def update_partial_by_id(
761
+ cls,
762
+ session: AsyncSession,
763
+ entity_id: int,
764
+ cu: CUModelT,
765
+ *,
766
+ need_refresh: bool = False,
767
+ fields: set[InstrumentedAttribute[Any]] | set[sa.Column[Any]] | None = None,
768
+ none_policy: Literal["ignore", "allow", "forbid"] = "ignore",
769
+ none_policy_overrides: dict[InstrumentedAttribute[Any] | sa.Column[Any], Literal["ignore", "allow", "forbid"]] | None = None,
770
+ strict: bool = False,
771
+ ) -> AsyncSQLATableT | None:
772
+ return await cls._update_partial_by_id_core(
773
+ session,
774
+ entity_id,
775
+ cu,
776
+ need_refresh=need_refresh,
777
+ fields=fields,
778
+ none_policy=none_policy,
779
+ none_policy_overrides=none_policy_overrides,
780
+ strict=strict,
781
+ )
782
+
690
783
  @classmethod
691
784
  async def delete_by_id(
692
785
  cls,
@@ -711,7 +804,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
711
804
  where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
712
805
  with_deleted: bool = False,
713
806
  batch_size: int = 500,
714
- ) -> AsyncIterator[AsyncSQLATableT]:
807
+ ) -> AsyncGenerator[AsyncSQLATableT, None]:
715
808
  async for entity in cls._iter_records(
716
809
  session,
717
810
  cls._Table,
@@ -722,11 +815,11 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
722
815
  yield entity
723
816
 
724
817
  @classmethod
725
- async def batch_update_by_conditions(
818
+ async def _batch_update_by_conditions_core(
726
819
  cls,
727
820
  session: AsyncSession,
728
821
  *,
729
- whereclause: list[ColumnExpressionArgument[bool]],
822
+ conditions: list[ColumnExpressionArgument[bool]],
730
823
  update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
731
824
  updater_id: int | None = None,
732
825
  ) -> int:
@@ -751,13 +844,24 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
751
844
  if hasattr(cls._Table, "update_operator_id") and updater_id is not None:
752
845
  final_update_data["update_operator_id"] = updater_id
753
846
 
754
- stmt = sa.update(cls._Table).where(*whereclause).values(**final_update_data)
847
+ stmt = sa.update(cls._Table).where(*conditions).values(**final_update_data)
755
848
 
756
849
  result = await session.execute(stmt)
757
850
  await session.flush()
758
851
 
759
852
  return result.rowcount # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType]
760
853
 
854
+ @classmethod
855
+ async def batch_update_by_conditions(
856
+ cls,
857
+ session: AsyncSession,
858
+ *,
859
+ whereclause: list[ColumnExpressionArgument[bool]],
860
+ update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
861
+ updater_id: int | None = None,
862
+ ) -> int:
863
+ return await cls._batch_update_by_conditions_core(session, conditions=whereclause, update_data=update_data, updater_id=updater_id)
864
+
761
865
  @classmethod
762
866
  async def batch_update_by_ids(
763
867
  cls,
@@ -771,15 +875,15 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
771
875
  if not filtered_ids:
772
876
  return 0
773
877
  _id_column = cls._Table.id # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType, reportUnknownMemberType]
774
- return await cls.batch_update_by_conditions(
878
+ return await cls._batch_update_by_conditions_core(
775
879
  session,
776
- whereclause=[_id_column.in_(filtered_ids)], # pyright: ignore[reportUnknownMemberType]
880
+ conditions=[_id_column.in_(filtered_ids)], # pyright: ignore[reportUnknownMemberType]
777
881
  update_data=update_data,
778
882
  updater_id=updater_id,
779
883
  )
780
884
 
781
885
  @classmethod
782
- async def update_only_set_with_optimistic_lock(
886
+ async def _update_only_set_with_optimistic_lock_core(
783
887
  cls,
784
888
  session: AsyncSession,
785
889
  entity_id: int,
@@ -830,13 +934,45 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
830
934
 
831
935
  raise DBRetryableError(f"{OPTIMISTIC_LOCK_ERROR_MSG_TRAIT}-版本号不匹配({entity_id=}, {expected_version=})")
832
936
 
937
+ @classmethod
938
+ async def update_only_set_with_optimistic_lock(
939
+ cls,
940
+ session: AsyncSession,
941
+ entity_id: int,
942
+ cu: CUModelT,
943
+ *,
944
+ expected_version: int,
945
+ need_refresh: bool = False,
946
+ version_field: str = "version",
947
+ ) -> AsyncSQLATableT | None:
948
+ return await cls._update_only_set_with_optimistic_lock_core(
949
+ session,
950
+ entity_id,
951
+ cu,
952
+ expected_version=expected_version,
953
+ need_refresh=need_refresh,
954
+ version_field=version_field,
955
+ )
956
+
833
957
 
834
958
  class AsyncXDALOp(AsyncRawReadDAL, AsyncRawDAL):
835
959
  """扩展数据访问操作类."""
836
960
 
837
961
 
838
962
  class AsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, DTOModelT], AsyncWriteDAL[AsyncSQLATableT, DTOModelT, CUModelT]):
839
- """基础数据访问层."""
963
+ """基础数据访问层.
964
+
965
+ .. deprecated:: 0.3.0
966
+ V1 DAL 将在 1.0 移除, 请迁移至 ``AsyncBaseDALV2``.
967
+ """
968
+
969
+ def __init_subclass__(cls, **kwargs: Any) -> None:
970
+ super().__init_subclass__(**kwargs)
971
+ warnings.warn(
972
+ f"{cls.__name__} 继承了 V1 AsyncBaseDAL, 建议迁移至 AsyncBaseDALV2",
973
+ DeprecationWarning,
974
+ stacklevel=2,
975
+ )
840
976
 
841
977
 
842
978
  class ReadOnlyAsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, ReadOnlyDTOModelT]):