ns-orm 0.0.0__tar.gz → 0.0.2__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.
- {ns_orm-0.0.0 → ns_orm-0.0.2}/MANIFEST.in +1 -1
- {ns_orm-0.0.0/src/ns_orm.egg-info → ns_orm-0.0.2}/PKG-INFO +60 -21
- ns_orm-0.0.2/README.md +108 -0
- ns_orm-0.0.2/doc/async.md +45 -0
- ns_orm-0.0.2/doc/database.md +126 -0
- ns_orm-0.0.2/doc/index.md +35 -0
- ns_orm-0.0.2/doc/migrations.md +90 -0
- ns_orm-0.0.2/doc/models.md +70 -0
- ns_orm-0.0.2/doc/queryset.md +69 -0
- ns_orm-0.0.2/doc/relations.md +92 -0
- ns_orm-0.0.2/doc/release.md +45 -0
- ns_orm-0.0.2/doc/schema.md +32 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/pyproject.toml +9 -3
- ns_orm-0.0.2/src/ns_orm/database.py +601 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/fields.py +57 -4
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/model.py +3 -3
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/query.py +28 -9
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/schema.py +6 -1
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/typing.py +5 -5
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/utils.py +13 -2
- {ns_orm-0.0.0 → ns_orm-0.0.2/src/ns_orm.egg-info}/PKG-INFO +60 -21
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/SOURCES.txt +9 -0
- ns_orm-0.0.2/src/ns_orm.egg-info/requires.txt +14 -0
- ns_orm-0.0.0/README.md +0 -70
- ns_orm-0.0.0/src/ns_orm/database.py +0 -292
- ns_orm-0.0.0/src/ns_orm.egg-info/requires.txt +0 -8
- {ns_orm-0.0.0 → ns_orm-0.0.2}/LICENSE +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/setup.cfg +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/__init__.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/cli.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/dialects.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/exceptions.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/expressions.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/manager.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/__init__.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/autodetector.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/executor.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/loader.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/migration.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/operations.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/state.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/writer.py +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/dependency_links.txt +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/entry_points.txt +0 -0
- {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: ns-orm
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: A ORM built on Pydantic models.
|
|
5
5
|
Author: ns-orm contributors
|
|
6
6
|
License: Apache License
|
|
@@ -210,12 +210,13 @@ Description-Content-Type: text/markdown
|
|
|
210
210
|
License-File: LICENSE
|
|
211
211
|
Requires-Dist: pydantic
|
|
212
212
|
Requires-Dist: lesscode-database
|
|
213
|
-
Requires-Dist: typing-extensions
|
|
213
|
+
Requires-Dist: typing-extensions
|
|
214
214
|
Provides-Extra: dev
|
|
215
|
-
Requires-Dist: pytest
|
|
216
|
-
Requires-Dist: pytest
|
|
217
|
-
Requires-Dist:
|
|
218
|
-
|
|
215
|
+
Requires-Dist: pytest<8,>=7; python_version < "3.8" and extra == "dev"
|
|
216
|
+
Requires-Dist: pytest>=8; python_version >= "3.8" and extra == "dev"
|
|
217
|
+
Requires-Dist: pytest-asyncio<0.21.2,>=0.20; python_version < "3.8" and extra == "dev"
|
|
218
|
+
Requires-Dist: pytest-asyncio>=0.23; python_version >= "3.8" and extra == "dev"
|
|
219
|
+
Requires-Dist: ruff>=0.5; python_version >= "3.8" and extra == "dev"
|
|
219
220
|
|
|
220
221
|
# ns-orm
|
|
221
222
|
|
|
@@ -240,44 +241,82 @@ from typing import Optional
|
|
|
240
241
|
|
|
241
242
|
from typing_extensions import Annotated
|
|
242
243
|
|
|
243
|
-
from
|
|
244
|
-
from
|
|
244
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
245
|
+
from lesscode_database.db_options import db_options
|
|
246
|
+
|
|
247
|
+
from ns_orm import ForeignKey, Int, Model, String, create_all, get_connection
|
|
245
248
|
|
|
246
249
|
|
|
247
250
|
class User(Model):
|
|
251
|
+
class Meta:
|
|
252
|
+
connect_name = "default"
|
|
253
|
+
|
|
248
254
|
id: Annotated[Optional[int], Int(primary_key=True)] = None
|
|
249
255
|
name: Annotated[str, String(max_length=50)]
|
|
250
256
|
|
|
251
257
|
|
|
252
258
|
class Post(Model):
|
|
259
|
+
class Meta:
|
|
260
|
+
connect_name = "default"
|
|
261
|
+
|
|
253
262
|
id: Annotated[Optional[int], Int(primary_key=True)] = None
|
|
254
263
|
user_id: Annotated[int, ForeignKey("User", on_delete="CASCADE")]
|
|
255
264
|
title: Annotated[str, String(max_length=200)]
|
|
256
265
|
|
|
257
266
|
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
db_options.conn_list = [
|
|
268
|
+
ConnectionInfo(dialect="sqlite3", name="default", dsn="test.db", async_enable=False),
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
db = get_connection("default")
|
|
260
272
|
create_all(db, [User, Post])
|
|
261
273
|
|
|
262
|
-
u = User.objects.
|
|
263
|
-
Post.objects.
|
|
274
|
+
u = User.objects.create(name="alice")
|
|
275
|
+
Post.objects.create(user_id=u.id, title="hello")
|
|
264
276
|
|
|
265
|
-
posts = Post.objects.
|
|
277
|
+
posts = Post.objects.prefetch_related("user").all()
|
|
266
278
|
print(posts[0].user.name)
|
|
279
|
+
|
|
280
|
+
connect, _connect_info = getattr(db_options, "default")
|
|
281
|
+
if not hasattr(connect, "cursor") and hasattr(connect, "connection"):
|
|
282
|
+
connect = connect.connection()
|
|
283
|
+
|
|
284
|
+
qs = User.objects.filter(id=u.id)
|
|
285
|
+
sql, params = qs._select_sql()
|
|
286
|
+
prepared = db.dialect.prepare(sql, params)
|
|
287
|
+
cursor = connect.cursor()
|
|
288
|
+
cursor.execute(prepared.sql, prepared.params)
|
|
289
|
+
row = cursor.fetchone()
|
|
290
|
+
print(row)
|
|
267
291
|
```
|
|
268
292
|
|
|
269
293
|
## 快速开始(异步)
|
|
270
294
|
|
|
271
295
|
```python
|
|
272
|
-
|
|
273
|
-
|
|
296
|
+
import asyncio
|
|
297
|
+
|
|
298
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
299
|
+
from lesscode_database.db_options import db_options
|
|
300
|
+
|
|
301
|
+
from ns_orm import acreate_all, get_connection
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def main():
|
|
305
|
+
db_options.conn_list = [
|
|
306
|
+
ConnectionInfo(dialect="sqlite3", name="default_async", dsn="test_async.db", async_enable=True),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
db = get_connection("default_async")
|
|
310
|
+
await acreate_all(db, [User, Post])
|
|
311
|
+
|
|
312
|
+
u = await User.objects.using("default_async").acreate(name="alice")
|
|
313
|
+
posts = await (
|
|
314
|
+
Post.objects.using("default_async").filter(user_id=u.id).prefetch_related("user").all()
|
|
315
|
+
)
|
|
316
|
+
print(posts[0].user.name)
|
|
274
317
|
|
|
275
|
-
executor = ... # 由 lesscode-database 创建(异步执行器)
|
|
276
|
-
db = AsyncDatabase(executor=executor, dialect=dialect_from_url("sqlite"))
|
|
277
|
-
await acreate_all(db, [User, Post])
|
|
278
318
|
|
|
279
|
-
|
|
280
|
-
posts = await Post.objects.using(db).filter(user_id=u.id).prefetch_related("user").all()
|
|
319
|
+
asyncio.run(main())
|
|
281
320
|
```
|
|
282
321
|
|
|
283
322
|
## 迁移命令
|
ns_orm-0.0.2/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# ns-orm
|
|
2
|
+
|
|
3
|
+
一个参考 Django ORM 设计的轻量级 ORM(不依赖 SQLAlchemy),以 Pydantic 作为模型层,支持同步/异步、CRUD、关联预取与最小建表能力。
|
|
4
|
+
|
|
5
|
+
数据库连接/连接池由 lesscode-database 负责,ns-orm 仅消费“执行器”来执行 SQL。
|
|
6
|
+
|
|
7
|
+
更多功能与完整用法见 [doc/index.md](doc/index.md)。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ns-orm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
数据库驱动与连接方式由 lesscode-database 决定。
|
|
16
|
+
|
|
17
|
+
## 快速开始(同步)
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from typing_extensions import Annotated
|
|
23
|
+
|
|
24
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
25
|
+
from lesscode_database.db_options import db_options
|
|
26
|
+
|
|
27
|
+
from ns_orm import ForeignKey, Int, Model, String, create_all, get_connection
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class User(Model):
|
|
31
|
+
class Meta:
|
|
32
|
+
connect_name = "default"
|
|
33
|
+
|
|
34
|
+
id: Annotated[Optional[int], Int(primary_key=True)] = None
|
|
35
|
+
name: Annotated[str, String(max_length=50)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Post(Model):
|
|
39
|
+
class Meta:
|
|
40
|
+
connect_name = "default"
|
|
41
|
+
|
|
42
|
+
id: Annotated[Optional[int], Int(primary_key=True)] = None
|
|
43
|
+
user_id: Annotated[int, ForeignKey("User", on_delete="CASCADE")]
|
|
44
|
+
title: Annotated[str, String(max_length=200)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
db_options.conn_list = [
|
|
48
|
+
ConnectionInfo(dialect="sqlite3", name="default", dsn="test.db", async_enable=False),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
db = get_connection("default")
|
|
52
|
+
create_all(db, [User, Post])
|
|
53
|
+
|
|
54
|
+
u = User.objects.create(name="alice")
|
|
55
|
+
Post.objects.create(user_id=u.id, title="hello")
|
|
56
|
+
|
|
57
|
+
posts = Post.objects.prefetch_related("user").all()
|
|
58
|
+
print(posts[0].user.name)
|
|
59
|
+
|
|
60
|
+
connect, _connect_info = getattr(db_options, "default")
|
|
61
|
+
if not hasattr(connect, "cursor") and hasattr(connect, "connection"):
|
|
62
|
+
connect = connect.connection()
|
|
63
|
+
|
|
64
|
+
qs = User.objects.filter(id=u.id)
|
|
65
|
+
sql, params = qs._select_sql()
|
|
66
|
+
prepared = db.dialect.prepare(sql, params)
|
|
67
|
+
cursor = connect.cursor()
|
|
68
|
+
cursor.execute(prepared.sql, prepared.params)
|
|
69
|
+
row = cursor.fetchone()
|
|
70
|
+
print(row)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 快速开始(异步)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
|
|
78
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
79
|
+
from lesscode_database.db_options import db_options
|
|
80
|
+
|
|
81
|
+
from ns_orm import acreate_all, get_connection
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def main():
|
|
85
|
+
db_options.conn_list = [
|
|
86
|
+
ConnectionInfo(dialect="sqlite3", name="default_async", dsn="test_async.db", async_enable=True),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
db = get_connection("default_async")
|
|
90
|
+
await acreate_all(db, [User, Post])
|
|
91
|
+
|
|
92
|
+
u = await User.objects.using("default_async").acreate(name="alice")
|
|
93
|
+
posts = await (
|
|
94
|
+
Post.objects.using("default_async").filter(user_id=u.id).prefetch_related("user").all()
|
|
95
|
+
)
|
|
96
|
+
print(posts[0].user.name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
asyncio.run(main())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 迁移命令
|
|
103
|
+
|
|
104
|
+
见 [doc/migrations.md](doc/migrations.md)。
|
|
105
|
+
|
|
106
|
+
## 打包与发布
|
|
107
|
+
|
|
108
|
+
见 [doc/release.md](doc/release.md)。
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 异步用法
|
|
2
|
+
|
|
3
|
+
## AsyncDatabase 与异步 CRUD
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
7
|
+
from lesscode_database.db_options import db_options
|
|
8
|
+
|
|
9
|
+
from ns_orm import acreate_all, get_connection
|
|
10
|
+
|
|
11
|
+
db_options.conn_list = [
|
|
12
|
+
ConnectionInfo(dialect="sqlite3", name="default_async", dsn="test_async.db", async_enable=True),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
db = get_connection("default_async")
|
|
16
|
+
await acreate_all(db, [User, Post])
|
|
17
|
+
|
|
18
|
+
u = await User.objects.using("default_async").acreate(name="alice")
|
|
19
|
+
posts = await Post.objects.using("default_async").filter(user_id=u.id).all()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 异步 QuerySet
|
|
23
|
+
|
|
24
|
+
当你通过 `Model.objects.using(async_db)` 绑定到 `AsyncDatabase` 后,使用以下方法:
|
|
25
|
+
|
|
26
|
+
- `acreate(...)`
|
|
27
|
+
- `aget(...)`
|
|
28
|
+
- `aall()`
|
|
29
|
+
|
|
30
|
+
QuerySet 本身的执行方法在异步模式下仍为协程:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
posts = await Post.objects.using(db).filter(title__contains="o").all()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 事务
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
async with db.transaction():
|
|
40
|
+
await User.objects.using(db).acreate(name="alice")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 注意事项
|
|
44
|
+
|
|
45
|
+
- 执行器实现由 lesscode-database 决定;ns-orm 仅做 SQL 生成与参数风格转换
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 数据库与执行器(lesscode-database)
|
|
2
|
+
|
|
3
|
+
## 与 db_options 集成(connect_name)
|
|
4
|
+
|
|
5
|
+
ns-orm 默认会从 `lesscode_database.db_options.db_options` 读取连接:
|
|
6
|
+
|
|
7
|
+
- `connect, connect_info = getattr(db_options, connect_name)`
|
|
8
|
+
- `connect_info.dialect` 用于选择 ns-orm dialect
|
|
9
|
+
|
|
10
|
+
示例(SQLite):
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
14
|
+
from lesscode_database.db_options import db_options
|
|
15
|
+
|
|
16
|
+
from ns_orm import create_all, get_connection
|
|
17
|
+
|
|
18
|
+
db_options.conn_list = [
|
|
19
|
+
ConnectionInfo(dialect="sqlite3", name="default", dsn="test.db", async_enable=False),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
db = get_connection("default")
|
|
23
|
+
create_all(db, [User, Post])
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
说明:
|
|
27
|
+
|
|
28
|
+
- `sqlite3` 连接池在 lesscode-database 中依赖 `DBUtils`(未安装会报错)
|
|
29
|
+
- 异步 `sqlite3` 连接依赖 `aiosqlite3`(未安装会报错)
|
|
30
|
+
- 某些情况下 `connect` 可能是连接池对象而不是 DB-API 连接;如果你需要直接 `cursor()`,可使用 `connect.connection()` 获取真实连接
|
|
31
|
+
|
|
32
|
+
使用 `connect.cursor()` 执行 ORM 生成的 SQL:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
connect_name = "default"
|
|
36
|
+
connect, _connect_info = getattr(db_options, connect_name)
|
|
37
|
+
if not hasattr(connect, "cursor") and hasattr(connect, "connection"):
|
|
38
|
+
connect = connect.connection()
|
|
39
|
+
|
|
40
|
+
qs = User.objects.filter(id=1)
|
|
41
|
+
sql, params = qs._select_sql()
|
|
42
|
+
prepared = db.dialect.prepare(sql, params)
|
|
43
|
+
|
|
44
|
+
cursor = connect.cursor()
|
|
45
|
+
cursor.execute(prepared.sql, prepared.params)
|
|
46
|
+
row = cursor.fetchone()
|
|
47
|
+
print(row)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
示例(SQLite 异步):
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
54
|
+
from lesscode_database.db_options import db_options
|
|
55
|
+
|
|
56
|
+
from ns_orm import acreate_all, get_connection
|
|
57
|
+
|
|
58
|
+
db_options.conn_list = [
|
|
59
|
+
ConnectionInfo(dialect="sqlite3", name="default_async", dsn="test_async.db", async_enable=True),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
db = get_connection("default_async")
|
|
63
|
+
await acreate_all(db, [User, Post])
|
|
64
|
+
|
|
65
|
+
u = await User.objects.using("default_async").acreate(name="alice")
|
|
66
|
+
posts = await Post.objects.using("default_async").filter(user_id=u.id).prefetch_related("user").all()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Database / AsyncDatabase(自定义 executor)
|
|
70
|
+
|
|
71
|
+
当你希望绕开 `db_options`(例如在框架外自行管理连接/执行器)时,也可以直接构造:
|
|
72
|
+
|
|
73
|
+
同步:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from ns_orm import Database
|
|
77
|
+
from ns_orm.dialects import dialect_from_url
|
|
78
|
+
|
|
79
|
+
executor = ... # 由 lesscode-database 创建
|
|
80
|
+
db = Database(executor=executor, dialect=dialect_from_url("sqlite"))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
异步:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from ns_orm import AsyncDatabase
|
|
87
|
+
from ns_orm.dialects import dialect_from_url
|
|
88
|
+
|
|
89
|
+
executor = ... # 由 lesscode-database 创建(异步执行器)
|
|
90
|
+
db = AsyncDatabase(executor=executor, dialect=dialect_from_url("postgresql"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 事务
|
|
94
|
+
|
|
95
|
+
同步:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
with db.transaction():
|
|
99
|
+
User.objects.using(db).create(name="alice")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
异步:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
async with db.transaction():
|
|
106
|
+
await User.objects.using(db).acreate(name="alice")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 方言(dialect)
|
|
110
|
+
|
|
111
|
+
当前方言用于:
|
|
112
|
+
|
|
113
|
+
- 标识符引用(引号)
|
|
114
|
+
- DDL 类型映射
|
|
115
|
+
- 参数风格转换(`:name` -> driver paramstyle)
|
|
116
|
+
|
|
117
|
+
常用值(可直接使用 URL scheme 的前缀):
|
|
118
|
+
|
|
119
|
+
- `sqlite`
|
|
120
|
+
- `clickhouse`
|
|
121
|
+
- `postgres` / `postgresql`
|
|
122
|
+
- `mysql`(也可传 `mariadb` / `tidb`)
|
|
123
|
+
- `mssql` / `sqlserver`
|
|
124
|
+
- `oracle`
|
|
125
|
+
|
|
126
|
+
驱动安装与连接配置请参考 lesscode-database。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ns-orm 文档
|
|
2
|
+
|
|
3
|
+
## 目录
|
|
4
|
+
|
|
5
|
+
- [模型与字段](models.md)
|
|
6
|
+
- [查询与 CRUD](queryset.md)
|
|
7
|
+
- [关联与预取](relations.md)
|
|
8
|
+
- [数据库与 URL](database.md)
|
|
9
|
+
- [建表(最小能力)](schema.md)
|
|
10
|
+
- [异步用法](async.md)
|
|
11
|
+
- [迁移命令(makemigrations/migrate)](migrations.md)
|
|
12
|
+
- [打包与发布](release.md)
|
|
13
|
+
|
|
14
|
+
## 设计概览
|
|
15
|
+
|
|
16
|
+
ns-orm 的目标是提供接近 Django ORM 的开发体验:
|
|
17
|
+
|
|
18
|
+
- 以 Pydantic 模型作为领域对象(数据校验、序列化、赋值校验)
|
|
19
|
+
- 以 QuerySet 作为查询构造器(链式调用、延迟执行的 SQL 构造)
|
|
20
|
+
- 以 Database/AsyncDatabase 作为数据库适配层(不同驱动、参数风格转换)
|
|
21
|
+
|
|
22
|
+
如果你需要在框架未内置的数据库上使用,请优先选择该数据库的 DB-API 2.0 驱动(同步)或提供等价的异步驱动。
|
|
23
|
+
|
|
24
|
+
## 开发与测试
|
|
25
|
+
|
|
26
|
+
安装开发依赖:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
说明:
|
|
33
|
+
|
|
34
|
+
- Python 3.7:`pytest<8`、较老版本的 `pytest-asyncio`,不安装 ruff
|
|
35
|
+
- Python >=3.8:`pytest>=8`、`pytest-asyncio>=0.23`、`ruff>=0.5`
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# 迁移命令(makemigrations / migrate)
|
|
2
|
+
|
|
3
|
+
ns-orm 提供类似 Django 的迁移能力:
|
|
4
|
+
|
|
5
|
+
- `makemigrations`:对比“上一次迁移快照”和当前 Model 定义,生成新的迁移文件
|
|
6
|
+
- `migrate`:执行迁移文件中的 SQL,并记录已应用迁移
|
|
7
|
+
|
|
8
|
+
## 前置:数据库连接由 lesscode-database 提供
|
|
9
|
+
|
|
10
|
+
ns-orm 不负责创建连接/连接池。你需要在自己的项目中使用 `lesscode-database` 创建执行器,并包装为 `ns_orm.Database` 或 `ns_orm.AsyncDatabase`。
|
|
11
|
+
|
|
12
|
+
迁移命令通过 `--db-factory` 调用你提供的工厂函数来获得 Database 对象。
|
|
13
|
+
|
|
14
|
+
## 1) makemigrations
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
ns-orm makemigrations \
|
|
18
|
+
--models app.models \
|
|
19
|
+
--dialect postgresql \
|
|
20
|
+
--name add_user_email \
|
|
21
|
+
--migrations-dir ./migrations
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
参数说明:
|
|
25
|
+
|
|
26
|
+
- `--models`:包含 Model 定义的模块路径(逗号分隔)
|
|
27
|
+
- `--dialect`:方言(或 URL scheme),例如 `postgresql` / `mysql` / `sqlite`
|
|
28
|
+
- `--name`:迁移名称后缀
|
|
29
|
+
- `--migrations-dir`:迁移文件目录(默认 `./migrations`)
|
|
30
|
+
|
|
31
|
+
输出:生成 `YYYYMMDDhhmmss_xxx.py` 迁移文件。
|
|
32
|
+
|
|
33
|
+
## 2) migrate
|
|
34
|
+
|
|
35
|
+
你需要提供 `db_factory(dialect)`:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# project/db.py
|
|
39
|
+
from lesscode_database.connection_info import ConnectionInfo
|
|
40
|
+
from lesscode_database.db_options import db_options
|
|
41
|
+
|
|
42
|
+
from ns_orm import get_connection
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_db(dialect):
|
|
46
|
+
_ = dialect
|
|
47
|
+
db_options.conn_list = [
|
|
48
|
+
ConnectionInfo(dialect="sqlite3", name="default", dsn="test.db", async_enable=False),
|
|
49
|
+
]
|
|
50
|
+
return get_connection("default")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
执行迁移:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ns-orm migrate \
|
|
57
|
+
--models app.models \
|
|
58
|
+
--dialect postgresql \
|
|
59
|
+
--db-factory project.db:get_db \
|
|
60
|
+
--migrations-dir ./migrations
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
只看计划(不执行):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
ns-orm migrate \
|
|
67
|
+
--models app.models \
|
|
68
|
+
--dialect postgresql \
|
|
69
|
+
--db-factory project.db:get_db \
|
|
70
|
+
--migrations-dir ./migrations \
|
|
71
|
+
--plan
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
异步执行(要求 `db_factory` 返回 `ns_orm.AsyncDatabase`):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ns-orm migrate \
|
|
78
|
+
--models app.models \
|
|
79
|
+
--dialect postgresql \
|
|
80
|
+
--db-factory project.db:get_async_db \
|
|
81
|
+
--migrations-dir ./migrations \
|
|
82
|
+
--async
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 当前支持范围与限制
|
|
86
|
+
|
|
87
|
+
- 支持:CreateTable、AddColumn、DropColumn(SQLite 的 DropColumn 不支持)
|
|
88
|
+
- 暂不支持:字段类型变更、重命名、索引/约束的自动迁移(可扩展 operations)
|
|
89
|
+
|
|
90
|
+
已应用迁移记录在数据库表 `ns_orm_migrations` 中。
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# 模型与字段
|
|
2
|
+
|
|
3
|
+
## 定义模型
|
|
4
|
+
|
|
5
|
+
ns-orm 的模型继承自 `ns_orm.Model`,字段通过 `typing_extensions.Annotated` 绑定字段元数据:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
from ns_orm import Int, Model, String
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class User(Model):
|
|
15
|
+
id: Annotated[Optional[int], Int(primary_key=True)] = None
|
|
16
|
+
name: Annotated[str, String(max_length=50)]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
说明:
|
|
20
|
+
|
|
21
|
+
- 类型注解用于 Pydantic 校验与序列化
|
|
22
|
+
- `Annotated[..., FieldDef]` 中的 `FieldDef` 用于 ORM 元信息(列类型、主键、是否为空等)
|
|
23
|
+
|
|
24
|
+
## 字段类型
|
|
25
|
+
|
|
26
|
+
内置字段定义见 `ns_orm.fields`:
|
|
27
|
+
|
|
28
|
+
- 数值:`TinyInt`、`SmallInt`、`Int`、`BigInt`、`UTinyInt`、`USmallInt`、`UInt`、`UBigInt`、`Float`、`Real`、`Double`、`Decimal`
|
|
29
|
+
- 字符串:`Char(length=...)`、`String(max_length=...)`、`Text`、`FixedString(length=...)`
|
|
30
|
+
- 时间:`Date`、`DateTime`
|
|
31
|
+
- 其他:`Boolean`、`Binary`、`JSON`、`UUID`、`IPv4`、`IPv6`
|
|
32
|
+
- ClickHouse 复合类型:`Array(item=...)`、`Map(key=..., value=...)`、`TupleType(items=[...])`、`Nullable(inner=...)`、`LowCardinality(inner=...)`、`Date32`、`DateTime64`
|
|
33
|
+
|
|
34
|
+
通用字段选项:
|
|
35
|
+
|
|
36
|
+
- `primary_key`:主键
|
|
37
|
+
- `nullable`:是否可空(默认由数据库决定;建议显式设置)
|
|
38
|
+
- `unique`:唯一约束(列级)
|
|
39
|
+
- `index`:索引标记(当前仅保留元信息,不自动建索引)
|
|
40
|
+
- `default`/`server_default`:默认值(当前用于模型侧与 DDL 侧的最小表达)
|
|
41
|
+
|
|
42
|
+
## 模型元信息(Meta)
|
|
43
|
+
|
|
44
|
+
可通过内部类 `Meta` 指定表名与 schema:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
class User(Model):
|
|
48
|
+
class Meta:
|
|
49
|
+
table_name = "app_user"
|
|
50
|
+
schema = "public"
|
|
51
|
+
|
|
52
|
+
id: Annotated[Optional[int], Int(primary_key=True)] = None
|
|
53
|
+
name: Annotated[str, String(max_length=50)]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
默认表名为类名的 snake_case(例如 `BlogPost` -> `blog_post`)。
|
|
57
|
+
|
|
58
|
+
## 主键约定
|
|
59
|
+
|
|
60
|
+
- 如果字段中存在 `primary_key=True`,将使用该字段作为主键
|
|
61
|
+
- 否则如果存在 `id` 字段,将使用 `id` 作为主键
|
|
62
|
+
|
|
63
|
+
## 模型序列化
|
|
64
|
+
|
|
65
|
+
模型仍然是 Pydantic 模型:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
u = User(id=1, name="alice")
|
|
69
|
+
payload = u.model_dump()
|
|
70
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 查询与 CRUD
|
|
2
|
+
|
|
3
|
+
## 基本用法
|
|
4
|
+
|
|
5
|
+
获取 Manager:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
m = User.objects.using(db)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
获取 QuerySet:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
qs = User.objects.using(db).qs()
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
常用方法:
|
|
18
|
+
|
|
19
|
+
- `all()`:返回列表
|
|
20
|
+
- `first()`:返回首个或 None
|
|
21
|
+
- `get(**lookups)`:返回单个对象,不存在抛 `DoesNotExist`,多条抛 `MultipleObjectsReturned`
|
|
22
|
+
- `create(**data)`:插入并返回实例
|
|
23
|
+
- `update(**data)`:批量更新,返回影响行数
|
|
24
|
+
- `delete()`:批量删除,返回影响行数
|
|
25
|
+
|
|
26
|
+
示例:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
u = User.objects.using(db).create(name="alice")
|
|
30
|
+
u2 = User.objects.using(db).get(id=u.id)
|
|
31
|
+
User.objects.using(db).filter(id=u.id).update(name="bob")
|
|
32
|
+
User.objects.using(db).filter(id=u.id).delete()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 链式查询
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
users = (
|
|
39
|
+
User.objects.using(db)
|
|
40
|
+
.filter(name__icontains="al")
|
|
41
|
+
.order_by("-id")
|
|
42
|
+
.limit(10)
|
|
43
|
+
.offset(0)
|
|
44
|
+
.all()
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## lookups
|
|
49
|
+
|
|
50
|
+
`filter/exclude` 支持 Django 风格的 `field__lookup=value`:
|
|
51
|
+
|
|
52
|
+
- 精确:`field=value` 或 `field__exact=value`
|
|
53
|
+
- 比较:`__gt` `__gte` `__lt` `__lte`
|
|
54
|
+
- 集合:`__in=[...]`
|
|
55
|
+
- 字符串匹配:`__contains` `__icontains` `__startswith` `__istartswith` `__endswith` `__iendswith`
|
|
56
|
+
- 空值:`__isnull=True/False`
|
|
57
|
+
|
|
58
|
+
示例:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
User.objects.using(db).filter(id__in=[1, 2, 3]).all()
|
|
62
|
+
User.objects.using(db).exclude(name__startswith="test").all()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Q 表达式
|
|
66
|
+
|
|
67
|
+
`ns_orm.Q` 提供了 AND/OR/NOT 的条件树抽象,当前版本主要用于内部组合条件。
|
|
68
|
+
|
|
69
|
+
当前 QuerySet API 暂不提供 `filter(Q(...))` 入口;日常查询建议使用 `filter/exclude` 的 lookups 组合表达业务条件。
|