cloudoll 4.2.1__tar.gz → 4.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudoll-4.2.1 → cloudoll-4.3.0}/CHANGELOG.md +27 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/PKG-INFO +7 -7
- {cloudoll-4.2.1 → cloudoll-4.3.0}/README.md +6 -6
- {cloudoll-4.2.1 → cloudoll-4.3.0}/README.zh-CN.md +9 -9
- cloudoll-4.3.0/cloudoll/__init__.py +1 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/AGENTS.md +4 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/ai.md +2 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/changelog.md +18 -1
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/database.md +253 -23
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/start.md +3 -3
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/manifest.json +5 -5
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/skills/cloudoll/SKILL.md +6 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/__init__.py +18 -1
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/compiler.py +152 -8
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/model.py +20 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/query.py +218 -9
- cloudoll-4.3.0/cloudoll/orm/relation.py +79 -0
- cloudoll-4.3.0/cloudoll/orm/subquery.py +15 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/README.md +1 -1
- cloudoll-4.3.0/cloudoll/template/requirements.txt +1 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/PKG-INFO +7 -7
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/SOURCES.txt +3 -0
- cloudoll-4.3.0/tests/integration/test_databases.py +576 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_datasources.py +28 -0
- cloudoll-4.3.0/tests/test_query_capabilities.py +202 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/query_contract.py +14 -1
- cloudoll-4.2.1/cloudoll/__init__.py +0 -1
- cloudoll-4.2.1/cloudoll/template/requirements.txt +0 -1
- cloudoll-4.2.1/tests/integration/test_databases.py +0 -294
- {cloudoll-4.2.1 → cloudoll-4.3.0}/AGENTS.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/AI.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/LICENSE +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/MANIFEST.in +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/SECURITY.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/__main__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/__init__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/commands.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/app-start.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/config.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/cookie-and-session.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/deployment.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/event-source.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/exception.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/file-uploads.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/http-client.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/jwt.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/logs.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/middleware.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/migration.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/router.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/structure.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/validation.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/view.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/websockets.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/cli.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/__init__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/cli_main.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/m2d.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/process.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/schema.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/spinner.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/watch.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/logging/__init__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/mail/__init__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/mail/smtp.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/observability.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/aws_engine.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/awsmysql.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/awspostgres.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/base.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/datasources.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/dialects.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/engine.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/field.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/mysql.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/parse.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/postgres.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/protocols.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/savepoints.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/streaming.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/test_sql.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/values.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/py.typed +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/__init__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/config/conf.local.yaml +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/admin/test.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/api/index.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/es/es.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/home/index.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/upload/upload.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/ws/ws.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/middlewares/auth.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/static/css/index.css +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/static/img/cat.avif +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/static/js/index.js +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/templates/index.html +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/__init__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/async_tools.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/common.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/password.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/snowflake.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/__init__.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/configuration.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/context.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/core.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/exception.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/html.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/jwt.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/lifecycle.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/request_data.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/requests.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/resources.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/routing.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/sessions.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/settings.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/types.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/validation.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/dependency_links.txt +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/entry_points.txt +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/requires.txt +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/top_level.txt +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/architecture.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/aurora.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/maintenance.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/production.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/reliability.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/schema-generation.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/streaming-and-types.md +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/pyproject.toml +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/scripts/sync_ai_docs.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/setup.cfg +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/setup.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/audit_dependencies.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/check_release.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/check_wheel.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/conftest.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_aws_failover.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_savepoints.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_schema.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_streaming.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/run_local_databases.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_ai.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_architecture.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_dev_lifecycle.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_field_types.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_production_contracts.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_regressions.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_release_workflow.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_request_contract.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_retry_and_query_regressions.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_review_fixes.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_save_snapshots_and_cleanup.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_savepoint_unit.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_schema_generation.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_sql_echo.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_startup_logging.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_streaming_unit.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_transactions.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_typing_regressions.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_validation.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/engine_contract.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/field_contract.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/middleware_contract.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/production_contract.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/request_contract.py +0 -0
- {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/web_contract.py +0 -0
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.3.0
|
|
4
|
+
|
|
5
|
+
### ORM queries and writes
|
|
6
|
+
|
|
7
|
+
- Add explicit LEFT/INNER/RIGHT JOIN kinds, query-level DISTINCT, `one_dict()`
|
|
8
|
+
for projected/aggregate rows, and non-consuming `exists()` checks.
|
|
9
|
+
- Add parameterized scalar/IN and EXISTS/NOT EXISTS subqueries with snapshot
|
|
10
|
+
semantics and datasource/dialect compatibility checks.
|
|
11
|
+
- Add read-only table aliases for self-joins, non-recursive CTEs, and
|
|
12
|
+
UNION/UNION ALL with branch-local and outer pagination semantics.
|
|
13
|
+
- Add atomic expression-valued updates and transaction-owned `for_update()`
|
|
14
|
+
row locks, including mutually exclusive NOWAIT and SKIP LOCKED options.
|
|
15
|
+
- Add single-statement `upsert()` with explicit update fields; PostgreSQL
|
|
16
|
+
supports conflict targets, while MySQL explicitly rejects that option.
|
|
17
|
+
- Export `Relation` and `Subquery` types. Reject writes against aliases,
|
|
18
|
+
CTE/UNION sources and joined queries rather than silently ignoring sources.
|
|
19
|
+
|
|
20
|
+
### Documentation and verification
|
|
21
|
+
|
|
22
|
+
- Expand grouping/HAVING, aggregates, default datasource usage and query
|
|
23
|
+
result contracts. Normal application CRUD defaults to entity APIs without
|
|
24
|
+
unnecessary `.use(db)` or handwritten SQL.
|
|
25
|
+
- Update bundled AI guidance and version-matched examples, including locking,
|
|
26
|
+
unique constraints, snapshots and unsupported advanced combinations.
|
|
27
|
+
- Add MySQL 8.4/PostgreSQL 16 integration coverage for query sources, upserts,
|
|
28
|
+
concurrency, rollback and row-lock contention. Aurora remains unverified.
|
|
29
|
+
|
|
3
30
|
## 4.2.1
|
|
4
31
|
|
|
5
32
|
- Correct the AI initialization regression test to compare the original bytes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudoll
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.0
|
|
4
4
|
Summary: Assist in quickly creating distributable microservices based on aiohttp, with a focus on simplicity and ease of use.
|
|
5
5
|
Author-email: Qiu <smallerqiu@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -54,7 +54,7 @@ Dynamic: license-file
|
|
|
54
54
|
|
|
55
55
|
English | [简体中文](README.zh-CN.md)
|
|
56
56
|
|
|
57
|
-
Cloudoll 4.
|
|
57
|
+
Cloudoll 4.3.0 is an aiohttp-based Python library for routing, middleware,
|
|
58
58
|
templates, sessions, JWT, database access and application scaffolding.
|
|
59
59
|
|
|
60
60
|
## AI-assisted development
|
|
@@ -71,8 +71,8 @@ does not have `cloudoll ai --help`, consult its matching README and source first
|
|
|
71
71
|
Python 3.9+ is required. Install only the optional drivers you use:
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
|
-
python -m pip install 'cloudoll==4.
|
|
75
|
-
python -m pip install 'cloudoll[mysql,postgres,cache]==4.
|
|
74
|
+
python -m pip install 'cloudoll==4.3.0'
|
|
75
|
+
python -m pip install 'cloudoll[mysql,postgres,cache]==4.3.0'
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
The AWS extra requires Python 3.10+. Aurora remains untested; see
|
|
@@ -87,7 +87,7 @@ python -m pip install -r requirements.txt
|
|
|
87
87
|
cloudoll start -n myapp
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
The generated project targets 4.
|
|
90
|
+
The generated project targets 4.3.0 and receives a random JWT secret. Controllers
|
|
91
91
|
and authentication middleware are examples: replace demo credentials and supply
|
|
92
92
|
your own authorization and upload policies before deployment.
|
|
93
93
|
|
|
@@ -172,8 +172,8 @@ python -m ruff check cloudoll tests
|
|
|
172
172
|
python -m mypy
|
|
173
173
|
python -m pytest -q -m 'not integration' -k 'not aws'
|
|
174
174
|
python -m build
|
|
175
|
-
python tests/check_wheel.py dist/cloudoll-4.
|
|
176
|
-
python tests/check_release.py dist/cloudoll-4.
|
|
175
|
+
python tests/check_wheel.py dist/cloudoll-4.3.0-py3-none-any.whl
|
|
176
|
+
python tests/check_release.py dist/cloudoll-4.3.0-py3-none-any.whl
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
Native integration tests require explicitly configured disposable databases.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
Cloudoll 4.
|
|
5
|
+
Cloudoll 4.3.0 is an aiohttp-based Python library for routing, middleware,
|
|
6
6
|
templates, sessions, JWT, database access and application scaffolding.
|
|
7
7
|
|
|
8
8
|
## AI-assisted development
|
|
@@ -19,8 +19,8 @@ does not have `cloudoll ai --help`, consult its matching README and source first
|
|
|
19
19
|
Python 3.9+ is required. Install only the optional drivers you use:
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
python -m pip install 'cloudoll==4.
|
|
23
|
-
python -m pip install 'cloudoll[mysql,postgres,cache]==4.
|
|
22
|
+
python -m pip install 'cloudoll==4.3.0'
|
|
23
|
+
python -m pip install 'cloudoll[mysql,postgres,cache]==4.3.0'
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
The AWS extra requires Python 3.10+. Aurora remains untested; see
|
|
@@ -35,7 +35,7 @@ python -m pip install -r requirements.txt
|
|
|
35
35
|
cloudoll start -n myapp
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
The generated project targets 4.
|
|
38
|
+
The generated project targets 4.3.0 and receives a random JWT secret. Controllers
|
|
39
39
|
and authentication middleware are examples: replace demo credentials and supply
|
|
40
40
|
your own authorization and upload policies before deployment.
|
|
41
41
|
|
|
@@ -120,8 +120,8 @@ python -m ruff check cloudoll tests
|
|
|
120
120
|
python -m mypy
|
|
121
121
|
python -m pytest -q -m 'not integration' -k 'not aws'
|
|
122
122
|
python -m build
|
|
123
|
-
python tests/check_wheel.py dist/cloudoll-4.
|
|
124
|
-
python tests/check_release.py dist/cloudoll-4.
|
|
123
|
+
python tests/check_wheel.py dist/cloudoll-4.3.0-py3-none-any.whl
|
|
124
|
+
python tests/check_release.py dist/cloudoll-4.3.0-py3-none-any.whl
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
Native integration tests require explicitly configured disposable databases.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Cloudoll 是一个基于 aiohttp 的 Python Web 开发库,提供路由自动注册、中间件、模板渲染、会话管理、JWT、数据库访问及命令行脚手架,帮助快速构建 Web 应用和微服务。
|
|
8
8
|
|
|
9
|
-
本文适用于 Cloudoll 4.
|
|
9
|
+
本文适用于 Cloudoll 4.3.0。从 3.x 升级前请阅读下方迁移说明。
|
|
10
10
|
|
|
11
11
|
## AI 辅助开发
|
|
12
12
|
|
|
@@ -25,10 +25,10 @@ Cloudoll 是一个基于 aiohttp 的 Python Web 开发库,提供路由自动
|
|
|
25
25
|
|
|
26
26
|
## 安装
|
|
27
27
|
|
|
28
|
-
安装 4.
|
|
28
|
+
安装 4.3.0:
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
python -m pip install 'cloudoll==4.
|
|
31
|
+
python -m pip install 'cloudoll==4.3.0'
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
在当前仓库目录安装源码:
|
|
@@ -41,16 +41,16 @@ python -m pip install -e .
|
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
43
|
# MySQL
|
|
44
|
-
python -m pip install 'cloudoll[mysql]==4.
|
|
44
|
+
python -m pip install 'cloudoll[mysql]==4.3.0'
|
|
45
45
|
|
|
46
46
|
# PostgreSQL
|
|
47
|
-
python -m pip install 'cloudoll[postgres]==4.
|
|
47
|
+
python -m pip install 'cloudoll[postgres]==4.3.0'
|
|
48
48
|
|
|
49
49
|
# AWS 数据库驱动
|
|
50
|
-
python -m pip install 'cloudoll[aws]==4.
|
|
50
|
+
python -m pip install 'cloudoll[aws]==4.3.0'
|
|
51
51
|
|
|
52
52
|
# Redis / Memcached 会话或 Redis 数据库
|
|
53
|
-
python -m pip install 'cloudoll[cache]==4.
|
|
53
|
+
python -m pip install 'cloudoll[cache]==4.3.0'
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
源码开发时可用 `python -m pip install -e '.[mysql,postgres,cache,dev]'` 安装。
|
|
@@ -546,8 +546,8 @@ cloudoll restart -n myapp
|
|
|
546
546
|
python -m pip install -e '.[mysql,postgres,cache,dev]'
|
|
547
547
|
python -m pytest -q -m 'not integration' -k 'not aws'
|
|
548
548
|
python -m build
|
|
549
|
-
python tests/check_wheel.py dist/cloudoll-4.
|
|
550
|
-
python tests/check_release.py dist/cloudoll-4.
|
|
549
|
+
python tests/check_wheel.py dist/cloudoll-4.3.0-py3-none-any.whl
|
|
550
|
+
python tests/check_release.py dist/cloudoll-4.3.0-py3-none-any.whl
|
|
551
551
|
```
|
|
552
552
|
|
|
553
553
|
最后一条命令用于验证构建出的 wheel,版本号变化后请调整文件名。测试使用模拟数据库连接及本机 HTTP 服务;真实数据库连接和 AWS 故障切换需要单独的集成环境。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.3.0"
|
|
@@ -5,3 +5,7 @@ Before building or changing a Cloudoll application, read `.agents/skills/cloudol
|
|
|
5
5
|
Official documentation is bundled with the installed library: `python -m cloudoll.cli ai list`, `ai read <topic>`, and `ai search <query>`. These commands work offline without MCP. Read `structure`, `config`, `router`, and `validation` before designing an application; read `database` before database work. Consult the installed source when resolving a signature discrepancy; report the discrepancy rather than inventing an API.
|
|
6
6
|
|
|
7
7
|
Prefer the documented scaffold, auto-discovered controllers/middlewares, YAML configuration, framework-owned database lifecycle, typed request validation, and ORM for ordinary CRUD. Low-level alternatives require a task-specific reason. Never silently replace Cloudoll's architecture with patterns from another framework.
|
|
8
|
+
|
|
9
|
+
Ordinary CRUD, joins, filters, aliases, counts and pagination use entity/Query APIs. With initialized application datasources and `orm.default`, omit `.use(db)` in routes and services; explicit binding is for intentional overrides or context-free scripts/tests. Before using raw SQL, verify and state the specific ORM capability gap, not merely that the query contains JOIN or COUNT.
|
|
10
|
+
|
|
11
|
+
Grouping, HAVING and aggregate expressions are also supported. Consult the installed database topic for the full capability inventory; 4.3.0 additions must not be assumed available in an older installed release.
|
|
@@ -49,6 +49,8 @@ python -m cloudoll.cli ai search transaction
|
|
|
49
49
|
.agents/skills/cloudoll/SKILL.md,再通过 cloudoll ai read 查阅相关文档。
|
|
50
50
|
新项目遵循自动发现的 controllers/middlewares、YAML 配置、框架管理的
|
|
51
51
|
数据库生命周期、类型化参数校验和 ORM。若需要偏离默认方案,先说明理由。
|
|
52
|
+
普通 CRUD、联表、筛选、计数和分页用实体 API;配置 orm.default 后,
|
|
53
|
+
应用代码默认省略 .use(db)。原始 SQL 须先确认并说明当前 ORM 的具体能力缺口。
|
|
52
54
|
完成后说明查阅的主题、测试结果和未验证的部分。
|
|
53
55
|
```
|
|
54
56
|
|
|
@@ -6,10 +6,27 @@ icon: logs
|
|
|
6
6
|
|
|
7
7
|
# 更新日志
|
|
8
8
|
|
|
9
|
-
按版本倒序记录 Cloudoll 的主要变化。当前使用方法请以本站 4.
|
|
9
|
+
按版本倒序记录 Cloudoll 的主要变化。当前使用方法请以本站 4.3.0 文档为准,历史功能不代表当前仍保留相同 API。
|
|
10
10
|
|
|
11
11
|
历史版本日期为对应源码提交日期。各版本源码可通过表格中的链接查看。
|
|
12
12
|
|
|
13
|
+
## 4.3.0
|
|
14
|
+
|
|
15
|
+
### ORM 查询与写入
|
|
16
|
+
|
|
17
|
+
- JOIN 新增显式 LEFT/INNER/RIGHT 类型,默认保持 LEFT;增加查询级 `distinct()`、保留聚合与别名列的 `one_dict()`、不消费查询的 `exists()`。
|
|
18
|
+
- 新增参数化 IN、标量、EXISTS/NOT EXISTS 子查询,创建时快照 SQL 与参数,并校验数据源和方言。
|
|
19
|
+
- 新增 `Model.alias()` 表别名和自连接、非递归 CTE、UNION/UNION ALL;区分分支分页与合并结果分页,保留正确计数语义。
|
|
20
|
+
- 新增表达式原子更新和 `for_update()` 行锁,支持 NOWAIT、SKIP LOCKED;事务由调用方管理,不自动重试。
|
|
21
|
+
- 新增单条原子 `upsert()`,显式指定更新列;PostgreSQL 支持冲突目标,MySQL 明确拒绝不能实现的冲突目标选项。
|
|
22
|
+
- 导出 `Relation`、`Subquery` 类型;明确拒绝别名、CTE、UNION 和 JOIN 查询的直接写入,避免静默忽略查询源。
|
|
23
|
+
|
|
24
|
+
### 文档与验证
|
|
25
|
+
|
|
26
|
+
- 补齐 GROUP BY、HAVING、聚合、返回类型和默认数据源示例;普通应用 CRUD 优先实体 API,不必重复 `.use(db)`。
|
|
27
|
+
- 同步 AI 指引与随包文档,说明行锁、唯一约束、版本适用范围和高级查询限制。
|
|
28
|
+
- 增加 MySQL 8.4 / PostgreSQL 16 真实数据库测试,覆盖并发 upsert、回滚、锁竞争、CTE 和 UNION。Aurora 仍未经过真实环境验证。
|
|
29
|
+
|
|
13
30
|
## 4.2.1
|
|
14
31
|
|
|
15
32
|
- 修正 AI 初始化测试对 Windows CRLF 换行的错误假设,继续验证原有内容保留和重复初始化不变。
|
|
@@ -6,12 +6,27 @@ icon: database
|
|
|
6
6
|
|
|
7
7
|
# 数据库与 ORM
|
|
8
8
|
|
|
9
|
-
本页介绍 Cloudoll 4.
|
|
9
|
+
本页介绍 Cloudoll 4.3.0 的原生 MySQL/PostgreSQL 接口,包括保存点、流式查询与 SQL 日志。从 3.x 升级请先阅读[升级说明](/migration)。
|
|
10
|
+
|
|
11
|
+
## 应用代码默认使用实体
|
|
12
|
+
|
|
13
|
+
配置好 `database` 和 `orm.default`、数据库由框架初始化后,在路由及其调用的业务服务中直接写
|
|
14
|
+
`User.select()`、`User.where(...).update(...)`,**默认省略 `.use(db)`**。
|
|
15
|
+
服务拆分为独立类或函数,不会自动丢失当前应用上下文;不必仅为普通 ORM 查询传递 db 参数。
|
|
16
|
+
|
|
17
|
+
筛选、排序、分页、别名、计数和联表都是 ORM 已有能力,不是改写原始 SQL 的理由:
|
|
18
|
+
`select`、`where`、`join`、`order_by`、`limit`、`offset`、字段 `like` / `As` / `count`、
|
|
19
|
+
查询 `count`、`one`、`one_model`、`all`,以及 `insert`、`update`、`delete`。
|
|
20
|
+
Python API 的准确拼写是 `order_by` 和大写 `As`,不是 `orderby` / `as`。
|
|
21
|
+
|
|
22
|
+
只有确实需要覆盖引擎时才用 `.use(engine)`;独立脚本/单元测试没有应用上下文时,
|
|
23
|
+
先建立 `datasource_context` 或显式绑定。离线编译 SQL 使用 `.use(None).… .test()`。
|
|
24
|
+
固定的第二数据源优先用模型 `__datasource__`,不是给每条查询重复传连接。
|
|
10
25
|
|
|
11
26
|
## 安装与连接
|
|
12
27
|
|
|
13
28
|
```sh
|
|
14
|
-
python -m pip install 'cloudoll[mysql,postgres]==4.
|
|
29
|
+
python -m pip install 'cloudoll[mysql,postgres]==4.3.0'
|
|
15
30
|
```
|
|
16
31
|
|
|
17
32
|
Web 应用在 [database 配置](/config)中声明连接,由框架建立、关闭,通过 `request.app.db.别名` 获取。不要每个请求创建一个池,也不要在请求结束时关闭共享池。
|
|
@@ -122,16 +137,16 @@ with datasource_context({"blog": engine}, default="blog"):
|
|
|
122
137
|
|
|
123
138
|
## 查询与返回值
|
|
124
139
|
|
|
125
|
-
|
|
140
|
+
以下片段在已初始化默认数据源的应用异步函数中执行,User 为上面的模型;无需传入 db:
|
|
126
141
|
|
|
127
142
|
```python
|
|
128
|
-
user = await User.
|
|
143
|
+
user = await User.where(User.id == 1).one_model()
|
|
129
144
|
if user is not None:
|
|
130
145
|
print(user.name.value)
|
|
131
146
|
print(user.to_dict(exclude_unset=True))
|
|
132
147
|
|
|
133
148
|
rows = await (
|
|
134
|
-
User
|
|
149
|
+
User
|
|
135
150
|
.select(User.id, User.name)
|
|
136
151
|
.where((User.id > 0) & (User.active == True))
|
|
137
152
|
.order_by(User.id.desc())
|
|
@@ -157,7 +172,7 @@ use(db) 返回独立、可变的 Query,不改变模型类的全局数据库绑
|
|
|
157
172
|
执行 one/all/写入操作会消费并重置查询条件。count() 不清空状态,忽略分页和排序,但保留 DISTINCT 投影语义。可这样分页:
|
|
158
173
|
|
|
159
174
|
```python
|
|
160
|
-
query = User.
|
|
175
|
+
query = User.where(User.active == True)
|
|
161
176
|
total = await query.count()
|
|
162
177
|
rows = await query.clone().order_by(User.id.asc()).limit(20).all()
|
|
163
178
|
sql, params = query.test()
|
|
@@ -172,31 +187,242 @@ class Profile(Model):
|
|
|
172
187
|
user_id = models.IntegerField()
|
|
173
188
|
nickname = models.VarCharField(max_length=100)
|
|
174
189
|
|
|
175
|
-
|
|
176
|
-
User
|
|
190
|
+
query = (
|
|
191
|
+
User
|
|
177
192
|
.join(Profile, User.id == Profile.user_id)
|
|
178
193
|
.select(User.id, User.name, Profile.nickname.As("nickname"))
|
|
179
|
-
.
|
|
194
|
+
.where(User.name.like("%Alice%"))
|
|
180
195
|
)
|
|
196
|
+
total = await query.count()
|
|
197
|
+
rows = await query.clone().order_by(User.id.asc()).limit(20).offset(0).all()
|
|
181
198
|
|
|
182
199
|
groups = await (
|
|
183
|
-
User
|
|
200
|
+
User
|
|
184
201
|
.select(User.active, User.id.count().As("total"))
|
|
185
202
|
.group_by(User.active)
|
|
186
203
|
.all()
|
|
187
204
|
)
|
|
188
205
|
```
|
|
189
206
|
|
|
190
|
-
|
|
207
|
+
join 默认生成 LEFT JOIN;4.3.0 可通过 kind 选择联表类型。同名列应显式取别名。聚合查询不要同时选择未分组、未聚合的列。此处 Profile 示例也需要事先建表。
|
|
208
|
+
|
|
209
|
+
这里的 total 是匹配的联表行数;一对多关系可能使一个 User 出现多行,不能将它自动当成去重用户数。
|
|
210
|
+
查询 `count()` 统计结果行,字段 `User.id.count().As("total")` 用于聚合投影,两者用途不同。
|
|
211
|
+
|
|
212
|
+
### GROUP BY、HAVING 与已有表达式
|
|
213
|
+
|
|
214
|
+
`where` 在分组前过滤行,`having` 在分组后过滤聚合结果。多次调用 where/having 会用 AND 累加。
|
|
215
|
+
以下接口在 4.2.1 已可用:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
query = (
|
|
219
|
+
User.select(User.active, User.id.count().As("total"))
|
|
220
|
+
.where(User.email.not_null())
|
|
221
|
+
.group_by(User.active)
|
|
222
|
+
.having(User.id.count() >= 2)
|
|
223
|
+
.order_by(User.active.asc())
|
|
224
|
+
)
|
|
225
|
+
group_count = await query.count() # 满足 HAVING 的分组数量,不是原始用户数
|
|
226
|
+
groups = await query.clone().limit(20).offset(0).all()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
字段还支持 `sum/avg/min/max`、`distinct`、`count_when/sum_when` 等条件聚合,
|
|
230
|
+
以及 `In([...])`、`not_in([...])`、`between(a, b)`、`not_between(a, b)`、
|
|
231
|
+
`is_null()`、`not_null()`、`like()`、`not_like()`。
|
|
232
|
+
`count_when(condition)` 默认不匹配分支为 NULL;显式传 0 给不匹配分支仍会被 COUNT 计数。
|
|
233
|
+
这些不是使用原始 SQL 的理由。字符串 SQL 条件/排序只接受可信开发者代码,不能拼接用户输入。
|
|
234
|
+
|
|
235
|
+
## 4.3.0 新增查询能力
|
|
236
|
+
|
|
237
|
+
本节接口从 **4.3.0** 开始提供,4.2.1 及更早版本不支持。
|
|
238
|
+
以下示例沿用 User、Profile,并在已初始化的应用上下文中执行;使用前核对项目依赖版本。
|
|
239
|
+
|
|
240
|
+
### 联表类型、去重与单行结果
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
query = (
|
|
244
|
+
User.join(Profile, User.id == Profile.user_id, kind="inner")
|
|
245
|
+
.select(User.id, User.name).distinct()
|
|
246
|
+
)
|
|
247
|
+
total = await query.count()
|
|
248
|
+
rows = await query.clone().order_by(User.id.asc()).limit(20).offset(0).all()
|
|
249
|
+
summary = await User.select(User.id.count().As("total")).one_dict()
|
|
250
|
+
present = await User.where(User.email == "alice@example.test").exists()
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`join(..., kind="left" | "inner" | "right")` 默认仍是 LEFT JOIN,保持兼容。
|
|
254
|
+
`distinct()` 对整行投影去重;字段 `User.id.distinct().count()` 生成 COUNT(DISTINCT ...)。
|
|
255
|
+
`one_dict()` 返回字典或 None,不把别名/聚合列装进实体;原有 one/one_model 返回规则不变。
|
|
256
|
+
`exists()` 返回布尔值,保留筛选、分组和 offset,最多读取一行且不消费原查询;它不保证并发唯一性。
|
|
257
|
+
`count()` 忽略分页、排序和行锁,保留去重、分组与 HAVING。
|
|
258
|
+
|
|
259
|
+
### 参数化子查询
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
ids = Profile.select(Profile.user_id).where(Profile.nickname.like("%Alice%")).subquery()
|
|
263
|
+
users = await User.where(User.id.In(ids)).all()
|
|
264
|
+
related = Profile.where(Profile.user_id == User.id)
|
|
265
|
+
users_with_profile = await User.where_exists(related).all()
|
|
266
|
+
users_without_profile = await User.where_exists(related, negated=True).all()
|
|
267
|
+
predicate = related.exists_expr() # 表达式,不执行查询
|
|
268
|
+
rows = await User.where((User.active == True) & predicate).all()
|
|
269
|
+
summary = await User.select(
|
|
270
|
+
Profile.select(Profile.id.count()).subquery().As("profile_count")
|
|
271
|
+
).one_dict()
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`subquery()` / `exists_expr()` 在创建时快照 SQL 和绑定参数,后续修改原查询不影响快照。
|
|
275
|
+
标量/IN 子查询须显式投影一列;标量查询返回多行时由数据库报错。
|
|
276
|
+
不接受带锁子查询,不能混用不同方言或显式绑定的不同引擎。
|
|
277
|
+
未绑定的离线查询并不选择数据库,执行始终使用外层数据源。
|
|
278
|
+
同表自关联请使用下面的表别名,不能把同一个模型引用直接当作两张不同的表。
|
|
279
|
+
|
|
280
|
+
### 表别名与自连接
|
|
281
|
+
|
|
282
|
+
`Model.alias(name)` 返回只读 `Relation[Model]`,不改模型的表名或字段,也不会立即连接数据库。
|
|
283
|
+
通过 `.c.字段名` 引用别名字段,`.query()` 创建使用默认/模型数据源的独立查询。
|
|
284
|
+
独立脚本可显式 `.use(engine)`;`.use(None)` 只编译 SQL。
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
class Employee(Model):
|
|
288
|
+
__table__ = "employees"
|
|
289
|
+
id = models.IntegerField(primary_key=True)
|
|
290
|
+
manager_id = models.IntegerField()
|
|
291
|
+
name = models.VarCharField(max_length=100)
|
|
292
|
+
|
|
293
|
+
# employees 表须事先创建。
|
|
294
|
+
employee = Employee.alias("employee")
|
|
295
|
+
manager = Employee.alias("manager")
|
|
296
|
+
rows = await (
|
|
297
|
+
employee.query()
|
|
298
|
+
.join(manager, employee.c.manager_id == manager.c.id)
|
|
299
|
+
.select(employee.c.id, employee.c.name, manager.c.name.As("manager_name"))
|
|
300
|
+
.order_by(employee.c.id.asc())
|
|
301
|
+
.all()
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`Employee.from_(employee)` 也可指定查询源;后续引用字段应使用该源的 `.c`。
|
|
306
|
+
字段与命名空间属性冲突时用 `employee.c["names"]` / `employee.c["table"]`。
|
|
307
|
+
标识符会引用转义,但动态列选择仍应由应用白名单控制。不同表引用须使用不同名称。
|
|
308
|
+
以 Relation 为源时,`one()` / `one_dict()` 返回映射,`one_model()` 明确拒绝,避免将投影别名误装成实体。
|
|
309
|
+
别名、CTE、UNION 和 JOIN 查询不支持直接写入;请通过原模型和明确的 where 条件操作。
|
|
310
|
+
|
|
311
|
+
### CTE(公共表表达式)
|
|
312
|
+
|
|
313
|
+
`.cte(name)` 将当前 SELECT 快照成非递归 CTE。可查询它,也可将它作为 JOIN 的一侧:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
totals = (
|
|
317
|
+
Profile.select(Profile.user_id, Profile.id.count().As("total"))
|
|
318
|
+
.group_by(Profile.user_id)
|
|
319
|
+
.having(Profile.id.count() >= 2)
|
|
320
|
+
.cte("profile_totals")
|
|
321
|
+
)
|
|
322
|
+
rows = await (
|
|
323
|
+
User.join(totals, User.id == totals.c.user_id, kind="inner")
|
|
324
|
+
.select(User.id, User.name, totals.c.total)
|
|
325
|
+
.all()
|
|
326
|
+
)
|
|
327
|
+
count = await totals.query().count()
|
|
328
|
+
rows = await totals.query().order_by(totals.c.total.desc()).limit(20).all()
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
CTE 定义自动放在 WITH 中,参数按 SQL 出现顺序绑定。投影列须有唯一名称,
|
|
332
|
+
聚合/运算列须 `.As("名称")`;JOIN 后的 `*` 不能用于创建 CTE,需显式选择列。
|
|
333
|
+
修改原查询不会改变已创建的 CTE。支持基于已有 CTE 再创建新的 CTE,但当前不支持递归 CTE、
|
|
334
|
+
数据修改 CTE 或 materialized 提示。一个 SELECT 中不允许重复 CTE 名称。
|
|
335
|
+
CTE/UNION 查询源不支持 `for_update()`,也不能跨已绑定数据源或方言组合。
|
|
336
|
+
|
|
337
|
+
### UNION 与 UNION ALL
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
active = User.select(User.id, User.name).where(User.active == True)
|
|
341
|
+
named = User.select(User.id, User.name).where(User.name.like("A%"))
|
|
342
|
+
combined = active.union(named) # 完整投影行去重
|
|
343
|
+
total = await combined.count()
|
|
344
|
+
rows = await (
|
|
345
|
+
combined.clone().order_by(combined.c.id.asc()).limit(20).offset(0).all()
|
|
346
|
+
)
|
|
347
|
+
all_rows = await active.union_all(named).all() # 保留重复行
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
UNION 不消费或修改两侧查询,创建时快照;两侧投影列数须一致,并使用有名称的列。
|
|
351
|
+
结果列名取左侧,数据类型是否兼容由数据库判断。聚合列请取别名,并通过 `combined.c.别名` 引用结果。
|
|
352
|
+
两侧原有排序/分页保留在各自分支;UNION 之后的 where/order_by/limit/offset 作用于合并结果。
|
|
353
|
+
`count()` 去掉合并结果的外层分页,保留分支分页和 UNION 的去重语义。
|
|
354
|
+
除非明确排序,否则结果顺序不作保证。MySQL 的 CTE / 本文组合查询要求 MySQL 8 系列;实测版本见本节末尾。
|
|
355
|
+
|
|
356
|
+
### Upsert(冲突时更新)
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
changed = await User.upsert(
|
|
360
|
+
{"id": 1, "name": "Alice", "email": "alice@example.test"},
|
|
361
|
+
update_fields=["name", "email"],
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# 仅 PostgreSQL:实际数据库须已为 email 建立对应唯一约束/唯一索引。
|
|
365
|
+
changed = await User.upsert(
|
|
366
|
+
{"name": "Alice", "email": "alice@example.test"},
|
|
367
|
+
conflict_fields=["email"],
|
|
368
|
+
update_fields=["name"],
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`upsert(values, *, update_fields, conflict_fields=None)` 使用数据库单条原子 INSERT/冲突更新,
|
|
373
|
+
不先 SELECT 再决定写入。`values` 是非空字典,插入时应用 ORM 默认值;更新只覆盖显式列出的
|
|
374
|
+
`update_fields`,不会把未列出的字段(包括默认值或自动时间字段)顺便覆盖。
|
|
375
|
+
更新列必须存在于应用默认值后的插入数据中;不允许更新主键,不接受 SQL 表达式值。
|
|
376
|
+
|
|
377
|
+
PostgreSQL 使用 ON CONFLICT,默认冲突目标为模型主键,也可指定 `conflict_fields`;
|
|
378
|
+
这些列必须包含在插入数据中、对应数据库的唯一约束,且不能同时列为更新列。
|
|
379
|
+
MySQL 使用 ON DUPLICATE KEY UPDATE,**任意主键/唯一键冲突都可能触发更新**,
|
|
380
|
+
不能选定某个冲突目标,所以传 `conflict_fields` 会明确报错,不会静默忽略。
|
|
381
|
+
ORM 不自动创建唯一约束。租户隔离需正确设计唯一键;upsert 不替代授权检查。
|
|
382
|
+
|
|
383
|
+
仅支持新建、无查询修饰的 Query;`where(...).upsert(...)` 会报错,不能把 where 误当成冲突更新的权限条件。
|
|
384
|
+
返回布尔写入结果,不返回 ID、不区分插入/更新,也不自动刷新已有实体。
|
|
385
|
+
MySQL 对值完全未变化的写入可能返回 False,不能将 False 等同于记录不存在。
|
|
386
|
+
可在调用方事务内使用,异常按普通写入传播;不会自动重试。当前未提供批量 upsert 或冲突忽略模式。
|
|
387
|
+
|
|
388
|
+
### 原子表达式更新与行锁
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
class Inventory(Model):
|
|
392
|
+
__table__ = "inventory"
|
|
393
|
+
id = models.IntegerField(primary_key=True)
|
|
394
|
+
stock = models.IntegerField()
|
|
395
|
+
|
|
396
|
+
# 表需提前建好;不要先读库存到 Python 再减。
|
|
397
|
+
changed = await Inventory.where(
|
|
398
|
+
(Inventory.id == 1) & (Inventory.stock >= 2)
|
|
399
|
+
).update(stock=Inventory.stock - 2)
|
|
400
|
+
|
|
401
|
+
async with Inventory.transaction():
|
|
402
|
+
item = await Inventory.where(Inventory.id == 1).for_update().one_model()
|
|
403
|
+
if item is not None and item.stock.value >= 2:
|
|
404
|
+
await Inventory.where(Inventory.id == 1).update(stock=Inventory.stock - 2)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
显式 `update(field=表达式)` 支持同表字段运算,常量参数绑定。where 中仍须保留权限、库存等条件。
|
|
408
|
+
返回布尔写入结果,不新增影响行数契约;没有匹配行不会当成成功扣减。
|
|
409
|
+
更新不自动刷新已加载实体,需重新查询;不允许将表达式赋给实体字段后用无参 update() 假装得到真实结果。
|
|
410
|
+
不要依赖多字段 SET 的求值顺序,MySQL 与 PostgreSQL 对相互引用赋值的语义可能不同。
|
|
411
|
+
|
|
412
|
+
`for_update(nowait=True)` 遇到锁竞争立即报错;
|
|
413
|
+
`for_update(skip_locked=True)` 跳过已锁行,适合任务领取,不适合完整性报表。两选项互斥。
|
|
414
|
+
调用方必须显式管理事务;不会自动开启事务或重试。锁查询不能使用只读 stream()。
|
|
415
|
+
聚合、DISTINCT、外联表的锁定限制由数据库决定,示例只锁普通实体行。
|
|
416
|
+
新增能力在原生 MySQL 8.4 / PostgreSQL 16 验证,不将该结果等同于 Aurora 验证。
|
|
191
417
|
|
|
192
418
|
## 写入与字段状态
|
|
193
419
|
|
|
194
420
|
```python
|
|
195
|
-
ok, inserted_id = await User.
|
|
196
|
-
updated = await User.
|
|
197
|
-
deleted = await User.
|
|
421
|
+
ok, inserted_id = await User.insert(name="Alice", email=None)
|
|
422
|
+
updated = await User.where(User.id == 1).update(name="Bob")
|
|
423
|
+
deleted = await User.where(User.id == 2).delete()
|
|
198
424
|
|
|
199
|
-
batch_result = await User.
|
|
425
|
+
batch_result = await User.insert_batch([
|
|
200
426
|
{"name": "Carol", "email": None},
|
|
201
427
|
{"name": "David", "email": None},
|
|
202
428
|
])
|
|
@@ -207,7 +433,7 @@ insert 返回 `(bool, id 或 None)`,不是直接返回 id,且不会自动回
|
|
|
207
433
|
更新和删除必须提供 where 或可识别的主键,避免无意全表写入。查询失败直接抛异常,不会伪装成没有结果。
|
|
208
434
|
|
|
209
435
|
```python
|
|
210
|
-
user = await User.
|
|
436
|
+
user = await User.select(User.id, User.name).where(User.id == 1).one_model()
|
|
211
437
|
if user is not None:
|
|
212
438
|
user.name = "Updated"
|
|
213
439
|
print(user.dirty_fields)
|
|
@@ -232,15 +458,15 @@ print(draft.to_dict(exclude_unset=True))
|
|
|
232
458
|
## 事务与保存点
|
|
233
459
|
|
|
234
460
|
```python
|
|
235
|
-
async with
|
|
236
|
-
await User.
|
|
461
|
+
async with User.transaction():
|
|
462
|
+
await User.where(User.id == 1).update(name="Alice")
|
|
237
463
|
try:
|
|
238
|
-
async with
|
|
239
|
-
await User.
|
|
464
|
+
async with User.transaction(): # 同一数据源的嵌套事务使用保存点
|
|
465
|
+
await User.where(User.id == 2).update(name="Temporary")
|
|
240
466
|
raise ValueError("撤销内层操作")
|
|
241
467
|
except ValueError:
|
|
242
468
|
pass
|
|
243
|
-
await User.
|
|
469
|
+
await User.where(User.id == 3).update(name="Carol")
|
|
244
470
|
```
|
|
245
471
|
|
|
246
472
|
正常退出提交,异常退出回滚。savepoint() 要求已有事务;嵌套 transaction() 自动使用保存点。异常要在保存点块外捕获,让上下文先执行回滚。
|
|
@@ -252,7 +478,7 @@ async with db.transaction():
|
|
|
252
478
|
## 流式查询
|
|
253
479
|
|
|
254
480
|
```python
|
|
255
|
-
query = User.
|
|
481
|
+
query = User.order_by(User.id.asc())
|
|
256
482
|
async with query.stream(batch_size=500) as rows:
|
|
257
483
|
async for row in rows:
|
|
258
484
|
print(row["id"]) # 替换为逐行消费,不要把所有行收集到列表
|
|
@@ -266,6 +492,10 @@ batch_size 为正整数,query_timeout 限制每次打开/批量读取,不是
|
|
|
266
492
|
|
|
267
493
|
## 原始 SQL
|
|
268
494
|
|
|
495
|
+
这是低层接口,不是业务 CRUD 的默认写法。先核对 ORM 是否能表达需要的查询语义;仅在版本化 DDL、数据库专有功能或当前 ORM 确实缺少能力时使用,并在调用处说明具体缺口。普通 JOIN、LIKE、COUNT、分页或“沿用现有 SQL”本身不构成缺口。改写时须保留 JOIN 类型、计数口径、权限条件及事务锁语义,不能机械替换。
|
|
496
|
+
|
|
497
|
+
以下例子只演示低层接口参数绑定;其中的普通查询/更新在应用中应使用上文实体 API。db 是已初始化的引擎。
|
|
498
|
+
|
|
269
499
|
```python
|
|
270
500
|
row = await db.one("SELECT id, name FROM users WHERE id = ?", [1])
|
|
271
501
|
rows = await db.all("SELECT id, name FROM users WHERE id > ?", [0])
|
|
@@ -319,7 +549,7 @@ cloudoll gen -db postgres -c table -t users -p generated_models.py -env local
|
|
|
319
549
|
from typing import Optional
|
|
320
550
|
from cloudoll.orm import Query
|
|
321
551
|
|
|
322
|
-
query: Query[User] = User.
|
|
552
|
+
query: Query[User] = User.where(User.id == 1)
|
|
323
553
|
user: Optional[User] = await query.one_model()
|
|
324
554
|
if user is not None:
|
|
325
555
|
value = user.id.value
|
|
@@ -9,7 +9,7 @@ path: /
|
|
|
9
9
|
|
|
10
10
|
Cloudoll 是基于 aiohttp 的异步 Python Web 库,提供路由、模板、Session、ORM 和 CLI。
|
|
11
11
|
|
|
12
|
-
> 本文档按 Cloudoll 4.
|
|
12
|
+
> 本文档按 Cloudoll 4.3.0 源码更新,实际发布状态以 PyPI 为准。从 3.x 升级前,请先阅读[升级说明](/migration),确认配置和 API 的兼容性变化。
|
|
13
13
|
|
|
14
14
|
## 环境与安装
|
|
15
15
|
|
|
@@ -18,8 +18,8 @@ Cloudoll 是基于 aiohttp 的异步 Python Web 库,提供路由、模板、Se
|
|
|
18
18
|
```sh
|
|
19
19
|
python3 -m venv .venv
|
|
20
20
|
source .venv/bin/activate
|
|
21
|
-
python -m pip install 'cloudoll==4.
|
|
22
|
-
python -m pip install 'cloudoll[mysql,postgres,cache]==4.
|
|
21
|
+
python -m pip install 'cloudoll==4.3.0'
|
|
22
|
+
python -m pip install 'cloudoll[mysql,postgres,cache]==4.3.0'
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Windows PowerShell 激活命令为 `.venv\Scripts\Activate.ps1`。只使用 Web 功能时不需要安装数据库 extras。
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
2
|
+
"version": "4.3.0",
|
|
3
3
|
"documents": {
|
|
4
4
|
"ai": {
|
|
5
5
|
"source": "src/views/ai.md",
|
|
6
6
|
"url": "https://cloudoll.chuchur.com/ai",
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "bb76e472ca409ef182bb8502efe392f17e6436292302631e88c75ecca2498e77"
|
|
8
8
|
},
|
|
9
9
|
"app-start": {
|
|
10
10
|
"source": "src/views/app-start.md",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"changelog": {
|
|
15
15
|
"source": "src/views/changelog.md",
|
|
16
16
|
"url": "https://cloudoll.chuchur.com/changelog",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "8231f060f194c398a03d9489e1d4490509c579e72eee5e2c38bfecf4066e3205"
|
|
18
18
|
},
|
|
19
19
|
"config": {
|
|
20
20
|
"source": "src/views/config.md",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"database": {
|
|
30
30
|
"source": "src/views/database.md",
|
|
31
31
|
"url": "https://cloudoll.chuchur.com/database",
|
|
32
|
-
"sha256": "
|
|
32
|
+
"sha256": "02bb709d17ce16bf3374e5c8999b24e6485273ecd81670ce8143da4bd17359c3"
|
|
33
33
|
},
|
|
34
34
|
"deployment": {
|
|
35
35
|
"source": "src/views/deployment.md",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"start": {
|
|
85
85
|
"source": "src/views/start.md",
|
|
86
86
|
"url": "https://cloudoll.chuchur.com/",
|
|
87
|
-
"sha256": "
|
|
87
|
+
"sha256": "d3e3cf149ce952230b7b3057dce3aaa97405ebc21b489f7c0e2b80ee0dc4d89b"
|
|
88
88
|
},
|
|
89
89
|
"structure": {
|
|
90
90
|
"source": "src/views/structure.md",
|