sqlmodel-ext 0.3.5__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/PKG-INFO +75 -36
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/README.md +73 -34
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/README_zh.md +58 -30
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/cached-table.md +20 -16
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/crud-pipeline.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/metaclass.md +375 -319
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/polymorphic-internals.md +24 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-load-checker.md +33 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/cache-queries.md +143 -142
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-sti-models.md +132 -132
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/handle-concurrent-updates.md +94 -88
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/index.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/integrate-with-fastapi.md +10 -7
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/prevent-missing-greenlet.md +167 -162
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/release-connection-during-long-io.md +21 -21
- sqlmodel_ext-0.4.1/docs/en/index.md +45 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/base-classes.md +22 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/crud-methods.md +7 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/decorators.md +8 -6
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/field-types.md +62 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/index.md +2 -2
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/mixins.md +35 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/reference/pagination-types.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/01-getting-started.md +7 -6
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/02-building-a-blog-api.md +389 -388
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/03-adding-redis-cache.md +277 -269
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/tutorials/index.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/cached-table.md +16 -16
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/crud-pipeline.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/metaclass.md +351 -319
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/polymorphic-internals.md +13 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/relation-load-checker.md +21 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/cache-queries.md +143 -142
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/define-sti-models.md +132 -132
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/handle-concurrent-updates.md +93 -87
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/index.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/integrate-with-fastapi.md +10 -7
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/prevent-missing-greenlet.md +164 -162
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/release-connection-during-long-io.md +21 -21
- sqlmodel_ext-0.4.1/docs/index.md +45 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/base-classes.md +19 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/crud-methods.md +5 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/decorators.md +8 -6
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/field-types.md +62 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/index.md +2 -2
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/mixins.md +30 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/reference/pagination-types.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/01-getting-started.md +7 -6
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/02-building-a-blog-api.md +388 -388
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/03-adding-redis-cache.md +277 -269
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/tutorials/index.md +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/examples/consumer_integration/run.py +149 -148
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/pyproject.toml +2 -2
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/__init__.py +42 -3
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/base.py +329 -8
- sqlmodel_ext-0.4.1/src/sqlmodel_ext/field_types/__init__.py +560 -0
- sqlmodel_ext-0.4.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +218 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/__init__.py +1 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/cached_table.py +276 -239
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/polymorphic.py +118 -2
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/table.py +105 -26
- sqlmodel_ext-0.4.1/src/sqlmodel_ext/session.py +105 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/_models.py +69 -45
- sqlmodel_ext-0.4.1/tests/test_all_fields_optional_constraints.py +76 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_array_explicit_field_sa_type.py +34 -5
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_basic_crud.py +71 -61
- sqlmodel_ext-0.4.1/tests/test_cached_table.py +513 -0
- sqlmodel_ext-0.4.1/tests/test_crud_extended.py +582 -0
- sqlmodel_ext-0.4.1/tests/test_decimal_types.py +256 -0
- sqlmodel_ext-0.4.1/tests/test_deferred_index.py +50 -0
- sqlmodel_ext-0.4.1/tests/test_enhanced_session.py +87 -0
- sqlmodel_ext-0.4.1/tests/test_field_types_misc.py +189 -0
- sqlmodel_ext-0.4.1/tests/test_field_types_str_num.py +400 -0
- sqlmodel_ext-0.4.1/tests/test_field_types_url_ssrf.py +267 -0
- sqlmodel_ext-0.4.1/tests/test_metaclass_extra.py +194 -0
- sqlmodel_ext-0.4.1/tests/test_optimistic_lock.py +190 -0
- sqlmodel_ext-0.4.1/tests/test_pagination.py +259 -0
- sqlmodel_ext-0.4.1/tests/test_pg_dialect_types.py +427 -0
- sqlmodel_ext-0.4.1/tests/test_polymorphic_jti.py +273 -0
- sqlmodel_ext-0.4.1/tests/test_polymorphic_sti_query.py +418 -0
- sqlmodel_ext-0.4.1/tests/test_relation_load_checker.py +232 -0
- sqlmodel_ext-0.4.1/tests/test_relation_preload.py +240 -0
- sqlmodel_ext-0.4.1/tests/test_relationship_pep604.py +115 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_smoke_imports.py +41 -1
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/uv.lock +1 -1
- sqlmodel_ext-0.3.5/docs/en/index.md +0 -36
- sqlmodel_ext-0.3.5/docs/index.md +0 -36
- sqlmodel_ext-0.3.5/src/sqlmodel_ext/field_types/__init__.py +0 -195
- sqlmodel_ext-0.3.5/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -119
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/.github/workflows/ci.yml +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/.gitignore +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/LICENSE +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/config.mts +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/en.mts +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/index.ts +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/style/custom.css +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/.vitepress/zh.mts +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/index.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/crud.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/field-types.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/index.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/pagination.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/crud.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/index.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/index.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/crud.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/field-types.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/index.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/pagination.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/crud.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/index.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/docs/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/package.json +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/pnpm-lock.yaml +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_compat.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_exceptions.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_sa_type.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_utils.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/url.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/pagination.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/relation_load_checker.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/__init__.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/conftest.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_annotated_default_factory_inheritance.py +0 -0
- {sqlmodel_ext-0.3.5 → sqlmodel_ext-0.4.1}/tests/test_sti_default_isolation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-ext
|
|
3
|
-
Version: 0.
|
|
3
|
+
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.
|
|
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
|
|
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:
|
|
116
|
-
email:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
896
|
-
amount:
|
|
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:
|
|
967
|
+
price: NonNegativeDecimal38_18
|
|
953
968
|
|
|
954
969
|
class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
955
970
|
config: GeneratorConfig = Relationship()
|
|
@@ -1391,29 +1406,53 @@ When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQ
|
|
|
1391
1406
|
|
|
1392
1407
|
```python
|
|
1393
1408
|
class UserBase(SQLModelBase):
|
|
1394
|
-
name:
|
|
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:
|
|
1414
|
+
# name: NonEmptyStrippedStr64 | None = None — description "User display name" is inherited
|
|
1400
1415
|
# Shows up correctly in OpenAPI/Swagger docs
|
|
1401
1416
|
```
|
|
1402
1417
|
|
|
1403
1418
|
---
|
|
1404
1419
|
|
|
1405
|
-
###
|
|
1420
|
+
### Enhanced AsyncSession (cache-aware commit/reset/refresh)
|
|
1406
1421
|
|
|
1407
|
-
|
|
1422
|
+
`sqlmodel_ext.AsyncSession` subclasses sqlmodel's `AsyncSession` and makes cache
|
|
1423
|
+
correctness automatic. Point your session factory at it:
|
|
1408
1424
|
|
|
1409
1425
|
```python
|
|
1410
|
-
from
|
|
1426
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
1427
|
+
from sqlmodel_ext import AsyncSession
|
|
1411
1428
|
|
|
1412
|
-
|
|
1413
|
-
await safe_reset(session)
|
|
1429
|
+
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=True)
|
|
1414
1430
|
```
|
|
1415
1431
|
|
|
1416
|
-
|
|
1432
|
+
What it does:
|
|
1433
|
+
|
|
1434
|
+
- **`commit()`** auto-registers every `CachedTableBaseMixin` mutation in the
|
|
1435
|
+
session (including bare `session.add()` / attribute mutation /
|
|
1436
|
+
`session.delete()` paths) and synchronously invalidates the Redis cache after
|
|
1437
|
+
commit — no more "forgot the cache-aware commit" stale windows.
|
|
1438
|
+
- **`reset()`** releases the connection and clears FOR UPDATE lock tracking +
|
|
1439
|
+
cache-invalidation tracking from `session.info` (replaces the removed
|
|
1440
|
+
`safe_reset()` helper — just call `await session.reset()`).
|
|
1441
|
+
- **`refresh()`** routes whole-object refreshes of cached models through
|
|
1442
|
+
`Model.get()` (Redis hit + STI polymorphic columns) instead of a raw DB query.
|
|
1443
|
+
- **`execute()`** warns when a bare `UPDATE`/`DELETE` hits a cached table
|
|
1444
|
+
without registered invalidation.
|
|
1445
|
+
|
|
1446
|
+
Models that don't use `CachedTableBaseMixin` are unaffected — every hook
|
|
1447
|
+
degrades to upstream behavior.
|
|
1448
|
+
|
|
1449
|
+
> **Breaking change in 0.4.0**: `safe_reset()` and
|
|
1450
|
+
> `CachedTableBaseMixin.cache_aware_commit()` were removed. CRUD methods no
|
|
1451
|
+
> longer invalidate the cache themselves; invalidation now happens inside
|
|
1452
|
+
> `AsyncSession.commit()`. If you use `CachedTableBaseMixin`, you **must**
|
|
1453
|
+
> construct sessions with `class_=sqlmodel_ext.AsyncSession` (plain sessions
|
|
1454
|
+
> fall back to the fire-and-forget `after_commit` compensation hook, which
|
|
1455
|
+
> reintroduces a brief stale-cache window).
|
|
1417
1456
|
|
|
1418
1457
|
---
|
|
1419
1458
|
|
|
@@ -1430,7 +1469,7 @@ sqlmodel_ext/
|
|
|
1430
1469
|
pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
|
|
1431
1470
|
mixins/
|
|
1432
1471
|
__init__.py # Mixin re-exports
|
|
1433
|
-
table.py # TableBaseMixin, UUIDTableBaseMixin
|
|
1472
|
+
table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
|
|
1434
1473
|
cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
|
|
1435
1474
|
polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
|
|
1436
1475
|
optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
|
|
@@ -25,7 +25,7 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
|
|
|
25
25
|
| **RelationPreloadMixin** | Decorator-based automatic relationship preloading (prevents `MissingGreenlet` errors) |
|
|
26
26
|
| **ListResponse[T]** | Generic paginated response model for list endpoints |
|
|
27
27
|
| **Field Types** | Reusable constrained types: `Str64`, `Port`, `IPAddress`, `HttpUrl`, `SafeHttpUrl`, and more |
|
|
28
|
-
| **PostgreSQL Types** | `Array[T]` for native ARRAY, `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
|
|
28
|
+
| **PostgreSQL Types** | `Array[T]` for native ARRAY (read-tolerant for enum arrays during rolling-deploy version skew), `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
|
|
29
29
|
| **Info Response DTOs** | Pre-built mixins for API response models with id/timestamp fields |
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
@@ -63,12 +63,13 @@ pip install sqlmodel-ext[pgvector]
|
|
|
63
63
|
### Define Models
|
|
64
64
|
|
|
65
65
|
```python
|
|
66
|
-
from
|
|
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:
|
|
71
|
-
email:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
851
|
-
amount:
|
|
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:
|
|
922
|
+
price: NonNegativeDecimal38_18
|
|
908
923
|
|
|
909
924
|
class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
910
925
|
config: GeneratorConfig = Relationship()
|
|
@@ -1346,29 +1361,53 @@ When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQ
|
|
|
1346
1361
|
|
|
1347
1362
|
```python
|
|
1348
1363
|
class UserBase(SQLModelBase):
|
|
1349
|
-
name:
|
|
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:
|
|
1369
|
+
# name: NonEmptyStrippedStr64 | None = None — description "User display name" is inherited
|
|
1355
1370
|
# Shows up correctly in OpenAPI/Swagger docs
|
|
1356
1371
|
```
|
|
1357
1372
|
|
|
1358
1373
|
---
|
|
1359
1374
|
|
|
1360
|
-
###
|
|
1375
|
+
### Enhanced AsyncSession (cache-aware commit/reset/refresh)
|
|
1361
1376
|
|
|
1362
|
-
|
|
1377
|
+
`sqlmodel_ext.AsyncSession` subclasses sqlmodel's `AsyncSession` and makes cache
|
|
1378
|
+
correctness automatic. Point your session factory at it:
|
|
1363
1379
|
|
|
1364
1380
|
```python
|
|
1365
|
-
from
|
|
1381
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
1382
|
+
from sqlmodel_ext import AsyncSession
|
|
1366
1383
|
|
|
1367
|
-
|
|
1368
|
-
await safe_reset(session)
|
|
1384
|
+
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=True)
|
|
1369
1385
|
```
|
|
1370
1386
|
|
|
1371
|
-
|
|
1387
|
+
What it does:
|
|
1388
|
+
|
|
1389
|
+
- **`commit()`** auto-registers every `CachedTableBaseMixin` mutation in the
|
|
1390
|
+
session (including bare `session.add()` / attribute mutation /
|
|
1391
|
+
`session.delete()` paths) and synchronously invalidates the Redis cache after
|
|
1392
|
+
commit — no more "forgot the cache-aware commit" stale windows.
|
|
1393
|
+
- **`reset()`** releases the connection and clears FOR UPDATE lock tracking +
|
|
1394
|
+
cache-invalidation tracking from `session.info` (replaces the removed
|
|
1395
|
+
`safe_reset()` helper — just call `await session.reset()`).
|
|
1396
|
+
- **`refresh()`** routes whole-object refreshes of cached models through
|
|
1397
|
+
`Model.get()` (Redis hit + STI polymorphic columns) instead of a raw DB query.
|
|
1398
|
+
- **`execute()`** warns when a bare `UPDATE`/`DELETE` hits a cached table
|
|
1399
|
+
without registered invalidation.
|
|
1400
|
+
|
|
1401
|
+
Models that don't use `CachedTableBaseMixin` are unaffected — every hook
|
|
1402
|
+
degrades to upstream behavior.
|
|
1403
|
+
|
|
1404
|
+
> **Breaking change in 0.4.0**: `safe_reset()` and
|
|
1405
|
+
> `CachedTableBaseMixin.cache_aware_commit()` were removed. CRUD methods no
|
|
1406
|
+
> longer invalidate the cache themselves; invalidation now happens inside
|
|
1407
|
+
> `AsyncSession.commit()`. If you use `CachedTableBaseMixin`, you **must**
|
|
1408
|
+
> construct sessions with `class_=sqlmodel_ext.AsyncSession` (plain sessions
|
|
1409
|
+
> fall back to the fire-and-forget `after_commit` compensation hook, which
|
|
1410
|
+
> reintroduces a brief stale-cache window).
|
|
1372
1411
|
|
|
1373
1412
|
---
|
|
1374
1413
|
|
|
@@ -1385,7 +1424,7 @@ sqlmodel_ext/
|
|
|
1385
1424
|
pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
|
|
1386
1425
|
mixins/
|
|
1387
1426
|
__init__.py # Mixin re-exports
|
|
1388
|
-
table.py # TableBaseMixin, UUIDTableBaseMixin
|
|
1427
|
+
table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
|
|
1389
1428
|
cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
|
|
1390
1429
|
polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
|
|
1391
1430
|
optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
|