sqlmodel-ext 0.1.1__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/.gitignore +5 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/PKG-INFO +4 -2
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/README.md +2 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/README_zh.md +2 -0
- sqlmodel_ext-0.2.1/docs/.vitepress/config.mts +27 -0
- sqlmodel_ext-0.2.1/docs/.vitepress/en.mts +59 -0
- sqlmodel_ext-0.2.1/docs/.vitepress/theme/index.ts +6 -0
- sqlmodel_ext-0.2.1/docs/.vitepress/theme/style/custom.css +4 -0
- sqlmodel_ext-0.2.1/docs/.vitepress/zh.mts +59 -0
- sqlmodel_ext-0.2.1/docs/en/guide/cached-table.md +156 -0
- sqlmodel_ext-0.2.1/docs/en/guide/crud.md +264 -0
- sqlmodel_ext-0.2.1/docs/en/guide/field-types.md +120 -0
- sqlmodel_ext-0.2.1/docs/en/guide/index.md +175 -0
- sqlmodel_ext-0.2.1/docs/en/guide/optimistic-lock.md +102 -0
- sqlmodel_ext-0.2.1/docs/en/guide/pagination.md +136 -0
- sqlmodel_ext-0.2.1/docs/en/guide/polymorphic.md +141 -0
- sqlmodel_ext-0.2.1/docs/en/guide/relation-load-checker.md +105 -0
- sqlmodel_ext-0.2.1/docs/en/guide/relation-preload.md +118 -0
- sqlmodel_ext-0.2.1/docs/en/index.md +30 -0
- sqlmodel_ext-0.2.1/docs/en/internals/cached-table.md +227 -0
- sqlmodel_ext-0.2.1/docs/en/internals/crud.md +283 -0
- sqlmodel_ext-0.2.1/docs/en/internals/index.md +78 -0
- sqlmodel_ext-0.2.1/docs/en/internals/metaclass.md +315 -0
- sqlmodel_ext-0.2.1/docs/en/internals/optimistic-lock.md +114 -0
- sqlmodel_ext-0.2.1/docs/en/internals/polymorphic.md +204 -0
- sqlmodel_ext-0.2.1/docs/en/internals/prerequisites.md +154 -0
- sqlmodel_ext-0.2.1/docs/en/internals/relation-load-checker.md +108 -0
- sqlmodel_ext-0.2.1/docs/en/internals/relation-preload.md +191 -0
- sqlmodel_ext-0.2.1/docs/guide/cached-table.md +155 -0
- sqlmodel_ext-0.2.1/docs/guide/crud.md +264 -0
- sqlmodel_ext-0.2.1/docs/guide/field-types.md +120 -0
- sqlmodel_ext-0.2.1/docs/guide/index.md +175 -0
- sqlmodel_ext-0.2.1/docs/guide/optimistic-lock.md +102 -0
- sqlmodel_ext-0.2.1/docs/guide/pagination.md +136 -0
- sqlmodel_ext-0.2.1/docs/guide/polymorphic.md +141 -0
- sqlmodel_ext-0.2.1/docs/guide/relation-load-checker.md +105 -0
- sqlmodel_ext-0.2.1/docs/guide/relation-preload.md +118 -0
- sqlmodel_ext-0.2.1/docs/index.md +30 -0
- sqlmodel_ext-0.2.1/docs/internals/cached-table.md +227 -0
- sqlmodel_ext-0.2.1/docs/internals/crud.md +283 -0
- sqlmodel_ext-0.2.1/docs/internals/index.md +78 -0
- sqlmodel_ext-0.2.1/docs/internals/metaclass.md +315 -0
- sqlmodel_ext-0.2.1/docs/internals/optimistic-lock.md +114 -0
- sqlmodel_ext-0.2.1/docs/internals/polymorphic.md +204 -0
- sqlmodel_ext-0.2.1/docs/internals/prerequisites.md +154 -0
- sqlmodel_ext-0.2.1/docs/internals/relation-load-checker.md +108 -0
- sqlmodel_ext-0.2.1/docs/internals/relation-preload.md +191 -0
- sqlmodel_ext-0.2.1/package.json +13 -0
- sqlmodel_ext-0.2.1/pnpm-lock.yaml +2390 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/pyproject.toml +2 -2
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/__init__.py +25 -2
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/_sa_type.py +3 -2
- sqlmodel_ext-0.2.1/src/sqlmodel_ext/_utils.py +5 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/base.py +239 -69
- sqlmodel_ext-0.2.1/src/sqlmodel_ext/field_types/__init__.py +143 -0
- sqlmodel_ext-0.2.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +113 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/__init__.py +7 -0
- sqlmodel_ext-0.2.1/src/sqlmodel_ext/mixins/cached_table.py +1707 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/polymorphic.py +205 -35
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/relation_preload.py +85 -35
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/table.py +416 -106
- sqlmodel_ext-0.2.1/src/sqlmodel_ext/relation_load_checker.py +2751 -0
- sqlmodel_ext-0.1.1/src/sqlmodel_ext/_utils.py +0 -5
- sqlmodel_ext-0.1.1/src/sqlmodel_ext/field_types/__init__.py +0 -114
- sqlmodel_ext-0.1.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -103
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/LICENSE +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/_compat.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/_exceptions.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/url.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/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.2.1
|
|
4
4
|
Summary: Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Foxerine/sqlmodel-ext
|
|
6
6
|
Project-URL: Repository, https://github.com/Foxerine/sqlmodel-ext
|
|
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
|
|
@@ -48,6 +48,8 @@ Description-Content-Type: text/markdown
|
|
|
48
48
|
|
|
49
49
|
**English** | [中文](README_zh.md)
|
|
50
50
|
|
|
51
|
+
> **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.
|
|
52
|
+
|
|
51
53
|
Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
|
|
52
54
|
|
|
53
55
|
**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.
|
|
@@ -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.
|
|
@@ -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 -- 分页、关系加载、多态查询、乐观锁一应俱全。
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DefaultTheme } from 'vitepress'
|
|
2
|
+
|
|
3
|
+
export const zh = {
|
|
4
|
+
lang: 'zh-CN',
|
|
5
|
+
description: 'SQLModel 增强库:使用指南与实现原理',
|
|
6
|
+
|
|
7
|
+
themeConfig: {
|
|
8
|
+
nav: [
|
|
9
|
+
{ text: '首页', link: '/' },
|
|
10
|
+
{ text: '指南', link: '/guide/' },
|
|
11
|
+
{ text: '原理', link: '/internals/' },
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
sidebar: {
|
|
15
|
+
'/guide/': [
|
|
16
|
+
{
|
|
17
|
+
text: '指南',
|
|
18
|
+
items: [
|
|
19
|
+
{ text: '快速开始', link: '/guide/' },
|
|
20
|
+
{ text: '字段类型', link: '/guide/field-types' },
|
|
21
|
+
{ text: 'CRUD 操作', link: '/guide/crud' },
|
|
22
|
+
{ text: '分页与列表', link: '/guide/pagination' },
|
|
23
|
+
{ text: '多态继承', link: '/guide/polymorphic' },
|
|
24
|
+
{ text: '乐观锁', link: '/guide/optimistic-lock' },
|
|
25
|
+
{ text: '关系预加载', link: '/guide/relation-preload' },
|
|
26
|
+
{ text: 'Redis 缓存', link: '/guide/cached-table' },
|
|
27
|
+
{ text: '静态分析器', link: '/guide/relation-load-checker' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
'/internals/': [
|
|
32
|
+
{
|
|
33
|
+
text: '实现原理',
|
|
34
|
+
items: [
|
|
35
|
+
{ text: '架构总览', link: '/internals/' },
|
|
36
|
+
{ text: '前置知识', link: '/internals/prerequisites' },
|
|
37
|
+
{ text: '元类与 SQLModelBase', link: '/internals/metaclass' },
|
|
38
|
+
{ text: 'CRUD 实现', link: '/internals/crud' },
|
|
39
|
+
{ text: '多态继承机制', link: '/internals/polymorphic' },
|
|
40
|
+
{ text: '乐观锁机制', link: '/internals/optimistic-lock' },
|
|
41
|
+
{ text: '关系预加载机制', link: '/internals/relation-preload' },
|
|
42
|
+
{ text: 'Redis 缓存机制', link: '/internals/cached-table' },
|
|
43
|
+
{ text: '静态分析器原理', link: '/internals/relation-load-checker' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
outline: {
|
|
50
|
+
level: [2, 3],
|
|
51
|
+
label: '目录',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
docFooter: {
|
|
55
|
+
prev: '上一页',
|
|
56
|
+
next: '下一页',
|
|
57
|
+
},
|
|
58
|
+
} satisfies DefaultTheme.Config,
|
|
59
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Redis Caching
|
|
2
|
+
|
|
3
|
+
`CachedTableBaseMixin` adds a Redis cache layer to the model's `get()` queries, with automatic invalidation on CRUD operations.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from sqlmodel_ext import CachedTableBaseMixin, SQLModelBase, UUIDTableBaseMixin
|
|
9
|
+
|
|
10
|
+
class CharacterBase(SQLModelBase):
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
class Character(CachedTableBaseMixin, CharacterBase, UUIDTableBaseMixin, table=True, cache_ttl=1800):
|
|
14
|
+
pass # Cache for 30 minutes
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
::: warning Warning
|
|
18
|
+
`CachedTableBaseMixin` must be placed **before** `UUIDTableBaseMixin` in the inheritance list.
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
### Configure Redis
|
|
22
|
+
|
|
23
|
+
Configure once at startup:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import redis.asyncio as redis
|
|
27
|
+
|
|
28
|
+
redis_client = redis.from_url("redis://localhost:6379")
|
|
29
|
+
CachedTableBaseMixin.configure_redis(redis_client)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Startup Check (Recommended)
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
CachedTableBaseMixin.check_cache_config()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Verifies configuration correctness for all cached models: Redis client is set, `__cache_ttl__` is a positive integer, no forbidden direct calls.
|
|
39
|
+
|
|
40
|
+
## Cache Behavior
|
|
41
|
+
|
|
42
|
+
### Automatic Caching
|
|
43
|
+
|
|
44
|
+
`get()` query results are automatically cached to Redis; subsequent identical queries read directly from cache:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# First time: queries database + writes cache
|
|
48
|
+
char = await Character.get(session, Character.id == char_id) # [!code highlight]
|
|
49
|
+
|
|
50
|
+
# Second time: reads from cache directly, zero SQL
|
|
51
|
+
char = await Character.get(session, Character.id == char_id) # [!code highlight]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Automatic Invalidation
|
|
55
|
+
|
|
56
|
+
CRUD operations automatically clean related caches:
|
|
57
|
+
|
|
58
|
+
| Operation | Invalidation Strategy |
|
|
59
|
+
|-----------|----------------------|
|
|
60
|
+
| `save()` / `update()` | Delete the record's ID cache + all query caches for the model |
|
|
61
|
+
| `delete(instances)` | Delete each instance's ID cache + all query caches |
|
|
62
|
+
| `delete(condition)` | Delete all caches for the model (ID + query) |
|
|
63
|
+
| `add()` | Delete all query caches (new objects have no old cache) |
|
|
64
|
+
|
|
65
|
+
### Skipping Cache
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# Explicitly skip cache
|
|
69
|
+
char = await Character.get(session, Character.id == char_id, no_cache=True) # [!code highlight]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
::: details Details
|
|
73
|
+
Cache is automatically skipped in these scenarios:
|
|
74
|
+
- `with_for_update=True` (needs latest data)
|
|
75
|
+
- `populate_existing=True`
|
|
76
|
+
- `options` parameter is non-empty
|
|
77
|
+
- `join` parameter is non-empty
|
|
78
|
+
- Transaction has uncommitted pending invalidation data
|
|
79
|
+
:::
|
|
80
|
+
|
|
81
|
+
## Dual-Layer Cache Architecture
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
1. ID Cache (id:{ModelName}:{id_value})
|
|
85
|
+
→ Single-row ID equality queries
|
|
86
|
+
→ Row-level invalidation O(1)
|
|
87
|
+
|
|
88
|
+
2. Query Cache (query:{ModelName}:{hash})
|
|
89
|
+
→ Conditional/list queries
|
|
90
|
+
→ Model-level invalidation SCAN+DEL
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
ID queries (`Character.id == some_id`) use precise ID cache keys, requiring only one key deletion for invalidation. Other queries use parameter hashes as cache keys.
|
|
94
|
+
|
|
95
|
+
## Manual Invalidation
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# Invalidate specific ID
|
|
99
|
+
await Character.invalidate_by_id(char_id)
|
|
100
|
+
await Character.invalidate_by_id(id1, id2, id3) # Multiple
|
|
101
|
+
|
|
102
|
+
# Invalidate all caches for the model
|
|
103
|
+
await Character.invalidate_all()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Polymorphic Inheritance Support
|
|
107
|
+
|
|
108
|
+
STI subclass caches automatically cascade to ancestor classes: when subclass data changes, ancestor class query caches are also cleaned.
|
|
109
|
+
|
|
110
|
+
## TTL Configuration
|
|
111
|
+
|
|
112
|
+
::: code-group
|
|
113
|
+
|
|
114
|
+
```python [Keyword argument (recommended)]
|
|
115
|
+
class Character(CachedTableBaseMixin, CharacterBase, UUIDTableBaseMixin,
|
|
116
|
+
table=True, cache_ttl=1800): # [!code highlight]
|
|
117
|
+
pass # 30 minutes
|
|
118
|
+
|
|
119
|
+
class Config(CachedTableBaseMixin, ConfigBase, UUIDTableBaseMixin,
|
|
120
|
+
table=True, cache_ttl=86400): # [!code highlight]
|
|
121
|
+
pass # 24 hours
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```python [Class variable]
|
|
125
|
+
class Character(CachedTableBaseMixin, CharacterBase, UUIDTableBaseMixin, table=True):
|
|
126
|
+
__cache_ttl__: ClassVar[int] = 1800 # [!code highlight]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
:::
|
|
130
|
+
|
|
131
|
+
Default TTL is 3600 seconds (1 hour).
|
|
132
|
+
|
|
133
|
+
## Graceful Degradation
|
|
134
|
+
|
|
135
|
+
::: tip Tip
|
|
136
|
+
When Redis is unavailable, the system automatically degrades to database queries without affecting business logic:
|
|
137
|
+
- Read failure → log + query database
|
|
138
|
+
- Write failure → log + continue (non-critical path)
|
|
139
|
+
- Delete failure → log + TTL provides eventual consistency
|
|
140
|
+
:::
|
|
141
|
+
|
|
142
|
+
## Raw SQL Scenarios
|
|
143
|
+
|
|
144
|
+
::: warning Warning
|
|
145
|
+
When bypassing the ORM, you need to manage cache manually:
|
|
146
|
+
:::
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from sqlmodel_ext.mixins.cached_table import CachedTableBaseMixin
|
|
150
|
+
|
|
151
|
+
# Register before raw SQL operations
|
|
152
|
+
CachedTableBaseMixin._register_pending_invalidation(session, Character, char_id) # [!code warning]
|
|
153
|
+
|
|
154
|
+
# Commit and invalidate
|
|
155
|
+
await instance._commit_and_invalidate(session)
|
|
156
|
+
```
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# CRUD Operations
|
|
2
|
+
|
|
3
|
+
Inherit `TableBaseMixin` (auto-increment ID) or `UUIDTableBaseMixin` (UUID ID) to get a full set of async CRUD methods.
|
|
4
|
+
|
|
5
|
+
## Utility Functions
|
|
6
|
+
|
|
7
|
+
### `rel()` — Type-safe Relationship Reference
|
|
8
|
+
|
|
9
|
+
basedpyright infers SQLModel's Relationship fields as the annotated type (e.g., `LLM`) rather than `QueryableAttribute`. `rel()` fixes this type issue:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from sqlmodel_ext import rel
|
|
13
|
+
|
|
14
|
+
# Without rel: basedpyright reports type error
|
|
15
|
+
user = await User.get(session, load=User.profile) # [!code --]
|
|
16
|
+
|
|
17
|
+
# With rel: correct type
|
|
18
|
+
user = await User.get(session, load=rel(User.profile)) # [!code ++]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### `cond()` — Type-safe Condition Composition
|
|
22
|
+
|
|
23
|
+
Similar issue: basedpyright infers `Model.field == value` as `bool`, causing `&` / `|` operator errors.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from sqlmodel_ext import cond
|
|
27
|
+
|
|
28
|
+
scope = cond(UserFile.user_id == current_user.id) # [!code highlight]
|
|
29
|
+
condition = scope & cond(UserFile.status == FileStatusEnum.uploaded) # [!code highlight]
|
|
30
|
+
users = await UserFile.get(session, condition, fetch_mode="all")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `sanitize_integrity_error()` — User-friendly Error Messages
|
|
34
|
+
|
|
35
|
+
Extracts user-safe error messages from `IntegrityError`. Specifically supports PostgreSQL trigger SQLSTATE 23514 errors:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from sqlalchemy.exc import IntegrityError
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
await order.save(session)
|
|
42
|
+
except IntegrityError as e:
|
|
43
|
+
msg = Order.sanitize_integrity_error(e, "Operation failed")
|
|
44
|
+
# PostgreSQL trigger messages are extracted; other constraint errors return the default message
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Built-in Fields
|
|
48
|
+
|
|
49
|
+
Three fields are automatically included upon inheritance:
|
|
50
|
+
|
|
51
|
+
| Field | Type | Description |
|
|
52
|
+
|-------|------|-------------|
|
|
53
|
+
| `id` | `int` or `UUID` | Primary key, auto-generated |
|
|
54
|
+
| `created_at` | `datetime` | Creation time, auto-set |
|
|
55
|
+
| `updated_at` | `datetime` | Update time, auto-refreshed on every UPDATE |
|
|
56
|
+
|
|
57
|
+
## `save()` — Create or Update
|
|
58
|
+
|
|
59
|
+
```python{3}
|
|
60
|
+
user = User(name="Alice", email="alice@example.com")
|
|
61
|
+
await user.save(session) # Don't do this — causes expiration // [!code --]
|
|
62
|
+
user = await user.save(session) # Correct usage // [!code ++]
|
|
63
|
+
print(user.id) # Has a value now
|
|
64
|
+
|
|
65
|
+
# Modify then save again = UPDATE
|
|
66
|
+
user.name = "Bob"
|
|
67
|
+
user = await user.save(session)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Parameters
|
|
71
|
+
|
|
72
|
+
| Parameter | Default | Description |
|
|
73
|
+
|-----------|---------|-------------|
|
|
74
|
+
| `commit` | `True` | When `False`, only flushes without committing — useful for batch operations |
|
|
75
|
+
| `load` | `None` | Preload specified relations after saving |
|
|
76
|
+
| `optimistic_retry_count` | `0` | Number of retries on optimistic lock conflicts |
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Batch operations: only flush earlier ones, commit with the last
|
|
80
|
+
await user1.save(session, commit=False)
|
|
81
|
+
await user2.save(session, commit=False)
|
|
82
|
+
user3 = await user3.save(session) # Commits all at once // [!code highlight]
|
|
83
|
+
|
|
84
|
+
# Preload relations after save
|
|
85
|
+
user = await user.save(session, load=User.profile)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## `add()` — Bulk Insert
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
users = [User(name="Alice"), User(name="Bob")]
|
|
92
|
+
users = await User.add(session, users)
|
|
93
|
+
|
|
94
|
+
# Single item works too
|
|
95
|
+
user = await User.add(session, User(name="Eve"))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## `update()` — Partial Update (PATCH Semantics)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
class UserUpdate(SQLModelBase):
|
|
102
|
+
name: str | None = None
|
|
103
|
+
email: str | None = None
|
|
104
|
+
|
|
105
|
+
data = UserUpdate(name="Bob") # Only name is set // [!code highlight]
|
|
106
|
+
user = await user.update(session, data)
|
|
107
|
+
# Only name is updated, email remains unchanged (exclude_unset=True)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Parameters
|
|
111
|
+
|
|
112
|
+
| Parameter | Default | Description |
|
|
113
|
+
|-----------|---------|-------------|
|
|
114
|
+
| `exclude_unset` | `True` | Only update explicitly set fields (PATCH semantics) |
|
|
115
|
+
| `exclude` | `None` | Exclude certain fields from being updated |
|
|
116
|
+
| `extra_data` | `None` | Append additional fields beyond the update model |
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# Append extra fields
|
|
120
|
+
user = await user.update(session, data, extra_data={"updated_by": admin.id})
|
|
121
|
+
|
|
122
|
+
# Exclude sensitive fields
|
|
123
|
+
user = await user.update(session, data, exclude={"role", "is_admin"})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## `delete()` — Delete
|
|
127
|
+
|
|
128
|
+
Two modes, mutually exclusive:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Delete by instance
|
|
132
|
+
await User.delete(session, user)
|
|
133
|
+
await User.delete(session, [user1, user2])
|
|
134
|
+
|
|
135
|
+
# Bulk delete by condition (⚠️ deletes ALL matching records)
|
|
136
|
+
count = await User.delete(session, condition=User.is_active == False) # [!code warning]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Returns the number of deleted records.
|
|
140
|
+
|
|
141
|
+
## `get()` — Query
|
|
142
|
+
|
|
143
|
+
A versatile query method that covers all query scenarios through different parameter combinations.
|
|
144
|
+
|
|
145
|
+
### Basic Queries
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Get first match by condition (default fetch_mode="first")
|
|
149
|
+
user = await User.get(session, User.email == "alice@example.com")
|
|
150
|
+
|
|
151
|
+
# Get all
|
|
152
|
+
users = await User.get(session, fetch_mode="all") # [!code highlight]
|
|
153
|
+
|
|
154
|
+
# Get exactly one (errors on 0 or multiple results)
|
|
155
|
+
user = await User.get(session, User.id == some_id, fetch_mode="one") # [!code highlight]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `fetch_mode` Return Values
|
|
159
|
+
|
|
160
|
+
| `fetch_mode` | Return Type | On 0 results | On multiple results |
|
|
161
|
+
|---|---|---|---|
|
|
162
|
+
| `"first"` (default) | `T \| None` | `None` | Returns first |
|
|
163
|
+
| `"one"` | `T` | Raises exception | Raises exception |
|
|
164
|
+
| `"all"` | `list[T]` | Empty list | Returns all |
|
|
165
|
+
|
|
166
|
+
### Pagination & Sorting
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from sqlmodel_ext import TableViewRequest
|
|
170
|
+
|
|
171
|
+
tv = TableViewRequest(offset=0, limit=20, desc=True, order="created_at")
|
|
172
|
+
users = await User.get(session, fetch_mode="all", table_view=tv)
|
|
173
|
+
# → SELECT ... ORDER BY created_at DESC LIMIT 20 OFFSET 0
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Time Filtering
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
users = await User.get(
|
|
180
|
+
session,
|
|
181
|
+
fetch_mode="all",
|
|
182
|
+
created_after=datetime(2024, 1, 1),
|
|
183
|
+
created_before=datetime(2024, 12, 31),
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Relation Preloading
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
user = await User.get(
|
|
191
|
+
session,
|
|
192
|
+
User.id == user_id,
|
|
193
|
+
load=[User.profile, Profile.avatar], # [!code highlight]
|
|
194
|
+
)
|
|
195
|
+
# Automatically builds: selectinload(User.profile).selectinload(Profile.avatar)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Other Parameters
|
|
199
|
+
|
|
200
|
+
| Parameter | Purpose |
|
|
201
|
+
|-----------|---------|
|
|
202
|
+
| `join` | JOIN another table |
|
|
203
|
+
| `options` | Custom SQLAlchemy query options |
|
|
204
|
+
| `filter` | Additional WHERE conditions |
|
|
205
|
+
| `with_for_update` | SELECT ... FOR UPDATE (row lock) |
|
|
206
|
+
|
|
207
|
+
## `count()` — Count
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
total = await User.count(session)
|
|
211
|
+
active = await User.count(session, User.is_active == True)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## `get_with_count()` — Paginated List
|
|
215
|
+
|
|
216
|
+
Combination of `count()` + `get(fetch_mode="all")`, returns `ListResponse`:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
result = await User.get_with_count(session, table_view=table_view)
|
|
220
|
+
# result.count = 42
|
|
221
|
+
# result.items = [User(...), User(...), ...]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## `get_one()` — Guaranteed Existence Query
|
|
225
|
+
|
|
226
|
+
Similar to `get(fetch_mode="one")` but with a simpler interface — pass the ID directly:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
user = await User.get_one(session, user_id)
|
|
230
|
+
# Record not found → NoResultFound // [!code error]
|
|
231
|
+
# Multiple records → MultipleResultsFound // [!code error]
|
|
232
|
+
|
|
233
|
+
# Query with lock
|
|
234
|
+
user = await User.get_one(session, user_id, with_for_update=True)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
::: tip Tip
|
|
238
|
+
`get_one()` raises SQLAlchemy exceptions (`NoResultFound`), while `get_exist_one()` raises HTTP 404 (with FastAPI) or `RecordNotFoundError`.
|
|
239
|
+
:::
|
|
240
|
+
|
|
241
|
+
## `get_exist_one()` — Find or 404
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
user = await User.get_exist_one(session, user_id) # [!code highlight]
|
|
245
|
+
# Not found → HTTPException(404) (with FastAPI)
|
|
246
|
+
# Or RecordNotFoundError (without FastAPI)
|
|
247
|
+
|
|
248
|
+
# With relation preloading
|
|
249
|
+
user = await User.get_exist_one(session, user_id, load=User.profile)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Method Quick Reference
|
|
253
|
+
|
|
254
|
+
| Method | Type | SQL Equivalent | Return Value |
|
|
255
|
+
|--------|------|---------------|--------------|
|
|
256
|
+
| `add()` | Class method | INSERT | Instance or list |
|
|
257
|
+
| `save()` | Instance method | INSERT or UPDATE | Refreshed instance |
|
|
258
|
+
| `update()` | Instance method | UPDATE (partial) | Refreshed instance |
|
|
259
|
+
| `delete()` | Class method | DELETE | Delete count |
|
|
260
|
+
| `get()` | Class method | SELECT + WHERE + ... | Instance / list / None |
|
|
261
|
+
| `get_one()` | Class method | SELECT + guaranteed existence | Instance |
|
|
262
|
+
| `count()` | Class method | SELECT COUNT(*) | int |
|
|
263
|
+
| `get_with_count()` | Class method | COUNT + SELECT | ListResponse |
|
|
264
|
+
| `get_exist_one()` | Class method | SELECT + 404 | Instance |
|