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