sqlmodel-ext 0.2.1__tar.gz → 0.3.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.2.1 → sqlmodel_ext-0.3.0}/PKG-INFO +140 -4
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/README.md +136 -3
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/README_zh.md +95 -3
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/cached-table.md +30 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/field-types.md +13 -4
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/relation-load-checker.md +36 -7
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/cached-table.md +21 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/field-types.md +9 -4
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/relation-load-checker.md +26 -7
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/pyproject.toml +2 -1
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/__init__.py +6 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/_compat.py +18 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/base.py +374 -13
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/__init__.py +33 -4
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +119 -113
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/__init__.py +1 -0
- sqlmodel_ext-0.3.0/src/sqlmodel_ext/mixins/cached_table.py +2074 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/polymorphic.py +34 -17
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/table.py +16 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/relation_load_checker.py +3645 -2751
- sqlmodel_ext-0.2.1/src/sqlmodel_ext/mixins/cached_table.py +0 -1707
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/.gitignore +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/LICENSE +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/.vitepress/config.mts +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/.vitepress/en.mts +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/.vitepress/theme/index.ts +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/.vitepress/theme/style/custom.css +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/.vitepress/zh.mts +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/crud.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/index.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/pagination.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/index.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/crud.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/index.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/en/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/crud.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/index.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/pagination.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/index.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/crud.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/index.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/docs/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/package.json +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/pnpm-lock.yaml +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/_exceptions.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/_sa_type.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/_utils.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/url.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
- {sqlmodel_ext-0.2.1 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/pagination.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-ext
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -25,6 +25,9 @@ Requires-Python: >=3.12
|
|
|
25
25
|
Requires-Dist: pydantic>=2.0
|
|
26
26
|
Requires-Dist: sqlalchemy>=2.0
|
|
27
27
|
Requires-Dist: sqlmodel>=0.0.22
|
|
28
|
+
Provides-Extra: cache
|
|
29
|
+
Requires-Dist: orjson>=3.0; extra == 'cache'
|
|
30
|
+
Requires-Dist: redis>=5.0; extra == 'cache'
|
|
28
31
|
Provides-Extra: dev
|
|
29
32
|
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
|
|
30
33
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
@@ -58,8 +61,9 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
|
|
|
58
61
|
|
|
59
62
|
| Feature | Description |
|
|
60
63
|
|---------|-------------|
|
|
61
|
-
| **SQLModelBase** | Smart metaclass with automatic `table=True`, `mapper_args` merging, and Python 3.14 (PEP 649) compatibility |
|
|
64
|
+
| **SQLModelBase** | Smart metaclass with automatic `table=True`, `mapper_args` merging, `all_fields_optional` for UpdateRequest DTOs, attribute docstring inheritance, and Python 3.14 (PEP 649) compatibility |
|
|
62
65
|
| **TableBaseMixin / UUIDTableBaseMixin** | Full async CRUD: `add()`, `save()`, `update()`, `delete()`, `get()`, `count()`, `get_with_count()`, `get_exist_one()` |
|
|
66
|
+
| **CachedTableBaseMixin** | Two-tier Redis cache (ID + query) with version-based invalidation, STI-aware cascade, and optional metrics callbacks |
|
|
63
67
|
| **PolymorphicBaseMixin** | Simplified Joined Table Inheritance (JTI) and Single Table Inheritance (STI) |
|
|
64
68
|
| **AutoPolymorphicIdentityMixin** | Auto-generated `polymorphic_identity` from class names |
|
|
65
69
|
| **OptimisticLockMixin** | Version-based optimistic locking with automatic retry |
|
|
@@ -87,6 +91,12 @@ With PostgreSQL ARRAY and JSONB types (requires `orjson`):
|
|
|
87
91
|
pip install sqlmodel-ext[postgresql]
|
|
88
92
|
```
|
|
89
93
|
|
|
94
|
+
With Redis caching support (enables `CachedTableBaseMixin`):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install sqlmodel-ext[cache]
|
|
98
|
+
```
|
|
99
|
+
|
|
90
100
|
With pgvector + NumPy vector support (includes `[postgresql]`):
|
|
91
101
|
|
|
92
102
|
```bash
|
|
@@ -1251,6 +1261,130 @@ These mixins define the fields as **required** (non-optional), because in API re
|
|
|
1251
1261
|
|
|
1252
1262
|
---
|
|
1253
1263
|
|
|
1264
|
+
### Redis Caching (CachedTableBaseMixin)
|
|
1265
|
+
|
|
1266
|
+
Add a two-tier Redis cache to any table model. Queries go through Redis first; cache misses fall through to the database.
|
|
1267
|
+
|
|
1268
|
+
```bash
|
|
1269
|
+
pip install sqlmodel-ext[cache] # installs redis + orjson
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
**Setup (once at application startup):**
|
|
1273
|
+
|
|
1274
|
+
```python
|
|
1275
|
+
from redis.asyncio import Redis
|
|
1276
|
+
from sqlmodel_ext import CachedTableBaseMixin
|
|
1277
|
+
|
|
1278
|
+
redis = Redis.from_url("redis://localhost:6379/0", decode_responses=False)
|
|
1279
|
+
CachedTableBaseMixin.configure_redis(redis)
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
**Define a cached model:**
|
|
1283
|
+
|
|
1284
|
+
```python
|
|
1285
|
+
class Character(CachedTableBaseMixin, CharacterBase, UUIDTableBaseMixin, table=True, cache_ttl=1800):
|
|
1286
|
+
pass # 30-minute cache TTL
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
That's it. All `get()` calls now check Redis first. Cache invalidation is automatic on `save()`, `update()`, and `delete()`.
|
|
1290
|
+
|
|
1291
|
+
**Cache architecture:**
|
|
1292
|
+
|
|
1293
|
+
| Layer | Key format | Invalidation |
|
|
1294
|
+
|-------|-----------|--------------|
|
|
1295
|
+
| ID cache | `id:{Model}:{id}` | Row-level DEL on save/update/delete |
|
|
1296
|
+
| Query cache | `query:{Model}:v{version}:{hash}` | Version bump (O(1) INCR) makes old keys unreachable |
|
|
1297
|
+
| Version | `ver:{Model}` | INCR on any write; STI subclass changes bump all ancestor versions |
|
|
1298
|
+
|
|
1299
|
+
**Optional metrics callbacks:**
|
|
1300
|
+
|
|
1301
|
+
```python
|
|
1302
|
+
CachedTableBaseMixin.on_cache_hit = lambda name: print(f"HIT: {name}")
|
|
1303
|
+
CachedTableBaseMixin.on_cache_miss = lambda name: print(f"MISS: {name}")
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
**Cache skip conditions** (automatically detected):
|
|
1307
|
+
- `no_cache=True` (explicit bypass)
|
|
1308
|
+
- `with_for_update=True` (pessimistic lock needs latest)
|
|
1309
|
+
- `join` is set (join target changes don't trigger invalidation)
|
|
1310
|
+
- `options` is set (custom loading options)
|
|
1311
|
+
|
|
1312
|
+
**No Redis? No problem.** If you don't call `configure_redis()` and don't inherit `CachedTableBaseMixin`, there is zero Redis dependency. The caching layer is entirely opt-in.
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
|
|
1316
|
+
### all_fields_optional
|
|
1317
|
+
|
|
1318
|
+
Automatically convert all inherited fields to optional for PATCH/UpdateRequest DTOs:
|
|
1319
|
+
|
|
1320
|
+
```python
|
|
1321
|
+
class ArticleBase(SQLModelBase):
|
|
1322
|
+
title: Str64
|
|
1323
|
+
"""Article title"""
|
|
1324
|
+
body: Text10K
|
|
1325
|
+
"""Article body"""
|
|
1326
|
+
is_published: bool = False
|
|
1327
|
+
"""Publication status"""
|
|
1328
|
+
|
|
1329
|
+
class ArticleUpdateRequest(ArticleBase, all_fields_optional=True):
|
|
1330
|
+
pass
|
|
1331
|
+
# All fields become: title: Str64 | None = None, body: Text10K | None = None, etc.
|
|
1332
|
+
# Annotated constraints (max_length, ge, le) are preserved
|
|
1333
|
+
# Attribute docstrings are inherited from ArticleBase
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
**What it does:**
|
|
1337
|
+
- Converts `T` to `T | None = None` for all inherited fields
|
|
1338
|
+
- Preserves `Annotated` metadata (e.g. `Field(ge=0)` constraints stay intact)
|
|
1339
|
+
- Skips `Literal` fields (discriminator fields must remain required)
|
|
1340
|
+
- Replaces `default_factory` with `default=None`
|
|
1341
|
+
|
|
1342
|
+
**No more manual overrides** like this:
|
|
1343
|
+
|
|
1344
|
+
```python
|
|
1345
|
+
# Before: tedious and error-prone
|
|
1346
|
+
class ArticleUpdateRequest(ArticleBase):
|
|
1347
|
+
title: Str64 | None = None # Must manually repeat type + optional
|
|
1348
|
+
body: Text10K | None = None # Easy to forget constraints
|
|
1349
|
+
is_published: bool | None = None
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
---
|
|
1353
|
+
|
|
1354
|
+
### Attribute Docstring Inheritance
|
|
1355
|
+
|
|
1356
|
+
When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQLModelBase`), field descriptions appear in OpenAPI schemas. However, Pydantic's AST-based docstring parser doesn't inherit descriptions when subclasses override fields.
|
|
1357
|
+
|
|
1358
|
+
**sqlmodel-ext fixes this automatically.** The metaclass inherits missing descriptions from parent classes via MRO traversal, and `__get_pydantic_json_schema__` patches bare `$ref` properties to include descriptions.
|
|
1359
|
+
|
|
1360
|
+
```python
|
|
1361
|
+
class UserBase(SQLModelBase):
|
|
1362
|
+
name: str
|
|
1363
|
+
"""User display name""" # ← parsed by Pydantic
|
|
1364
|
+
|
|
1365
|
+
class UserUpdateRequest(UserBase, all_fields_optional=True):
|
|
1366
|
+
pass
|
|
1367
|
+
# name: str | None = None — description "User display name" is automatically inherited
|
|
1368
|
+
# Shows up correctly in OpenAPI/Swagger docs
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
---
|
|
1372
|
+
|
|
1373
|
+
### safe_reset
|
|
1374
|
+
|
|
1375
|
+
Safely reset an async session, clearing both the DB connection and FOR UPDATE lock tracking:
|
|
1376
|
+
|
|
1377
|
+
```python
|
|
1378
|
+
from sqlmodel_ext import safe_reset
|
|
1379
|
+
|
|
1380
|
+
# In long-lived tasks (e.g. background workers) between operations:
|
|
1381
|
+
await safe_reset(session)
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
This prevents stale `FOR UPDATE` lock tracking in `session.info` when Python reuses object IDs across `reset()` cycles.
|
|
1385
|
+
|
|
1386
|
+
---
|
|
1387
|
+
|
|
1254
1388
|
## Architecture
|
|
1255
1389
|
|
|
1256
1390
|
```
|
|
@@ -1264,7 +1398,8 @@ sqlmodel_ext/
|
|
|
1264
1398
|
pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
|
|
1265
1399
|
mixins/
|
|
1266
1400
|
__init__.py # Mixin re-exports
|
|
1267
|
-
table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
|
|
1401
|
+
table.py # TableBaseMixin, UUIDTableBaseMixin, safe_reset (async CRUD)
|
|
1402
|
+
cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
|
|
1268
1403
|
polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
|
|
1269
1404
|
optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
|
|
1270
1405
|
relation_preload.py # RelationPreloadMixin, @requires_relations
|
|
@@ -1292,7 +1427,8 @@ sqlmodel_ext/
|
|
|
1292
1427
|
- **pydantic** >= 2.0
|
|
1293
1428
|
- **sqlalchemy** >= 2.0
|
|
1294
1429
|
- (optional) **fastapi** >= 0.100.0
|
|
1295
|
-
- (optional) **
|
|
1430
|
+
- (optional) **redis** >= 5.0 -- for `CachedTableBaseMixin`
|
|
1431
|
+
- (optional) **orjson** >= 3.0 -- for `CachedTableBaseMixin` and `JSON100K` / `JSONList100K`
|
|
1296
1432
|
- (optional) **numpy** >= 1.24 -- for `NumpyVector`
|
|
1297
1433
|
- (optional) **pgvector** >= 0.3 -- for `NumpyVector`
|
|
1298
1434
|
|
|
@@ -16,8 +16,9 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
|
|
|
16
16
|
|
|
17
17
|
| Feature | Description |
|
|
18
18
|
|---------|-------------|
|
|
19
|
-
| **SQLModelBase** | Smart metaclass with automatic `table=True`, `mapper_args` merging, and Python 3.14 (PEP 649) compatibility |
|
|
19
|
+
| **SQLModelBase** | Smart metaclass with automatic `table=True`, `mapper_args` merging, `all_fields_optional` for UpdateRequest DTOs, attribute docstring inheritance, and Python 3.14 (PEP 649) compatibility |
|
|
20
20
|
| **TableBaseMixin / UUIDTableBaseMixin** | Full async CRUD: `add()`, `save()`, `update()`, `delete()`, `get()`, `count()`, `get_with_count()`, `get_exist_one()` |
|
|
21
|
+
| **CachedTableBaseMixin** | Two-tier Redis cache (ID + query) with version-based invalidation, STI-aware cascade, and optional metrics callbacks |
|
|
21
22
|
| **PolymorphicBaseMixin** | Simplified Joined Table Inheritance (JTI) and Single Table Inheritance (STI) |
|
|
22
23
|
| **AutoPolymorphicIdentityMixin** | Auto-generated `polymorphic_identity` from class names |
|
|
23
24
|
| **OptimisticLockMixin** | Version-based optimistic locking with automatic retry |
|
|
@@ -45,6 +46,12 @@ With PostgreSQL ARRAY and JSONB types (requires `orjson`):
|
|
|
45
46
|
pip install sqlmodel-ext[postgresql]
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
With Redis caching support (enables `CachedTableBaseMixin`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install sqlmodel-ext[cache]
|
|
53
|
+
```
|
|
54
|
+
|
|
48
55
|
With pgvector + NumPy vector support (includes `[postgresql]`):
|
|
49
56
|
|
|
50
57
|
```bash
|
|
@@ -1209,6 +1216,130 @@ These mixins define the fields as **required** (non-optional), because in API re
|
|
|
1209
1216
|
|
|
1210
1217
|
---
|
|
1211
1218
|
|
|
1219
|
+
### Redis Caching (CachedTableBaseMixin)
|
|
1220
|
+
|
|
1221
|
+
Add a two-tier Redis cache to any table model. Queries go through Redis first; cache misses fall through to the database.
|
|
1222
|
+
|
|
1223
|
+
```bash
|
|
1224
|
+
pip install sqlmodel-ext[cache] # installs redis + orjson
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
**Setup (once at application startup):**
|
|
1228
|
+
|
|
1229
|
+
```python
|
|
1230
|
+
from redis.asyncio import Redis
|
|
1231
|
+
from sqlmodel_ext import CachedTableBaseMixin
|
|
1232
|
+
|
|
1233
|
+
redis = Redis.from_url("redis://localhost:6379/0", decode_responses=False)
|
|
1234
|
+
CachedTableBaseMixin.configure_redis(redis)
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
**Define a cached model:**
|
|
1238
|
+
|
|
1239
|
+
```python
|
|
1240
|
+
class Character(CachedTableBaseMixin, CharacterBase, UUIDTableBaseMixin, table=True, cache_ttl=1800):
|
|
1241
|
+
pass # 30-minute cache TTL
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
That's it. All `get()` calls now check Redis first. Cache invalidation is automatic on `save()`, `update()`, and `delete()`.
|
|
1245
|
+
|
|
1246
|
+
**Cache architecture:**
|
|
1247
|
+
|
|
1248
|
+
| Layer | Key format | Invalidation |
|
|
1249
|
+
|-------|-----------|--------------|
|
|
1250
|
+
| ID cache | `id:{Model}:{id}` | Row-level DEL on save/update/delete |
|
|
1251
|
+
| Query cache | `query:{Model}:v{version}:{hash}` | Version bump (O(1) INCR) makes old keys unreachable |
|
|
1252
|
+
| Version | `ver:{Model}` | INCR on any write; STI subclass changes bump all ancestor versions |
|
|
1253
|
+
|
|
1254
|
+
**Optional metrics callbacks:**
|
|
1255
|
+
|
|
1256
|
+
```python
|
|
1257
|
+
CachedTableBaseMixin.on_cache_hit = lambda name: print(f"HIT: {name}")
|
|
1258
|
+
CachedTableBaseMixin.on_cache_miss = lambda name: print(f"MISS: {name}")
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**Cache skip conditions** (automatically detected):
|
|
1262
|
+
- `no_cache=True` (explicit bypass)
|
|
1263
|
+
- `with_for_update=True` (pessimistic lock needs latest)
|
|
1264
|
+
- `join` is set (join target changes don't trigger invalidation)
|
|
1265
|
+
- `options` is set (custom loading options)
|
|
1266
|
+
|
|
1267
|
+
**No Redis? No problem.** If you don't call `configure_redis()` and don't inherit `CachedTableBaseMixin`, there is zero Redis dependency. The caching layer is entirely opt-in.
|
|
1268
|
+
|
|
1269
|
+
---
|
|
1270
|
+
|
|
1271
|
+
### all_fields_optional
|
|
1272
|
+
|
|
1273
|
+
Automatically convert all inherited fields to optional for PATCH/UpdateRequest DTOs:
|
|
1274
|
+
|
|
1275
|
+
```python
|
|
1276
|
+
class ArticleBase(SQLModelBase):
|
|
1277
|
+
title: Str64
|
|
1278
|
+
"""Article title"""
|
|
1279
|
+
body: Text10K
|
|
1280
|
+
"""Article body"""
|
|
1281
|
+
is_published: bool = False
|
|
1282
|
+
"""Publication status"""
|
|
1283
|
+
|
|
1284
|
+
class ArticleUpdateRequest(ArticleBase, all_fields_optional=True):
|
|
1285
|
+
pass
|
|
1286
|
+
# All fields become: title: Str64 | None = None, body: Text10K | None = None, etc.
|
|
1287
|
+
# Annotated constraints (max_length, ge, le) are preserved
|
|
1288
|
+
# Attribute docstrings are inherited from ArticleBase
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
**What it does:**
|
|
1292
|
+
- Converts `T` to `T | None = None` for all inherited fields
|
|
1293
|
+
- Preserves `Annotated` metadata (e.g. `Field(ge=0)` constraints stay intact)
|
|
1294
|
+
- Skips `Literal` fields (discriminator fields must remain required)
|
|
1295
|
+
- Replaces `default_factory` with `default=None`
|
|
1296
|
+
|
|
1297
|
+
**No more manual overrides** like this:
|
|
1298
|
+
|
|
1299
|
+
```python
|
|
1300
|
+
# Before: tedious and error-prone
|
|
1301
|
+
class ArticleUpdateRequest(ArticleBase):
|
|
1302
|
+
title: Str64 | None = None # Must manually repeat type + optional
|
|
1303
|
+
body: Text10K | None = None # Easy to forget constraints
|
|
1304
|
+
is_published: bool | None = None
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
---
|
|
1308
|
+
|
|
1309
|
+
### Attribute Docstring Inheritance
|
|
1310
|
+
|
|
1311
|
+
When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQLModelBase`), field descriptions appear in OpenAPI schemas. However, Pydantic's AST-based docstring parser doesn't inherit descriptions when subclasses override fields.
|
|
1312
|
+
|
|
1313
|
+
**sqlmodel-ext fixes this automatically.** The metaclass inherits missing descriptions from parent classes via MRO traversal, and `__get_pydantic_json_schema__` patches bare `$ref` properties to include descriptions.
|
|
1314
|
+
|
|
1315
|
+
```python
|
|
1316
|
+
class UserBase(SQLModelBase):
|
|
1317
|
+
name: str
|
|
1318
|
+
"""User display name""" # ← parsed by Pydantic
|
|
1319
|
+
|
|
1320
|
+
class UserUpdateRequest(UserBase, all_fields_optional=True):
|
|
1321
|
+
pass
|
|
1322
|
+
# name: str | None = None — description "User display name" is automatically inherited
|
|
1323
|
+
# Shows up correctly in OpenAPI/Swagger docs
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
---
|
|
1327
|
+
|
|
1328
|
+
### safe_reset
|
|
1329
|
+
|
|
1330
|
+
Safely reset an async session, clearing both the DB connection and FOR UPDATE lock tracking:
|
|
1331
|
+
|
|
1332
|
+
```python
|
|
1333
|
+
from sqlmodel_ext import safe_reset
|
|
1334
|
+
|
|
1335
|
+
# In long-lived tasks (e.g. background workers) between operations:
|
|
1336
|
+
await safe_reset(session)
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
This prevents stale `FOR UPDATE` lock tracking in `session.info` when Python reuses object IDs across `reset()` cycles.
|
|
1340
|
+
|
|
1341
|
+
---
|
|
1342
|
+
|
|
1212
1343
|
## Architecture
|
|
1213
1344
|
|
|
1214
1345
|
```
|
|
@@ -1222,7 +1353,8 @@ sqlmodel_ext/
|
|
|
1222
1353
|
pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
|
|
1223
1354
|
mixins/
|
|
1224
1355
|
__init__.py # Mixin re-exports
|
|
1225
|
-
table.py # TableBaseMixin, UUIDTableBaseMixin (async CRUD)
|
|
1356
|
+
table.py # TableBaseMixin, UUIDTableBaseMixin, safe_reset (async CRUD)
|
|
1357
|
+
cached_table.py # CachedTableBaseMixin (two-tier Redis cache with version invalidation)
|
|
1226
1358
|
polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
|
|
1227
1359
|
optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
|
|
1228
1360
|
relation_preload.py # RelationPreloadMixin, @requires_relations
|
|
@@ -1250,7 +1382,8 @@ sqlmodel_ext/
|
|
|
1250
1382
|
- **pydantic** >= 2.0
|
|
1251
1383
|
- **sqlalchemy** >= 2.0
|
|
1252
1384
|
- (optional) **fastapi** >= 0.100.0
|
|
1253
|
-
- (optional) **
|
|
1385
|
+
- (optional) **redis** >= 5.0 -- for `CachedTableBaseMixin`
|
|
1386
|
+
- (optional) **orjson** >= 3.0 -- for `CachedTableBaseMixin` and `JSON100K` / `JSONList100K`
|
|
1254
1387
|
- (optional) **numpy** >= 1.24 -- for `NumpyVector`
|
|
1255
1388
|
- (optional) **pgvector** >= 0.3 -- for `NumpyVector`
|
|
1256
1389
|
|
|
@@ -16,8 +16,9 @@ SQLModel 增强基础设施:智能元类、异步 CRUD Mixin、多态继承、
|
|
|
16
16
|
|
|
17
17
|
| 特性 | 说明 |
|
|
18
18
|
|------|------|
|
|
19
|
-
| **SQLModelBase** |
|
|
19
|
+
| **SQLModelBase** | 智能元类,自动 `table=True`、合并 `mapper_args`、`all_fields_optional` UpdateRequest DTO、属性 docstring 继承、兼容 Python 3.14 (PEP 649) |
|
|
20
20
|
| **TableBaseMixin / UUIDTableBaseMixin** | 完整异步 CRUD:`add()`、`save()`、`update()`、`delete()`、`get()`、`count()`、`get_with_count()`、`get_exist_one()` |
|
|
21
|
+
| **CachedTableBaseMixin** | 双层 Redis 缓存(ID + 查询),版本号失效、STI 级联感知、可选 metrics 回调 |
|
|
21
22
|
| **PolymorphicBaseMixin** | 简化联表继承 (JTI) 和单表继承 (STI) 配置 |
|
|
22
23
|
| **AutoPolymorphicIdentityMixin** | 根据类名自动生成 `polymorphic_identity` |
|
|
23
24
|
| **OptimisticLockMixin** | 基于版本号的乐观锁,支持自动重试 |
|
|
@@ -45,6 +46,12 @@ pip install sqlmodel-ext[fastapi]
|
|
|
45
46
|
pip install sqlmodel-ext[postgresql]
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
使用 Redis 缓存(启用 `CachedTableBaseMixin`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install sqlmodel-ext[cache]
|
|
53
|
+
```
|
|
54
|
+
|
|
48
55
|
使用 pgvector + NumPy 向量支持(包含 `[postgresql]`):
|
|
49
56
|
|
|
50
57
|
```bash
|
|
@@ -1209,6 +1216,89 @@ class UserResponse(UserBase, UUIDIdDatetimeInfoMixin):
|
|
|
1209
1216
|
|
|
1210
1217
|
---
|
|
1211
1218
|
|
|
1219
|
+
### Redis 缓存 (CachedTableBaseMixin)
|
|
1220
|
+
|
|
1221
|
+
为任何表模型添加双层 Redis 缓存。查询先走 Redis,未命中再查数据库。
|
|
1222
|
+
|
|
1223
|
+
```bash
|
|
1224
|
+
pip install sqlmodel-ext[cache] # 安装 redis + orjson
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
**启动配置(一次性):**
|
|
1228
|
+
|
|
1229
|
+
```python
|
|
1230
|
+
from redis.asyncio import Redis
|
|
1231
|
+
from sqlmodel_ext import CachedTableBaseMixin
|
|
1232
|
+
|
|
1233
|
+
redis = Redis.from_url("redis://localhost:6379/0", decode_responses=False)
|
|
1234
|
+
CachedTableBaseMixin.configure_redis(redis)
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
**定义缓存模型:**
|
|
1238
|
+
|
|
1239
|
+
```python
|
|
1240
|
+
class Character(CachedTableBaseMixin, CharacterBase, UUIDTableBaseMixin, table=True, cache_ttl=1800):
|
|
1241
|
+
pass # 30 分钟缓存 TTL
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
所有 `get()` 调用自动先查 Redis。`save()`、`update()`、`delete()` 时自动失效缓存。
|
|
1245
|
+
|
|
1246
|
+
**缓存架构:**
|
|
1247
|
+
|
|
1248
|
+
| 层 | Key 格式 | 失效方式 |
|
|
1249
|
+
|----|---------|---------|
|
|
1250
|
+
| ID 缓存 | `id:{Model}:{id}` | 行级 DEL |
|
|
1251
|
+
| 查询缓存 | `query:{Model}:v{version}:{hash}` | 版本号 INCR(O(1))使旧 key 不可达 |
|
|
1252
|
+
| 版本号 | `ver:{Model}` | 写入时 INCR;STI 子类变更级联 bump 所有祖先 |
|
|
1253
|
+
|
|
1254
|
+
**可选 metrics 回调:**
|
|
1255
|
+
|
|
1256
|
+
```python
|
|
1257
|
+
CachedTableBaseMixin.on_cache_hit = lambda name: print(f"命中: {name}")
|
|
1258
|
+
CachedTableBaseMixin.on_cache_miss = lambda name: print(f"未命中: {name}")
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**不用 Redis?** 不调用 `configure_redis()`、不继承 `CachedTableBaseMixin`,则零 Redis 依赖。缓存层完全可选。
|
|
1262
|
+
|
|
1263
|
+
---
|
|
1264
|
+
|
|
1265
|
+
### all_fields_optional
|
|
1266
|
+
|
|
1267
|
+
自动将所有继承字段转为可选,用于 PATCH/UpdateRequest DTO:
|
|
1268
|
+
|
|
1269
|
+
```python
|
|
1270
|
+
class ArticleBase(SQLModelBase):
|
|
1271
|
+
title: Str64
|
|
1272
|
+
"""文章标题"""
|
|
1273
|
+
body: Text10K
|
|
1274
|
+
"""文章内容"""
|
|
1275
|
+
|
|
1276
|
+
class ArticleUpdateRequest(ArticleBase, all_fields_optional=True):
|
|
1277
|
+
pass
|
|
1278
|
+
# 所有字段变为: title: Str64 | None = None, body: Text10K | None = None, ...
|
|
1279
|
+
# Annotated 约束(max_length, ge, le)保留
|
|
1280
|
+
# 属性 docstring 从 ArticleBase 继承
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
---
|
|
1284
|
+
|
|
1285
|
+
### 属性 Docstring 继承
|
|
1286
|
+
|
|
1287
|
+
`SQLModelBase` 默认启用 Pydantic 的 `use_attribute_docstrings=True`,字段后的 docstring 自动出现在 OpenAPI Schema 中。当子类覆盖字段时,Pydantic 不会继承父类的 description——**sqlmodel-ext 在元类中自动修复此问题**。
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
### safe_reset
|
|
1292
|
+
|
|
1293
|
+
安全重置异步会话,同时清理 FOR UPDATE 锁跟踪:
|
|
1294
|
+
|
|
1295
|
+
```python
|
|
1296
|
+
from sqlmodel_ext import safe_reset
|
|
1297
|
+
await safe_reset(session)
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
---
|
|
1301
|
+
|
|
1212
1302
|
## 架构
|
|
1213
1303
|
|
|
1214
1304
|
```
|
|
@@ -1222,7 +1312,8 @@ sqlmodel_ext/
|
|
|
1222
1312
|
pagination.py # ListResponse、TimeFilterRequest、PaginationRequest、TableViewRequest
|
|
1223
1313
|
mixins/
|
|
1224
1314
|
__init__.py # Mixin 重导出
|
|
1225
|
-
table.py # TableBaseMixin、UUIDTableBaseMixin(异步 CRUD)
|
|
1315
|
+
table.py # TableBaseMixin、UUIDTableBaseMixin、safe_reset(异步 CRUD)
|
|
1316
|
+
cached_table.py # CachedTableBaseMixin(双层 Redis 缓存 + 版本号失效)
|
|
1226
1317
|
polymorphic.py # PolymorphicBaseMixin、AutoPolymorphicIdentityMixin、create_subclass_id_mixin
|
|
1227
1318
|
optimistic_lock.py # OptimisticLockMixin、OptimisticLockError
|
|
1228
1319
|
relation_preload.py # RelationPreloadMixin、@requires_relations
|
|
@@ -1250,7 +1341,8 @@ sqlmodel_ext/
|
|
|
1250
1341
|
- **pydantic** >= 2.0
|
|
1251
1342
|
- **sqlalchemy** >= 2.0
|
|
1252
1343
|
- (可选)**fastapi** >= 0.100.0
|
|
1253
|
-
- (可选)**
|
|
1344
|
+
- (可选)**redis** >= 5.0 -- 用于 `CachedTableBaseMixin`
|
|
1345
|
+
- (可选)**orjson** >= 3.0 -- 用于 `CachedTableBaseMixin` 和 `JSON100K` / `JSONList100K`
|
|
1254
1346
|
- (可选)**numpy** >= 1.24 -- 用于 `NumpyVector`
|
|
1255
1347
|
- (可选)**pgvector** >= 0.3 -- 用于 `NumpyVector`
|
|
1256
1348
|
|
|
@@ -103,6 +103,36 @@ await Character.invalidate_by_id(id1, id2, id3) # Multiple
|
|
|
103
103
|
await Character.invalidate_all()
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
## Cache invalidation on cascade deletes
|
|
107
|
+
|
|
108
|
+
When a parent row is deleted, ``CachedTableBaseMixin`` follows one of two
|
|
109
|
+
paths depending on the relationship's ``passive_deletes`` flag, and both paths
|
|
110
|
+
correctly clean up the child model's cache.
|
|
111
|
+
|
|
112
|
+
| `passive_deletes` | Who removes the children | How the cache is invalidated |
|
|
113
|
+
|-------------------|--------------------------|------------------------------|
|
|
114
|
+
| `False` (default) | SA issues one `DELETE` per child during flush | `persistent_to_deleted` event fires automatically |
|
|
115
|
+
| `True` | The database `ON DELETE CASCADE` removes them silently | `delete()` pre-queries child IDs, then explicitly invalidates after `DELETE` |
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
class User(CachedTableBaseMixin, UserBase, UUIDTableBaseMixin, table=True):
|
|
119
|
+
# passive_deletes=True: rely on the DB CASCADE; SA will not load children
|
|
120
|
+
files: list['UserFile'] = Relationship(
|
|
121
|
+
back_populates='user',
|
|
122
|
+
cascade_delete=True,
|
|
123
|
+
passive_deletes=True,
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
``passive_deletes=True`` is especially useful under high write pressure
|
|
128
|
+
because it keeps SA from pulling thousands of child rows into memory. Older
|
|
129
|
+
releases rejected this combination outright. Starting with 0.3.0, ``delete()``
|
|
130
|
+
pre-queries the child IDs before the DB CASCADE runs, then explicitly calls
|
|
131
|
+
``_invalidate_id_cache`` + ``_invalidate_query_caches`` on the target class,
|
|
132
|
+
so the child model's Redis cache can never serve stale data. Condition-based
|
|
133
|
+
deletes (``delete(condition=...)``) cannot enumerate parent IDs and therefore
|
|
134
|
+
perform a model-level full invalidation on the target.
|
|
135
|
+
|
|
106
136
|
## Polymorphic Inheritance Support
|
|
107
137
|
|
|
108
138
|
STI subclass caches automatically cascade to ancestor classes: when subclass data changes, ancestor class query caches are also cleaned.
|
|
@@ -8,6 +8,7 @@ Writing `name: Str64` simultaneously tells Pydantic to validate `max_length=64`
|
|
|
8
8
|
|
|
9
9
|
| Type | Max Length | Typical Use |
|
|
10
10
|
|------|-----------|-------------|
|
|
11
|
+
| `Str16` | 16 | Trigger words, short tokens |
|
|
11
12
|
| `Str24` | 24 | Short codes |
|
|
12
13
|
| `Str32` | 32 | Tokens, hashes |
|
|
13
14
|
| `Str36` | 36 | UUID string format |
|
|
@@ -34,11 +35,19 @@ class Article(SQLModelBase):
|
|
|
34
35
|
|------|-------|-------------|
|
|
35
36
|
| `Port` | 1 ~ 65535 | Network ports |
|
|
36
37
|
| `Percentage` | 0 ~ 100 | Percentages |
|
|
37
|
-
| `PositiveInt` |
|
|
38
|
-
| `NonNegativeInt` |
|
|
38
|
+
| `PositiveInt` | 1 ~ `INT32_MAX` (2³¹−1) | Counts, quantities (fits PostgreSQL INTEGER) |
|
|
39
|
+
| `NonNegativeInt` | 0 ~ `INT32_MAX` | Indices, counters |
|
|
39
40
|
| `PositiveFloat` | > 0.0 | Prices, weights |
|
|
40
|
-
| `PositiveBigInt` |
|
|
41
|
-
| `NonNegativeBigInt` |
|
|
41
|
+
| `PositiveBigInt` | 1 ~ `JS_MAX_SAFE_INTEGER` (2⁵³−1) | Large integer IDs, timestamps (BigInteger storage) |
|
|
42
|
+
| `NonNegativeBigInt` | 0 ~ `JS_MAX_SAFE_INTEGER` | Large integer counters (BigInteger storage) |
|
|
43
|
+
|
|
44
|
+
::: tip BigInt is capped at `JS_MAX_SAFE_INTEGER`
|
|
45
|
+
`PositiveBigInt` / `NonNegativeBigInt` use `JS_MAX_SAFE_INTEGER = 2⁵³ − 1`
|
|
46
|
+
as their upper bound instead of `INT64_MAX = 2⁶³ − 1`. Integers larger
|
|
47
|
+
than `JS_MAX_SAFE_INTEGER` lose precision when parsed by JavaScript
|
|
48
|
+
clients. Define a custom alias with `le=INT64_MAX` if the field is never
|
|
49
|
+
consumed by a browser. Both constants are exported from `sqlmodel_ext`.
|
|
50
|
+
:::
|
|
42
51
|
|
|
43
52
|
```python
|
|
44
53
|
from sqlmodel_ext import Port, Percentage
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Static Analyzer
|
|
2
2
|
|
|
3
|
+
::: warning Experimental (off by default)
|
|
4
|
+
Starting with 0.3.0 the relation-load static analyzer is **experimental** and
|
|
5
|
+
ships with `check_on_startup = False`. Every auto-check entry point
|
|
6
|
+
(`run_model_checks`, `RelationLoadCheckMiddleware`, the `atexit` warning)
|
|
7
|
+
short-circuits immediately and the analyzer has no effect on your app.
|
|
8
|
+
|
|
9
|
+
The analyzer's AST rules are tuned for a specific project layout (FastAPI
|
|
10
|
+
endpoints, STI inheritance conventions, `save`/`update`/`delete` naming, and
|
|
11
|
+
more). On projects that deviate from those assumptions it may emit false
|
|
12
|
+
positives or fail to parse source files. Only enable it after verifying that
|
|
13
|
+
your project matches the assumptions described below:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import sqlmodel_ext.relation_load_checker as rlc
|
|
17
|
+
rlc.check_on_startup = True # explicit opt-in, experimental
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The module API is not covered by semver stability guarantees; rules and
|
|
21
|
+
signatures may change in subsequent releases.
|
|
22
|
+
:::
|
|
23
|
+
|
|
3
24
|
The static analyzer uses AST analysis to scan source code **at application startup**, detecting code that could cause `MissingGreenlet` errors before any request is served.
|
|
4
25
|
|
|
5
26
|
## Purpose
|
|
@@ -20,25 +41,33 @@ flowchart LR
|
|
|
20
41
|
| **RLC002** | Accessing relations after `save()`/`update()` without `load=` |
|
|
21
42
|
| **RLC003** | Accessing relations without prior `load=` loading (local variables only) |
|
|
22
43
|
| **RLC005** | Dependency function doesn't preload relations needed by `response_model` |
|
|
23
|
-
| **RLC007** | Accessing column attributes on expired objects after commit |
|
|
24
|
-
| **RLC008** | Calling methods on expired objects after commit |
|
|
25
|
-
| **
|
|
44
|
+
| **RLC007** | Accessing column attributes on expired objects after commit (triggers sync lazy load → MissingGreenlet) |
|
|
45
|
+
| **RLC008** | Calling business methods on expired objects after commit (method body may touch expired columns) |
|
|
46
|
+
| **RLC010** | Passing expired ORM objects after commit as arguments into other functions/methods |
|
|
47
|
+
| **RLC011** | Implicit dunder access (`if not obj:` → `__len__`, `for x in obj:` → `__iter__`) triggers relation load |
|
|
48
|
+
| **RLC012** | `response_model` declares STI-subclass-specific columns but the endpoint returns STI base-class results (heterogeneous serialization touches missing columns) |
|
|
26
49
|
|
|
27
50
|
## Usage
|
|
28
51
|
|
|
29
|
-
### Automatic Checking (
|
|
52
|
+
### Automatic Checking (requires explicit opt-in)
|
|
30
53
|
|
|
31
54
|
```python
|
|
32
|
-
#
|
|
55
|
+
# 1. Enable the experimental checker at your earliest bootstrap entrypoint
|
|
56
|
+
import sqlmodel_ext.relation_load_checker as rlc
|
|
57
|
+
rlc.check_on_startup = True # experimental, off by default
|
|
58
|
+
|
|
59
|
+
# 2. In models/__init__.py, after configure_mappers():
|
|
33
60
|
from sqlmodel_ext import run_model_checks, SQLModelBase
|
|
34
61
|
run_model_checks(SQLModelBase)
|
|
35
62
|
|
|
36
|
-
# In main.py:
|
|
63
|
+
# 3. In main.py:
|
|
37
64
|
from sqlmodel_ext import RelationLoadCheckMiddleware
|
|
38
65
|
app.add_middleware(RelationLoadCheckMiddleware)
|
|
39
66
|
```
|
|
40
67
|
|
|
41
|
-
`run_model_checks` scans
|
|
68
|
+
`run_model_checks` scans every model class method; `RelationLoadCheckMiddleware`
|
|
69
|
+
scans FastAPI routes when the lifespan startup completes. Without
|
|
70
|
+
`check_on_startup = True`, both entry points return immediately and do nothing.
|
|
42
71
|
|
|
43
72
|
### Manual Checking
|
|
44
73
|
|
|
@@ -102,6 +102,27 @@ await Character.invalidate_by_id(id1, id2, id3) # 多个
|
|
|
102
102
|
await Character.invalidate_all()
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
## 级联删除的缓存失效
|
|
106
|
+
|
|
107
|
+
当父模型被删除时,``CachedTableBaseMixin`` 会按 SQLAlchemy 关系的 ``passive_deletes`` 设置走两条不同的路径,两种都能正确清理子模型的缓存。
|
|
108
|
+
|
|
109
|
+
| `passive_deletes` | 子行谁删除 | 缓存怎么失效 |
|
|
110
|
+
|-------------------|-----------|-------------|
|
|
111
|
+
| `False`(默认) | SA 在 flush 时逐行 `DELETE` | `persistent_to_deleted` 事件自动触发 |
|
|
112
|
+
| `True` | 数据库 `ON DELETE CASCADE` 静默删除 | `delete()` 在 DELETE 前预查 child ID,DELETE 后显式失效 |
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
class User(CachedTableBaseMixin, UserBase, UUIDTableBaseMixin, table=True):
|
|
116
|
+
# passive_deletes=True:依赖 DB CASCADE,SA 不会加载子行
|
|
117
|
+
files: list['UserFile'] = Relationship(
|
|
118
|
+
back_populates='user',
|
|
119
|
+
cascade_delete=True,
|
|
120
|
+
passive_deletes=True,
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
``passive_deletes=True`` 在高并发环境下尤其有用,可以避免 SA 把几千行子记录拉进内存。旧版本会拒绝这个组合(要求 ``passive_deletes=False``);新版本在 ``delete()`` 中预查 child ID 后再交给 DB CASCADE,之后显式 ``_invalidate_id_cache`` + ``_invalidate_query_caches``,保证子模型的 Redis 缓存不会出现陈旧数据。条件删除(``delete(condition=...)``)下无法枚举父 ID,会对子模型执行模型级全量失效。
|
|
125
|
+
|
|
105
126
|
## 多态继承支持
|
|
106
127
|
|
|
107
128
|
STI 子类的缓存会自动联动祖先类:子类数据变更时,祖先类的查询缓存也会被清理。
|