sqlmodel-ext 0.2.0__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.0 → sqlmodel_ext-0.3.0}/.gitignore +5 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/PKG-INFO +143 -5
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/README.md +138 -3
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/README_zh.md +97 -3
- sqlmodel_ext-0.3.0/docs/.vitepress/config.mts +27 -0
- sqlmodel_ext-0.3.0/docs/.vitepress/en.mts +59 -0
- sqlmodel_ext-0.3.0/docs/.vitepress/theme/index.ts +6 -0
- sqlmodel_ext-0.3.0/docs/.vitepress/theme/style/custom.css +4 -0
- sqlmodel_ext-0.3.0/docs/.vitepress/zh.mts +59 -0
- sqlmodel_ext-0.3.0/docs/en/guide/cached-table.md +186 -0
- sqlmodel_ext-0.3.0/docs/en/guide/crud.md +264 -0
- sqlmodel_ext-0.3.0/docs/en/guide/field-types.md +129 -0
- sqlmodel_ext-0.3.0/docs/en/guide/index.md +175 -0
- sqlmodel_ext-0.3.0/docs/en/guide/optimistic-lock.md +102 -0
- sqlmodel_ext-0.3.0/docs/en/guide/pagination.md +136 -0
- sqlmodel_ext-0.3.0/docs/en/guide/polymorphic.md +141 -0
- sqlmodel_ext-0.3.0/docs/en/guide/relation-load-checker.md +134 -0
- sqlmodel_ext-0.3.0/docs/en/guide/relation-preload.md +118 -0
- sqlmodel_ext-0.3.0/docs/en/index.md +30 -0
- sqlmodel_ext-0.3.0/docs/en/internals/cached-table.md +227 -0
- sqlmodel_ext-0.3.0/docs/en/internals/crud.md +283 -0
- sqlmodel_ext-0.3.0/docs/en/internals/index.md +78 -0
- sqlmodel_ext-0.3.0/docs/en/internals/metaclass.md +315 -0
- sqlmodel_ext-0.3.0/docs/en/internals/optimistic-lock.md +114 -0
- sqlmodel_ext-0.3.0/docs/en/internals/polymorphic.md +204 -0
- sqlmodel_ext-0.3.0/docs/en/internals/prerequisites.md +154 -0
- sqlmodel_ext-0.3.0/docs/en/internals/relation-load-checker.md +108 -0
- sqlmodel_ext-0.3.0/docs/en/internals/relation-preload.md +191 -0
- sqlmodel_ext-0.3.0/docs/guide/cached-table.md +176 -0
- sqlmodel_ext-0.3.0/docs/guide/crud.md +264 -0
- sqlmodel_ext-0.3.0/docs/guide/field-types.md +125 -0
- sqlmodel_ext-0.3.0/docs/guide/index.md +175 -0
- sqlmodel_ext-0.3.0/docs/guide/optimistic-lock.md +102 -0
- sqlmodel_ext-0.3.0/docs/guide/pagination.md +136 -0
- sqlmodel_ext-0.3.0/docs/guide/polymorphic.md +141 -0
- sqlmodel_ext-0.3.0/docs/guide/relation-load-checker.md +124 -0
- sqlmodel_ext-0.3.0/docs/guide/relation-preload.md +118 -0
- sqlmodel_ext-0.3.0/docs/index.md +30 -0
- sqlmodel_ext-0.3.0/docs/internals/cached-table.md +227 -0
- sqlmodel_ext-0.3.0/docs/internals/crud.md +283 -0
- sqlmodel_ext-0.3.0/docs/internals/index.md +78 -0
- sqlmodel_ext-0.3.0/docs/internals/metaclass.md +315 -0
- sqlmodel_ext-0.3.0/docs/internals/optimistic-lock.md +114 -0
- sqlmodel_ext-0.3.0/docs/internals/polymorphic.md +204 -0
- sqlmodel_ext-0.3.0/docs/internals/prerequisites.md +154 -0
- sqlmodel_ext-0.3.0/docs/internals/relation-load-checker.md +108 -0
- sqlmodel_ext-0.3.0/docs/internals/relation-preload.md +191 -0
- sqlmodel_ext-0.3.0/package.json +13 -0
- sqlmodel_ext-0.3.0/pnpm-lock.yaml +2390 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/pyproject.toml +3 -2
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/__init__.py +31 -2
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/_compat.py +18 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/_sa_type.py +3 -2
- sqlmodel_ext-0.3.0/src/sqlmodel_ext/_utils.py +5 -0
- sqlmodel_ext-0.3.0/src/sqlmodel_ext/base.py +1032 -0
- sqlmodel_ext-0.3.0/src/sqlmodel_ext/field_types/__init__.py +172 -0
- sqlmodel_ext-0.3.0/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +119 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/__init__.py +8 -0
- sqlmodel_ext-0.3.0/src/sqlmodel_ext/mixins/cached_table.py +2074 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/polymorphic.py +222 -35
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/relation_preload.py +85 -35
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/table.py +432 -106
- sqlmodel_ext-0.3.0/src/sqlmodel_ext/relation_load_checker.py +3645 -0
- sqlmodel_ext-0.2.0/src/sqlmodel_ext/_utils.py +0 -5
- sqlmodel_ext-0.2.0/src/sqlmodel_ext/base.py +0 -501
- sqlmodel_ext-0.2.0/src/sqlmodel_ext/field_types/__init__.py +0 -114
- sqlmodel_ext-0.2.0/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -103
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/LICENSE +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/_exceptions.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/field_types/url.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.2.0 → sqlmodel_ext-0.3.0}/src/sqlmodel_ext/pagination.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
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
|
|
7
7
|
Project-URL: Issues, https://github.com/Foxerine/sqlmodel-ext/issues
|
|
8
|
-
Author:
|
|
8
|
+
Author: Foxerine
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: async,crud,orm,polymorphic,pydantic,sqlalchemy,sqlmodel
|
|
@@ -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'
|
|
@@ -48,6 +51,8 @@ Description-Content-Type: text/markdown
|
|
|
48
51
|
|
|
49
52
|
**English** | [中文](README_zh.md)
|
|
50
53
|
|
|
54
|
+
> **Warning**: This project is under active development. APIs may change without notice between releases. No stability or backward-compatibility guarantees are provided at this stage. Use at your own risk.
|
|
55
|
+
|
|
51
56
|
Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
|
|
52
57
|
|
|
53
58
|
**sqlmodel-ext** eliminates the boilerplate of building async database applications with [SQLModel](https://sqlmodel.tiangolo.com/). Define your models, inherit a mixin, and get a full async CRUD API -- pagination, relationship loading, polymorphic queries, and optimistic locking included.
|
|
@@ -56,8 +61,9 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
|
|
|
56
61
|
|
|
57
62
|
| Feature | Description |
|
|
58
63
|
|---------|-------------|
|
|
59
|
-
| **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 |
|
|
60
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 |
|
|
61
67
|
| **PolymorphicBaseMixin** | Simplified Joined Table Inheritance (JTI) and Single Table Inheritance (STI) |
|
|
62
68
|
| **AutoPolymorphicIdentityMixin** | Auto-generated `polymorphic_identity` from class names |
|
|
63
69
|
| **OptimisticLockMixin** | Version-based optimistic locking with automatic retry |
|
|
@@ -85,6 +91,12 @@ With PostgreSQL ARRAY and JSONB types (requires `orjson`):
|
|
|
85
91
|
pip install sqlmodel-ext[postgresql]
|
|
86
92
|
```
|
|
87
93
|
|
|
94
|
+
With Redis caching support (enables `CachedTableBaseMixin`):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install sqlmodel-ext[cache]
|
|
98
|
+
```
|
|
99
|
+
|
|
88
100
|
With pgvector + NumPy vector support (includes `[postgresql]`):
|
|
89
101
|
|
|
90
102
|
```bash
|
|
@@ -1249,6 +1261,130 @@ These mixins define the fields as **required** (non-optional), because in API re
|
|
|
1249
1261
|
|
|
1250
1262
|
---
|
|
1251
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
|
+
|
|
1252
1388
|
## Architecture
|
|
1253
1389
|
|
|
1254
1390
|
```
|
|
@@ -1262,7 +1398,8 @@ sqlmodel_ext/
|
|
|
1262
1398
|
pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
|
|
1263
1399
|
mixins/
|
|
1264
1400
|
__init__.py # Mixin re-exports
|
|
1265
|
-
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)
|
|
1266
1403
|
polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
|
|
1267
1404
|
optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
|
|
1268
1405
|
relation_preload.py # RelationPreloadMixin, @requires_relations
|
|
@@ -1290,7 +1427,8 @@ sqlmodel_ext/
|
|
|
1290
1427
|
- **pydantic** >= 2.0
|
|
1291
1428
|
- **sqlalchemy** >= 2.0
|
|
1292
1429
|
- (optional) **fastapi** >= 0.100.0
|
|
1293
|
-
- (optional) **
|
|
1430
|
+
- (optional) **redis** >= 5.0 -- for `CachedTableBaseMixin`
|
|
1431
|
+
- (optional) **orjson** >= 3.0 -- for `CachedTableBaseMixin` and `JSON100K` / `JSONList100K`
|
|
1294
1432
|
- (optional) **numpy** >= 1.24 -- for `NumpyVector`
|
|
1295
1433
|
- (optional) **pgvector** >= 0.3 -- for `NumpyVector`
|
|
1296
1434
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
**English** | [中文](README_zh.md)
|
|
8
8
|
|
|
9
|
+
> **Warning**: This project is under active development. APIs may change without notice between releases. No stability or backward-compatibility guarantees are provided at this stage. Use at your own risk.
|
|
10
|
+
|
|
9
11
|
Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
|
|
10
12
|
|
|
11
13
|
**sqlmodel-ext** eliminates the boilerplate of building async database applications with [SQLModel](https://sqlmodel.tiangolo.com/). Define your models, inherit a mixin, and get a full async CRUD API -- pagination, relationship loading, polymorphic queries, and optimistic locking included.
|
|
@@ -14,8 +16,9 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
|
|
|
14
16
|
|
|
15
17
|
| Feature | Description |
|
|
16
18
|
|---------|-------------|
|
|
17
|
-
| **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 |
|
|
18
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 |
|
|
19
22
|
| **PolymorphicBaseMixin** | Simplified Joined Table Inheritance (JTI) and Single Table Inheritance (STI) |
|
|
20
23
|
| **AutoPolymorphicIdentityMixin** | Auto-generated `polymorphic_identity` from class names |
|
|
21
24
|
| **OptimisticLockMixin** | Version-based optimistic locking with automatic retry |
|
|
@@ -43,6 +46,12 @@ With PostgreSQL ARRAY and JSONB types (requires `orjson`):
|
|
|
43
46
|
pip install sqlmodel-ext[postgresql]
|
|
44
47
|
```
|
|
45
48
|
|
|
49
|
+
With Redis caching support (enables `CachedTableBaseMixin`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install sqlmodel-ext[cache]
|
|
53
|
+
```
|
|
54
|
+
|
|
46
55
|
With pgvector + NumPy vector support (includes `[postgresql]`):
|
|
47
56
|
|
|
48
57
|
```bash
|
|
@@ -1207,6 +1216,130 @@ These mixins define the fields as **required** (non-optional), because in API re
|
|
|
1207
1216
|
|
|
1208
1217
|
---
|
|
1209
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
|
+
|
|
1210
1343
|
## Architecture
|
|
1211
1344
|
|
|
1212
1345
|
```
|
|
@@ -1220,7 +1353,8 @@ sqlmodel_ext/
|
|
|
1220
1353
|
pagination.py # ListResponse, TimeFilterRequest, PaginationRequest, TableViewRequest
|
|
1221
1354
|
mixins/
|
|
1222
1355
|
__init__.py # Mixin re-exports
|
|
1223
|
-
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)
|
|
1224
1358
|
polymorphic.py # PolymorphicBaseMixin, AutoPolymorphicIdentityMixin, create_subclass_id_mixin
|
|
1225
1359
|
optimistic_lock.py # OptimisticLockMixin, OptimisticLockError
|
|
1226
1360
|
relation_preload.py # RelationPreloadMixin, @requires_relations
|
|
@@ -1248,7 +1382,8 @@ sqlmodel_ext/
|
|
|
1248
1382
|
- **pydantic** >= 2.0
|
|
1249
1383
|
- **sqlalchemy** >= 2.0
|
|
1250
1384
|
- (optional) **fastapi** >= 0.100.0
|
|
1251
|
-
- (optional) **
|
|
1385
|
+
- (optional) **redis** >= 5.0 -- for `CachedTableBaseMixin`
|
|
1386
|
+
- (optional) **orjson** >= 3.0 -- for `CachedTableBaseMixin` and `JSON100K` / `JSONList100K`
|
|
1252
1387
|
- (optional) **numpy** >= 1.24 -- for `NumpyVector`
|
|
1253
1388
|
- (optional) **pgvector** >= 0.3 -- for `NumpyVector`
|
|
1254
1389
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
[English](README.md) | **中文**
|
|
8
8
|
|
|
9
|
+
> **警告**:本项目正在积极开发中。API 可能在版本之间发生不兼容变更,且不提供任何稳定性或向后兼容性保证。请自行承担使用风险。
|
|
10
|
+
|
|
9
11
|
SQLModel 增强基础设施:智能元类、异步 CRUD Mixin、多态继承、乐观锁、关系预加载、可复用字段类型。
|
|
10
12
|
|
|
11
13
|
**sqlmodel-ext** 消除了使用 [SQLModel](https://sqlmodel.tiangolo.com/) 构建异步数据库应用时的样板代码。定义模型、继承一个 Mixin,即可获得完整的异步 CRUD API -- 分页、关系加载、多态查询、乐观锁一应俱全。
|
|
@@ -14,8 +16,9 @@ SQLModel 增强基础设施:智能元类、异步 CRUD Mixin、多态继承、
|
|
|
14
16
|
|
|
15
17
|
| 特性 | 说明 |
|
|
16
18
|
|------|------|
|
|
17
|
-
| **SQLModelBase** |
|
|
19
|
+
| **SQLModelBase** | 智能元类,自动 `table=True`、合并 `mapper_args`、`all_fields_optional` UpdateRequest DTO、属性 docstring 继承、兼容 Python 3.14 (PEP 649) |
|
|
18
20
|
| **TableBaseMixin / UUIDTableBaseMixin** | 完整异步 CRUD:`add()`、`save()`、`update()`、`delete()`、`get()`、`count()`、`get_with_count()`、`get_exist_one()` |
|
|
21
|
+
| **CachedTableBaseMixin** | 双层 Redis 缓存(ID + 查询),版本号失效、STI 级联感知、可选 metrics 回调 |
|
|
19
22
|
| **PolymorphicBaseMixin** | 简化联表继承 (JTI) 和单表继承 (STI) 配置 |
|
|
20
23
|
| **AutoPolymorphicIdentityMixin** | 根据类名自动生成 `polymorphic_identity` |
|
|
21
24
|
| **OptimisticLockMixin** | 基于版本号的乐观锁,支持自动重试 |
|
|
@@ -43,6 +46,12 @@ pip install sqlmodel-ext[fastapi]
|
|
|
43
46
|
pip install sqlmodel-ext[postgresql]
|
|
44
47
|
```
|
|
45
48
|
|
|
49
|
+
使用 Redis 缓存(启用 `CachedTableBaseMixin`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install sqlmodel-ext[cache]
|
|
53
|
+
```
|
|
54
|
+
|
|
46
55
|
使用 pgvector + NumPy 向量支持(包含 `[postgresql]`):
|
|
47
56
|
|
|
48
57
|
```bash
|
|
@@ -1207,6 +1216,89 @@ class UserResponse(UserBase, UUIDIdDatetimeInfoMixin):
|
|
|
1207
1216
|
|
|
1208
1217
|
---
|
|
1209
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
|
+
|
|
1210
1302
|
## 架构
|
|
1211
1303
|
|
|
1212
1304
|
```
|
|
@@ -1220,7 +1312,8 @@ sqlmodel_ext/
|
|
|
1220
1312
|
pagination.py # ListResponse、TimeFilterRequest、PaginationRequest、TableViewRequest
|
|
1221
1313
|
mixins/
|
|
1222
1314
|
__init__.py # Mixin 重导出
|
|
1223
|
-
table.py # TableBaseMixin、UUIDTableBaseMixin(异步 CRUD)
|
|
1315
|
+
table.py # TableBaseMixin、UUIDTableBaseMixin、safe_reset(异步 CRUD)
|
|
1316
|
+
cached_table.py # CachedTableBaseMixin(双层 Redis 缓存 + 版本号失效)
|
|
1224
1317
|
polymorphic.py # PolymorphicBaseMixin、AutoPolymorphicIdentityMixin、create_subclass_id_mixin
|
|
1225
1318
|
optimistic_lock.py # OptimisticLockMixin、OptimisticLockError
|
|
1226
1319
|
relation_preload.py # RelationPreloadMixin、@requires_relations
|
|
@@ -1248,7 +1341,8 @@ sqlmodel_ext/
|
|
|
1248
1341
|
- **pydantic** >= 2.0
|
|
1249
1342
|
- **sqlalchemy** >= 2.0
|
|
1250
1343
|
- (可选)**fastapi** >= 0.100.0
|
|
1251
|
-
- (可选)**
|
|
1344
|
+
- (可选)**redis** >= 5.0 -- 用于 `CachedTableBaseMixin`
|
|
1345
|
+
- (可选)**orjson** >= 3.0 -- 用于 `CachedTableBaseMixin` 和 `JSON100K` / `JSONList100K`
|
|
1252
1346
|
- (可选)**numpy** >= 1.24 -- 用于 `NumpyVector`
|
|
1253
1347
|
- (可选)**pgvector** >= 0.3 -- 用于 `NumpyVector`
|
|
1254
1348
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { withMermaid } from 'vitepress-plugin-mermaid'
|
|
2
|
+
import { zh } from './zh.mts'
|
|
3
|
+
import { en } from './en.mts'
|
|
4
|
+
|
|
5
|
+
export default withMermaid({
|
|
6
|
+
title: 'sqlmodel-ext',
|
|
7
|
+
|
|
8
|
+
locales: {
|
|
9
|
+
root: {
|
|
10
|
+
label: '简体中文',
|
|
11
|
+
...zh,
|
|
12
|
+
},
|
|
13
|
+
en: {
|
|
14
|
+
label: 'English',
|
|
15
|
+
...en,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
themeConfig: {
|
|
20
|
+
socialLinks: [
|
|
21
|
+
{ icon: 'github', link: 'https://github.com/Foxerine/sqlmodel-ext' },
|
|
22
|
+
],
|
|
23
|
+
search: {
|
|
24
|
+
provider: 'local',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DefaultTheme } from 'vitepress'
|
|
2
|
+
|
|
3
|
+
export const en = {
|
|
4
|
+
lang: 'en',
|
|
5
|
+
description: 'SQLModel Enhancement Library: Usage Guide & Implementation Internals',
|
|
6
|
+
|
|
7
|
+
themeConfig: {
|
|
8
|
+
nav: [
|
|
9
|
+
{ text: 'Home', link: '/en/' },
|
|
10
|
+
{ text: 'Guide', link: '/en/guide/' },
|
|
11
|
+
{ text: 'Internals', link: '/en/internals/' },
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
sidebar: {
|
|
15
|
+
'/en/guide/': [
|
|
16
|
+
{
|
|
17
|
+
text: 'Guide',
|
|
18
|
+
items: [
|
|
19
|
+
{ text: 'Quick Start', link: '/en/guide/' },
|
|
20
|
+
{ text: 'Field Types', link: '/en/guide/field-types' },
|
|
21
|
+
{ text: 'CRUD Operations', link: '/en/guide/crud' },
|
|
22
|
+
{ text: 'Pagination & Lists', link: '/en/guide/pagination' },
|
|
23
|
+
{ text: 'Polymorphic Inheritance', link: '/en/guide/polymorphic' },
|
|
24
|
+
{ text: 'Optimistic Locking', link: '/en/guide/optimistic-lock' },
|
|
25
|
+
{ text: 'Relation Preloading', link: '/en/guide/relation-preload' },
|
|
26
|
+
{ text: 'Redis Caching', link: '/en/guide/cached-table' },
|
|
27
|
+
{ text: 'Static Analyzer', link: '/en/guide/relation-load-checker' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
'/en/internals/': [
|
|
32
|
+
{
|
|
33
|
+
text: 'Implementation Internals',
|
|
34
|
+
items: [
|
|
35
|
+
{ text: 'Architecture Overview', link: '/en/internals/' },
|
|
36
|
+
{ text: 'Prerequisites', link: '/en/internals/prerequisites' },
|
|
37
|
+
{ text: 'Metaclass & SQLModelBase', link: '/en/internals/metaclass' },
|
|
38
|
+
{ text: 'CRUD Implementation', link: '/en/internals/crud' },
|
|
39
|
+
{ text: 'Polymorphic Inheritance', link: '/en/internals/polymorphic' },
|
|
40
|
+
{ text: 'Optimistic Locking', link: '/en/internals/optimistic-lock' },
|
|
41
|
+
{ text: 'Relation Preloading', link: '/en/internals/relation-preload' },
|
|
42
|
+
{ text: 'Redis Caching', link: '/en/internals/cached-table' },
|
|
43
|
+
{ text: 'Static Analyzer', link: '/en/internals/relation-load-checker' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
outline: {
|
|
50
|
+
level: [2, 3],
|
|
51
|
+
label: 'On this page',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
docFooter: {
|
|
55
|
+
prev: 'Previous',
|
|
56
|
+
next: 'Next',
|
|
57
|
+
},
|
|
58
|
+
} satisfies DefaultTheme.Config,
|
|
59
|
+
}
|