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.
Files changed (45) hide show
  1. {ns_orm-0.0.0 → ns_orm-0.0.2}/MANIFEST.in +1 -1
  2. {ns_orm-0.0.0/src/ns_orm.egg-info → ns_orm-0.0.2}/PKG-INFO +60 -21
  3. ns_orm-0.0.2/README.md +108 -0
  4. ns_orm-0.0.2/doc/async.md +45 -0
  5. ns_orm-0.0.2/doc/database.md +126 -0
  6. ns_orm-0.0.2/doc/index.md +35 -0
  7. ns_orm-0.0.2/doc/migrations.md +90 -0
  8. ns_orm-0.0.2/doc/models.md +70 -0
  9. ns_orm-0.0.2/doc/queryset.md +69 -0
  10. ns_orm-0.0.2/doc/relations.md +92 -0
  11. ns_orm-0.0.2/doc/release.md +45 -0
  12. ns_orm-0.0.2/doc/schema.md +32 -0
  13. {ns_orm-0.0.0 → ns_orm-0.0.2}/pyproject.toml +9 -3
  14. ns_orm-0.0.2/src/ns_orm/database.py +601 -0
  15. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/fields.py +57 -4
  16. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/model.py +3 -3
  17. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/query.py +28 -9
  18. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/schema.py +6 -1
  19. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/typing.py +5 -5
  20. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/utils.py +13 -2
  21. {ns_orm-0.0.0 → ns_orm-0.0.2/src/ns_orm.egg-info}/PKG-INFO +60 -21
  22. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/SOURCES.txt +9 -0
  23. ns_orm-0.0.2/src/ns_orm.egg-info/requires.txt +14 -0
  24. ns_orm-0.0.0/README.md +0 -70
  25. ns_orm-0.0.0/src/ns_orm/database.py +0 -292
  26. ns_orm-0.0.0/src/ns_orm.egg-info/requires.txt +0 -8
  27. {ns_orm-0.0.0 → ns_orm-0.0.2}/LICENSE +0 -0
  28. {ns_orm-0.0.0 → ns_orm-0.0.2}/setup.cfg +0 -0
  29. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/__init__.py +0 -0
  30. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/cli.py +0 -0
  31. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/dialects.py +0 -0
  32. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/exceptions.py +0 -0
  33. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/expressions.py +0 -0
  34. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/manager.py +0 -0
  35. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/__init__.py +0 -0
  36. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/autodetector.py +0 -0
  37. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/executor.py +0 -0
  38. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/loader.py +0 -0
  39. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/migration.py +0 -0
  40. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/operations.py +0 -0
  41. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/state.py +0 -0
  42. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm/migrations/writer.py +0 -0
  43. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/dependency_links.txt +0 -0
  44. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/entry_points.txt +0 -0
  45. {ns_orm-0.0.0 → ns_orm-0.0.2}/src/ns_orm.egg-info/top_level.txt +0 -0
@@ -1,3 +1,3 @@
1
1
  recursive-exclude .gitee *
2
2
  recursive-exclude tests *
3
- recursive-exclude doc *
3
+ recursive-include doc *.md
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: ns-orm
3
- Version: 0.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>=4.0
213
+ Requires-Dist: typing-extensions
214
214
  Provides-Extra: dev
215
- Requires-Dist: pytest>=8; extra == "dev"
216
- Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
217
- Requires-Dist: ruff>=0.5; extra == "dev"
218
- Dynamic: license-file
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 ns_orm import Database, ForeignKey, Int, Model, String, create_all
244
- from ns_orm.dialects import dialect_from_url
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
- executor = ... # 由 lesscode-database 创建
259
- db = Database(executor=executor, dialect=dialect_from_url("sqlite"))
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.using(db).create(name="alice")
263
- Post.objects.using(db).create(user_id=u.id, title="hello")
274
+ u = User.objects.create(name="alice")
275
+ Post.objects.create(user_id=u.id, title="hello")
264
276
 
265
- posts = Post.objects.using(db).prefetch_related("user").all()
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
- from ns_orm import AsyncDatabase, acreate_all
273
- from ns_orm.dialects import dialect_from_url
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
- u = await User.objects.using(db).acreate(name="alice")
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 组合表达业务条件。