sqlmodel-ext 0.4.0__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 (173) hide show
  1. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/PKG-INFO +44 -29
  2. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/README.md +42 -27
  3. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/README_zh.md +40 -25
  4. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/cached-table.md +20 -16
  5. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/crud-pipeline.md +1 -1
  6. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/metaclass.md +375 -319
  7. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/polymorphic-internals.md +24 -0
  8. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-load-checker.md +33 -1
  9. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/cache-queries.md +143 -142
  10. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-sti-models.md +132 -132
  11. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/handle-concurrent-updates.md +94 -88
  12. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/integrate-with-fastapi.md +10 -7
  13. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/prevent-missing-greenlet.md +167 -162
  14. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/index.md +4 -4
  15. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/base-classes.md +22 -0
  16. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/crud-methods.md +7 -1
  17. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/field-types.md +4 -0
  18. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/index.md +1 -1
  19. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/mixins.md +35 -0
  20. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/pagination-types.md +1 -1
  21. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/01-getting-started.md +6 -5
  22. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/02-building-a-blog-api.md +389 -388
  23. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/03-adding-redis-cache.md +277 -269
  24. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/cached-table.md +16 -16
  25. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/crud-pipeline.md +1 -1
  26. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/metaclass.md +351 -319
  27. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/polymorphic-internals.md +13 -0
  28. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/relation-load-checker.md +21 -1
  29. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/cache-queries.md +143 -142
  30. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/define-sti-models.md +132 -132
  31. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/handle-concurrent-updates.md +93 -87
  32. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/integrate-with-fastapi.md +10 -7
  33. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/prevent-missing-greenlet.md +164 -162
  34. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/index.md +4 -4
  35. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/base-classes.md +19 -0
  36. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/crud-methods.md +5 -1
  37. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/field-types.md +4 -0
  38. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/index.md +1 -1
  39. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/mixins.md +30 -0
  40. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/pagination-types.md +1 -1
  41. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/01-getting-started.md +6 -5
  42. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/02-building-a-blog-api.md +388 -388
  43. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/03-adding-redis-cache.md +277 -269
  44. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/examples/consumer_integration/run.py +149 -148
  45. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/pyproject.toml +2 -2
  46. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/__init__.py +1 -1
  47. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/base.py +127 -3
  48. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/__init__.py +53 -1
  49. sqlmodel_ext-0.4.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +218 -0
  50. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/table.py +70 -13
  51. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/_models.py +69 -67
  52. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_array_explicit_field_sa_type.py +18 -4
  53. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_basic_crud.py +71 -61
  54. sqlmodel_ext-0.4.1/tests/test_cached_table.py +513 -0
  55. sqlmodel_ext-0.4.1/tests/test_crud_extended.py +582 -0
  56. sqlmodel_ext-0.4.1/tests/test_field_types_misc.py +189 -0
  57. sqlmodel_ext-0.4.1/tests/test_field_types_str_num.py +400 -0
  58. sqlmodel_ext-0.4.1/tests/test_field_types_url_ssrf.py +267 -0
  59. sqlmodel_ext-0.4.1/tests/test_metaclass_extra.py +194 -0
  60. sqlmodel_ext-0.4.1/tests/test_optimistic_lock.py +190 -0
  61. sqlmodel_ext-0.4.1/tests/test_pagination.py +259 -0
  62. sqlmodel_ext-0.4.1/tests/test_pg_dialect_types.py +427 -0
  63. sqlmodel_ext-0.4.1/tests/test_polymorphic_jti.py +273 -0
  64. sqlmodel_ext-0.4.1/tests/test_polymorphic_sti_query.py +418 -0
  65. sqlmodel_ext-0.4.1/tests/test_relation_load_checker.py +232 -0
  66. sqlmodel_ext-0.4.1/tests/test_relation_preload.py +240 -0
  67. sqlmodel_ext-0.4.1/tests/test_relationship_pep604.py +115 -0
  68. sqlmodel_ext-0.4.0/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -119
  69. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/.github/workflows/ci.yml +0 -0
  70. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/.gitignore +0 -0
  71. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/LICENSE +0 -0
  72. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/config.mts +0 -0
  73. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/en.mts +0 -0
  74. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/index.ts +0 -0
  75. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/style/custom.css +0 -0
  76. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/zh.mts +0 -0
  77. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/cascade-delete-semantics.md +0 -0
  78. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/index.md +0 -0
  79. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/optimistic-lock.md +0 -0
  80. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/prerequisites.md +0 -0
  81. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-preload.md +0 -0
  82. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/cached-table.md +0 -0
  83. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/crud.md +0 -0
  84. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/field-types.md +0 -0
  85. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/index.md +0 -0
  86. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/optimistic-lock.md +0 -0
  87. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/pagination.md +0 -0
  88. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/polymorphic.md +0 -0
  89. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-load-checker.md +0 -0
  90. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-preload.md +0 -0
  91. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/configure-cascade-delete.md +0 -0
  92. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-jti-models.md +0 -0
  93. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/index.md +0 -0
  94. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
  95. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/release-connection-during-long-io.md +0 -0
  96. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/cached-table.md +0 -0
  97. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/crud.md +0 -0
  98. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/index.md +0 -0
  99. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/metaclass.md +0 -0
  100. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/optimistic-lock.md +0 -0
  101. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/polymorphic.md +0 -0
  102. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/prerequisites.md +0 -0
  103. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-load-checker.md +0 -0
  104. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-preload.md +0 -0
  105. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/decorators.md +0 -0
  106. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/index.md +0 -0
  107. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/cascade-delete-semantics.md +0 -0
  108. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/index.md +0 -0
  109. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/optimistic-lock.md +0 -0
  110. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/prerequisites.md +0 -0
  111. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/relation-preload.md +0 -0
  112. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/cached-table.md +0 -0
  113. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/crud.md +0 -0
  114. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/field-types.md +0 -0
  115. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/index.md +0 -0
  116. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/optimistic-lock.md +0 -0
  117. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/pagination.md +0 -0
  118. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/polymorphic.md +0 -0
  119. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/relation-load-checker.md +0 -0
  120. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/relation-preload.md +0 -0
  121. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/configure-cascade-delete.md +0 -0
  122. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/define-jti-models.md +0 -0
  123. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/index.md +0 -0
  124. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/paginate-a-list-endpoint.md +0 -0
  125. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/release-connection-during-long-io.md +0 -0
  126. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/cached-table.md +0 -0
  127. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/crud.md +0 -0
  128. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/index.md +0 -0
  129. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/metaclass.md +0 -0
  130. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/optimistic-lock.md +0 -0
  131. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/polymorphic.md +0 -0
  132. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/prerequisites.md +0 -0
  133. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/relation-load-checker.md +0 -0
  134. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/relation-preload.md +0 -0
  135. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/decorators.md +0 -0
  136. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/index.md +0 -0
  137. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/package.json +0 -0
  138. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/pnpm-lock.yaml +0 -0
  139. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_compat.py +0 -0
  140. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_exceptions.py +0 -0
  141. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_sa_type.py +0 -0
  142. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_utils.py +0 -0
  143. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
  144. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
  145. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
  146. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
  147. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
  148. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
  149. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
  150. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
  151. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
  152. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
  153. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
  154. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/url.py +0 -0
  155. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/__init__.py +0 -0
  156. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/cached_table.py +0 -0
  157. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/info_response.py +0 -0
  158. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
  159. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/polymorphic.py +0 -0
  160. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
  161. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/pagination.py +0 -0
  162. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/relation_load_checker.py +0 -0
  163. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/session.py +0 -0
  164. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/__init__.py +0 -0
  165. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/conftest.py +0 -0
  166. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_all_fields_optional_constraints.py +0 -0
  167. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_annotated_default_factory_inheritance.py +0 -0
  168. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_decimal_types.py +0 -0
  169. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_deferred_index.py +0 -0
  170. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_enhanced_session.py +0 -0
  171. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_smoke_imports.py +0 -0
  172. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_sti_default_isolation.py +0 -0
  173. {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmodel-ext
3
- Version: 0.4.0
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,12 +1406,12 @@ 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
 
@@ -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,12 +1361,12 @@ 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
 
@@ -25,7 +25,7 @@ SQLModel 增强基础设施:智能元类、异步 CRUD Mixin、多态继承、
25
25
  | **RelationPreloadMixin** | 基于装饰器的关系自动预加载(防止 `MissingGreenlet` 错误) |
26
26
  | **ListResponse[T]** | 泛型分页响应模型,适用于列表接口 |
27
27
  | **字段类型** | 可复用的约束类型:`Str64`、`Port`、`IPAddress`、`HttpUrl`、`SafeHttpUrl` 等 |
28
- | **PostgreSQL 类型** | `Array[T]` 原生 ARRAY、`JSON100K`/`JSONList100K` 限长 JSONB、`NumpyVector` pgvector+NumPy 集成 |
28
+ | **PostgreSQL 类型** | `Array[T]` 原生 ARRAY(枚举数组在滚动部署版本偏差窗口读容忍)、`JSON100K`/`JSONList100K` 限长 JSONB、`NumpyVector` pgvector+NumPy 集成 |
29
29
  | **响应 DTO Mixin** | 预构建的 API 响应模型 Mixin,包含 id/时间戳字段 |
30
30
 
31
31
  ## 安装
@@ -63,12 +63,13 @@ pip install sqlmodel-ext[pgvector]
63
63
  ### 定义模型
64
64
 
65
65
  ```python
66
- from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64
66
+ from pydantic import EmailStr # 需要:pip install 'pydantic[email]'
67
+ from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, NonEmptyStrippedStr64
67
68
 
68
69
  # Base 类 -- 仅定义字段,不创建数据库表
69
70
  class UserBase(SQLModelBase):
70
- name: Str64
71
- email: str
71
+ name: NonEmptyStrippedStr64 # 用户可见命名:拒绝空串与纯空白
72
+ email: EmailStr
72
73
 
73
74
  # Table 类 -- 继承字段 + 获得异步 CRUD + UUID 主键
74
75
  class User(UserBase, UUIDTableBaseMixin, table=True):
@@ -361,17 +362,19 @@ async def delete_article(session: SessionDep, article_id: UUID) -> None:
361
362
 
362
363
  ```python
363
364
  from abc import ABC, abstractmethod
365
+ from pydantic import EmailStr
364
366
  from sqlmodel_ext import (
365
367
  SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin,
366
368
  AutoPolymorphicIdentityMixin, create_subclass_id_mixin,
367
369
  ListResponse, TableViewRequest,
370
+ Str512, Text1K,
368
371
  )
369
372
 
370
373
  # ── 多态模型 ─────────────────────────────────────────────────────
371
374
 
372
375
  class NotificationBase(SQLModelBase):
373
376
  user_id: UUID = Field(foreign_key='user.id')
374
- message: str
377
+ message: Text1K
375
378
 
376
379
  class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
377
380
  @abstractmethod
@@ -380,13 +383,13 @@ class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, A
380
383
  NotifSubclassId = create_subclass_id_mixin('notification')
381
384
 
382
385
  class EmailNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
383
- email_to: str
386
+ email_to: EmailStr
384
387
 
385
388
  def summary(self) -> str:
386
389
  return f"Email to {self.email_to}: {self.message}"
387
390
 
388
391
  class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
389
- device_token: str
392
+ device_token: Str512
390
393
 
391
394
  def summary(self) -> str:
392
395
  return f"Push to {self.device_token}: {self.message}"
@@ -416,15 +419,15 @@ async def list_notifications(
416
419
  两者均自动添加 `id`、`created_at` 和 `updated_at` 字段。
417
420
 
418
421
  ```python
419
- from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
422
+ from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, NonEmptyStrippedStr64, Text1K
420
423
 
421
424
  # 整数主键
422
425
  class LogEntry(SQLModelBase, TableBaseMixin, table=True):
423
- message: str
426
+ message: Text1K
424
427
 
425
428
  # UUID 主键(大多数场景推荐)
426
429
  class Project(SQLModelBase, UUIDTableBaseMixin, table=True):
427
- name: str
430
+ name: NonEmptyStrippedStr64
428
431
  ```
429
432
 
430
433
  #### `add()` -- 批量插入
@@ -452,7 +455,9 @@ user = await User.add(session, User(name="Alice", email="a@x.com"))
452
455
  user = await user.save(session)
453
456
 
454
457
  # 保存后预加载关系
455
- user = await user.save(session, load=User.profile)
458
+ # rel() 把 Relationship 字段 cast 为 QueryableAttribute——
459
+ # 否则类型检查器把 User.profile 推断为 Profile 而非可加载属性
460
+ user = await user.save(session, load=rel(User.profile))
456
461
 
457
462
  # 乐观锁自动重试
458
463
  user = await user.save(session, optimistic_retry_count=3)
@@ -485,9 +490,10 @@ user3 = await user3.save(session) # 一次性 commit 全部三条
485
490
  #### `update()` -- 从模型实例局部更新
486
491
 
487
492
  ```python
488
- class UserUpdate(SQLModelBase):
489
- name: str | None = None
490
- email: str | None = None
493
+ # all_fields_optional 自动把所有继承字段转为 ``T | None = None``
494
+ # 并保留约束——无需手写 Optional 样板
495
+ class UserUpdate(UserBase, all_fields_optional=True):
496
+ pass
491
497
 
492
498
  # 仅更新显式设置的字段
493
499
  user = await user.update(session, UserUpdate(name="Charlie"))
@@ -566,7 +572,7 @@ users = await User.get(session, fetch_mode="all")
566
572
  user = await User.get(
567
573
  session,
568
574
  User.id == user_id,
569
- load=[User.profile, User.orders],
575
+ load=[rel(User.profile), rel(User.orders)],
570
576
  )
571
577
 
572
578
  # JOIN 查询
@@ -690,11 +696,12 @@ from sqlmodel_ext import (
690
696
  SQLModelBase, UUIDTableBaseMixin,
691
697
  PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
692
698
  create_subclass_id_mixin,
699
+ HttpUrl, NonEmptyStrippedStr64, NonNegativeInt,
693
700
  )
694
701
 
695
702
  # 1. Base 类(仅定义字段,无表)
696
703
  class ToolBase(SQLModelBase):
697
- name: str
704
+ name: NonEmptyStrippedStr64
698
705
 
699
706
  # 2. 抽象父类(创建父表)
700
707
  class Tool(ToolBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
@@ -706,13 +713,13 @@ ToolSubclassIdMixin = create_subclass_id_mixin('tool')
706
713
 
707
714
  # 4. 具体子类(各自拥有独立表)
708
715
  class WebSearchTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
709
- search_url: str
716
+ search_url: HttpUrl
710
717
 
711
718
  async def execute(self) -> str:
712
719
  return f"Searching {self.search_url}"
713
720
 
714
721
  class CalculatorTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
715
- precision: int = 2
722
+ precision: NonNegativeInt = 2
716
723
 
717
724
  async def execute(self) -> str:
718
725
  return "Calculating..."
@@ -746,16 +753,17 @@ from sqlmodel_ext import (
746
753
  PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
747
754
  register_sti_columns_for_all_subclasses,
748
755
  register_sti_column_properties_for_all_subclasses,
756
+ NonNegativeBigInt, Str256,
749
757
  )
750
758
 
751
759
  class UserFile(SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin, table=True):
752
- filename: str
760
+ filename: Str256
753
761
 
754
762
  class PendingFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
755
763
  upload_deadline: datetime | None = None # 作为 nullable 列添加到 userfile 表
756
764
 
757
765
  class CompletedFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
758
- file_size: int | None = None # 作为 nullable 列添加到 userfile 表
766
+ file_size: NonNegativeBigInt | None = None # 作为 nullable 列添加到 userfile 表
759
767
 
760
768
  # 所有模型定义完成后,在 configure_mappers() 之前调用:
761
769
  register_sti_columns_for_all_subclasses()
@@ -781,7 +789,7 @@ class ToolSet(SQLModelBase, UUIDTableBaseMixin, table=True):
781
789
  tool_set = await ToolSet.get(
782
790
  session,
783
791
  ToolSet.id == ts_id,
784
- load=ToolSet.tools,
792
+ load=rel(ToolSet.tools),
785
793
  jti_subclasses='all', # 加载所有子类特有的列
786
794
  )
787
795
  ```
@@ -808,15 +816,22 @@ Tool._is_joined_table_inheritance() # JTI 返回 True,STI 返回 False
808
816
  利用 SQLAlchemy 的 `version_id_col` 机制防止并发环境下的更新丢失。
809
817
 
810
818
  ```python
819
+ from enum import StrEnum
820
+
811
821
  from sqlmodel_ext import (
812
822
  SQLModelBase, UUIDTableBaseMixin,
813
823
  OptimisticLockMixin, OptimisticLockError,
824
+ NonNegativeDecimal38_18,
814
825
  )
815
826
 
827
+ class OrderStatusEnum(StrEnum):
828
+ pending = 'pending'
829
+ paid = 'paid'
830
+
816
831
  # OptimisticLockMixin 在 MRO 中必须位于 TableBaseMixin 之前
817
832
  class Order(OptimisticLockMixin, UUIDTableBaseMixin, table=True):
818
- status: str
819
- amount: int
833
+ status: OrderStatusEnum = OrderStatusEnum.pending
834
+ amount: NonNegativeDecimal38_18
820
835
  ```
821
836
 
822
837
  该 Mixin 添加一个 `version` 整数字段(初始值为 0)。每次 `UPDATE` 生成如下 SQL:
@@ -868,11 +883,11 @@ order = await order.update(session, update_data, optimistic_retry_count=3)
868
883
 
869
884
  ```python
870
885
  from sqlmodel import Relationship
871
- from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase
886
+ from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase, NonNegativeDecimal38_18
872
887
  from sqlmodel_ext.mixins import RelationPreloadMixin, requires_relations
873
888
 
874
889
  class GeneratorConfig(SQLModelBase, UUIDTableBaseMixin, table=True):
875
- price: int
890
+ price: NonNegativeDecimal38_18
876
891
 
877
892
  class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
878
893
  config: GeneratorConfig = Relationship()