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.
Files changed (83) hide show
  1. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/.gitignore +5 -0
  2. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/PKG-INFO +4 -2
  3. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/README.md +2 -0
  4. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/README_zh.md +2 -0
  5. sqlmodel_ext-0.2.1/docs/.vitepress/config.mts +27 -0
  6. sqlmodel_ext-0.2.1/docs/.vitepress/en.mts +59 -0
  7. sqlmodel_ext-0.2.1/docs/.vitepress/theme/index.ts +6 -0
  8. sqlmodel_ext-0.2.1/docs/.vitepress/theme/style/custom.css +4 -0
  9. sqlmodel_ext-0.2.1/docs/.vitepress/zh.mts +59 -0
  10. sqlmodel_ext-0.2.1/docs/en/guide/cached-table.md +156 -0
  11. sqlmodel_ext-0.2.1/docs/en/guide/crud.md +264 -0
  12. sqlmodel_ext-0.2.1/docs/en/guide/field-types.md +120 -0
  13. sqlmodel_ext-0.2.1/docs/en/guide/index.md +175 -0
  14. sqlmodel_ext-0.2.1/docs/en/guide/optimistic-lock.md +102 -0
  15. sqlmodel_ext-0.2.1/docs/en/guide/pagination.md +136 -0
  16. sqlmodel_ext-0.2.1/docs/en/guide/polymorphic.md +141 -0
  17. sqlmodel_ext-0.2.1/docs/en/guide/relation-load-checker.md +105 -0
  18. sqlmodel_ext-0.2.1/docs/en/guide/relation-preload.md +118 -0
  19. sqlmodel_ext-0.2.1/docs/en/index.md +30 -0
  20. sqlmodel_ext-0.2.1/docs/en/internals/cached-table.md +227 -0
  21. sqlmodel_ext-0.2.1/docs/en/internals/crud.md +283 -0
  22. sqlmodel_ext-0.2.1/docs/en/internals/index.md +78 -0
  23. sqlmodel_ext-0.2.1/docs/en/internals/metaclass.md +315 -0
  24. sqlmodel_ext-0.2.1/docs/en/internals/optimistic-lock.md +114 -0
  25. sqlmodel_ext-0.2.1/docs/en/internals/polymorphic.md +204 -0
  26. sqlmodel_ext-0.2.1/docs/en/internals/prerequisites.md +154 -0
  27. sqlmodel_ext-0.2.1/docs/en/internals/relation-load-checker.md +108 -0
  28. sqlmodel_ext-0.2.1/docs/en/internals/relation-preload.md +191 -0
  29. sqlmodel_ext-0.2.1/docs/guide/cached-table.md +155 -0
  30. sqlmodel_ext-0.2.1/docs/guide/crud.md +264 -0
  31. sqlmodel_ext-0.2.1/docs/guide/field-types.md +120 -0
  32. sqlmodel_ext-0.2.1/docs/guide/index.md +175 -0
  33. sqlmodel_ext-0.2.1/docs/guide/optimistic-lock.md +102 -0
  34. sqlmodel_ext-0.2.1/docs/guide/pagination.md +136 -0
  35. sqlmodel_ext-0.2.1/docs/guide/polymorphic.md +141 -0
  36. sqlmodel_ext-0.2.1/docs/guide/relation-load-checker.md +105 -0
  37. sqlmodel_ext-0.2.1/docs/guide/relation-preload.md +118 -0
  38. sqlmodel_ext-0.2.1/docs/index.md +30 -0
  39. sqlmodel_ext-0.2.1/docs/internals/cached-table.md +227 -0
  40. sqlmodel_ext-0.2.1/docs/internals/crud.md +283 -0
  41. sqlmodel_ext-0.2.1/docs/internals/index.md +78 -0
  42. sqlmodel_ext-0.2.1/docs/internals/metaclass.md +315 -0
  43. sqlmodel_ext-0.2.1/docs/internals/optimistic-lock.md +114 -0
  44. sqlmodel_ext-0.2.1/docs/internals/polymorphic.md +204 -0
  45. sqlmodel_ext-0.2.1/docs/internals/prerequisites.md +154 -0
  46. sqlmodel_ext-0.2.1/docs/internals/relation-load-checker.md +108 -0
  47. sqlmodel_ext-0.2.1/docs/internals/relation-preload.md +191 -0
  48. sqlmodel_ext-0.2.1/package.json +13 -0
  49. sqlmodel_ext-0.2.1/pnpm-lock.yaml +2390 -0
  50. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/pyproject.toml +2 -2
  51. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/__init__.py +25 -2
  52. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/_sa_type.py +3 -2
  53. sqlmodel_ext-0.2.1/src/sqlmodel_ext/_utils.py +5 -0
  54. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/base.py +239 -69
  55. sqlmodel_ext-0.2.1/src/sqlmodel_ext/field_types/__init__.py +143 -0
  56. sqlmodel_ext-0.2.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +113 -0
  57. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/__init__.py +7 -0
  58. sqlmodel_ext-0.2.1/src/sqlmodel_ext/mixins/cached_table.py +1707 -0
  59. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/polymorphic.py +205 -35
  60. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/relation_preload.py +85 -35
  61. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/table.py +416 -106
  62. sqlmodel_ext-0.2.1/src/sqlmodel_ext/relation_load_checker.py +2751 -0
  63. sqlmodel_ext-0.1.1/src/sqlmodel_ext/_utils.py +0 -5
  64. sqlmodel_ext-0.1.1/src/sqlmodel_ext/field_types/__init__.py +0 -114
  65. sqlmodel_ext-0.1.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -103
  66. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/LICENSE +0 -0
  67. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/_compat.py +0 -0
  68. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/_exceptions.py +0 -0
  69. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
  70. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
  71. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
  72. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
  73. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
  74. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
  75. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
  76. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
  77. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
  78. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
  79. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
  80. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/field_types/url.py +0 -0
  81. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/info_response.py +0 -0
  82. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
  83. {sqlmodel_ext-0.1.1 → sqlmodel_ext-0.2.1}/src/sqlmodel_ext/pagination.py +0 -0
@@ -14,6 +14,7 @@ build/
14
14
  .venv/
15
15
  venv/
16
16
  ENV/
17
+ node_modules/
17
18
 
18
19
  # IDE
19
20
  .idea/
@@ -26,6 +27,10 @@ ENV/
26
27
  .coverage
27
28
  htmlcov/
28
29
 
30
+ # VitePress
31
+ docs/.vitepress/cache/
32
+ docs/.vitepress/dist/
33
+
29
34
  # OS
30
35
  .DS_Store
31
36
  Thumbs.db
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmodel-ext
3
- Version: 0.1.1
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: Foxline AI
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,6 @@
1
+ // .vitepress/theme/index.ts
2
+ import DefaultTheme from 'vitepress/theme'
3
+ import '@fontsource/maple-mono';
4
+ import './style/custom.css'
5
+
6
+ export default DefaultTheme
@@ -0,0 +1,4 @@
1
+ /* .vitepress/theme/custom.css */
2
+ :root {
3
+ --vp-font-family-mono: Maple Mono, JetBrains Mono, Menlo, Monaco, Consolas, "Courier New", monospace !important;
4
+ }
@@ -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 |