sqlmodel-ext 0.5.1__tar.gz → 0.5.3__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 (267) hide show
  1. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/CHANGELOG.md +69 -0
  2. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/PKG-INFO +2 -2
  3. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/metaclass.md +4 -4
  4. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/metaclass.md +4 -4
  5. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/pyproject.toml +4 -2
  6. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/__init__.py +1 -1
  7. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/base.py +123 -134
  8. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/cached_table.py +18 -3
  9. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_cached_commit_paths.py +35 -0
  10. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_enhanced_session.py +21 -0
  11. sqlmodel_ext-0.5.3/tests/test_metaclass_field_resolution_052.py +406 -0
  12. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/.github/workflows/ci.yml +0 -0
  13. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/.gitignore +0 -0
  14. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/LICENSE +0 -0
  15. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/README.md +0 -0
  16. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/README_zh.md +0 -0
  17. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/ai-rules/AGENTS.md +0 -0
  18. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/ai-rules/CLAUDE.md +0 -0
  19. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/ai-rules/README.md +0 -0
  20. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/.vitepress/config.mts +0 -0
  21. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/.vitepress/en.mts +0 -0
  22. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/.vitepress/theme/index.ts +0 -0
  23. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/.vitepress/theme/style/custom.css +0 -0
  24. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/.vitepress/zh.mts +0 -0
  25. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/cached-table.md +0 -0
  26. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/cascade-delete-semantics.md +0 -0
  27. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/crud-pipeline.md +0 -0
  28. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/index.md +0 -0
  29. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/optimistic-lock.md +0 -0
  30. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/polymorphic-internals.md +0 -0
  31. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/prerequisites.md +0 -0
  32. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/relation-load-checker.md +0 -0
  33. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/relation-preload.md +0 -0
  34. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/single-source-of-truth.md +0 -0
  35. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/transactional-cache-transparency.md +0 -0
  36. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/explanation/unset-three-state.md +0 -0
  37. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/cached-table.md +0 -0
  38. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/crud.md +0 -0
  39. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/field-types.md +0 -0
  40. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/index.md +0 -0
  41. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/optimistic-lock.md +0 -0
  42. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/pagination.md +0 -0
  43. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/polymorphic.md +0 -0
  44. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/relation-load-checker.md +0 -0
  45. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/guide/relation-preload.md +0 -0
  46. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/aggregate-queries.md +0 -0
  47. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/cache-queries.md +0 -0
  48. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/check-partial-dtos.md +0 -0
  49. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/configure-cascade-delete.md +0 -0
  50. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/define-jti-models.md +0 -0
  51. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/define-sti-models.md +0 -0
  52. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/enforce-locking-and-isolation.md +0 -0
  53. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/extra-mixins.md +0 -0
  54. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/handle-concurrent-updates.md +0 -0
  55. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/handle-referenced-deletes.md +0 -0
  56. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/index.md +0 -0
  57. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/integrate-with-fastapi.md +0 -0
  58. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/keyset-pagination.md +0 -0
  59. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/migrate-to-0-5.md +0 -0
  60. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
  61. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/prevent-missing-greenlet.md +0 -0
  62. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/release-connection-during-long-io.md +0 -0
  63. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/type-check-with-basedpyright.md +0 -0
  64. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/use-with-ai-assistants.md +0 -0
  65. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/how-to/write-patch-endpoints.md +0 -0
  66. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/index.md +0 -0
  67. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/cached-table.md +0 -0
  68. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/crud.md +0 -0
  69. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/index.md +0 -0
  70. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/metaclass.md +0 -0
  71. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/optimistic-lock.md +0 -0
  72. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/polymorphic.md +0 -0
  73. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/prerequisites.md +0 -0
  74. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/relation-load-checker.md +0 -0
  75. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/internals/relation-preload.md +0 -0
  76. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/reference/base-classes.md +0 -0
  77. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/reference/crud-methods.md +0 -0
  78. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/reference/decorators.md +0 -0
  79. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/reference/field-types.md +0 -0
  80. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/reference/index.md +0 -0
  81. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/reference/mixins.md +0 -0
  82. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/reference/pagination-types.md +0 -0
  83. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/tutorials/01-getting-started.md +0 -0
  84. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/tutorials/02-building-a-blog-api.md +0 -0
  85. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/tutorials/03-adding-redis-cache.md +0 -0
  86. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/en/tutorials/index.md +0 -0
  87. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/cached-table.md +0 -0
  88. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/cascade-delete-semantics.md +0 -0
  89. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/crud-pipeline.md +0 -0
  90. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/index.md +0 -0
  91. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/optimistic-lock.md +0 -0
  92. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/polymorphic-internals.md +0 -0
  93. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/prerequisites.md +0 -0
  94. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/relation-load-checker.md +0 -0
  95. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/relation-preload.md +0 -0
  96. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/single-source-of-truth.md +0 -0
  97. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/transactional-cache-transparency.md +0 -0
  98. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/explanation/unset-three-state.md +0 -0
  99. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/cached-table.md +0 -0
  100. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/crud.md +0 -0
  101. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/field-types.md +0 -0
  102. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/index.md +0 -0
  103. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/optimistic-lock.md +0 -0
  104. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/pagination.md +0 -0
  105. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/polymorphic.md +0 -0
  106. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/relation-load-checker.md +0 -0
  107. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/guide/relation-preload.md +0 -0
  108. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/aggregate-queries.md +0 -0
  109. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/cache-queries.md +0 -0
  110. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/check-partial-dtos.md +0 -0
  111. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/configure-cascade-delete.md +0 -0
  112. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/define-jti-models.md +0 -0
  113. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/define-sti-models.md +0 -0
  114. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/enforce-locking-and-isolation.md +0 -0
  115. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/extra-mixins.md +0 -0
  116. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/handle-concurrent-updates.md +0 -0
  117. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/handle-referenced-deletes.md +0 -0
  118. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/index.md +0 -0
  119. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/integrate-with-fastapi.md +0 -0
  120. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/keyset-pagination.md +0 -0
  121. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/migrate-to-0-5.md +0 -0
  122. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/paginate-a-list-endpoint.md +0 -0
  123. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/prevent-missing-greenlet.md +0 -0
  124. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/release-connection-during-long-io.md +0 -0
  125. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/type-check-with-basedpyright.md +0 -0
  126. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/use-with-ai-assistants.md +0 -0
  127. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/how-to/write-patch-endpoints.md +0 -0
  128. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/index.md +0 -0
  129. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/cached-table.md +0 -0
  130. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/crud.md +0 -0
  131. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/index.md +0 -0
  132. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/metaclass.md +0 -0
  133. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/optimistic-lock.md +0 -0
  134. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/polymorphic.md +0 -0
  135. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/prerequisites.md +0 -0
  136. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/relation-load-checker.md +0 -0
  137. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/internals/relation-preload.md +0 -0
  138. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/reference/base-classes.md +0 -0
  139. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/reference/crud-methods.md +0 -0
  140. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/reference/decorators.md +0 -0
  141. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/reference/field-types.md +0 -0
  142. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/reference/index.md +0 -0
  143. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/reference/mixins.md +0 -0
  144. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/reference/pagination-types.md +0 -0
  145. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/tutorials/01-getting-started.md +0 -0
  146. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/tutorials/02-building-a-blog-api.md +0 -0
  147. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/tutorials/03-adding-redis-cache.md +0 -0
  148. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/docs/tutorials/index.md +0 -0
  149. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/01_unset_patch.py +0 -0
  150. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/02_single_source_types.py +0 -0
  151. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/03_keyset_pagination.py +0 -0
  152. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/04_locking_and_isolation.py +0 -0
  153. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/05_aggregates.py +0 -0
  154. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/06_referenced_delete.py +0 -0
  155. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/07_transactional_cache.py +0 -0
  156. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/08_optimistic_lock.py +0 -0
  157. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/09_relation_preload_bulk.py +0 -0
  158. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/10_typed_select.py +0 -0
  159. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/11_type_errors_caught_by_basedpyright.py +0 -0
  160. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/README.md +0 -0
  161. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/check_derived_demo/pyrightconfig.json +0 -0
  162. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/check_derived_demo/shop/__init__.py +0 -0
  163. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/check_derived_demo/shop/handlers.py +0 -0
  164. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/check_derived_demo/shop/models.py +0 -0
  165. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/examples/consumer_integration/run.py +0 -0
  166. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/package.json +0 -0
  167. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/pnpm-lock.yaml +0 -0
  168. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/pyrightconfig.json +0 -0
  169. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/_compat.py +0 -0
  170. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/_exceptions.py +0 -0
  171. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/_sa_type.py +0 -0
  172. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/_type_unwrap.py +0 -0
  173. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/_utils.py +0 -0
  174. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/check_derived.py +0 -0
  175. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/constants.py +0 -0
  176. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/dependencies.py +0 -0
  177. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/derived_decls.py +0 -0
  178. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/__init__.py +0 -0
  179. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
  180. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
  181. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
  182. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
  183. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
  184. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -0
  185. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
  186. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
  187. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
  188. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
  189. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
  190. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
  191. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/field_types/url.py +0 -0
  192. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/__init__.py +0 -0
  193. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/_uuid.py +0 -0
  194. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/constants.py +0 -0
  195. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/exceptions.py +0 -0
  196. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/info_response.py +0 -0
  197. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/migration_cache_invalidation.py +0 -0
  198. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/mixin_table_scan.py +0 -0
  199. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
  200. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/polymorphic.py +0 -0
  201. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
  202. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/resource_quota.py +0 -0
  203. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/table.py +0 -0
  204. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/mixins/trgm_searchable.py +0 -0
  205. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/pagination.py +0 -0
  206. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/py.typed +0 -0
  207. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/relation_load_checker.py +0 -0
  208. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/select.py +0 -0
  209. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/session.py +0 -0
  210. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/src/sqlmodel_ext/unset.py +0 -0
  211. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/__init__.py +0 -0
  212. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/_models.py +0 -0
  213. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/conftest.py +0 -0
  214. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_annotated_default_factory_inheritance.py +0 -0
  215. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_array_explicit_field_sa_type.py +0 -0
  216. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_basic_crud.py +0 -0
  217. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_cached_table.py +0 -0
  218. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_check_derived.py +0 -0
  219. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_crud_extended.py +0 -0
  220. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_decimal_types.py +0 -0
  221. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_deferred_index.py +0 -0
  222. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_derived_decls.py +0 -0
  223. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_examples.py +0 -0
  224. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_field_types_aliases_extra.py +0 -0
  225. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_field_types_jsonb_limits.py +0 -0
  226. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_field_types_misc.py +0 -0
  227. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_field_types_str_num.py +0 -0
  228. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_field_types_url_ssrf.py +0 -0
  229. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_051_rereview.py +0 -0
  230. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_051_review.py +0 -0
  231. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_051_review2.py +0 -0
  232. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_check_derived_review2.py +0 -0
  233. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_check_derived_temp_root.py +0 -0
  234. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_nested_duplicate_names.py +0 -0
  235. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_omitted_sentinel_alias_modes.py +0 -0
  236. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_partial_alias_generator.py +0 -0
  237. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_partial_annotated_field_warnings.py +0 -0
  238. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_partial_discriminator.py +0 -0
  239. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_partial_frozen_field.py +0 -0
  240. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_partial_rhs_field_metadata.py +0 -0
  241. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_review4_followup.py +0 -0
  242. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_final_review5.py +0 -0
  243. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_inherited_column_attributes.py +0 -0
  244. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_metaclass_extra.py +0 -0
  245. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_mixins_extra.py +0 -0
  246. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_optimistic_lock.py +0 -0
  247. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_pagination.py +0 -0
  248. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_partial.py +0 -0
  249. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_pg_dialect_types.py +0 -0
  250. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_polymorphic_jti.py +0 -0
  251. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_polymorphic_sti_query.py +0 -0
  252. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_query_dependency.py +0 -0
  253. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_query_dependency_metadata.py +0 -0
  254. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_relation_load_checker.py +0 -0
  255. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_relation_load_checker_load_resolution.py +0 -0
  256. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_relation_load_checker_sessions.py +0 -0
  257. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_relation_preload.py +0 -0
  258. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_relationship_pep604.py +0 -0
  259. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_review_mixed_version_cache.py +0 -0
  260. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_review_raw_insert_cache.py +0 -0
  261. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_review_raw_insert_edges.py +0 -0
  262. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_review_raw_insert_late_model.py +0 -0
  263. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_review_raw_insert_sti.py +0 -0
  264. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_select.py +0 -0
  265. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_smoke_imports.py +0 -0
  266. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_sti_default_isolation.py +0 -0
  267. {sqlmodel_ext-0.5.1 → sqlmodel_ext-0.5.3}/tests/test_unset.py +0 -0
@@ -7,6 +7,75 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
7
7
  > there is no stability or backward-compatibility guarantee between versions, and
8
8
  > you use it at your own risk. Pin the exact version you depend on.
9
9
 
10
+ ## [0.5.3]
11
+
12
+ ### Fixed
13
+
14
+ - After `session.invalidate()`, a session that kept being used skipped the Redis cache
15
+ for every read and write on the tables it had written in the invalidated transaction.
16
+ `invalidate()` ends the transaction without dispatching `after_rollback`, so the
17
+ pending-invalidation dict was never cleared, and that dict is also one of the sources
18
+ of the "uncommitted writes" check that makes `get()` bypass the cache. The dict is now
19
+ cleared when the outermost transaction ends, the same place that already cleared the
20
+ flushed-tables set. Commit, rollback, `close()` and `reset()` were not affected. The
21
+ cache contents were always correct; the effect was lost cache hits, not stale data.
22
+ - A fresh `pip install sqlmodel-ext` could not be imported: SQLAlchemy 2.1 no longer
23
+ installs `greenlet` by default, and `sqlmodel_ext` imports `sqlalchemy.ext.asyncio` when
24
+ the package is imported, which raised `ImportError` without it. The dependency is now
25
+ `sqlalchemy[asyncio]`, which installs `greenlet`. Environments that already had
26
+ `greenlet` (or SQLAlchemy 2.0) were not affected. Also present in 0.5.2 and earlier.
27
+
28
+ ## [0.5.2]
29
+
30
+ ### Fixed
31
+
32
+ - A table class field declared with a type alias and a plain right-hand value
33
+ (`x: NonNegativeDecimal38_18 = Decimal(0)`, `s: Str64 = 'a'`) had its default only in
34
+ `model_fields`: the JSON schema listed the field as `required` with no `default`, and
35
+ `model_validate({})` rejected the payload with "Field required". With an alias that
36
+ carries its own default (`opt: OptionalNonNegativeDecimal38_18 = Decimal(1)`),
37
+ `model_fields` even showed the alias's default (`None`) instead of the declared one.
38
+ The metaclass set the value on the field after constructing it, so Pydantic did not
39
+ count it as set when it built the core schema. A metaclass rebuild (an inherited field
40
+ description, a relationship) hid the defect in some classes. The same applied to a
41
+ field re-declared in the table class body without a right-hand side, which takes the
42
+ base's default. The table class now matches a non-table class with the same
43
+ declaration (schema, validation and `model_fields`). Non-table classes and fields the
44
+ table class inherits without re-declaring were not affected. Also present in 0.5.0
45
+ and 0.5.1.
46
+ - **Behavior change.** A table class field declared with **several** `Field(...)` in one
47
+ `Annotated` and no right-hand side (`value: Legacy` with
48
+ `Legacy = Annotated[str, Field(alias='legacy', title='T'), Field(unique=True)]`, or a
49
+ nested alias such as `Annotated[Str64, Field(unique=True)]`) now gets its Pydantic
50
+ attributes as Pydantic merges them -- the same field a non-table class, plain
51
+ SQLModel and a table class inheriting the declaration already got. A later `Field`
52
+ overrides an earlier one, `None` included, and sqlmodel's `Field()` passes
53
+ `alias` / `validation_alias` / `serialization_alias` / `title` / `description` /
54
+ `default_factory` / `discriminator` / `exclude` explicitly as `None` and `repr` as
55
+ `True`. So when an earlier `Field` of the annotation sets any of these and a later
56
+ one does not, the table class no longer keeps the earlier value: the alias / title /
57
+ description are gone, and a `default_factory` is dropped, which makes the field
58
+ **required**. A `default` is not affected (sqlmodel's `Field()` does not pass it).
59
+ None of the bundled aliases combines several `Field`s or sets such an attribute. To keep the value, set it in the last `Field` or on a right-hand
60
+ `= Field(...)`. The same holds for a non-table class whose field needs an injected
61
+ `sa_type` (`Annotated[FilePathType, Field(alias='p'), Field(index=True)]`), which kept
62
+ the earlier `alias` too. The same path also applied the `Field` of an alias nested in a
63
+ union with no right-hand side, which Pydantic does not merge at field level:
64
+ `x: OptionalNonNegativeDecimal38_18 | None` declared in a table class had
65
+ `default=None`; it is now **required**, as it already was in a non-table class, when
66
+ inherited, and with a right-hand `Field` that sets no default (0.5.1). Drop the
67
+ redundant `| None` or write `= None`. The column attributes of all the declaration's `Field`s are
68
+ still folded into the column, now with the precedence Pydantic uses for the Pydantic
69
+ attributes: the right-hand `Field` first, then the annotation's `Field`s from the last
70
+ to the first; `False` still never switches off `True`. So a table class **inheriting**
71
+ such a declaration agrees with one declaring it directly:
72
+ `Annotated[NonNegativeDecimal38_18, Field(sa_type=Numeric(20, 2))]` gives
73
+ `NUMERIC(20, 2)` in both (inheriting it gave `NUMERIC(38, 18)`, the alias's
74
+ `sa_type`). Plain SQLModel, which reads only the first carrier, still gives
75
+ `NUMERIC(38, 18)`. Also present in 0.5.0 and 0.5.1. This only changes the metadata
76
+ the models build: existing databases are not altered. Tables created from now on
77
+ (`create_all`) get the new column type; to align an existing table, write a migration.
78
+
10
79
  ## [0.5.1]
11
80
 
12
81
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlmodel-ext
3
- Version: 0.5.1
3
+ Version: 0.5.3
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
@@ -23,7 +23,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.12
25
25
  Requires-Dist: pydantic>=2.12
26
- Requires-Dist: sqlalchemy>=2.0
26
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
27
27
  Requires-Dist: sqlmodel>=0.0.32
28
28
  Requires-Dist: typing-extensions>=4.14.1
29
29
  Provides-Extra: alembic
@@ -142,7 +142,7 @@ This snapshot must be taken **before** any annotation injection: later steps inj
142
142
 
143
143
  ### Step 4.5: Recover SQLModel attributes from `Annotated[T, Field(...)]`
144
144
 
145
- When Pydantic v2 processes `Annotated` metadata it replaces `sqlmodel.main.FieldInfo` with `pydantic.fields.FieldInfo`, which knows nothing about SQLModel attributes such as `foreign_key` or `sa_type`. For **table classes** (including `Annotated` fields inherited from parents), `_recover_annotated_sqlmodel_fields()` converts them back into the `= Field(...)` form; non-table classes are left as-is so child table classes can inherit them. When several `FieldInfo`s are merged, an explicit `default=None` is kept as a real value (not "unset") — otherwise the field would silently become required. A field with a right-hand `Field` (`name: Alias = Field(...)`) is rebuilt from Pydantic's resolution of that declaration — `FieldInfo.from_annotated_attribute()` when the table class declares it, the base's resolved field `Base.model_fields[name]` when the table class **inherits** it (without re-declaring it). Both give the same field: its Pydantic attributes are taken as-is (an explicit `alias=None` stays `None`, as in plain SQLModel), and only the column attributes are completed — the `FieldInfoMetadata` carriers of the alias's and the right-hand `Field` are folded into one, since SQLModel reads only the first. Metadata that stays in the class annotation (an `AfterValidator` next to the alias's `Field`, for example) is not copied into the field, so it is applied once.
145
+ When Pydantic v2 processes `Annotated` metadata it replaces `sqlmodel.main.FieldInfo` with `pydantic.fields.FieldInfo`, which knows nothing about SQLModel attributes such as `foreign_key` or `sa_type`. For **table classes** (including `Annotated` fields inherited from parents), `_recover_annotated_sqlmodel_fields()` converts them back into the `= Field(...)` form; non-table classes are left as-is so child table classes can inherit them. Every such field is rebuilt from **Pydantic's own resolution of its declaration** — the field a non-table class (or plain SQLModel) gets for the same declaration: `FieldInfo.from_annotated_attribute()` when the table class declares it with a right-hand side (`= Field(...)` or a plain value such as `= 'a'`), `FieldInfo.from_annotation()` when it declares it without one, and the base's resolved field `Base.model_fields[name]` when the table class **inherits** it (without re-declaring it). Its Pydantic attributes are taken as-is. Several `Field`s in one `Annotated` merge as Pydantic merges them: a later `Field` overrides an earlier one, `None` included — sqlmodel's `Field()` passes `alias` / `validation_alias` / `serialization_alias` / `title` / `description` / `default_factory` / `discriminator` / `exclude` explicitly as `None` (and `repr` as `True`), so `Annotated[str, Field(alias='a', title='T'), Field(unique=True)]` has neither alias nor title; a right-hand `Field` overrides them all. The `Field` of an alias nested in a union (`OptionalNonNegativeDecimal38_18 | None`) is not merged at field level, so its `default=None` does not apply — only its constraints and column attributes are carried over. The attributes are passed to the constructor, so Pydantic records them as set and they reach the core schema: a default of `x: Str64 = 'a'` is in the JSON schema and in validation, not only in `model_fields`. Only the column attributes are completed — the `FieldInfoMetadata` carriers of all the declaration's `Field`s are folded into one, since SQLModel reads only the first: the right-hand `Field`'s first, then the annotation's from the last to the first (`Annotated[NonNegativeDecimal38_18, Field(sa_type=Numeric(20, 2))]` gets `NUMERIC(20, 2)`), and `False` never switches off `True`. One library rule departs from Pydantic: a field re-declared in the table class body without a right-hand side keeps the base's default (Pydantic would make it required). Metadata that stays in the class annotation (an `AfterValidator` next to the alias's `Field`, for example) is not copied into the field, so it is applied once.
146
146
 
147
147
  ### Step 4.5.b: `oplock_version` is a reserved name
148
148
 
@@ -182,8 +182,8 @@ for field_name, field_type in annotations.items():
182
182
  if sa_type is not None:
183
183
  field_value = attrs.get(field_name, Undefined)
184
184
  if field_value is Undefined:
185
- # no "= Field(...)": prefer recovering the user's FieldInfo from inside Annotated,
186
- # keeping default_factory / max_length etc., and only add sa_type
185
+ # no "= Field(...)": a right-hand Field carrying Pydantic's own resolution of the
186
+ # Annotated (default_factory etc. kept, nothing changed) plus sa_type
187
187
  annotated_fi = _find_field_info_in_annotated(field_type)
188
188
  attrs[field_name] = annotated_fi if annotated_fi is not None else Field(sa_type=sa_type)
189
189
  elif isinstance(field_value, FieldInfo):
@@ -218,7 +218,7 @@ result = super().__new__(cls, name, bases, attrs, **kwargs)
218
218
  # merge the saved SQLModelFieldInfo back and rebuild the Column
219
219
  ```
220
220
 
221
- While merging, boolean flags are never overwritten with `False` (`unique=False` never switches off an inherited `unique=True`), and `FieldInfoMetadata` carriers are folded into one (SQLModel reads only the first; otherwise an all-unset carrier inside a type alias shadows `= Field(primary_key=True)` — which is exactly why `id: NonNegativeInt = Field(primary_key=True)` lost its primary key on sqlmodel ≥ 0.0.32).
221
+ Step 7 only copies back what Pydantic filled in; there `None` and `False` never overwrite a saved value. The `FieldInfoMetadata` carriers of one declaration were already folded into one in step 4.5 (SQLModel reads only the first; otherwise an all-unset carrier inside a type alias shadows `= Field(primary_key=True)` — which is exactly why `id: NonNegativeInt = Field(primary_key=True)` lost its primary key on sqlmodel ≥ 0.0.32).
222
222
 
223
223
  ### Steps 8–9: Relationship fields under inheritance
224
224
 
@@ -142,7 +142,7 @@ _own_annotation_names = frozenset(annotations)
142
142
 
143
143
  ### 第 4.5 步:恢复 `Annotated[T, Field(...)]` 中的 SQLModel 属性
144
144
 
145
- Pydantic v2 处理 `Annotated` 元数据时会把 `sqlmodel.main.FieldInfo` 换成 `pydantic.fields.FieldInfo`,后者不认识 `foreign_key`、`sa_type` 等 SQLModel 属性。`_recover_annotated_sqlmodel_fields()` 对 **table 类**(包括从父类继承来的 `Annotated` 字段)把它们还原成 `= Field(...)` 形式;非 table 类保留原样,供子 table 类继承。合并多个 `FieldInfo` 时,显式的 `default=None` 被当作真实的值(而不是"未设置")保留——否则字段会静默变成必填。带右侧 `Field` 的字段(`name: Alias = Field(...)`)以 Pydantic 对该声明的解析结果为准重建——table 类自己声明时用 `FieldInfo.from_annotated_attribute()`,table 类**继承**(未在类体里重新声明)时用基类已解析的字段 `Base.model_fields[name]`。两者得到同一个字段:Pydantic 层属性原样采用(显式的 `alias=None` 仍是 `None`,与纯 SQLModel 一致),只补全列属性——别名的 `Field` 与右侧 `Field` 各自的 `FieldInfoMetadata` 载体被折叠成一个,因为 SQLModel 只读第一个。仍留在类注解里的元数据(例如与别名 `Field` 并列的 `AfterValidator`)不会再复制进字段,因此只执行一次。
145
+ Pydantic v2 处理 `Annotated` 元数据时会把 `sqlmodel.main.FieldInfo` 换成 `pydantic.fields.FieldInfo`,后者不认识 `foreign_key`、`sa_type` 等 SQLModel 属性。`_recover_annotated_sqlmodel_fields()` 对 **table 类**(包括从父类继承来的 `Annotated` 字段)把它们还原成 `= Field(...)` 形式;非 table 类保留原样,供子 table 类继承。每个这样的字段都以 **Pydantic 对该声明自己的解析结果**为准重建——也就是同一份声明写在非 table 类(或纯 SQLModel)里得到的字段:table 类自己声明且带右侧值(`= Field(...)` 或 `= 'a'` 这样的普通值)时用 `FieldInfo.from_annotated_attribute()`,自己声明但没有右侧值时用 `FieldInfo.from_annotation()`,table 类**继承**(未在类体里重新声明)时用基类已解析的字段 `Base.model_fields[name]`。Pydantic 层属性原样采用。同一个 `Annotated` 里的多个 `Field` 按 Pydantic 的规则合并:后一个 `Field` 覆盖前一个,`None` 也算——sqlmodel 的 `Field()` 会把 `alias` / `validation_alias` / `serialization_alias` / `title` / `description` / `default_factory` / `discriminator` / `exclude` 显式传成 `None`(`repr` 传成 `True`),所以 `Annotated[str, Field(alias='a', title='T'), Field(unique=True)]` 既没有 alias 也没有 title;右侧 `Field` 覆盖它们全部。嵌在联合类型里的别名(`OptionalNonNegativeDecimal38_18 | None`)的 `Field` 不在字段层合并,所以它的 `default=None` 不生效——只带过来它的约束与列属性。这些属性经构造函数传入,Pydantic 把它们记为"已设置",于是能进入 core schema:`x: Str64 = 'a'` 的默认值会出现在 JSON schema 与校验里,而不只是在 `model_fields` 里。只补全列属性——声明中所有 `Field` 的 `FieldInfoMetadata` 载体被折叠成一个,因为 SQLModel 只读第一个:先右侧 `Field` 的,再按注解里从后往前的顺序(`Annotated[NonNegativeDecimal38_18, Field(sa_type=Numeric(20, 2))]` 得到 `NUMERIC(20, 2)`),且 `False` 不会关掉 `True`。有一条本库规则与 Pydantic 不同:在 table 类体里重新声明、但没有右侧值的字段,沿用基类的默认值(Pydantic 会让它变成必填)。仍留在类注解里的元数据(例如与别名 `Field` 并列的 `AfterValidator`)不会再复制进字段,因此只执行一次。
146
146
 
147
147
  ### 第 4.5.b 步:`oplock_version` 是保留名
148
148
 
@@ -182,8 +182,8 @@ for field_name, field_type in annotations.items():
182
182
  if sa_type is not None:
183
183
  field_value = attrs.get(field_name, Undefined)
184
184
  if field_value is Undefined:
185
- # 没有 "= Field(...)":优先从 Annotated 里取回用户的 FieldInfo,
186
- # 保住 default_factory / max_length 等,只把 sa_type 加进去
185
+ # 没有 "= Field(...)":造一个携带 Pydantic 对该 Annotated 自身解析结果的右侧 Field
186
+ # (default_factory 等原样保住,不改变任何东西),再把 sa_type 加进去
187
187
  annotated_fi = _find_field_info_in_annotated(field_type)
188
188
  attrs[field_name] = annotated_fi if annotated_fi is not None else Field(sa_type=sa_type)
189
189
  elif isinstance(field_value, FieldInfo):
@@ -218,7 +218,7 @@ result = super().__new__(cls, name, bases, attrs, **kwargs)
218
218
  # 用保存的 SQLModelFieldInfo 合并回去并重建 Column
219
219
  ```
220
220
 
221
- 合并时,布尔标志不会被 `False` 覆盖(`unique=False` 不会关掉继承来的 `unique=True`),`FieldInfoMetadata` 载体会被折叠成一个(SQLModel 只读第一个;否则类型别名里全空的载体会遮住 `= Field(primary_key=True)`——这正是 sqlmodel ≥ 0.0.32 上 `id: NonNegativeInt = Field(primary_key=True)` 丢失主键的原因)。
221
+ 第 7 步只把 Pydantic 填上的值拷回来,此处 `None` 与 `False` 不会覆盖已保存的值。同一声明的 `FieldInfoMetadata` 载体在第 4.5 步已折叠成一个(SQLModel 只读第一个;否则类型别名里全空的载体会遮住 `= Field(primary_key=True)`——这正是 sqlmodel ≥ 0.0.32 上 `id: NonNegativeInt = Field(primary_key=True)` 丢失主键的原因)。
222
222
 
223
223
  ### 第 8–9 步:继承中的关系字段
224
224
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlmodel-ext"
7
- version = "0.5.1"
7
+ version = "0.5.3"
8
8
  description = "Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -32,7 +32,9 @@ dependencies = [
32
32
  # 2.12 is the first release shipping ``pydantic.experimental.missing_sentinel.MISSING``
33
33
  # (the object ``Unset`` is) and ``Field(exclude_if=...)`` (behind ``EXCLUDE_IF_NONE``).
34
34
  "pydantic>=2.12",
35
- "sqlalchemy>=2.0",
35
+ # The ``asyncio`` extra pulls in ``greenlet``: ``sqlmodel_ext.session`` imports
36
+ # ``sqlalchemy.ext.asyncio`` at module level, and SQLAlchemy 2.1 made greenlet optional.
37
+ "sqlalchemy[asyncio]>=2.0",
36
38
  # ``sqlmodel_ext.unset`` imports ``typing_extensions.Sentinel`` directly (to register
37
39
  # its deepcopy reducer); pydantic 2.12 already requires this floor transitively.
38
40
  "typing-extensions>=4.14.1",
@@ -20,7 +20,7 @@ Quick start::
20
20
  user = await user.save(session)
21
21
  users = await User.get(session, fetch_mode="all")
22
22
  """
23
- __version__ = "0.5.1"
23
+ __version__ = "0.5.3"
24
24
 
25
25
  # Every re-export below uses the redundant ``X as X`` form: the package ships
26
26
  # ``py.typed``, and type checkers treat a plain ``from m import X`` in a typed
@@ -129,11 +129,13 @@ def _merge_field_info_attrs(target: SQLModelFieldInfo, source: FieldInfo) -> Non
129
129
  """
130
130
  Merge explicitly-set attributes from ``source`` into ``target``.
131
131
 
132
- Used when ``Annotated[Str64, Field(unique=True)]`` expands to multiple FieldInfo:
133
- ``Annotated[str, Field(max_length=64), Field(unique=True)]``, and to restore the
134
- SQLModel attributes Pydantic drops when it rebuilds ``model_fields``. A
135
- right-hand-side ``= Field(...)`` is not merged here: it follows Pydantic's
136
- resolution, ``None`` included (see ``_inherit_resolved_field``).
132
+ Only used by the metaclass's final restore step (step 7), where ``source``
133
+ is the field Pydantic built **from** ``target`` and ``target`` gets back
134
+ the values Pydantic filled in; there ``None`` never means "cleared", so
135
+ skipping it is correct. It is **not** a field-declaration merge: the
136
+ ``Field(...)`` objects of one declaration (several ``Field`` in one
137
+ ``Annotated``, an alias plus a right-hand ``= Field(...)``) follow
138
+ Pydantic's resolution, ``None`` included (see ``_inherit_resolved_field``).
137
139
  Merges attributes dynamically (via ``__slots__`` / ``__annotations__`` / ``__dict__``),
138
140
  no hardcoded attribute names — upstream additions are handled automatically.
139
141
 
@@ -205,10 +207,10 @@ def _merge_sqlmodel_metadata_carrier(
205
207
  Since sqlmodel 0.0.32, ``Field(primary_key=..., sa_type=..., ...)`` stores the
206
208
  SQLModel-specific attributes in a ``FieldInfoMetadata`` entry of
207
209
  ``metadata``, and SQLModel reads only the **first** such entry. Plain
208
- concatenation would therefore let an earlier, all-unset carrier (e.g. the
209
- one inside a constrained alias like ``NonNegativeInt``) shadow the values
210
- set by the later, more specific source (e.g. a right-hand
211
- ``= Field(primary_key=True)``).
210
+ concatenation would therefore let an earlier, all-unset carrier shadow the
211
+ values set by the later source. Only ``_merge_field_info_attrs`` (the
212
+ metaclass's restore step) uses it; the carriers of one field declaration
213
+ are folded by ``_fold_column_carriers``.
212
214
 
213
215
  Set values of the source carrier are folded into a **copy** of the target
214
216
  carrier (carriers inside type aliases are shared singletons and must not be
@@ -248,7 +250,7 @@ Public Pydantic ``FieldInfo`` attributes an inherited field takes verbatim from
248
250
 
249
251
 
250
252
  def _inherit_resolved_field(
251
- alias_fi: SQLModelFieldInfo,
253
+ alias_fis: list[SQLModelFieldInfo],
252
254
  base_fi: FieldInfo,
253
255
  annotation_metadata: list[Any],
254
256
  ) -> SQLModelFieldInfo:
@@ -256,81 +258,91 @@ def _inherit_resolved_field(
256
258
  Build the ``FieldInfo`` of a table-class field from Pydantic's resolution of its declaration.
257
259
 
258
260
  ``base_fi`` is the field as Pydantic resolves the declaration
259
- ``name: Alias = Field(...)``: the base class's entry in ``model_fields``
260
- for an inherited field, ``FieldInfo.from_annotated_attribute`` for a
261
- declaration in the table class itself. Pydantic has merged the type
262
- alias's ``Field(...)`` and the right-hand ``= Field(...)`` into it. It is
263
- the single source of truth for the field:
261
+ ``name: Alias [= value]``: the base class's entry in ``model_fields`` for
262
+ an inherited field; ``FieldInfo.from_annotated_attribute`` for a
263
+ declaration with a right-hand side (``= Field(...)`` or a plain value) in
264
+ the table class itself; ``FieldInfo.from_annotation`` for one without.
265
+ Pydantic has merged every ``Field(...)`` of the declaration into it -- the
266
+ ones inside the ``Annotated`` (later ones override earlier ones, ``None``
267
+ included) and the right-hand one (which overrides them all). ``alias_fis``
268
+ are the SQLModel ``Field(...)`` objects found in the annotation, in
269
+ declaration order. ``base_fi`` is the single source of truth for the field:
264
270
 
265
271
  - **Pydantic attributes** (``default`` / ``default_factory`` / ``alias`` /
266
272
  ``validation_alias`` / ``serialization_alias`` / ``title`` /
267
273
  ``description`` / ``examples`` / ``exclude`` / ``frozen`` / ...) are
268
274
  taken from ``base_fi`` verbatim, ``None`` included: ``Field(alias=None)``
269
- in the base clears the alias's ``alias`` and the table class must see
270
- the same field as the base does. They are passed to the constructor so
271
- that Pydantic records every one of them as explicitly set -- the same
275
+ clears an earlier ``alias`` and the table class must see the same field
276
+ as a non-table class does. They are passed to the constructor so that
277
+ Pydantic records every one of them as explicitly set -- the same
272
278
  treatment Pydantic gives a resolved ``FieldInfo`` reused as a default.
279
+ Setting them afterwards (``fi.default = value``) would not record them:
280
+ Pydantic would then drop them when it builds the field and its core
281
+ schema, while ``model_fields`` (restored by the metaclass) keeps them.
273
282
  - **Metadata** starts from ``base_fi.metadata`` minus ``annotation_metadata``:
274
283
  the metadata Pydantic collects from the rebuilt class annotation (the
275
284
  ``Annotated`` arguments other than the SQLModel ``Field``, e.g. an
276
285
  ``AfterValidator``). Pydantic adds those again when it builds the
277
286
  field, so keeping them here would run such a validator twice. Each
278
- annotation item removes one equal item. Items of ``alias_fi.metadata``
279
- whose type ``base_fi`` lacks are appended: an alias nested in a union
280
- (``Alias | None = Field(...)``) is not merged by Pydantic at field level,
281
- so its constraints (``MaxLen`` -> column length) and column carrier are
282
- absent from ``base_fi``.
287
+ annotation item removes one equal item. Items of the ``alias_fis``
288
+ metadata whose type ``base_fi`` lacks are appended: an alias nested in a
289
+ union (``Alias | None``) is not merged by Pydantic at field level, so its
290
+ constraints (``MaxLen`` -> column length) are absent from ``base_fi``.
283
291
  - **Column attributes**: SQLModel reads only the first ``FieldInfoMetadata``
284
- carrier, while ``base_fi`` holds one per ``Field(...)`` -- the right-hand
285
- one first. All carriers are folded into one (see
286
- ``_fold_column_carriers``) and the folded values are also set as
287
- instance attributes for readers using ``getattr(field, 'primary_key')``.
288
-
289
- ``alias_fi`` is not mutated.
292
+ carrier. All carriers of the declaration are folded into one (see
293
+ ``_fold_column_carriers``), by the precedence Pydantic applies to the
294
+ Pydantic attributes: the carriers of ``base_fi`` that are not the
295
+ annotation's (those of the right-hand ``Field``) first, then the
296
+ annotation's from the last ``Field`` to the first. The folded values
297
+ are also set as instance attributes for readers using
298
+ ``getattr(field, 'primary_key')``.
299
+
300
+ Neither ``alias_fis`` nor their carriers are mutated.
290
301
  """
291
302
  kwargs: dict[str, Any] = {name: getattr(base_fi, name) for name in _PYDANTIC_FIELD_ATTRS}
292
303
  inherited = SQLModelFieldInfo(**kwargs)
293
304
 
305
+ alias_meta = [item for alias_fi in alias_fis for item in alias_fi.metadata]
306
+ alias_carriers = [item for item in alias_meta if isinstance(item, FieldInfoMetadata)]
307
+ base_types = {type(item) for item in base_fi.metadata}
294
308
  base_meta = list(base_fi.metadata)
295
309
  for item in annotation_metadata:
296
310
  base_meta.remove(item)
297
- base_types = {type(item) for item in base_meta}
298
- alias_extra = [
299
- item for item in alias_fi.metadata
300
- if isinstance(item, FieldInfoMetadata) or type(item) not in base_types
311
+ right_hand_carriers = [
312
+ item for item in base_meta
313
+ if isinstance(item, FieldInfoMetadata) and not any(item is c for c in alias_carriers)
301
314
  ]
302
- metadata = _fold_column_carriers(base_meta + alias_extra)
303
- inherited.metadata = metadata
304
-
305
- carrier = next((m for m in metadata if isinstance(m, FieldInfoMetadata)), None)
315
+ metadata = [item for item in base_meta if not isinstance(item, FieldInfoMetadata)]
316
+ metadata += [
317
+ item for item in alias_meta
318
+ if not isinstance(item, FieldInfoMetadata) and type(item) not in base_types
319
+ ]
320
+ carrier = _fold_column_carriers(right_hand_carriers + alias_carriers[::-1])
306
321
  if carrier is not None:
322
+ metadata.append(carrier)
307
323
  for fim_field in dataclasses.fields(carrier):
308
324
  value = getattr(carrier, fim_field.name)
309
325
  if value is not _FIM_UNSET_SA_TYPE:
310
326
  setattr(inherited, fim_field.name, value)
327
+ inherited.metadata = metadata
311
328
  return inherited
312
329
 
313
330
 
314
- def _fold_column_carriers(metadata: list[Any]) -> list[Any]:
331
+ def _fold_column_carriers(carriers: list[FieldInfoMetadata]) -> FieldInfoMetadata | None:
315
332
  """
316
- Fold every ``FieldInfoMetadata`` carrier of ``metadata`` into the first one.
317
-
318
- Carriers are ordered by precedence: an earlier carrier's set value wins; a
319
- later carrier only fills attributes the earlier ones left unset -- except
320
- that ``True`` replaces an earlier ``False`` (the rule
321
- ``_merge_field_info_attrs`` applies to boolean flags, so ``unique=False`` on
322
- the right-hand side never switches off ``unique=True`` from the alias).
323
- The first carrier is replaced by a folded **copy** (carriers inside type
324
- aliases are shared); the others are dropped. Non-carrier items are kept
325
- unchanged and in order.
333
+ Fold ``FieldInfoMetadata`` carriers, ordered by precedence, into a new one.
334
+
335
+ An earlier carrier's set value wins; a later carrier only fills attributes
336
+ the earlier ones left unset -- except that ``True`` replaces an earlier
337
+ ``False``, so ``unique=False`` never switches off ``unique=True`` set
338
+ elsewhere in the declaration. The result is always a **copy** (carriers
339
+ inside type aliases are shared singletons and are later written to, e.g.
340
+ by ``_durably_set_sa_type``). ``None`` when there is no carrier.
326
341
  """
327
- carrier_indexes = [i for i, m in enumerate(metadata) if isinstance(m, FieldInfoMetadata)]
328
- if len(carrier_indexes) < 2:
329
- return list(metadata)
330
- first = carrier_indexes[0]
331
- folded = copy.copy(metadata[first])
332
- for index in carrier_indexes[1:]:
333
- later = metadata[index]
342
+ if not carriers:
343
+ return None
344
+ folded = copy.copy(carriers[0])
345
+ for later in carriers[1:]:
334
346
  for fim_field in dataclasses.fields(later):
335
347
  value = getattr(later, fim_field.name)
336
348
  if value is _FIM_UNSET_SA_TYPE:
@@ -338,49 +350,46 @@ def _fold_column_carriers(metadata: list[Any]) -> list[Any]:
338
350
  current = getattr(folded, fim_field.name)
339
351
  if current is _FIM_UNSET_SA_TYPE or (current is False and value is True):
340
352
  setattr(folded, fim_field.name, value)
341
- dropped = frozenset(carrier_indexes[1:])
342
- return [folded if i == first else m for i, m in enumerate(metadata) if i not in dropped]
353
+ return folded
343
354
 
344
355
 
345
356
  def _find_field_info_in_annotated(annotation: Any) -> SQLModelFieldInfo | None:
346
357
  """
347
- Extract SQLModel ``FieldInfo`` embedded in ``Annotated[T, Field(...)]`` metadata.
358
+ Build a right-hand ``Field`` for ``Annotated[T, Field(...)]`` that changes nothing Pydantic resolves.
348
359
 
349
360
  Used by the metaclass sa_type injection loop. When a field is declared
350
361
  ``Annotated[X, Field(default_factory=list, ...)]`` *without* an explicit
351
362
  ``= ...`` assignment, ``attrs[field_name]`` is ``Undefined``. Replacing
352
363
  it with a fresh ``Field(sa_type=sa_type)`` would discard the user's
353
364
  Field metadata (``default_factory``, ``max_length``, validators, ...)
354
- that lives inside the Annotated args. This helper recovers that
355
- FieldInfo so the caller can attach ``sa_type`` to the user's Field
356
- instead of clobbering it. The bug only surfaces after 2+ levels of
357
- inheritance: single-class instantiation goes through Pydantic's native
358
- Annotated path and works, but child classes rebuild ``model_fields``
359
- from the clobbered ``attrs`` and the field becomes silently
360
- ``is_required=True``.
361
-
362
- Multiple FieldInfo args (e.g. ``Annotated[Str64, Field(unique=True)]``
363
- expands to ``Annotated[str, Field(max_length=64), Field(unique=True)]``)
364
- are merged into a single shallow copy so the shared Annotated metadata
365
- singletons are never mutated.
365
+ that lives inside the Annotated args. The bug only surfaces after 2+
366
+ levels of inheritance: single-class instantiation goes through
367
+ Pydantic's native Annotated path and works, but child classes rebuild
368
+ ``model_fields`` from the clobbered ``attrs`` and the field becomes
369
+ silently ``is_required=True``.
370
+
371
+ The returned field carries Pydantic's own resolution of the annotation
372
+ (every ``Field`` in it merged, later ones overriding earlier ones,
373
+ ``None`` included) with every Pydantic attribute explicitly set, so
374
+ Pydantic, merging it back in as the right-hand side, gets the same field
375
+ as without it. Its only metadata is the folded column carrier, to which
376
+ the caller attaches ``sa_type``; the annotation's constraints stay in the
377
+ annotation. Reached for non-table classes only: a table class's
378
+ annotation has already been stripped of its SQLModel ``Field``s by
379
+ ``_recover_annotated_sqlmodel_fields``.
366
380
 
367
381
  :param annotation: Field type annotation
368
- :returns: Merged SQLModelFieldInfo (shallow copy, safe to mutate), or None
382
+ :returns: A new SQLModelFieldInfo (safe to mutate), or None
369
383
  """
370
384
  if get_origin(annotation) is not typing.Annotated:
371
385
  return None
372
- args = get_args(annotation)
373
- if len(args) < 2:
374
- return None
375
386
  sqlmodel_fis: list[SQLModelFieldInfo] = [
376
- arg for arg in args[1:] if isinstance(arg, SQLModelFieldInfo)
387
+ arg for arg in get_args(annotation)[1:] if isinstance(arg, SQLModelFieldInfo)
377
388
  ]
378
389
  if not sqlmodel_fis:
379
390
  return None
380
- merged = copy.copy(sqlmodel_fis[0])
381
- for extra_fi in sqlmodel_fis[1:]:
382
- _merge_field_info_attrs(merged, extra_fi)
383
- return merged
391
+ resolved = FieldInfo.from_annotation(annotation)
392
+ return _inherit_resolved_field(sqlmodel_fis, resolved, list(resolved.metadata))
384
393
 
385
394
 
386
395
  def _durably_set_sa_type(field_info: Any, sa_type: Any) -> None:
@@ -835,12 +844,6 @@ def _recover_annotated_sqlmodel_fields(
835
844
  if not sqlmodel_fis:
836
845
  continue
837
846
 
838
- # Merge multiple FieldInfo: shallow-copy first then merge to avoid mutating
839
- # shared Annotated metadata singletons (e.g. Str64 = Annotated[str, Field(max_length=64)])
840
- sqlmodel_fi = copy.copy(sqlmodel_fis[0])
841
- for extra_fi in sqlmodel_fis[1:]:
842
- _merge_field_info_attrs(sqlmodel_fi, extra_fi)
843
-
844
847
  # The class annotation without the SQLModel FieldInfo; any other
845
848
  # ``Annotated`` metadata (validators, serializers, ...) stays in it.
846
849
  base_type = args[0]
@@ -864,62 +867,48 @@ def _recover_annotated_sqlmodel_fields(
864
867
  else:
865
868
  rebuilt = new_inner
866
869
 
867
- # Transfer plain defaults from attrs (e.g. = 0, = None) to FieldInfo
870
+ # The field is rebuilt from Pydantic's resolution of the declaration --
871
+ # the same ``FieldInfo`` a non-table class gets for it (see
872
+ # ``_inherit_resolved_field``): its Pydantic attributes follow that
873
+ # resolution, ``None`` included; only the column attributes of every
874
+ # ``Field`` of the declaration are folded together.
868
875
  existing_default = attrs.get(field_name, Undefined)
869
- if existing_default is not Undefined and not isinstance(existing_default, (FieldInfo, SQLModelFieldInfo)):
870
- sqlmodel_fi.default = existing_default
871
- elif isinstance(existing_default, (FieldInfo, SQLModelFieldInfo)):
872
- # A right-hand-side ``= Field(...)`` coexists with the FieldInfo inside
873
- # the annotation, e.g.
874
- # ``value: NonNegativeBigInt | None = Field(default=None, sa_type=BigInteger)``.
875
- # Without this branch the right-hand FieldInfo would be discarded
876
- # (overwritten by ``attrs[field_name] = sqlmodel_fi`` below) and an
877
- # explicit ``default=None`` would vanish, silently making the field
878
- # required.
879
- #
880
- # Pydantic resolves the declaration exactly as it does for a
881
- # non-table class (``from_annotated_attribute`` is the step that
882
- # builds ``model_fields`` entries), so the field is then treated like
883
- # an inherited one: its Pydantic attributes follow that resolution,
884
- # ``None`` included (``Field(alias=None)`` clears the alias's
885
- # ``alias``, as in plain SQLModel and in a table class inheriting
886
- # the same declaration); only the column attributes of the alias
887
- # and the right-hand ``Field`` are folded together (see
888
- # ``_inherit_resolved_field``).
876
+ base_fi = next(
877
+ (
878
+ base_fields[field_name]
879
+ for base in bases
880
+ if (base_fields := getattr(base, 'model_fields', None)) and field_name in base_fields
881
+ ),
882
+ None,
883
+ )
884
+ if existing_default is not Undefined:
885
+ # A right-hand side in the class body: ``= Field(...)`` or a plain
886
+ # value (``= 0``, ``= None``). ``from_annotated_attribute`` is the
887
+ # step Pydantic uses to build ``model_fields`` entries.
889
888
  resolved = FieldInfo.from_annotated_attribute(field_type, existing_default)
890
- sqlmodel_fi = _inherit_resolved_field(
891
- sqlmodel_fi, resolved, FieldInfo.from_annotation(rebuilt).metadata,
892
- )
893
- elif existing_default is Undefined:
894
- base_fi = next(
895
- (
896
- base_fields[field_name]
897
- for base in bases
898
- if (base_fields := getattr(base, 'model_fields', None)) and field_name in base_fields
899
- ),
900
- None,
901
- )
902
- if base_fi is not None and field_name not in own_names:
903
- # An inherited field (the class body does not declare it). Its
904
- # right-hand ``= Field(...)`` lives in the base class, not in
905
- # ``attrs``; without this branch the injected
906
- # ``attrs[field_name]`` replaces the inherited field with the
907
- # alias's ``Field`` alone, and a column declared
908
- # ``Field(index=True)`` in a base class loses its index.
909
- sqlmodel_fi = _inherit_resolved_field(
910
- sqlmodel_fi, base_fi, FieldInfo.from_annotation(rebuilt).metadata,
911
- )
912
- elif (
889
+ elif base_fi is not None and field_name not in own_names:
890
+ # An inherited field (the class body does not declare it). Its
891
+ # right-hand side lives in the base class, not in ``attrs``: the
892
+ # base's resolved field is the declaration's resolution. Without it
893
+ # a column declared ``Field(index=True)`` in a base class loses its
894
+ # index.
895
+ resolved = base_fi
896
+ else:
897
+ resolved = FieldInfo.from_annotation(field_type)
898
+ if (
913
899
  base_fi is not None
914
- and sqlmodel_fi.default is Undefined
915
- and sqlmodel_fi.default_factory is None
900
+ and resolved.default is Undefined
901
+ and resolved.default_factory is None
916
902
  ):
917
903
  # Re-declared in the class body without a right-hand side:
918
- # only the default is inherited.
904
+ # the base's default is inherited.
919
905
  if base_fi.default is not Undefined:
920
- sqlmodel_fi.default = base_fi.default
906
+ resolved.default = base_fi.default
921
907
  elif base_fi.default_factory is not None:
922
- sqlmodel_fi.default_factory = base_fi.default_factory
908
+ resolved.default_factory = base_fi.default_factory
909
+ sqlmodel_fi = _inherit_resolved_field(
910
+ sqlmodel_fis, resolved, FieldInfo.from_annotation(rebuilt).metadata,
911
+ )
923
912
 
924
913
  # Inject SQLModel FieldInfo as field default (equivalent to = Field(...) style)
925
914
  attrs[field_name] = sqlmodel_fi
@@ -265,7 +265,7 @@ _FORBIDDEN_DIRECT_CALLS: frozenset[str] = frozenset({
265
265
 
266
266
 
267
267
  def _on_outermost_transaction_end(session: _SyncSession, transaction: SessionTransaction) -> None:
268
- """Clear the "tables with uncommitted writes" set when the outermost transaction ends.
268
+ """Clear the state that feeds the "uncommitted writes" check when the outermost transaction ends.
269
269
 
270
270
  ``transaction.parent is None`` is the *common exit* of commit / rollback /
271
271
  ``close()`` / ``reset()`` / ``invalidate()`` -- all of them dispatch this
@@ -276,12 +276,26 @@ def _on_outermost_transaction_end(session: _SyncSession, transaction: SessionTra
276
276
  cleanup always misses an entry point. Nested savepoints and the
277
277
  SUBTRANSACTION of a flush (``parent`` set) are ignored.
278
278
 
279
+ ``_SESSION_PENDING_CACHE_KEY`` is cleared here as well. Besides being the
280
+ list of pending invalidations, it is one of the three sources of
281
+ ``_tables_with_uncommitted_writes``: if it survives an ``invalidate()``,
282
+ every later ``get()`` on those tables skips the cache for as long as the
283
+ session is reused. On commit and on the outermost rollback,
284
+ ``after_commit`` / ``after_rollback`` have already popped it, so this is a
285
+ no-op there; ``invalidate()`` fires only this event.
286
+
287
+ Do **not** clear ``_SESSION_COMMITTED_PENDING_KEY`` here: the enhanced
288
+ ``commit()`` reads it after ``super().commit()`` returns, and this event
289
+ fires inside ``super().commit()``, right after ``after_commit``. Clearing
290
+ it here would drop the synchronous invalidation of every enhanced commit.
291
+
279
292
  Registered at import time (not by ``_register_session_commit_hook``):
280
293
  ``register_raw_dml_write`` fills the set for every session, cached models
281
294
  or not, so its lifetime must not depend on cache configuration.
282
295
  """
283
296
  if transaction.parent is None:
284
297
  session.info.pop(_SESSION_FLUSHED_TABLES, None)
298
+ session.info.pop(_SESSION_PENDING_CACHE_KEY, None)
285
299
 
286
300
 
287
301
  event.listen(_SyncSession, "after_transaction_end", _on_outermost_transaction_end)
@@ -2590,8 +2604,9 @@ class CachedTableBaseMixin(TableBaseMixin):
2590
2604
 
2591
2605
  This is **not** the common exit of transaction termination:
2592
2606
  ``close()`` / ``reset()`` do not dispatch ``after_rollback``, and the
2593
- async ``invalidate()`` bypasses them entirely. Transaction-level state
2594
- (such as ``_SESSION_FLUSHED_TABLES``) is therefore cleared in the
2607
+ async ``invalidate()`` bypasses them entirely. State that feeds the
2608
+ uncommitted-writes check (``_SESSION_FLUSHED_TABLES`` and
2609
+ ``_SESSION_PENDING_CACHE_KEY``) is therefore cleared in the
2595
2610
  always-on ``_on_outermost_transaction_end`` listener -- do not
2596
2611
  add more keys here. The invalidation-tracking keys cleared here are
2597
2612
  normally consumed by ``after_commit`` / cleared by ``after_rollback``