lush-sqlalchemyx 0.2.1__tar.gz → 0.3.1__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.1}/PKG-INFO +1 -1
  2. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/pyproject.toml +26 -1
  3. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/_compat.py +1 -1
  4. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/__init__.py +72 -24
  5. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/_async.py +166 -29
  6. lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_async_v2.py +184 -0
  7. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/_common.py +42 -21
  8. lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_pagination.py +90 -0
  9. lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_params.py +32 -0
  10. lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_repository.py +311 -0
  11. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/_sync.py +166 -64
  12. lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_sync_v2.py +183 -0
  13. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/flask/ext.py +2 -2
  14. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/README.md +0 -0
  15. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/__init__.py +0 -0
  16. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/__init__.py +0 -0
  17. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/__init__.py +0 -0
  18. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/fastapi/__init__.py +0 -0
  19. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/fastapi/depends/__init__.py +0 -0
  20. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/flask/__init__.py +0 -0
  21. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/__init__.py +0 -0
  22. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/__init__.py +0 -0
  23. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/manager.py +0 -0
  24. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/mapper.py +0 -0
  25. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/sync_manager.py +0 -0
  26. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/sync_mapper.py +0 -0
  27. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/py.typed +0 -0
  28. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/same_impl_just_warn_wrapper.py +0 -0
  29. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/shortcuts/__init__.py +0 -0
  30. {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/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.1
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.1"
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,29 @@ 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"
307
+
308
+ [[tool.basedpyright.executionEnvironments]]
309
+ root = "examples"
310
+ # examples 作为下游类型检查 SSOT, 仅继承全局设置, 不做额外放宽.
311
+ # 如果此处报错, 说明库的类型变更破坏了合法下游用法.
@@ -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
@@ -120,7 +121,8 @@ async def async_temp_set_lock_wait_timeout(
120
121
  # Async Table bases
121
122
  # ---------------------------------------------------------------------------
122
123
 
123
- AsyncSQLATableT = TypeVar("AsyncSQLATableT", bound="AsyncSqlATableBase")
124
+ # 隐含约束: AsyncSQLATableT 应为 AsyncSqlATableBase (AsyncAttrs + DeclarativeBase) 子类.
125
+ AsyncSQLATableT = TypeVar("AsyncSQLATableT")
124
126
 
125
127
 
126
128
  class AsyncSqlATableBase(AsyncAttrs, DeclarativeBase):
@@ -140,10 +142,23 @@ class ReadOnlyBasicAsyncBaseTable(AsyncSqlATableBase, ReadOnlyMixin):
140
142
 
141
143
 
142
144
  class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
143
- """标准异步表类:符合新规范的表应该使用这个."""
145
+ """标准异步表类: 包含 id/时间戳/操作人/软删除等标准字段.
146
+
147
+ .. deprecated::
148
+ 此类预设了特定业务字段, 下游应自行继承 ``BasicAsyncBaseTable`` 定义所需字段.
149
+ """
144
150
 
145
151
  __abstract__ = True
146
152
 
153
+ def __init_subclass__(cls, **kwargs: Any) -> None:
154
+ super().__init_subclass__(**kwargs)
155
+ if not cls.__dict__.get("__abstract__", False):
156
+ warnings.warn(
157
+ f"{cls.__name__} 继承了已废弃的 StdAsyncBaseTable, 请改为直接继承 BasicAsyncBaseTable 并自行定义所需字段",
158
+ DeprecationWarning,
159
+ stacklevel=2,
160
+ )
161
+
147
162
  id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
148
163
 
149
164
  create_datetime: Mapped[datetime.datetime] = mapped_column(
@@ -177,10 +192,23 @@ class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
177
192
 
178
193
 
179
194
  class StdReadOnlyBasicAsyncBaseTable(ReadOnlyBasicAsyncBaseTable):
180
- """标准只读表基类."""
195
+ """标准只读异步表基类: 包含 id/时间戳/操作人等标准字段.
196
+
197
+ .. deprecated::
198
+ 此类预设了特定业务字段, 下游应自行继承 ``ReadOnlyBasicAsyncBaseTable`` 定义所需字段.
199
+ """
181
200
 
182
201
  __abstract__ = True
183
202
 
203
+ def __init_subclass__(cls, **kwargs: Any) -> None:
204
+ super().__init_subclass__(**kwargs)
205
+ if not cls.__dict__.get("__abstract__", False):
206
+ warnings.warn(
207
+ f"{cls.__name__} 继承了已废弃的 StdReadOnlyBasicAsyncBaseTable, 请改为直接继承 ReadOnlyBasicAsyncBaseTable 并自行定义所需字段",
208
+ DeprecationWarning,
209
+ stacklevel=2,
210
+ )
211
+
184
212
  id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
185
213
 
186
214
  create_datetime: Mapped[datetime.datetime] = mapped_column(
@@ -252,7 +280,7 @@ class AsyncRawReadDAL:
252
280
  where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
253
281
  with_deleted: bool = False,
254
282
  batch_size: int = 500,
255
- ) -> AsyncIterator[AsyncSQLATableT]:
283
+ ) -> AsyncGenerator[AsyncSQLATableT, None]:
256
284
  if not hasattr(table_class, "id") or not isinstance(getattr(table_class, "id", None), InstrumentedAttribute):
257
285
  raise ValueError(f"表 {table_class.__name__} 必须有 id 字段才能使用迭代方法")
258
286
 
@@ -398,15 +426,15 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
398
426
  return entity is not None
399
427
 
400
428
  @classmethod
401
- async def get_by_id_for_update(
429
+ async def _get_by_id_for_update_core(
402
430
  cls,
403
431
  session: AsyncSession,
404
432
  entity_id: int,
405
433
  *,
406
- lock_wait_timeout: int | None = None,
434
+ timeout: int | None = None,
407
435
  ) -> AsyncSQLATableT | None:
408
436
  try:
409
- async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
437
+ async with async_temp_set_lock_wait_timeout(session, timeout):
410
438
  stmt = (
411
439
  sa.select(cls._Table)
412
440
  .where(cls._Table.id == entity_id) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
@@ -421,19 +449,29 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
421
449
  raise
422
450
 
423
451
  @classmethod
424
- async def batch_get_for_update(
452
+ async def get_by_id_for_update(
425
453
  cls,
426
454
  session: AsyncSession,
427
- entity_ids: Iterable[int],
455
+ entity_id: int,
428
456
  *,
429
457
  lock_wait_timeout: int | None = None,
458
+ ) -> AsyncSQLATableT | None:
459
+ return await cls._get_by_id_for_update_core(session, entity_id, timeout=lock_wait_timeout)
460
+
461
+ @classmethod
462
+ async def _batch_get_for_update_core(
463
+ cls,
464
+ session: AsyncSession,
465
+ entity_ids: Iterable[int],
466
+ *,
467
+ timeout: int | None = None,
430
468
  ) -> list[AsyncSQLATableT]:
431
469
  filtered_ids = filtered_in_sql_values(entity_ids, int)
432
470
  if not filtered_ids:
433
471
  return []
434
472
 
435
473
  try:
436
- async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
474
+ async with async_temp_set_lock_wait_timeout(session, timeout):
437
475
  stmt = (
438
476
  sa.select(cls._Table)
439
477
  .where(cls._Table.id.in_(filtered_ids)) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
@@ -448,15 +486,25 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
448
486
  raise
449
487
 
450
488
  @classmethod
451
- async def get_one_for_update(
489
+ async def batch_get_for_update(
452
490
  cls,
453
491
  session: AsyncSession,
492
+ entity_ids: Iterable[int],
454
493
  *,
455
- where_clauses: list[ColumnExpressionArgument[bool]],
456
494
  lock_wait_timeout: int | None = None,
495
+ ) -> list[AsyncSQLATableT]:
496
+ return await cls._batch_get_for_update_core(session, entity_ids, timeout=lock_wait_timeout)
497
+
498
+ @classmethod
499
+ async def _get_one_for_update_core(
500
+ cls,
501
+ session: AsyncSession,
502
+ *,
503
+ where_clauses: list[ColumnExpressionArgument[bool]],
504
+ timeout: int | None = None,
457
505
  ) -> AsyncSQLATableT | None:
458
506
  try:
459
- async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
507
+ async with async_temp_set_lock_wait_timeout(session, timeout):
460
508
  stmt = sa.select(cls._Table).with_for_update()
461
509
 
462
510
  for clause in where_clauses:
@@ -470,6 +518,16 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
470
518
  raise DBRetryableError(f"{PESSIMISTIC_LOCK_ERROR_MSG_TRAIT}-条件锁等待超时: {error_msg}") from e
471
519
  raise
472
520
 
521
+ @classmethod
522
+ async def get_one_for_update(
523
+ cls,
524
+ session: AsyncSession,
525
+ *,
526
+ where_clauses: list[ColumnExpressionArgument[bool]],
527
+ lock_wait_timeout: int | None = None,
528
+ ) -> AsyncSQLATableT | None:
529
+ return await cls._get_one_for_update_core(session, where_clauses=where_clauses, timeout=lock_wait_timeout)
530
+
473
531
  @classmethod
474
532
  async def iter_record_dtos(
475
533
  cls,
@@ -478,7 +536,7 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
478
536
  where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
479
537
  with_deleted: bool = False,
480
538
  batch_size: int = 500,
481
- ) -> AsyncIterator[DTOModelT]:
539
+ ) -> AsyncGenerator[DTOModelT, None]:
482
540
  async for entity in cls._iter_records(
483
541
  session,
484
542
  cls._Table,
@@ -522,7 +580,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
522
580
  ) -> AsyncSQLATableT:
523
581
  if session.info.get(READONLY_SESSION_FLAG):
524
582
  raise TypeError("当前会话被标记为只读, 不允许执行写入操作")
525
- entity = cu.to_sqla_model()
583
+ entity = cu.to_orm_model()
526
584
  session.add(entity)
527
585
  await session.flush()
528
586
  if need_refresh:
@@ -568,10 +626,10 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
568
626
  cls,
569
627
  session: AsyncSession,
570
628
  entity_id: int,
571
- vo: CUModelT,
629
+ cu: CUModelT,
572
630
  need_refresh: bool = True,
573
631
  ) -> DTOModelT | None:
574
- entity = await cls.update_only_set_by_id(session, entity_id, vo, need_refresh)
632
+ entity = await cls.update_only_set_by_id(session, entity_id, cu, need_refresh)
575
633
  if entity:
576
634
  return cls._DTO.model_validate(entity)
577
635
  return None
@@ -586,7 +644,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
586
644
  _ensure_strict_fields(provided_keys=provided_keys, allowed_names=allowed_names, strict=strict)
587
645
 
588
646
  @classmethod
589
- async def update_full_by_id(
647
+ async def _update_full_by_id_core(
590
648
  cls,
591
649
  session: AsyncSession,
592
650
  entity_id: int,
@@ -620,7 +678,19 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
620
678
  return entity
621
679
 
622
680
  @classmethod
623
- async def update_partial_by_id(
681
+ async def update_full_by_id(
682
+ cls,
683
+ session: AsyncSession,
684
+ entity_id: int,
685
+ cu: CUModelT,
686
+ *,
687
+ need_refresh: bool = False,
688
+ strict_missing: bool = True,
689
+ ) -> AsyncSQLATableT | None:
690
+ return await cls._update_full_by_id_core(session, entity_id, cu, need_refresh=need_refresh, strict_missing=strict_missing)
691
+
692
+ @classmethod
693
+ async def _update_partial_by_id_core(
624
694
  cls,
625
695
  session: AsyncSession,
626
696
  entity_id: int,
@@ -687,6 +757,30 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
687
757
  await session.refresh(entity)
688
758
  return entity
689
759
 
760
+ @classmethod
761
+ async def update_partial_by_id(
762
+ cls,
763
+ session: AsyncSession,
764
+ entity_id: int,
765
+ cu: CUModelT,
766
+ *,
767
+ need_refresh: bool = False,
768
+ fields: set[InstrumentedAttribute[Any]] | set[sa.Column[Any]] | None = None,
769
+ none_policy: Literal["ignore", "allow", "forbid"] = "ignore",
770
+ none_policy_overrides: dict[InstrumentedAttribute[Any] | sa.Column[Any], Literal["ignore", "allow", "forbid"]] | None = None,
771
+ strict: bool = False,
772
+ ) -> AsyncSQLATableT | None:
773
+ return await cls._update_partial_by_id_core(
774
+ session,
775
+ entity_id,
776
+ cu,
777
+ need_refresh=need_refresh,
778
+ fields=fields,
779
+ none_policy=none_policy,
780
+ none_policy_overrides=none_policy_overrides,
781
+ strict=strict,
782
+ )
783
+
690
784
  @classmethod
691
785
  async def delete_by_id(
692
786
  cls,
@@ -711,7 +805,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
711
805
  where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
712
806
  with_deleted: bool = False,
713
807
  batch_size: int = 500,
714
- ) -> AsyncIterator[AsyncSQLATableT]:
808
+ ) -> AsyncGenerator[AsyncSQLATableT, None]:
715
809
  async for entity in cls._iter_records(
716
810
  session,
717
811
  cls._Table,
@@ -722,11 +816,11 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
722
816
  yield entity
723
817
 
724
818
  @classmethod
725
- async def batch_update_by_conditions(
819
+ async def _batch_update_by_conditions_core(
726
820
  cls,
727
821
  session: AsyncSession,
728
822
  *,
729
- whereclause: list[ColumnExpressionArgument[bool]],
823
+ conditions: list[ColumnExpressionArgument[bool]],
730
824
  update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
731
825
  updater_id: int | None = None,
732
826
  ) -> int:
@@ -751,13 +845,24 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
751
845
  if hasattr(cls._Table, "update_operator_id") and updater_id is not None:
752
846
  final_update_data["update_operator_id"] = updater_id
753
847
 
754
- stmt = sa.update(cls._Table).where(*whereclause).values(**final_update_data)
848
+ stmt = sa.update(cls._Table).where(*conditions).values(**final_update_data)
755
849
 
756
850
  result = await session.execute(stmt)
757
851
  await session.flush()
758
852
 
759
853
  return result.rowcount # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType]
760
854
 
855
+ @classmethod
856
+ async def batch_update_by_conditions(
857
+ cls,
858
+ session: AsyncSession,
859
+ *,
860
+ whereclause: list[ColumnExpressionArgument[bool]],
861
+ update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
862
+ updater_id: int | None = None,
863
+ ) -> int:
864
+ return await cls._batch_update_by_conditions_core(session, conditions=whereclause, update_data=update_data, updater_id=updater_id)
865
+
761
866
  @classmethod
762
867
  async def batch_update_by_ids(
763
868
  cls,
@@ -771,15 +876,15 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
771
876
  if not filtered_ids:
772
877
  return 0
773
878
  _id_column = cls._Table.id # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType, reportUnknownMemberType]
774
- return await cls.batch_update_by_conditions(
879
+ return await cls._batch_update_by_conditions_core(
775
880
  session,
776
- whereclause=[_id_column.in_(filtered_ids)], # pyright: ignore[reportUnknownMemberType]
881
+ conditions=[_id_column.in_(filtered_ids)], # pyright: ignore[reportUnknownMemberType]
777
882
  update_data=update_data,
778
883
  updater_id=updater_id,
779
884
  )
780
885
 
781
886
  @classmethod
782
- async def update_only_set_with_optimistic_lock(
887
+ async def _update_only_set_with_optimistic_lock_core(
783
888
  cls,
784
889
  session: AsyncSession,
785
890
  entity_id: int,
@@ -830,13 +935,45 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
830
935
 
831
936
  raise DBRetryableError(f"{OPTIMISTIC_LOCK_ERROR_MSG_TRAIT}-版本号不匹配({entity_id=}, {expected_version=})")
832
937
 
938
+ @classmethod
939
+ async def update_only_set_with_optimistic_lock(
940
+ cls,
941
+ session: AsyncSession,
942
+ entity_id: int,
943
+ cu: CUModelT,
944
+ *,
945
+ expected_version: int,
946
+ need_refresh: bool = False,
947
+ version_field: str = "version",
948
+ ) -> AsyncSQLATableT | None:
949
+ return await cls._update_only_set_with_optimistic_lock_core(
950
+ session,
951
+ entity_id,
952
+ cu,
953
+ expected_version=expected_version,
954
+ need_refresh=need_refresh,
955
+ version_field=version_field,
956
+ )
957
+
833
958
 
834
959
  class AsyncXDALOp(AsyncRawReadDAL, AsyncRawDAL):
835
960
  """扩展数据访问操作类."""
836
961
 
837
962
 
838
963
  class AsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, DTOModelT], AsyncWriteDAL[AsyncSQLATableT, DTOModelT, CUModelT]):
839
- """基础数据访问层."""
964
+ """基础数据访问层.
965
+
966
+ .. deprecated:: 0.3.0
967
+ V1 DAL 将在 1.0 移除, 请迁移至 ``AsyncBaseDALV2``.
968
+ """
969
+
970
+ def __init_subclass__(cls, **kwargs: Any) -> None:
971
+ super().__init_subclass__(**kwargs)
972
+ warnings.warn(
973
+ f"{cls.__name__} 继承了 V1 AsyncBaseDAL, 建议迁移至 AsyncBaseDALV2",
974
+ DeprecationWarning,
975
+ stacklevel=2,
976
+ )
840
977
 
841
978
 
842
979
  class ReadOnlyAsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, ReadOnlyDTOModelT]):