sqlmodel-ext 0.3.5__tar.gz → 0.4.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 (176) hide show
  1. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/PKG-INFO +75 -36
  2. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/README.md +73 -34
  3. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/README_zh.md +58 -30
  4. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/cached-table.md +20 -16
  5. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/crud-pipeline.md +1 -1
  6. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/metaclass.md +375 -319
  7. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/polymorphic-internals.md +24 -0
  8. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-load-checker.md +33 -1
  9. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/cache-queries.md +143 -142
  10. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-sti-models.md +132 -132
  11. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/handle-concurrent-updates.md +94 -88
  12. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/index.md +1 -1
  13. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/integrate-with-fastapi.md +10 -7
  14. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/prevent-missing-greenlet.md +167 -162
  15. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/release-connection-during-long-io.md +21 -21
  16. sqlmodel_ext-0.4.1/docs/en/index.md +45 -0
  17. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/base-classes.md +22 -0
  18. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/crud-methods.md +7 -1
  19. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/decorators.md +8 -6
  20. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/field-types.md +62 -1
  21. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/index.md +2 -2
  22. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/mixins.md +35 -0
  23. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/pagination-types.md +1 -1
  24. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/01-getting-started.md +7 -6
  25. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/02-building-a-blog-api.md +389 -388
  26. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/03-adding-redis-cache.md +277 -269
  27. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/index.md +1 -1
  28. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/cached-table.md +16 -16
  29. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/crud-pipeline.md +1 -1
  30. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/metaclass.md +351 -319
  31. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/polymorphic-internals.md +13 -0
  32. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/relation-load-checker.md +21 -1
  33. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/cache-queries.md +143 -142
  34. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/define-sti-models.md +132 -132
  35. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/handle-concurrent-updates.md +93 -87
  36. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/index.md +1 -1
  37. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/integrate-with-fastapi.md +10 -7
  38. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/prevent-missing-greenlet.md +164 -162
  39. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/release-connection-during-long-io.md +21 -21
  40. sqlmodel_ext-0.4.1/docs/index.md +45 -0
  41. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/base-classes.md +19 -0
  42. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/crud-methods.md +5 -1
  43. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/decorators.md +8 -6
  44. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/field-types.md +62 -1
  45. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/index.md +2 -2
  46. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/mixins.md +30 -0
  47. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/pagination-types.md +1 -1
  48. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/01-getting-started.md +7 -6
  49. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/02-building-a-blog-api.md +388 -388
  50. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/03-adding-redis-cache.md +277 -269
  51. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/index.md +1 -1
  52. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/examples/consumer_integration/run.py +149 -148
  53. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/pyproject.toml +2 -2
  54. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/__init__.py +42 -3
  55. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/base.py +329 -8
  56. sqlmodel_ext-0.4.1/src/sqlmodel_ext/field_types/__init__.py +560 -0
  57. sqlmodel_ext-0.4.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +218 -0
  58. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/__init__.py +1 -1
  59. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/cached_table.py +276 -239
  60. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/polymorphic.py +118 -2
  61. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/table.py +105 -26
  62. sqlmodel_ext-0.4.1/src/sqlmodel_ext/session.py +105 -0
  63. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/_models.py +69 -45
  64. sqlmodel_ext-0.4.1/tests/test_all_fields_optional_constraints.py +76 -0
  65. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_array_explicit_field_sa_type.py +34 -5
  66. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_basic_crud.py +71 -61
  67. sqlmodel_ext-0.4.1/tests/test_cached_table.py +513 -0
  68. sqlmodel_ext-0.4.1/tests/test_crud_extended.py +582 -0
  69. sqlmodel_ext-0.4.1/tests/test_decimal_types.py +256 -0
  70. sqlmodel_ext-0.4.1/tests/test_deferred_index.py +50 -0
  71. sqlmodel_ext-0.4.1/tests/test_enhanced_session.py +87 -0
  72. sqlmodel_ext-0.4.1/tests/test_field_types_misc.py +189 -0
  73. sqlmodel_ext-0.4.1/tests/test_field_types_str_num.py +400 -0
  74. sqlmodel_ext-0.4.1/tests/test_field_types_url_ssrf.py +267 -0
  75. sqlmodel_ext-0.4.1/tests/test_metaclass_extra.py +194 -0
  76. sqlmodel_ext-0.4.1/tests/test_optimistic_lock.py +190 -0
  77. sqlmodel_ext-0.4.1/tests/test_pagination.py +259 -0
  78. sqlmodel_ext-0.4.1/tests/test_pg_dialect_types.py +427 -0
  79. sqlmodel_ext-0.4.1/tests/test_polymorphic_jti.py +273 -0
  80. sqlmodel_ext-0.4.1/tests/test_polymorphic_sti_query.py +418 -0
  81. sqlmodel_ext-0.4.1/tests/test_relation_load_checker.py +232 -0
  82. sqlmodel_ext-0.4.1/tests/test_relation_preload.py +240 -0
  83. sqlmodel_ext-0.4.1/tests/test_relationship_pep604.py +115 -0
  84. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_smoke_imports.py +41 -1
  85. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/uv.lock +1 -1
  86. sqlmodel_ext-0.3.5/docs/en/index.md +0 -36
  87. sqlmodel_ext-0.3.5/docs/index.md +0 -36
  88. sqlmodel_ext-0.3.5/src/sqlmodel_ext/field_types/__init__.py +0 -195
  89. sqlmodel_ext-0.3.5/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -119
  90. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/.github/workflows/ci.yml +0 -0
  91. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/.gitignore +0 -0
  92. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/LICENSE +0 -0
  93. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/config.mts +0 -0
  94. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/en.mts +0 -0
  95. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/index.ts +0 -0
  96. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/style/custom.css +0 -0
  97. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/zh.mts +0 -0
  98. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/cascade-delete-semantics.md +0 -0
  99. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/index.md +0 -0
  100. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/optimistic-lock.md +0 -0
  101. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/prerequisites.md +0 -0
  102. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-preload.md +0 -0
  103. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/cached-table.md +0 -0
  104. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/crud.md +0 -0
  105. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/field-types.md +0 -0
  106. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/index.md +0 -0
  107. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/optimistic-lock.md +0 -0
  108. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/pagination.md +0 -0
  109. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/polymorphic.md +0 -0
  110. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-load-checker.md +0 -0
  111. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-preload.md +0 -0
  112. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/configure-cascade-delete.md +0 -0
  113. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-jti-models.md +0 -0
  114. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
  115. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/cached-table.md +0 -0
  116. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/crud.md +0 -0
  117. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/index.md +0 -0
  118. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/metaclass.md +0 -0
  119. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/optimistic-lock.md +0 -0
  120. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/polymorphic.md +0 -0
  121. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/prerequisites.md +0 -0
  122. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-load-checker.md +0 -0
  123. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-preload.md +0 -0
  124. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/cascade-delete-semantics.md +0 -0
  125. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/index.md +0 -0
  126. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/optimistic-lock.md +0 -0
  127. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/prerequisites.md +0 -0
  128. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/relation-preload.md +0 -0
  129. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/cached-table.md +0 -0
  130. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/crud.md +0 -0
  131. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/field-types.md +0 -0
  132. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/index.md +0 -0
  133. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/optimistic-lock.md +0 -0
  134. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/pagination.md +0 -0
  135. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/polymorphic.md +0 -0
  136. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/relation-load-checker.md +0 -0
  137. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/relation-preload.md +0 -0
  138. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/configure-cascade-delete.md +0 -0
  139. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/define-jti-models.md +0 -0
  140. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/paginate-a-list-endpoint.md +0 -0
  141. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/cached-table.md +0 -0
  142. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/crud.md +0 -0
  143. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/index.md +0 -0
  144. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/metaclass.md +0 -0
  145. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/optimistic-lock.md +0 -0
  146. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/polymorphic.md +0 -0
  147. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/prerequisites.md +0 -0
  148. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/relation-load-checker.md +0 -0
  149. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/relation-preload.md +0 -0
  150. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/package.json +0 -0
  151. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/pnpm-lock.yaml +0 -0
  152. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_compat.py +0 -0
  153. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_exceptions.py +0 -0
  154. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_sa_type.py +0 -0
  155. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_utils.py +0 -0
  156. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
  157. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
  158. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
  159. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
  160. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
  161. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
  162. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
  163. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
  164. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
  165. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
  166. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
  167. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/url.py +0 -0
  168. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/info_response.py +0 -0
  169. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
  170. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
  171. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/pagination.py +0 -0
  172. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/relation_load_checker.py +0 -0
  173. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/__init__.py +0 -0
  174. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/conftest.py +0 -0
  175. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_annotated_default_factory_inheritance.py +0 -0
  176. {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/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.1
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
@@ -24,7 +24,7 @@ Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.12
25
25
  Requires-Dist: pydantic>=2.0
26
26
  Requires-Dist: sqlalchemy>=2.0
27
- Requires-Dist: sqlmodel>=0.0.22
27
+ Requires-Dist: sqlmodel>=0.0.32
28
28
  Provides-Extra: cache
29
29
  Requires-Dist: orjson>=3.0; extra == 'cache'
30
30
  Requires-Dist: redis>=5.0; extra == 'cache'
@@ -70,7 +70,7 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
70
70
  | **RelationPreloadMixin** | Decorator-based automatic relationship preloading (prevents `MissingGreenlet` errors) |
71
71
  | **ListResponse[T]** | Generic paginated response model for list endpoints |
72
72
  | **Field Types** | Reusable constrained types: `Str64`, `Port`, `IPAddress`, `HttpUrl`, `SafeHttpUrl`, and more |
73
- | **PostgreSQL Types** | `Array[T]` for native ARRAY, `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
73
+ | **PostgreSQL Types** | `Array[T]` for native ARRAY (read-tolerant for enum arrays during rolling-deploy version skew), `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
74
74
  | **Info Response DTOs** | Pre-built mixins for API response models with id/timestamp fields |
75
75
 
76
76
  ## Installation
@@ -108,12 +108,13 @@ pip install sqlmodel-ext[pgvector]
108
108
  ### Define Models
109
109
 
110
110
  ```python
111
- from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64
111
+ from pydantic import EmailStr # requires: pip install 'pydantic[email]'
112
+ from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, NonEmptyStrippedStr64
112
113
 
113
114
  # Base class -- fields only, no database table
114
115
  class UserBase(SQLModelBase):
115
- name: Str64
116
- email: str
116
+ name: NonEmptyStrippedStr64 # user-visible name: rejects "" and whitespace-only
117
+ email: EmailStr
117
118
 
118
119
  # Table class -- inherits fields + gains async CRUD + UUID primary key
119
120
  class User(UserBase, UUIDTableBaseMixin, table=True):
@@ -438,17 +439,19 @@ async def delete_article(session: SessionDep, article_id: UUID) -> None:
438
439
 
439
440
  ```python
440
441
  from abc import ABC, abstractmethod
442
+ from pydantic import EmailStr
441
443
  from sqlmodel_ext import (
442
444
  SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin,
443
445
  AutoPolymorphicIdentityMixin, create_subclass_id_mixin,
444
446
  ListResponse, TableViewRequest,
447
+ Str512, Text1K,
445
448
  )
446
449
 
447
450
  # ── Polymorphic models ────────────────────────────────────────────
448
451
 
449
452
  class NotificationBase(SQLModelBase):
450
453
  user_id: UUID = Field(foreign_key='user.id')
451
- message: str
454
+ message: Text1K
452
455
 
453
456
  class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
454
457
  @abstractmethod
@@ -457,13 +460,13 @@ class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, A
457
460
  NotifSubclassId = create_subclass_id_mixin('notification')
458
461
 
459
462
  class EmailNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
460
- email_to: str
463
+ email_to: EmailStr
461
464
 
462
465
  def summary(self) -> str:
463
466
  return f"Email to {self.email_to}: {self.message}"
464
467
 
465
468
  class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
466
- device_token: str
469
+ device_token: Str512
467
470
 
468
471
  def summary(self) -> str:
469
472
  return f"Push to {self.device_token}: {self.message}"
@@ -493,15 +496,15 @@ These mixins provide the async CRUD interface. `TableBaseMixin` uses an auto-inc
493
496
  Both mixins automatically add `id`, `created_at`, and `updated_at` fields.
494
497
 
495
498
  ```python
496
- from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
499
+ from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, NonEmptyStrippedStr64, Text1K
497
500
 
498
501
  # Integer primary key
499
502
  class LogEntry(SQLModelBase, TableBaseMixin, table=True):
500
- message: str
503
+ message: Text1K
501
504
 
502
505
  # UUID primary key (recommended for most use cases)
503
506
  class Project(SQLModelBase, UUIDTableBaseMixin, table=True):
504
- name: str
507
+ name: NonEmptyStrippedStr64
505
508
  ```
506
509
 
507
510
  #### `add()` -- Batch Insert
@@ -529,7 +532,9 @@ user = await User.add(session, User(name="Alice", email="a@x.com"))
529
532
  user = await user.save(session)
530
533
 
531
534
  # Save with relationship preloading
532
- user = await user.save(session, load=User.profile)
535
+ # (rel() casts the Relationship field for type checkers, which would
536
+ # otherwise infer User.profile as Profile instead of a loadable attribute)
537
+ user = await user.save(session, load=rel(User.profile))
533
538
 
534
539
  # Save with optimistic lock retry
535
540
  user = await user.save(session, optimistic_retry_count=3)
@@ -562,9 +567,10 @@ user3 = await user3.save(session) # commits all three
562
567
  #### `update()` -- Partial Update from a Model
563
568
 
564
569
  ```python
565
- class UserUpdate(SQLModelBase):
566
- name: str | None = None
567
- email: str | None = None
570
+ # all_fields_optional auto-converts every inherited field to ``T | None = None``
571
+ # while preserving constraints -- no hand-written Optional boilerplate
572
+ class UserUpdate(UserBase, all_fields_optional=True):
573
+ pass
568
574
 
569
575
  # Only updates fields that were explicitly set
570
576
  user = await user.update(session, UserUpdate(name="Charlie"))
@@ -643,7 +649,7 @@ users = await User.get(session, fetch_mode="all")
643
649
  user = await User.get(
644
650
  session,
645
651
  User.id == user_id,
646
- load=[User.profile, User.orders],
652
+ load=[rel(User.profile), rel(User.orders)],
647
653
  )
648
654
 
649
655
  # With JOIN
@@ -767,11 +773,12 @@ from sqlmodel_ext import (
767
773
  SQLModelBase, UUIDTableBaseMixin,
768
774
  PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
769
775
  create_subclass_id_mixin,
776
+ HttpUrl, NonEmptyStrippedStr64, NonNegativeInt,
770
777
  )
771
778
 
772
779
  # 1. Base class (fields only, no table)
773
780
  class ToolBase(SQLModelBase):
774
- name: str
781
+ name: NonEmptyStrippedStr64
775
782
 
776
783
  # 2. Abstract parent (creates the parent table)
777
784
  class Tool(ToolBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
@@ -783,13 +790,13 @@ ToolSubclassIdMixin = create_subclass_id_mixin('tool')
783
790
 
784
791
  # 4. Concrete subclasses (each gets its own table)
785
792
  class WebSearchTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
786
- search_url: str
793
+ search_url: HttpUrl
787
794
 
788
795
  async def execute(self) -> str:
789
796
  return f"Searching {self.search_url}"
790
797
 
791
798
  class CalculatorTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
792
- precision: int = 2
799
+ precision: NonNegativeInt = 2
793
800
 
794
801
  async def execute(self) -> str:
795
802
  return "Calculating..."
@@ -823,16 +830,17 @@ from sqlmodel_ext import (
823
830
  PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
824
831
  register_sti_columns_for_all_subclasses,
825
832
  register_sti_column_properties_for_all_subclasses,
833
+ NonNegativeBigInt, Str256,
826
834
  )
827
835
 
828
836
  class UserFile(SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin, table=True):
829
- filename: str
837
+ filename: Str256
830
838
 
831
839
  class PendingFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
832
840
  upload_deadline: datetime | None = None # Added to userfile table as nullable
833
841
 
834
842
  class CompletedFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
835
- file_size: int | None = None # Added to userfile table as nullable
843
+ file_size: NonNegativeBigInt | None = None # Added to userfile table as nullable
836
844
 
837
845
  # After all models are defined, before configure_mappers():
838
846
  register_sti_columns_for_all_subclasses()
@@ -858,7 +866,7 @@ class ToolSet(SQLModelBase, UUIDTableBaseMixin, table=True):
858
866
  tool_set = await ToolSet.get(
859
867
  session,
860
868
  ToolSet.id == ts_id,
861
- load=ToolSet.tools,
869
+ load=rel(ToolSet.tools),
862
870
  jti_subclasses='all', # Loads all subclass-specific columns
863
871
  )
864
872
  ```
@@ -885,15 +893,22 @@ Tool._is_joined_table_inheritance() # True for JTI, False for STI
885
893
  Prevents lost updates in concurrent environments using SQLAlchemy's `version_id_col` mechanism.
886
894
 
887
895
  ```python
896
+ from enum import StrEnum
897
+
888
898
  from sqlmodel_ext import (
889
899
  SQLModelBase, UUIDTableBaseMixin,
890
900
  OptimisticLockMixin, OptimisticLockError,
901
+ NonNegativeDecimal38_18,
891
902
  )
892
903
 
904
+ class OrderStatusEnum(StrEnum):
905
+ pending = 'pending'
906
+ paid = 'paid'
907
+
893
908
  # OptimisticLockMixin MUST come before TableBaseMixin in MRO
894
909
  class Order(OptimisticLockMixin, UUIDTableBaseMixin, table=True):
895
- status: str
896
- amount: int
910
+ status: OrderStatusEnum = OrderStatusEnum.pending
911
+ amount: NonNegativeDecimal38_18
897
912
  ```
898
913
 
899
914
  The mixin adds a `version` integer field (starting at 0). Every `UPDATE` generates SQL like:
@@ -945,11 +960,11 @@ The `RelationPreloadMixin` and `@requires_relations` decorator automatically loa
945
960
 
946
961
  ```python
947
962
  from sqlmodel import Relationship
948
- from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase
963
+ from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase, NonNegativeDecimal38_18
949
964
  from sqlmodel_ext.mixins import RelationPreloadMixin, requires_relations
950
965
 
951
966
  class GeneratorConfig(SQLModelBase, UUIDTableBaseMixin, table=True):
952
- price: int
967
+ price: NonNegativeDecimal38_18
953
968
 
954
969
  class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
955
970
  config: GeneratorConfig = Relationship()
@@ -1391,29 +1406,53 @@ When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQ
1391
1406
 
1392
1407
  ```python
1393
1408
  class UserBase(SQLModelBase):
1394
- name: str
1409
+ name: NonEmptyStrippedStr64
1395
1410
  """User display name""" # ← parsed by Pydantic
1396
1411
 
1397
1412
  class UserUpdateRequest(UserBase, all_fields_optional=True):
1398
1413
  pass
1399
- # name: str | None = None — description "User display name" is automatically inherited
1414
+ # name: NonEmptyStrippedStr64 | None = None — description "User display name" is inherited
1400
1415
  # Shows up correctly in OpenAPI/Swagger docs
1401
1416
  ```
1402
1417
 
1403
1418
  ---
1404
1419
 
1405
- ### safe_reset
1420
+ ### Enhanced AsyncSession (cache-aware commit/reset/refresh)
1406
1421
 
1407
- Safely reset an async session, clearing both the DB connection and FOR UPDATE lock tracking:
1422
+ `sqlmodel_ext.AsyncSession` subclasses sqlmodel's `AsyncSession` and makes cache
1423
+ correctness automatic. Point your session factory at it:
1408
1424
 
1409
1425
  ```python
1410
- from sqlmodel_ext import safe_reset
1426
+ from sqlalchemy.ext.asyncio import async_sessionmaker
1427
+ from sqlmodel_ext import AsyncSession
1411
1428
 
1412
- # In long-lived tasks (e.g. background workers) between operations:
1413
- await safe_reset(session)
1429
+ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=True)
1414
1430
  ```
1415
1431
 
1416
- This prevents stale `FOR UPDATE` lock tracking in `session.info` when Python reuses object IDs across `reset()` cycles.
1432
+ What it does:
1433
+
1434
+ - **`commit()`** auto-registers every `CachedTableBaseMixin` mutation in the
1435
+ session (including bare `session.add()` / attribute mutation /
1436
+ `session.delete()` paths) and synchronously invalidates the Redis cache after
1437
+ commit — no more "forgot the cache-aware commit" stale windows.
1438
+ - **`reset()`** releases the connection and clears FOR UPDATE lock tracking +
1439
+ cache-invalidation tracking from `session.info` (replaces the removed
1440
+ `safe_reset()` helper — just call `await session.reset()`).
1441
+ - **`refresh()`** routes whole-object refreshes of cached models through
1442
+ `Model.get()` (Redis hit + STI polymorphic columns) instead of a raw DB query.
1443
+ - **`execute()`** warns when a bare `UPDATE`/`DELETE` hits a cached table
1444
+ without registered invalidation.
1445
+
1446
+ Models that don't use `CachedTableBaseMixin` are unaffected — every hook
1447
+ degrades to upstream behavior.
1448
+
1449
+ > **Breaking change in 0.4.0**: `safe_reset()` and
1450
+ > `CachedTableBaseMixin.cache_aware_commit()` were removed. CRUD methods no
1451
+ > longer invalidate the cache themselves; invalidation now happens inside
1452
+ > `AsyncSession.commit()`. If you use `CachedTableBaseMixin`, you **must**
1453
+ > construct sessions with `class_=sqlmodel_ext.AsyncSession` (plain sessions
1454
+ > fall back to the fire-and-forget `after_commit` compensation hook, which
1455
+ > reintroduces a brief stale-cache window).
1417
1456
 
1418
1457
  ---
1419
1458
 
@@ -1430,7 +1469,7 @@ sqlmodel_ext/
1430
1469
  pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
1431
1470
  mixins/
1432
1471
  __init__.py # Mixin re-exports
1433
- table.py # TableBaseMixin, UUIDTableBaseMixin, safe_reset (async CRUD)
1472
+ table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
1434
1473
  cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
1435
1474
  polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
1436
1475
  optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
@@ -25,7 +25,7 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
25
25
  | **RelationPreloadMixin** | Decorator-based automatic relationship preloading (prevents `MissingGreenlet` errors) |
26
26
  | **ListResponse[T]** | Generic paginated response model for list endpoints |
27
27
  | **Field Types** | Reusable constrained types: `Str64`, `Port`, `IPAddress`, `HttpUrl`, `SafeHttpUrl`, and more |
28
- | **PostgreSQL Types** | `Array[T]` for native ARRAY, `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
28
+ | **PostgreSQL Types** | `Array[T]` for native ARRAY (read-tolerant for enum arrays during rolling-deploy version skew), `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
29
29
  | **Info Response DTOs** | Pre-built mixins for API response models with id/timestamp fields |
30
30
 
31
31
  ## Installation
@@ -63,12 +63,13 @@ pip install sqlmodel-ext[pgvector]
63
63
  ### Define Models
64
64
 
65
65
  ```python
66
- from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64
66
+ from pydantic import EmailStr # requires: pip install 'pydantic[email]'
67
+ from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, NonEmptyStrippedStr64
67
68
 
68
69
  # Base class -- fields only, no database table
69
70
  class UserBase(SQLModelBase):
70
- name: Str64
71
- email: str
71
+ name: NonEmptyStrippedStr64 # user-visible name: rejects "" and whitespace-only
72
+ email: EmailStr
72
73
 
73
74
  # Table class -- inherits fields + gains async CRUD + UUID primary key
74
75
  class User(UserBase, UUIDTableBaseMixin, table=True):
@@ -393,17 +394,19 @@ async def delete_article(session: SessionDep, article_id: UUID) -> None:
393
394
 
394
395
  ```python
395
396
  from abc import ABC, abstractmethod
397
+ from pydantic import EmailStr
396
398
  from sqlmodel_ext import (
397
399
  SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin,
398
400
  AutoPolymorphicIdentityMixin, create_subclass_id_mixin,
399
401
  ListResponse, TableViewRequest,
402
+ Str512, Text1K,
400
403
  )
401
404
 
402
405
  # ── Polymorphic models ────────────────────────────────────────────
403
406
 
404
407
  class NotificationBase(SQLModelBase):
405
408
  user_id: UUID = Field(foreign_key='user.id')
406
- message: str
409
+ message: Text1K
407
410
 
408
411
  class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
409
412
  @abstractmethod
@@ -412,13 +415,13 @@ class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, A
412
415
  NotifSubclassId = create_subclass_id_mixin('notification')
413
416
 
414
417
  class EmailNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
415
- email_to: str
418
+ email_to: EmailStr
416
419
 
417
420
  def summary(self) -> str:
418
421
  return f"Email to {self.email_to}: {self.message}"
419
422
 
420
423
  class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
421
- device_token: str
424
+ device_token: Str512
422
425
 
423
426
  def summary(self) -> str:
424
427
  return f"Push to {self.device_token}: {self.message}"
@@ -448,15 +451,15 @@ These mixins provide the async CRUD interface. `TableBaseMixin` uses an auto-inc
448
451
  Both mixins automatically add `id`, `created_at`, and `updated_at` fields.
449
452
 
450
453
  ```python
451
- from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
454
+ from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, NonEmptyStrippedStr64, Text1K
452
455
 
453
456
  # Integer primary key
454
457
  class LogEntry(SQLModelBase, TableBaseMixin, table=True):
455
- message: str
458
+ message: Text1K
456
459
 
457
460
  # UUID primary key (recommended for most use cases)
458
461
  class Project(SQLModelBase, UUIDTableBaseMixin, table=True):
459
- name: str
462
+ name: NonEmptyStrippedStr64
460
463
  ```
461
464
 
462
465
  #### `add()` -- Batch Insert
@@ -484,7 +487,9 @@ user = await User.add(session, User(name="Alice", email="a@x.com"))
484
487
  user = await user.save(session)
485
488
 
486
489
  # Save with relationship preloading
487
- user = await user.save(session, load=User.profile)
490
+ # (rel() casts the Relationship field for type checkers, which would
491
+ # otherwise infer User.profile as Profile instead of a loadable attribute)
492
+ user = await user.save(session, load=rel(User.profile))
488
493
 
489
494
  # Save with optimistic lock retry
490
495
  user = await user.save(session, optimistic_retry_count=3)
@@ -517,9 +522,10 @@ user3 = await user3.save(session) # commits all three
517
522
  #### `update()` -- Partial Update from a Model
518
523
 
519
524
  ```python
520
- class UserUpdate(SQLModelBase):
521
- name: str | None = None
522
- email: str | None = None
525
+ # all_fields_optional auto-converts every inherited field to ``T | None = None``
526
+ # while preserving constraints -- no hand-written Optional boilerplate
527
+ class UserUpdate(UserBase, all_fields_optional=True):
528
+ pass
523
529
 
524
530
  # Only updates fields that were explicitly set
525
531
  user = await user.update(session, UserUpdate(name="Charlie"))
@@ -598,7 +604,7 @@ users = await User.get(session, fetch_mode="all")
598
604
  user = await User.get(
599
605
  session,
600
606
  User.id == user_id,
601
- load=[User.profile, User.orders],
607
+ load=[rel(User.profile), rel(User.orders)],
602
608
  )
603
609
 
604
610
  # With JOIN
@@ -722,11 +728,12 @@ from sqlmodel_ext import (
722
728
  SQLModelBase, UUIDTableBaseMixin,
723
729
  PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
724
730
  create_subclass_id_mixin,
731
+ HttpUrl, NonEmptyStrippedStr64, NonNegativeInt,
725
732
  )
726
733
 
727
734
  # 1. Base class (fields only, no table)
728
735
  class ToolBase(SQLModelBase):
729
- name: str
736
+ name: NonEmptyStrippedStr64
730
737
 
731
738
  # 2. Abstract parent (creates the parent table)
732
739
  class Tool(ToolBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
@@ -738,13 +745,13 @@ ToolSubclassIdMixin = create_subclass_id_mixin('tool')
738
745
 
739
746
  # 4. Concrete subclasses (each gets its own table)
740
747
  class WebSearchTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
741
- search_url: str
748
+ search_url: HttpUrl
742
749
 
743
750
  async def execute(self) -> str:
744
751
  return f"Searching {self.search_url}"
745
752
 
746
753
  class CalculatorTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
747
- precision: int = 2
754
+ precision: NonNegativeInt = 2
748
755
 
749
756
  async def execute(self) -> str:
750
757
  return "Calculating..."
@@ -778,16 +785,17 @@ from sqlmodel_ext import (
778
785
  PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
779
786
  register_sti_columns_for_all_subclasses,
780
787
  register_sti_column_properties_for_all_subclasses,
788
+ NonNegativeBigInt, Str256,
781
789
  )
782
790
 
783
791
  class UserFile(SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin, table=True):
784
- filename: str
792
+ filename: Str256
785
793
 
786
794
  class PendingFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
787
795
  upload_deadline: datetime | None = None # Added to userfile table as nullable
788
796
 
789
797
  class CompletedFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
790
- file_size: int | None = None # Added to userfile table as nullable
798
+ file_size: NonNegativeBigInt | None = None # Added to userfile table as nullable
791
799
 
792
800
  # After all models are defined, before configure_mappers():
793
801
  register_sti_columns_for_all_subclasses()
@@ -813,7 +821,7 @@ class ToolSet(SQLModelBase, UUIDTableBaseMixin, table=True):
813
821
  tool_set = await ToolSet.get(
814
822
  session,
815
823
  ToolSet.id == ts_id,
816
- load=ToolSet.tools,
824
+ load=rel(ToolSet.tools),
817
825
  jti_subclasses='all', # Loads all subclass-specific columns
818
826
  )
819
827
  ```
@@ -840,15 +848,22 @@ Tool._is_joined_table_inheritance() # True for JTI, False for STI
840
848
  Prevents lost updates in concurrent environments using SQLAlchemy's `version_id_col` mechanism.
841
849
 
842
850
  ```python
851
+ from enum import StrEnum
852
+
843
853
  from sqlmodel_ext import (
844
854
  SQLModelBase, UUIDTableBaseMixin,
845
855
  OptimisticLockMixin, OptimisticLockError,
856
+ NonNegativeDecimal38_18,
846
857
  )
847
858
 
859
+ class OrderStatusEnum(StrEnum):
860
+ pending = 'pending'
861
+ paid = 'paid'
862
+
848
863
  # OptimisticLockMixin MUST come before TableBaseMixin in MRO
849
864
  class Order(OptimisticLockMixin, UUIDTableBaseMixin, table=True):
850
- status: str
851
- amount: int
865
+ status: OrderStatusEnum = OrderStatusEnum.pending
866
+ amount: NonNegativeDecimal38_18
852
867
  ```
853
868
 
854
869
  The mixin adds a `version` integer field (starting at 0). Every `UPDATE` generates SQL like:
@@ -900,11 +915,11 @@ The `RelationPreloadMixin` and `@requires_relations` decorator automatically loa
900
915
 
901
916
  ```python
902
917
  from sqlmodel import Relationship
903
- from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase
918
+ from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase, NonNegativeDecimal38_18
904
919
  from sqlmodel_ext.mixins import RelationPreloadMixin, requires_relations
905
920
 
906
921
  class GeneratorConfig(SQLModelBase, UUIDTableBaseMixin, table=True):
907
- price: int
922
+ price: NonNegativeDecimal38_18
908
923
 
909
924
  class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
910
925
  config: GeneratorConfig = Relationship()
@@ -1346,29 +1361,53 @@ When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQ
1346
1361
 
1347
1362
  ```python
1348
1363
  class UserBase(SQLModelBase):
1349
- name: str
1364
+ name: NonEmptyStrippedStr64
1350
1365
  """User display name""" # ← parsed by Pydantic
1351
1366
 
1352
1367
  class UserUpdateRequest(UserBase, all_fields_optional=True):
1353
1368
  pass
1354
- # name: str | None = None — description "User display name" is automatically inherited
1369
+ # name: NonEmptyStrippedStr64 | None = None — description "User display name" is inherited
1355
1370
  # Shows up correctly in OpenAPI/Swagger docs
1356
1371
  ```
1357
1372
 
1358
1373
  ---
1359
1374
 
1360
- ### safe_reset
1375
+ ### Enhanced AsyncSession (cache-aware commit/reset/refresh)
1361
1376
 
1362
- Safely reset an async session, clearing both the DB connection and FOR UPDATE lock tracking:
1377
+ `sqlmodel_ext.AsyncSession` subclasses sqlmodel's `AsyncSession` and makes cache
1378
+ correctness automatic. Point your session factory at it:
1363
1379
 
1364
1380
  ```python
1365
- from sqlmodel_ext import safe_reset
1381
+ from sqlalchemy.ext.asyncio import async_sessionmaker
1382
+ from sqlmodel_ext import AsyncSession
1366
1383
 
1367
- # In long-lived tasks (e.g. background workers) between operations:
1368
- await safe_reset(session)
1384
+ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=True)
1369
1385
  ```
1370
1386
 
1371
- This prevents stale `FOR UPDATE` lock tracking in `session.info` when Python reuses object IDs across `reset()` cycles.
1387
+ What it does:
1388
+
1389
+ - **`commit()`** auto-registers every `CachedTableBaseMixin` mutation in the
1390
+ session (including bare `session.add()` / attribute mutation /
1391
+ `session.delete()` paths) and synchronously invalidates the Redis cache after
1392
+ commit — no more "forgot the cache-aware commit" stale windows.
1393
+ - **`reset()`** releases the connection and clears FOR UPDATE lock tracking +
1394
+ cache-invalidation tracking from `session.info` (replaces the removed
1395
+ `safe_reset()` helper — just call `await session.reset()`).
1396
+ - **`refresh()`** routes whole-object refreshes of cached models through
1397
+ `Model.get()` (Redis hit + STI polymorphic columns) instead of a raw DB query.
1398
+ - **`execute()`** warns when a bare `UPDATE`/`DELETE` hits a cached table
1399
+ without registered invalidation.
1400
+
1401
+ Models that don't use `CachedTableBaseMixin` are unaffected — every hook
1402
+ degrades to upstream behavior.
1403
+
1404
+ > **Breaking change in 0.4.0**: `safe_reset()` and
1405
+ > `CachedTableBaseMixin.cache_aware_commit()` were removed. CRUD methods no
1406
+ > longer invalidate the cache themselves; invalidation now happens inside
1407
+ > `AsyncSession.commit()`. If you use `CachedTableBaseMixin`, you **must**
1408
+ > construct sessions with `class_=sqlmodel_ext.AsyncSession` (plain sessions
1409
+ > fall back to the fire-and-forget `after_commit` compensation hook, which
1410
+ > reintroduces a brief stale-cache window).
1372
1411
 
1373
1412
  ---
1374
1413
 
@@ -1385,7 +1424,7 @@ sqlmodel_ext/
1385
1424
  pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
1386
1425
  mixins/
1387
1426
  __init__.py # Mixin re-exports
1388
- table.py # TableBaseMixin, UUIDTableBaseMixin, safe_reset (async CRUD)
1427
+ table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
1389
1428
  cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
1390
1429
  polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
1391
1430
  optimistic_lock.py # OptimisticLockMixin, OptimisticLockError