sqlmodel-ext 0.3.5__tar.gz → 0.4.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 (159) hide show
  1. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/PKG-INFO +32 -8
  2. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/README.md +31 -7
  3. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/README_zh.md +18 -5
  4. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/index.md +1 -1
  5. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/release-connection-during-long-io.md +21 -21
  6. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/index.md +9 -0
  7. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/reference/decorators.md +8 -6
  8. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/reference/field-types.md +58 -1
  9. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/reference/index.md +1 -1
  10. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/tutorials/01-getting-started.md +1 -1
  11. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/tutorials/index.md +1 -1
  12. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/index.md +1 -1
  13. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/release-connection-during-long-io.md +21 -21
  14. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/index.md +9 -0
  15. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/reference/decorators.md +8 -6
  16. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/reference/field-types.md +58 -1
  17. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/reference/index.md +1 -1
  18. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/tutorials/01-getting-started.md +1 -1
  19. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/tutorials/index.md +1 -1
  20. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/pyproject.toml +1 -1
  21. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/__init__.py +42 -3
  22. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/base.py +202 -5
  23. sqlmodel_ext-0.4.0/src/sqlmodel_ext/field_types/__init__.py +508 -0
  24. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/mixins/__init__.py +1 -1
  25. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/mixins/cached_table.py +276 -239
  26. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/mixins/polymorphic.py +118 -2
  27. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/mixins/table.py +35 -13
  28. sqlmodel_ext-0.4.0/src/sqlmodel_ext/session.py +105 -0
  29. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/_models.py +22 -0
  30. sqlmodel_ext-0.4.0/tests/test_all_fields_optional_constraints.py +76 -0
  31. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/test_array_explicit_field_sa_type.py +16 -1
  32. sqlmodel_ext-0.4.0/tests/test_decimal_types.py +256 -0
  33. sqlmodel_ext-0.4.0/tests/test_deferred_index.py +50 -0
  34. sqlmodel_ext-0.4.0/tests/test_enhanced_session.py +87 -0
  35. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/test_smoke_imports.py +41 -1
  36. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/uv.lock +1 -1
  37. sqlmodel_ext-0.3.5/src/sqlmodel_ext/field_types/__init__.py +0 -195
  38. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/.github/workflows/ci.yml +0 -0
  39. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/.gitignore +0 -0
  40. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/LICENSE +0 -0
  41. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/.vitepress/config.mts +0 -0
  42. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/.vitepress/en.mts +0 -0
  43. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/.vitepress/theme/index.ts +0 -0
  44. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/.vitepress/theme/style/custom.css +0 -0
  45. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/.vitepress/zh.mts +0 -0
  46. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/cached-table.md +0 -0
  47. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/cascade-delete-semantics.md +0 -0
  48. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/crud-pipeline.md +0 -0
  49. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/index.md +0 -0
  50. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/metaclass.md +0 -0
  51. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/optimistic-lock.md +0 -0
  52. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/polymorphic-internals.md +0 -0
  53. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/prerequisites.md +0 -0
  54. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/relation-load-checker.md +0 -0
  55. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/explanation/relation-preload.md +0 -0
  56. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/cached-table.md +0 -0
  57. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/crud.md +0 -0
  58. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/field-types.md +0 -0
  59. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/index.md +0 -0
  60. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/optimistic-lock.md +0 -0
  61. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/pagination.md +0 -0
  62. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/polymorphic.md +0 -0
  63. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/relation-load-checker.md +0 -0
  64. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/guide/relation-preload.md +0 -0
  65. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/cache-queries.md +0 -0
  66. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/configure-cascade-delete.md +0 -0
  67. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/define-jti-models.md +0 -0
  68. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/define-sti-models.md +0 -0
  69. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/handle-concurrent-updates.md +0 -0
  70. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/integrate-with-fastapi.md +0 -0
  71. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
  72. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/how-to/prevent-missing-greenlet.md +0 -0
  73. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/cached-table.md +0 -0
  74. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/crud.md +0 -0
  75. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/index.md +0 -0
  76. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/metaclass.md +0 -0
  77. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/optimistic-lock.md +0 -0
  78. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/polymorphic.md +0 -0
  79. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/prerequisites.md +0 -0
  80. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/relation-load-checker.md +0 -0
  81. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/internals/relation-preload.md +0 -0
  82. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/reference/base-classes.md +0 -0
  83. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/reference/crud-methods.md +0 -0
  84. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/reference/mixins.md +0 -0
  85. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/reference/pagination-types.md +0 -0
  86. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/tutorials/02-building-a-blog-api.md +0 -0
  87. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/en/tutorials/03-adding-redis-cache.md +0 -0
  88. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/cached-table.md +0 -0
  89. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/cascade-delete-semantics.md +0 -0
  90. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/crud-pipeline.md +0 -0
  91. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/index.md +0 -0
  92. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/metaclass.md +0 -0
  93. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/optimistic-lock.md +0 -0
  94. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/polymorphic-internals.md +0 -0
  95. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/prerequisites.md +0 -0
  96. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/relation-load-checker.md +0 -0
  97. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/explanation/relation-preload.md +0 -0
  98. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/cached-table.md +0 -0
  99. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/crud.md +0 -0
  100. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/field-types.md +0 -0
  101. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/index.md +0 -0
  102. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/optimistic-lock.md +0 -0
  103. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/pagination.md +0 -0
  104. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/polymorphic.md +0 -0
  105. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/relation-load-checker.md +0 -0
  106. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/guide/relation-preload.md +0 -0
  107. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/cache-queries.md +0 -0
  108. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/configure-cascade-delete.md +0 -0
  109. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/define-jti-models.md +0 -0
  110. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/define-sti-models.md +0 -0
  111. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/handle-concurrent-updates.md +0 -0
  112. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/integrate-with-fastapi.md +0 -0
  113. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/paginate-a-list-endpoint.md +0 -0
  114. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/how-to/prevent-missing-greenlet.md +0 -0
  115. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/cached-table.md +0 -0
  116. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/crud.md +0 -0
  117. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/index.md +0 -0
  118. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/metaclass.md +0 -0
  119. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/optimistic-lock.md +0 -0
  120. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/polymorphic.md +0 -0
  121. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/prerequisites.md +0 -0
  122. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/relation-load-checker.md +0 -0
  123. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/internals/relation-preload.md +0 -0
  124. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/reference/base-classes.md +0 -0
  125. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/reference/crud-methods.md +0 -0
  126. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/reference/mixins.md +0 -0
  127. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/reference/pagination-types.md +0 -0
  128. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/tutorials/02-building-a-blog-api.md +0 -0
  129. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/docs/tutorials/03-adding-redis-cache.md +0 -0
  130. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/examples/consumer_integration/run.py +0 -0
  131. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/package.json +0 -0
  132. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/pnpm-lock.yaml +0 -0
  133. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/_compat.py +0 -0
  134. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/_exceptions.py +0 -0
  135. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/_sa_type.py +0 -0
  136. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/_utils.py +0 -0
  137. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
  138. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
  139. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
  140. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
  141. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
  142. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -0
  143. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
  144. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
  145. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
  146. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
  147. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
  148. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
  149. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/field_types/url.py +0 -0
  150. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/mixins/info_response.py +0 -0
  151. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
  152. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
  153. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/pagination.py +0 -0
  154. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/src/sqlmodel_ext/relation_load_checker.py +0 -0
  155. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/__init__.py +0 -0
  156. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/conftest.py +0 -0
  157. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/test_annotated_default_factory_inheritance.py +0 -0
  158. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/test_basic_crud.py +0 -0
  159. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.0}/tests/test_sti_default_isolation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmodel-ext
3
- Version: 0.3.5
3
+ Version: 0.4.0
4
4
  Summary: Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
5
5
  Project-URL: Homepage, https://github.com/Foxerine/sqlmodel-ext
6
6
  Project-URL: Repository, https://github.com/Foxerine/sqlmodel-ext
@@ -1402,18 +1402,42 @@ class UserUpdateRequest(UserBase, all_fields_optional=True):
1402
1402
 
1403
1403
  ---
1404
1404
 
1405
- ### safe_reset
1405
+ ### Enhanced AsyncSession (cache-aware commit/reset/refresh)
1406
1406
 
1407
- Safely reset an async session, clearing both the DB connection and FOR UPDATE lock tracking:
1407
+ `sqlmodel_ext.AsyncSession` subclasses sqlmodel's `AsyncSession` and makes cache
1408
+ correctness automatic. Point your session factory at it:
1408
1409
 
1409
1410
  ```python
1410
- from sqlmodel_ext import safe_reset
1411
+ from sqlalchemy.ext.asyncio import async_sessionmaker
1412
+ from sqlmodel_ext import AsyncSession
1411
1413
 
1412
- # In long-lived tasks (e.g. background workers) between operations:
1413
- await safe_reset(session)
1414
+ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=True)
1414
1415
  ```
1415
1416
 
1416
- This prevents stale `FOR UPDATE` lock tracking in `session.info` when Python reuses object IDs across `reset()` cycles.
1417
+ What it does:
1418
+
1419
+ - **`commit()`** auto-registers every `CachedTableBaseMixin` mutation in the
1420
+ session (including bare `session.add()` / attribute mutation /
1421
+ `session.delete()` paths) and synchronously invalidates the Redis cache after
1422
+ commit — no more "forgot the cache-aware commit" stale windows.
1423
+ - **`reset()`** releases the connection and clears FOR UPDATE lock tracking +
1424
+ cache-invalidation tracking from `session.info` (replaces the removed
1425
+ `safe_reset()` helper — just call `await session.reset()`).
1426
+ - **`refresh()`** routes whole-object refreshes of cached models through
1427
+ `Model.get()` (Redis hit + STI polymorphic columns) instead of a raw DB query.
1428
+ - **`execute()`** warns when a bare `UPDATE`/`DELETE` hits a cached table
1429
+ without registered invalidation.
1430
+
1431
+ Models that don't use `CachedTableBaseMixin` are unaffected — every hook
1432
+ degrades to upstream behavior.
1433
+
1434
+ > **Breaking change in 0.4.0**: `safe_reset()` and
1435
+ > `CachedTableBaseMixin.cache_aware_commit()` were removed. CRUD methods no
1436
+ > longer invalidate the cache themselves; invalidation now happens inside
1437
+ > `AsyncSession.commit()`. If you use `CachedTableBaseMixin`, you **must**
1438
+ > construct sessions with `class_=sqlmodel_ext.AsyncSession` (plain sessions
1439
+ > fall back to the fire-and-forget `after_commit` compensation hook, which
1440
+ > reintroduces a brief stale-cache window).
1417
1441
 
1418
1442
  ---
1419
1443
 
@@ -1430,7 +1454,7 @@ sqlmodel_ext/
1430
1454
  pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
1431
1455
  mixins/
1432
1456
  __init__.py # Mixin re-exports
1433
- table.py # TableBaseMixin, UUIDTableBaseMixin, safe_reset (async CRUD)
1457
+ table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
1434
1458
  cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
1435
1459
  polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
1436
1460
  optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
@@ -1357,18 +1357,42 @@ class UserUpdateRequest(UserBase, all_fields_optional=True):
1357
1357
 
1358
1358
  ---
1359
1359
 
1360
- ### safe_reset
1360
+ ### Enhanced AsyncSession (cache-aware commit/reset/refresh)
1361
1361
 
1362
- Safely reset an async session, clearing both the DB connection and FOR UPDATE lock tracking:
1362
+ `sqlmodel_ext.AsyncSession` subclasses sqlmodel's `AsyncSession` and makes cache
1363
+ correctness automatic. Point your session factory at it:
1363
1364
 
1364
1365
  ```python
1365
- from sqlmodel_ext import safe_reset
1366
+ from sqlalchemy.ext.asyncio import async_sessionmaker
1367
+ from sqlmodel_ext import AsyncSession
1366
1368
 
1367
- # In long-lived tasks (e.g. background workers) between operations:
1368
- await safe_reset(session)
1369
+ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=True)
1369
1370
  ```
1370
1371
 
1371
- This prevents stale `FOR UPDATE` lock tracking in `session.info` when Python reuses object IDs across `reset()` cycles.
1372
+ What it does:
1373
+
1374
+ - **`commit()`** auto-registers every `CachedTableBaseMixin` mutation in the
1375
+ session (including bare `session.add()` / attribute mutation /
1376
+ `session.delete()` paths) and synchronously invalidates the Redis cache after
1377
+ commit — no more "forgot the cache-aware commit" stale windows.
1378
+ - **`reset()`** releases the connection and clears FOR UPDATE lock tracking +
1379
+ cache-invalidation tracking from `session.info` (replaces the removed
1380
+ `safe_reset()` helper — just call `await session.reset()`).
1381
+ - **`refresh()`** routes whole-object refreshes of cached models through
1382
+ `Model.get()` (Redis hit + STI polymorphic columns) instead of a raw DB query.
1383
+ - **`execute()`** warns when a bare `UPDATE`/`DELETE` hits a cached table
1384
+ without registered invalidation.
1385
+
1386
+ Models that don't use `CachedTableBaseMixin` are unaffected — every hook
1387
+ degrades to upstream behavior.
1388
+
1389
+ > **Breaking change in 0.4.0**: `safe_reset()` and
1390
+ > `CachedTableBaseMixin.cache_aware_commit()` were removed. CRUD methods no
1391
+ > longer invalidate the cache themselves; invalidation now happens inside
1392
+ > `AsyncSession.commit()`. If you use `CachedTableBaseMixin`, you **must**
1393
+ > construct sessions with `class_=sqlmodel_ext.AsyncSession` (plain sessions
1394
+ > fall back to the fire-and-forget `after_commit` compensation hook, which
1395
+ > reintroduces a brief stale-cache window).
1372
1396
 
1373
1397
  ---
1374
1398
 
@@ -1385,7 +1409,7 @@ sqlmodel_ext/
1385
1409
  pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
1386
1410
  mixins/
1387
1411
  __init__.py # Mixin re-exports
1388
- table.py # TableBaseMixin, UUIDTableBaseMixin, safe_reset (async CRUD)
1412
+ table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
1389
1413
  cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
1390
1414
  polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
1391
1415
  optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
@@ -1288,15 +1288,28 @@ class ArticleUpdateRequest(ArticleBase, all_fields_optional=True):
1288
1288
 
1289
1289
  ---
1290
1290
 
1291
- ### safe_reset
1291
+ ### 增强 AsyncSession(缓存感知 commit/reset/refresh)
1292
1292
 
1293
- 安全重置异步会话,同时清理 FOR UPDATE 锁跟踪:
1293
+ `sqlmodel_ext.AsyncSession` 是 sqlmodel `AsyncSession` 的增强子类,把缓存正确性变为自动行为。把 session 工厂指向它:
1294
1294
 
1295
1295
  ```python
1296
- from sqlmodel_ext import safe_reset
1297
- await safe_reset(session)
1296
+ from sqlalchemy.ext.asyncio import async_sessionmaker
1297
+ from sqlmodel_ext import AsyncSession
1298
+
1299
+ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=True)
1298
1300
  ```
1299
1301
 
1302
+ 能力:
1303
+
1304
+ - **`commit()`** 自动登记 session 中所有 `CachedTableBaseMixin` 变更(含裸 `session.add()` / 改属性 / `session.delete()` 路径),commit 后同步失效 Redis 缓存——消除"忘记 cache-aware commit"的 stale 窗口。
1305
+ - **`reset()`** 释放连接并清理 `session.info` 中的 FOR UPDATE 锁跟踪 + 缓存失效跟踪(替代已删除的 `safe_reset()`——直接 `await session.reset()` 即可)。
1306
+ - **`refresh()`** 对缓存模型的整体刷新走 `Model.get()`(Redis 命中 + STI 多态列),而非裸查 DB。
1307
+ - **`execute()`** 对绕过缓存失效的裸 `UPDATE`/`DELETE` 打 WARNING。
1308
+
1309
+ 不使用 `CachedTableBaseMixin` 的模型完全不受影响——所有钩子退化为上游行为。
1310
+
1311
+ > **0.4.0 Breaking change**:`safe_reset()` 与 `CachedTableBaseMixin.cache_aware_commit()` 已删除。CRUD 方法不再自行失效缓存,失效统一发生在 `AsyncSession.commit()` 内。使用 `CachedTableBaseMixin` 时**必须**以 `class_=sqlmodel_ext.AsyncSession` 构造 session(普通 session 会退化为 fire-and-forget 的 `after_commit` 补偿钩子,重新引入短暂的 stale 窗口)。
1312
+
1300
1313
  ---
1301
1314
 
1302
1315
  ## 架构
@@ -1312,7 +1325,7 @@ sqlmodel_ext/
1312
1325
  pagination.py # ListResponse、TimeFilterRequest、PaginationRequest、TableViewRequest
1313
1326
  mixins/
1314
1327
  __init__.py # Mixin 重导出
1315
- table.py # TableBaseMixin、UUIDTableBaseMixin、safe_reset(异步 CRUD)
1328
+ table.py # TableBaseMixin、UUIDTableBaseMixin(异步 CRUD)
1316
1329
  cached_table.py # CachedTableBaseMixin(双层 Redis 缓存 + 版本号失效)
1317
1330
  polymorphic.py # PolymorphicBaseMixin、AutoPolymorphicIdentityMixin、create_subclass_id_mixin
1318
1331
  optimistic_lock.py # OptimisticLockMixin、OptimisticLockError
@@ -20,7 +20,7 @@
20
20
 
21
21
  - [Handle concurrent updates](./handle-concurrent-updates) — Use `OptimisticLockMixin` to prevent lost updates
22
22
  - [Prevent MissingGreenlet errors](./prevent-missing-greenlet) — `@requires_relations` + `lazy='raise_on_sql'` + static analysis: three layers of defense
23
- - [Release the DB connection during long I/O](./release-connection-during-long-io) — Use `safe_reset()` to keep external I/O from exhausting the pool
23
+ - [Release the DB connection during long I/O](./release-connection-during-long-io) — Use `session.reset()` to keep external I/O from exhausting the pool
24
24
 
25
25
  ### Performance
26
26
 
@@ -28,11 +28,11 @@ async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
28
28
  return file
29
29
  ```
30
30
 
31
- ## 2. Use `safe_reset()` around the I/O block
31
+ ## 2. Use `session.reset()` around the I/O block
32
32
 
33
- ```python
34
- from sqlmodel_ext import safe_reset
33
+ > Since 0.4.0 `safe_reset()` has been removed -- its responsibility moved into the enhanced `sqlmodel_ext.AsyncSession.reset()` (construct sessions with `async_sessionmaker(class_=AsyncSession)`). `reset()` releases the connection and also clears FOR UPDATE lock tracking and cache-invalidation tracking.
35
34
 
35
+ ```python
36
36
  @router.get("/files/{file_id}")
37
37
  async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
38
38
  file = await UserFile.get(session, UserFile.id == file_id)
@@ -42,7 +42,7 @@ async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
42
42
  key = file.key
43
43
  file_id = file.id
44
44
 
45
- await safe_reset(session) # ← release the DB connection // [!code ++]
45
+ await session.reset() # ← release the DB connection // [!code ++]
46
46
 
47
47
  # ↓ This block holds NO DB connection — concurrent requests can use the pool
48
48
  data = await s3.download_file(bucket_id, key)
@@ -56,26 +56,26 @@ async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
56
56
  ```
57
57
 
58
58
  **Key points**:
59
- - `safe_reset` releases the current transaction and connection, but the session object itself stays alive (it's not closed)
59
+ - `session.reset()` releases the current transaction and connection, but the session object itself stays alive (it's not closed)
60
60
  - Any subsequent `await Model.get/save` that issues SQL transparently checks out a new connection from the pool
61
61
  - During the released window all 30 connections are idle (unless other requests use them) — concurrent requests **are not stalled**
62
62
 
63
- ## 3. Object state after `safe_reset`: detached but not expired
63
+ ## 3. Object state after `session.reset()`: detached but not expired
64
64
 
65
- After `safe_reset`, every ORM object in the session enters the **detached** state (no longer in `session.identity_map`), but **already-loaded scalar fields remain in `obj.__dict__`** — accessing them does not trigger a SQL query, so it does **not** raise `MissingGreenlet`.
65
+ After `session.reset()`, every ORM object in the session enters the **detached** state (no longer in `session.identity_map`), but **already-loaded scalar fields remain in `obj.__dict__`** — accessing them does not trigger a SQL query, so it does **not** raise `MissingGreenlet`.
66
66
 
67
67
  ```python
68
68
  from sqlalchemy import inspect as sa_inspect
69
69
 
70
- # Before safe_reset:
70
+ # Before reset:
71
71
  file = await UserFile.get(session, UserFile.id == fid)
72
72
  print(sa_inspect(file).detached) # False
73
73
  print(sa_inspect(file).expired) # False
74
74
  print(file.bucket_id) # OK, loaded
75
75
 
76
- await safe_reset(session)
76
+ await session.reset()
77
77
 
78
- # After safe_reset:
78
+ # After reset:
79
79
  print(sa_inspect(file).detached) # True ← note // [!code highlight]
80
80
  print(sa_inspect(file).expired) # False ← loaded fields are NOT expired // [!code highlight]
81
81
  print(file.bucket_id) # OK! scalar field access is safe // [!code ++]
@@ -84,7 +84,7 @@ print(file.parent) # InvalidRequestError or MissingGreenlet // [!
84
84
 
85
85
  ### Safe-access rules
86
86
 
87
- | Access | After safe_reset | Why |
87
+ | Access | After session.reset() | Why |
88
88
  |--------|-----------------|-----|
89
89
  | Already-loaded scalar field | ✓ Safe | In `__dict__`, no DB query |
90
90
  | Already-loaded relation (preloaded via `load=`) | ✓ Safe | Same |
@@ -93,7 +93,7 @@ print(file.parent) # InvalidRequestError or MissingGreenlet // [!
93
93
 
94
94
  ### Recommended style
95
95
 
96
- When writing the code that runs **before** `safe_reset`, extract any field you'll need later into local variables:
96
+ When writing the code that runs **before** `session.reset()`, extract any field you'll need later into local variables:
97
97
 
98
98
  ```python
99
99
  # Recommended:
@@ -102,7 +102,7 @@ bucket_id = file.bucket_id
102
102
  key = file.key
103
103
  user_id = file.user_id
104
104
 
105
- await safe_reset(session)
105
+ await session.reset()
106
106
 
107
107
  # The code below uses local variables (file_id / bucket_id / ...),
108
108
  # never touches file.X again.
@@ -112,10 +112,10 @@ This way, even if some field on the model becomes lazy-loaded in the future, the
112
112
 
113
113
  ## 4. You must re-query before writing
114
114
 
115
- After `safe_reset` the object is detached and **cannot be saved directly**. To write, re-query a fresh attached instance:
115
+ After `session.reset()` the object is detached and **cannot be saved directly**. To write, re-query a fresh attached instance:
116
116
 
117
117
  ```python
118
- await safe_reset(session)
118
+ await session.reset()
119
119
 
120
120
  # Long I/O...
121
121
  metadata = await fetch_external_metadata(...)
@@ -130,15 +130,15 @@ file.metadata = metadata
130
130
  await file.save(session)
131
131
  ```
132
132
 
133
- ## 5. When to use safe_reset vs Taskiq dispatch
133
+ ## 5. When to use session.reset() vs Taskiq dispatch
134
134
 
135
- `safe_reset` fits when you **must** synchronously return a result to the client. If your business can tolerate async — dispatch a job, return immediately, let the client poll — use Taskiq instead and **don't do long I/O in the HTTP endpoint at all**.
135
+ `session.reset()` fits when you **must** synchronously return a result to the client. If your business can tolerate async — dispatch a job, return immediately, let the client poll — use Taskiq instead and **don't do long I/O in the HTTP endpoint at all**.
136
136
 
137
137
  ```mermaid
138
138
  flowchart TD
139
139
  A["Need long I/O in the endpoint?"] --> B{Can the response wait until the next poll?}
140
140
  B -- Yes --> C["Dispatch a Taskiq task<br/>return status=pending immediately"]
141
- B -- No --> D["safe_reset to release connection<br/>execute the long I/O synchronously"]
141
+ B -- No --> D["session.reset() to release connection<br/>execute the long I/O synchronously"]
142
142
  C --> E["Cleanest, holds zero connection time"]
143
143
  D --> F["Pool is usable during the released window<br/>but still occupies a worker coroutine"]
144
144
  ```
@@ -147,7 +147,7 @@ flowchart TD
147
147
  |----------|-------------|
148
148
  | Polling for upload status | Taskiq (the polling client picks up the state change) |
149
149
  | AI generation tasks (image/video) | Taskiq (the project's mainstream pattern) |
150
- | Synchronous response that must contain the full result (e.g. PDF export) | safe_reset |
150
+ | Synchronous response that must contain the full result (e.g. PDF export) | session.reset() |
151
151
  | WebSocket long connections | `session.close()` + per-message short-lived sessions |
152
152
 
153
153
  ## 6. Verification: did your endpoint actually release the connection?
@@ -169,13 +169,13 @@ def _on_checkin(conn, rec):
169
169
 
170
170
  Trigger a request that hits the long-I/O path. You should see:
171
171
  1. `checkout 1/N` — endpoint starts
172
- 2. `checkin 0/N` — safe_reset fires
172
+ 2. `checkin 0/N` — session.reset() fires
173
173
  3. 30 seconds of external I/O with no checkout log in between
174
174
  4. `checkout 1/N` — subsequent DB op
175
175
  5. `checkin 0/N` — endpoint cleanup
176
176
 
177
177
  ## 7. Related
178
178
 
179
- - [`safe_reset()` API reference](/en/reference/decorators#safe-reset)
179
+ - [`session.reset()` API reference](/en/reference/decorators#session-reset)
180
180
  - [Integrate with FastAPI](./integrate-with-fastapi)
181
181
  - [Prevent MissingGreenlet](./prevent-missing-greenlet)
@@ -34,3 +34,12 @@ features:
34
34
  - title: Safe & Reliable
35
35
  details: Optimistic locking for concurrency control, @requires_relations to prevent MissingGreenlet, and AST static analysis at startup
36
36
  ---
37
+
38
+ ## Design orientation
39
+
40
+ sqlmodel-ext is not a new ORM. It is a set of **opt-in Mixins** layered on top of SQLModel / Pydantic v2 / SQLAlchemy 2.0. The library only introduces two kinds of machinery:
41
+
42
+ - **Metaclass enhancements** — automatic SQLAlchemy column setup driven by `Annotated` type hints, `mapper_args` merging, on-demand `*UpdateRequest` DTO generation, attribute-docstring inheritance, and a Python 3.14 (PEP 649) compatibility patch.
43
+ - **Composable Mixins** — `TableBaseMixin` (async CRUD), `PolymorphicBaseMixin` (JTI/STI), `OptimisticLockMixin` (version-column concurrency), `RelationPreloadMixin` (eager loading), `CachedTableBaseMixin` (Redis two-tier cache). Each Mixin stands alone and has no hard dependency on the others.
44
+
45
+ The underlying `Session`, `select()`, query construction, and migration tooling remain native SQLAlchemy — there is no custom DSL, and the library does not take over `engine` / `session` lifecycle. In practice that means you can drop a single Mixin into an existing SQLModel/SQLAlchemy project without rewriting your data-access layer.
@@ -113,21 +113,23 @@ scope = cond(UserFile.user_id == current_user.id)
113
113
  condition = scope & cond(UserFile.status == FileStatusEnum.uploaded)
114
114
  ```
115
115
 
116
- ## `safe_reset()`
116
+ ## `session.reset()` (enhanced AsyncSession) {#session-reset}
117
117
 
118
118
  ```python
119
- from sqlmodel_ext import safe_reset
119
+ from sqlmodel_ext import AsyncSession
120
120
  ```
121
121
 
122
- **Signature**:
122
+ **Signature** (enhanced method on `sqlmodel_ext.session.AsyncSession`):
123
123
 
124
124
  ```python
125
- async def safe_reset(session: AsyncSession) -> None
125
+ async def reset(self) -> None
126
126
  ```
127
127
 
128
- **Purpose**: clears the FOR UPDATE lock tracking set in `session.info[SESSION_FOR_UPDATE_KEY]` before calling `session.reset()`. Safer than calling `session.reset()` directly — prevents the lock-tracking set from leaking into the next session reuse cycle.
128
+ > Replaces the removed `safe_reset()` helper since 0.4.0. Construct sessions with `async_sessionmaker(class_=AsyncSession)` and simply `await session.reset()`.
129
+
130
+ **Purpose**: after the upstream `reset()` (releasing the transaction and connection), automatically clears the FOR UPDATE lock-tracking set in `session.info[SESSION_FOR_UPDATE_KEY]` and the cache-invalidation tracking state — preventing tracking state from leaking into the next session reuse cycle.
129
131
 
130
- **Typical use case**: an HTTP endpoint / Taskiq task needs to do long external I/O mid-flight (S3, ffprobe, third-party HTTP polling, etc.). Call `safe_reset` first to release the DB connection, so the connection isn't blocked on network I/O and the pool isn't exhausted. See [Release the DB connection during long I/O](/en/how-to/release-connection-during-long-io).
132
+ **Typical use case**: an HTTP endpoint / Taskiq task needs to do long external I/O mid-flight (S3, ffprobe, third-party HTTP polling, etc.). Call `session.reset()` first to release the DB connection, so the connection isn't blocked on network I/O and the pool isn't exhausted. See [Release the DB connection during long I/O](/en/how-to/release-connection-during-long-io).
131
133
 
132
134
  **Object state after the call**:
133
135
 
@@ -30,10 +30,27 @@ from sqlmodel_ext import Str16, Str24, Str32, Str36, Str48, Str64, Str100, Str12
30
30
  | `Str512` | 512 | same form |
31
31
  | `Str2048` | 2048 | same form |
32
32
 
33
+ ### Non-empty & special-purpose strings
34
+
35
+ ```python
36
+ from sqlmodel_ext import (
37
+ NonEmptyStr64, NonEmptyStr128, NonEmptyStr256,
38
+ NonEmptyStrippedStr64, NonEmptyStrippedStr128, NonEmptyStrippedStr256,
39
+ Sha256Hex, BCP47LanguageCode,
40
+ )
41
+ ```
42
+
43
+ | Type | Constraint |
44
+ |------|-----------|
45
+ | `NonEmptyStr64/128/256` | `1 <= len <= N`, rejects empty `""` |
46
+ | `NonEmptyStrippedStr64/128/256` | same + `strip_whitespace`, rejects whitespace-only (`" "` / `"\t"`) |
47
+ | `Sha256Hex` | exactly 64 lowercase hex chars (SHA-256 digest) |
48
+ | `BCP47LanguageCode` | BCP-47 language tag syntax (e.g. `zh-Hans-CN`), `max_length=16` |
49
+
33
50
  ## Text constraints
34
51
 
35
52
  ```python
36
- from sqlmodel_ext import Text1K, Text1024, Text2K, Text2500, Text3K, Text5K, Text10K, Text32K, Text60K, Text64K, Text100K, Text1M
53
+ from sqlmodel_ext import Text1K, Text1024, Text2K, Text2500, Text3K, Text5K, Text8K, Text10K, Text16K, Text32K, Text48K, Text60K, Text64K, Text100K, Text128K, Text1M
37
54
  ```
38
55
 
39
56
  | Type | `max_length` |
@@ -44,11 +61,15 @@ from sqlmodel_ext import Text1K, Text1024, Text2K, Text2500, Text3K, Text5K, Tex
44
61
  | `Text2500` | 2500 |
45
62
  | `Text3K` | 3000 |
46
63
  | `Text5K` | 5000 |
64
+ | `Text8K` | 8000 |
47
65
  | `Text10K` | 10000 |
66
+ | `Text16K` | 16000 |
48
67
  | `Text32K` | 32000 |
68
+ | `Text48K` | 48000 |
49
69
  | `Text60K` | 60000 |
50
70
  | `Text64K` | 65536 |
51
71
  | `Text100K` | 100000 |
72
+ | `Text128K` | 131072 (= 128 × 1024) |
52
73
  | `Text1M` | 1000000 |
53
74
 
54
75
  ## Numeric constraints
@@ -89,6 +110,42 @@ from sqlmodel_ext import INT32_MAX, INT64_MAX, JS_MAX_SAFE_INTEGER
89
110
  | `INT64_MAX` | `9_223_372_036_854_775_807` (2⁶³−1) |
90
111
  | `JS_MAX_SAFE_INTEGER` | `9_007_199_254_740_991` (2⁵³−1) |
91
112
 
113
+ ## Decimal constraints (new in 0.4.0)
114
+
115
+ ```python
116
+ from sqlmodel_ext import (
117
+ SignedDecimal38_18, NonNegativeDecimal38_18, PositiveDecimal38_18, OptionalNonNegativeDecimal38_18,
118
+ SignedDecimal20_10, NonNegativeDecimal20_10, OptionalNonNegativeDecimal20_10,
119
+ )
120
+ ```
121
+
122
+ Naming convention: `[Optional][Signed|NonNegative|Positive]Decimal{precision}_{scale}`, mapping to a `NUMERIC(precision, scale)` database column.
123
+
124
+ | Type | Sign | Database column |
125
+ |------|------|------------------|
126
+ | `SignedDecimal38_18` | any | `NUMERIC(38, 18)` |
127
+ | `NonNegativeDecimal38_18` | `>= 0` | `NUMERIC(38, 18)` |
128
+ | `PositiveDecimal38_18` | `> 0` | `NUMERIC(38, 18)` |
129
+ | `OptionalNonNegativeDecimal38_18` | `>= 0` or `None` | `NUMERIC(38, 18)` |
130
+ | `SignedDecimal20_10` | any | `NUMERIC(20, 10)` |
131
+ | `NonNegativeDecimal20_10` | `>= 0` | `NUMERIC(20, 10)` |
132
+ | `OptionalNonNegativeDecimal20_10` | `>= 0` or `None` | `NUMERIC(20, 10)` |
133
+
134
+ Behavioral contract:
135
+
136
+ - **Rejects float / bool input** (precision already lost via IEEE 754) — accepts `Decimal` / `int` / `str`
137
+ - **Serializes to a fixed-point JSON string** (`model_dump_json()`): never scientific notation (`0E-18` → `'0'`), trailing zeros stripped (`1200.000...0` → `'1200'`) — protects JS clients from Number precision loss
138
+ - **dict mode keeps the `Decimal` object** (`model_dump()`)
139
+ - The `Optional*` variants nest their constraints in the inner `Annotated`, so JSON `null` parses safely
140
+
141
+ ## Bounded-length list aliases (new in 0.4.0)
142
+
143
+ ```python
144
+ from sqlmodel_ext import List, List1, List2, List3, List7, List10, List16, List20, List32, List40, List50, List64, List100, List128, List200, List256, List1024
145
+ ```
146
+
147
+ `List<N>[T]` is equivalent to `Annotated[list[T], Field(max_length=N)]` — the max length is encoded in the type name (consistent with `Str64` / `Text1K`). `List[T]` (no number) is equivalent to `list[T]`. Intended for request DTOs / protocol layers, unrelated to the PG `Array[T]` column type.
148
+
92
149
  ## URL types
93
150
 
94
151
  ```python
@@ -28,7 +28,7 @@ from sqlmodel_ext import (
28
28
  | [CRUD methods](./crud-methods) | Full signatures for `add` / `save` / `update` / `delete` / `get` / `get_one` / `get_exist_one` / `count` / `get_with_count` |
29
29
  | [Field types](./field-types) | `Str16`–`Text1M`, `Port`, `Percentage`, `PositiveInt`, `HttpUrl`, `SafeHttpUrl`, `IPAddress`, `Array[T]`, `JSON100K`, `NumpyVector` |
30
30
  | [Mixins](./mixins) | `CachedTableBaseMixin`, `OptimisticLockMixin`, `PolymorphicBaseMixin`, `AutoPolymorphicIdentityMixin`, `RelationPreloadMixin`, info response mixins |
31
- | [Decorators & helpers](./decorators) | `@requires_relations`, `@requires_for_update`, `rel()`, `cond()`, `safe_reset()` |
31
+ | [Decorators & helpers](./decorators) | `@requires_relations`, `@requires_for_update`, `rel()`, `cond()`, enhanced `session.reset()` |
32
32
  | [Pagination types](./pagination-types) | `ListResponse[T]`, `TableViewRequest`, `PaginationRequest`, `TimeFilterRequest` |
33
33
 
34
34
  ## Constants
@@ -8,7 +8,7 @@ This is your first conversation with sqlmodel-ext. In 15 minutes you'll have:
8
8
  - A clear mental model for "model + Mixin = table"
9
9
 
10
10
  ::: tip You don't need prior SQLAlchemy or SQLModel experience
11
- This tutorial introduces those concepts on demand. You only need Python 3.10+ and basic `async` / `await` knowledge. If you've never touched an ORM, skim [Prerequisites](/en/explanation/prerequisites) first.
11
+ This tutorial introduces those concepts on demand. You only need Python 3.12+ and basic `async` / `await` knowledge. If you've never touched an ORM, skim [Prerequisites](/en/explanation/prerequisites) first.
12
12
  :::
13
13
 
14
14
  ## 0. Set up the environment
@@ -25,7 +25,7 @@ Tutorials don't **exhaustively cover the API**. If a feature you need is missing
25
25
 
26
26
  All tutorials assume you know:
27
27
 
28
- - Python 3.10+
28
+ - Python 3.12+ (3.12 / 3.13 / 3.14 are all exercised in CI)
29
29
  - Basic `async` / `await` syntax
30
30
  - What SELECT / INSERT / UPDATE / DELETE do in SQL
31
31
 
@@ -20,7 +20,7 @@
20
20
 
21
21
  - [处理并发更新](./handle-concurrent-updates) — 用 `OptimisticLockMixin` 防止丢失更新
22
22
  - [防止 MissingGreenlet 错误](./prevent-missing-greenlet) — `@requires_relations` + `lazy='raise_on_sql'` + 静态分析三道防线
23
- - [长 I/O 期间释放数据库连接](./release-connection-during-long-io) — 用 `safe_reset()` 防连接池被外部 I/O 拖死
23
+ - [长 I/O 期间释放数据库连接](./release-connection-during-long-io) — 用 `session.reset()` 防连接池被外部 I/O 拖死
24
24
 
25
25
  ### 性能优化
26
26
 
@@ -28,11 +28,11 @@ async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
28
28
  return file
29
29
  ```
30
30
 
31
- ## 2. 用 `safe_reset()` 在 I/O 前后释放连接
31
+ ## 2. 用 `session.reset()` 在 I/O 前后释放连接
32
32
 
33
- ```python
34
- from sqlmodel_ext import safe_reset
33
+ > 0.4.0 起 `safe_reset()` 已删除——其职责并入增强版 `sqlmodel_ext.AsyncSession.reset()`(用 `async_sessionmaker(class_=AsyncSession)` 构造 session),`reset()` 在释放连接的同时自动清理 FOR UPDATE 锁跟踪和缓存失效跟踪。
35
34
 
35
+ ```python
36
36
  @router.get("/files/{file_id}")
37
37
  async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
38
38
  file = await UserFile.get(session, UserFile.id == file_id)
@@ -42,7 +42,7 @@ async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
42
42
  key = file.key
43
43
  file_id = file.id
44
44
 
45
- await safe_reset(session) # ← 释放 DB 连接 // [!code ++]
45
+ await session.reset() # ← 释放 DB 连接 // [!code ++]
46
46
 
47
47
  # ↓ 下面这段不持有 DB 连接,并发请求可以拿到连接
48
48
  data = await s3.download_file(bucket_id, key)
@@ -56,26 +56,26 @@ async def get_file(session: SessionDep, s3: S3APIClientDep, file_id: UUID):
56
56
  ```
57
57
 
58
58
  **关键点**:
59
- - `safe_reset` 释放当前事务和 connection,但 session 对象本身仍存活(不是 close)
59
+ - `session.reset()` 释放当前事务和 connection,但 session 对象本身仍存活(不是 close)
60
60
  - 后续任何 `await Model.get/save` 触发 SQL 时会自动从池 checkout 新连接
61
61
  - 释放期间池有 30 个连接全部空闲(除非别的请求占用),**并发请求不会被拖死**
62
62
 
63
- ## 3. `safe_reset` 后对象的状态:detached 但不 expired
63
+ ## 3. `session.reset()` 后对象的状态:detached 但不 expired
64
64
 
65
- 调用 `safe_reset` 后,session 中所有 ORM 对象进入 **detached** 状态(不在 `session.identity_map` 中),但**已加载的 scalar 字段仍在 `obj.__dict__`** —— 访问它们不会触发 SQL 查询,因此**不会抛 `MissingGreenlet`**。
65
+ 调用 `session.reset()` 后,session 中所有 ORM 对象进入 **detached** 状态(不在 `session.identity_map` 中),但**已加载的 scalar 字段仍在 `obj.__dict__`** —— 访问它们不会触发 SQL 查询,因此**不会抛 `MissingGreenlet`**。
66
66
 
67
67
  ```python
68
68
  from sqlalchemy import inspect as sa_inspect
69
69
 
70
- # Before safe_reset:
70
+ # Before reset:
71
71
  file = await UserFile.get(session, UserFile.id == fid)
72
72
  print(sa_inspect(file).detached) # False
73
73
  print(sa_inspect(file).expired) # False
74
74
  print(file.bucket_id) # OK,已加载
75
75
 
76
- await safe_reset(session)
76
+ await session.reset()
77
77
 
78
- # After safe_reset:
78
+ # After reset:
79
79
  print(sa_inspect(file).detached) # True ← 注意 // [!code highlight]
80
80
  print(sa_inspect(file).expired) # False ← 已加载字段没被 expire // [!code highlight]
81
81
  print(file.bucket_id) # OK!scalar 字段访问安全 // [!code ++]
@@ -84,7 +84,7 @@ print(file.parent) # InvalidRequestError 或 MissingGreenlet // [
84
84
 
85
85
  ### 安全访问规则
86
86
 
87
- | 访问类型 | safe_reset 后 | 说明 |
87
+ | 访问类型 | session.reset() 后 | 说明 |
88
88
  |----------|--------------|------|
89
89
  | 已加载的 scalar 字段 | ✓ 安全 | 在 `__dict__` 中,无需 DB 查询 |
90
90
  | 已加载的关系(`load=` 预加载过) | ✓ 安全 | 同上 |
@@ -93,7 +93,7 @@ print(file.parent) # InvalidRequestError 或 MissingGreenlet // [
93
93
 
94
94
  ### 实操建议
95
95
 
96
- 写到 safe_reset 前的代码时,把后面要用到的字段**提前提取到局部变量**:
96
+ 写到 session.reset() 前的代码时,把后面要用到的字段**提前提取到局部变量**:
97
97
 
98
98
  ```python
99
99
  # 推荐风格:
@@ -102,7 +102,7 @@ bucket_id = file.bucket_id
102
102
  key = file.key
103
103
  user_id = file.user_id
104
104
 
105
- await safe_reset(session)
105
+ await session.reset()
106
106
 
107
107
  # 后面的代码用 file_id / bucket_id 等局部变量,
108
108
  # 永远不要碰 file.X
@@ -112,10 +112,10 @@ await safe_reset(session)
112
112
 
113
113
  ## 4. 写操作前必须重新查询
114
114
 
115
- `safe_reset` 后对象处于 detached 状态,**不能直接 save**。需要写操作的话,重新查询拿到 attached 实例:
115
+ `session.reset()` 后对象处于 detached 状态,**不能直接 save**。需要写操作的话,重新查询拿到 attached 实例:
116
116
 
117
117
  ```python
118
- await safe_reset(session)
118
+ await session.reset()
119
119
 
120
120
  # 长 I/O ...
121
121
  metadata = await fetch_external_metadata(...)
@@ -130,15 +130,15 @@ file.metadata = metadata
130
130
  await file.save(session)
131
131
  ```
132
132
 
133
- ## 5. 什么时候用 safe_reset,什么时候用 Taskiq
133
+ ## 5. 什么时候用 session.reset(),什么时候用 Taskiq
134
134
 
135
- `safe_reset` 适合"必须同步返回结果给客户端"的场景。如果业务可以接受异步——派发任务 + 立即返回 + 客户端轮询——那应该用 Taskiq,**根本不在 HTTP 端点里做长 I/O**。
135
+ `session.reset()` 适合"必须同步返回结果给客户端"的场景。如果业务可以接受异步——派发任务 + 立即返回 + 客户端轮询——那应该用 Taskiq,**根本不在 HTTP 端点里做长 I/O**。
136
136
 
137
137
  ```mermaid
138
138
  flowchart TD
139
139
  A["端点中需要长 I/O?"] --> B{响应能延后到下次轮询吗?}
140
140
  B -- 可以 --> C["派发 Taskiq 任务<br/>立即返回 status=pending"]
141
- B -- 不能 --> D["safe_reset 释放连接<br/>同步执行长 I/O"]
141
+ B -- 不能 --> D["session.reset() 释放连接<br/>同步执行长 I/O"]
142
142
  C --> E["最干净,连接零持有"]
143
143
  D --> F["连接释放期间池可用<br/>仍占 1 个 worker 协程"]
144
144
  ```
@@ -147,7 +147,7 @@ flowchart TD
147
147
  |------|---------|
148
148
  | 文件上传后状态轮询 | Taskiq(前端轮询能感知状态变更)|
149
149
  | AI 生成任务(图片/视频) | Taskiq(项目主流模式)|
150
- | 同步必须返回完整结果(如导出 PDF)| safe_reset |
150
+ | 同步必须返回完整结果(如导出 PDF)| session.reset() |
151
151
  | WebSocket 长连接 | 用 `session.close()` + 短生命周期 session(每次消息处理一个 session) |
152
152
 
153
153
  ## 6. 验证:你的端点真的释放了连接吗?
@@ -169,13 +169,13 @@ def _on_checkin(conn, rec):
169
169
 
170
170
  跑一个能触发长 I/O 的请求,应该看到:
171
171
  1. `checkout 1/N` 端点开始
172
- 2. `checkin 0/N` safe_reset 触发
172
+ 2. `checkin 0/N` session.reset() 触发
173
173
  3. 30 秒外部 I/O,期间没有 checkout 日志
174
174
  4. `checkout 1/N` 后续 DB 操作触发
175
175
  5. `checkin 0/N` 端点 cleanup
176
176
 
177
177
  ## 7. 相关参考
178
178
 
179
- - [`safe_reset()` API 参考](/reference/decorators#safe-reset)
179
+ - [`session.reset()` API 参考](/reference/decorators#session-reset)
180
180
  - [集成 FastAPI](./integrate-with-fastapi)
181
181
  - [防止 MissingGreenlet 错误](./prevent-missing-greenlet)
@@ -34,3 +34,12 @@ features:
34
34
  - title: 安全与可靠
35
35
  details: 乐观锁并发控制、@requires_relations 防止 MissingGreenlet、启动时 AST 静态分析
36
36
  ---
37
+
38
+ ## 设计取向
39
+
40
+ sqlmodel-ext 不是新的 ORM,而是构建在 SQLModel / Pydantic v2 / SQLAlchemy 2.0 之上的一组**可单独取用的 Mixin**。库本身只引入两类东西:
41
+
42
+ - **元类增强**——根据 `Annotated` 类型注解自动设置 SQLAlchemy 列、合并 `mapper_args`、按需生成 `*UpdateRequest` DTO、保留属性 docstring,并打上 Python 3.14 (PEP 649) 兼容补丁。
43
+ - **可插拔 Mixin**——`TableBaseMixin`(异步 CRUD)、`PolymorphicBaseMixin`(JTI/STI)、`OptimisticLockMixin`(版本号并发)、`RelationPreloadMixin`(关系预加载)、`CachedTableBaseMixin`(Redis 双层缓存)。每个 Mixin 都可以独立使用,不互相依赖。
44
+
45
+ 底层 `Session`、`select()`、查询构造、迁移工具仍是原生 SQLAlchemy,没有自创 DSL,也不接管 `engine`/`session` 的生命周期。换句话说:在已有 SQLModel/SQLAlchemy 项目里,可以单独引入需要的 Mixin,而不必重写既有数据访问层。