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.
Files changed (166) hide show
  1. {cloudoll-4.2.1 → cloudoll-4.3.0}/CHANGELOG.md +27 -0
  2. {cloudoll-4.2.1 → cloudoll-4.3.0}/PKG-INFO +7 -7
  3. {cloudoll-4.2.1 → cloudoll-4.3.0}/README.md +6 -6
  4. {cloudoll-4.2.1 → cloudoll-4.3.0}/README.zh-CN.md +9 -9
  5. cloudoll-4.3.0/cloudoll/__init__.py +1 -0
  6. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/AGENTS.md +4 -0
  7. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/ai.md +2 -0
  8. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/changelog.md +18 -1
  9. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/database.md +253 -23
  10. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/start.md +3 -3
  11. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/manifest.json +5 -5
  12. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/skills/cloudoll/SKILL.md +6 -0
  13. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/__init__.py +18 -1
  14. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/compiler.py +152 -8
  15. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/model.py +20 -0
  16. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/query.py +218 -9
  17. cloudoll-4.3.0/cloudoll/orm/relation.py +79 -0
  18. cloudoll-4.3.0/cloudoll/orm/subquery.py +15 -0
  19. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/README.md +1 -1
  20. cloudoll-4.3.0/cloudoll/template/requirements.txt +1 -0
  21. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/PKG-INFO +7 -7
  22. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/SOURCES.txt +3 -0
  23. cloudoll-4.3.0/tests/integration/test_databases.py +576 -0
  24. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_datasources.py +28 -0
  25. cloudoll-4.3.0/tests/test_query_capabilities.py +202 -0
  26. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/query_contract.py +14 -1
  27. cloudoll-4.2.1/cloudoll/__init__.py +0 -1
  28. cloudoll-4.2.1/cloudoll/template/requirements.txt +0 -1
  29. cloudoll-4.2.1/tests/integration/test_databases.py +0 -294
  30. {cloudoll-4.2.1 → cloudoll-4.3.0}/AGENTS.md +0 -0
  31. {cloudoll-4.2.1 → cloudoll-4.3.0}/AI.md +0 -0
  32. {cloudoll-4.2.1 → cloudoll-4.3.0}/LICENSE +0 -0
  33. {cloudoll-4.2.1 → cloudoll-4.3.0}/MANIFEST.in +0 -0
  34. {cloudoll-4.2.1 → cloudoll-4.3.0}/SECURITY.md +0 -0
  35. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/__main__.py +0 -0
  36. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/__init__.py +0 -0
  37. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/commands.py +0 -0
  38. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/app-start.md +0 -0
  39. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/config.md +0 -0
  40. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/cookie-and-session.md +0 -0
  41. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/deployment.md +0 -0
  42. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/event-source.md +0 -0
  43. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/exception.md +0 -0
  44. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/file-uploads.md +0 -0
  45. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/http-client.md +0 -0
  46. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/jwt.md +0 -0
  47. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/logs.md +0 -0
  48. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/middleware.md +0 -0
  49. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/migration.md +0 -0
  50. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/router.md +0 -0
  51. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/structure.md +0 -0
  52. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/validation.md +0 -0
  53. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/view.md +0 -0
  54. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/ai/docs/websockets.md +0 -0
  55. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/cli.py +0 -0
  56. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/__init__.py +0 -0
  57. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/cli_main.py +0 -0
  58. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/m2d.py +0 -0
  59. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/process.py +0 -0
  60. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/schema.py +0 -0
  61. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/spinner.py +0 -0
  62. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/clitool/watch.py +0 -0
  63. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/logging/__init__.py +0 -0
  64. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/mail/__init__.py +0 -0
  65. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/mail/smtp.py +0 -0
  66. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/observability.py +0 -0
  67. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/aws_engine.py +0 -0
  68. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/awsmysql.py +0 -0
  69. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/awspostgres.py +0 -0
  70. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/base.py +0 -0
  71. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/datasources.py +0 -0
  72. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/dialects.py +0 -0
  73. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/engine.py +0 -0
  74. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/field.py +0 -0
  75. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/mysql.py +0 -0
  76. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/parse.py +0 -0
  77. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/postgres.py +0 -0
  78. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/protocols.py +0 -0
  79. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/savepoints.py +0 -0
  80. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/streaming.py +0 -0
  81. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/test_sql.py +0 -0
  82. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/orm/values.py +0 -0
  83. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/py.typed +0 -0
  84. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/__init__.py +0 -0
  85. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/config/conf.local.yaml +0 -0
  86. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/admin/test.py +0 -0
  87. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/api/index.py +0 -0
  88. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/es/es.py +0 -0
  89. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/home/index.py +0 -0
  90. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/upload/upload.py +0 -0
  91. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/controllers/ws/ws.py +0 -0
  92. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/middlewares/auth.py +0 -0
  93. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/static/css/index.css +0 -0
  94. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/static/img/cat.avif +0 -0
  95. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/static/js/index.js +0 -0
  96. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/template/templates/index.html +0 -0
  97. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/__init__.py +0 -0
  98. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/async_tools.py +0 -0
  99. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/common.py +0 -0
  100. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/password.py +0 -0
  101. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/utils/snowflake.py +0 -0
  102. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/__init__.py +0 -0
  103. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/configuration.py +0 -0
  104. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/context.py +0 -0
  105. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/core.py +0 -0
  106. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/exception.py +0 -0
  107. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/html.py +0 -0
  108. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/jwt.py +0 -0
  109. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/lifecycle.py +0 -0
  110. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/request_data.py +0 -0
  111. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/requests.py +0 -0
  112. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/resources.py +0 -0
  113. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/routing.py +0 -0
  114. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/sessions.py +0 -0
  115. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/settings.py +0 -0
  116. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/types.py +0 -0
  117. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll/web/validation.py +0 -0
  118. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/dependency_links.txt +0 -0
  119. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/entry_points.txt +0 -0
  120. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/requires.txt +0 -0
  121. {cloudoll-4.2.1 → cloudoll-4.3.0}/cloudoll.egg-info/top_level.txt +0 -0
  122. {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/architecture.md +0 -0
  123. {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/aurora.md +0 -0
  124. {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/maintenance.md +0 -0
  125. {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/production.md +0 -0
  126. {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/reliability.md +0 -0
  127. {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/schema-generation.md +0 -0
  128. {cloudoll-4.2.1 → cloudoll-4.3.0}/docs/streaming-and-types.md +0 -0
  129. {cloudoll-4.2.1 → cloudoll-4.3.0}/pyproject.toml +0 -0
  130. {cloudoll-4.2.1 → cloudoll-4.3.0}/scripts/sync_ai_docs.py +0 -0
  131. {cloudoll-4.2.1 → cloudoll-4.3.0}/setup.cfg +0 -0
  132. {cloudoll-4.2.1 → cloudoll-4.3.0}/setup.py +0 -0
  133. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/audit_dependencies.py +0 -0
  134. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/check_release.py +0 -0
  135. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/check_wheel.py +0 -0
  136. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/conftest.py +0 -0
  137. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_aws_failover.py +0 -0
  138. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_savepoints.py +0 -0
  139. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_schema.py +0 -0
  140. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/integration/test_streaming.py +0 -0
  141. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/run_local_databases.py +0 -0
  142. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_ai.py +0 -0
  143. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_architecture.py +0 -0
  144. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_dev_lifecycle.py +0 -0
  145. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_field_types.py +0 -0
  146. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_production_contracts.py +0 -0
  147. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_regressions.py +0 -0
  148. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_release_workflow.py +0 -0
  149. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_request_contract.py +0 -0
  150. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_retry_and_query_regressions.py +0 -0
  151. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_review_fixes.py +0 -0
  152. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_save_snapshots_and_cleanup.py +0 -0
  153. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_savepoint_unit.py +0 -0
  154. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_schema_generation.py +0 -0
  155. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_sql_echo.py +0 -0
  156. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_startup_logging.py +0 -0
  157. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_streaming_unit.py +0 -0
  158. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_transactions.py +0 -0
  159. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_typing_regressions.py +0 -0
  160. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/test_validation.py +0 -0
  161. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/engine_contract.py +0 -0
  162. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/field_contract.py +0 -0
  163. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/middleware_contract.py +0 -0
  164. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/production_contract.py +0 -0
  165. {cloudoll-4.2.1 → cloudoll-4.3.0}/tests/typing/request_contract.py +0 -0
  166. {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.2.1
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.2.1 is an aiohttp-based Python library for routing, middleware,
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.2.1'
75
- python -m pip install 'cloudoll[mysql,postgres,cache]==4.2.1'
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.2.1 and receives a random JWT secret. Controllers
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.2.1-py3-none-any.whl
176
- python tests/check_release.py dist/cloudoll-4.2.1-py3-none-any.whl
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.2.1 is an aiohttp-based Python library for routing, middleware,
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.2.1'
23
- python -m pip install 'cloudoll[mysql,postgres,cache]==4.2.1'
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.2.1 and receives a random JWT secret. Controllers
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.2.1-py3-none-any.whl
124
- python tests/check_release.py dist/cloudoll-4.2.1-py3-none-any.whl
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.2.1。从 3.x 升级前请阅读下方迁移说明。
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.2.1:
28
+ 安装 4.3.0:
29
29
 
30
30
  ```sh
31
- python -m pip install 'cloudoll==4.2.1'
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.2.1'
44
+ python -m pip install 'cloudoll[mysql]==4.3.0'
45
45
 
46
46
  # PostgreSQL
47
- python -m pip install 'cloudoll[postgres]==4.2.1'
47
+ python -m pip install 'cloudoll[postgres]==4.3.0'
48
48
 
49
49
  # AWS 数据库驱动
50
- python -m pip install 'cloudoll[aws]==4.2.1'
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.2.1'
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.2.1-py3-none-any.whl
550
- python tests/check_release.py dist/cloudoll-4.2.1-py3-none-any.whl
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.2.1 文档为准,历史功能不代表当前仍保留相同 API。
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.1.0 的原生 MySQL/PostgreSQL 接口,包括保存点、流式查询与 SQL 日志。从 3.x 升级请先阅读[升级说明](/migration)。
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.1.0'
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
- 以下片段均放在异步函数内,db 是已经建立的连接池,User 为上面的模型:
140
+ 以下片段在已初始化默认数据源的应用异步函数中执行,User 为上面的模型;无需传入 db:
126
141
 
127
142
  ```python
128
- user = await User.use(db).where(User.id == 1).one_model()
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.use(db)
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.use(db).where(User.active == True)
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
- rows = await (
176
- User.use(db)
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
- .all()
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.use(db)
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
- 当前 join 生成 LEFT JOIN;同名列应显式取别名。聚合查询不要同时选择未分组、未聚合的列。此处 Profile 示例也需要事先建表。
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.use(db).insert(name="Alice", email=None)
196
- updated = await User.use(db).where(User.id == 1).update(name="Bob")
197
- deleted = await User.use(db).where(User.id == 2).delete()
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.use(db).insert_batch([
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.use(db).select(User.id, User.name).where(User.id == 1).one_model()
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 db.transaction():
236
- await User.use(db).where(User.id == 1).update(name="Alice")
461
+ async with User.transaction():
462
+ await User.where(User.id == 1).update(name="Alice")
237
463
  try:
238
- async with db.savepoint():
239
- await User.use(db).where(User.id == 2).update(name="Temporary")
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.use(db).where(User.id == 3).update(name="Carol")
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.use(db).order_by(User.id.asc())
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.use(db).where(User.id == 1)
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.2.1 源码更新,实际发布状态以 PyPI 为准。从 3.x 升级前,请先阅读[升级说明](/migration),确认配置和 API 的兼容性变化。
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.2.1'
22
- python -m pip install 'cloudoll[mysql,postgres,cache]==4.2.1'
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.1",
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": "752878a272bc9004b8fe83fc1ba2f26300d016a9dac1ab9565bc3c5938a304d7"
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": "c55d20ec3a246d1337a53962aa8602fba717f22fe7dd02c14df67b791366f3fc"
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": "de2000f356f27b3cfe89eccb00eb28c6d151871c2071bb8c742240aa35af2076"
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": "162438f32bd4eec4efec6151c0eea52f5741aa02decf5bbcb06965695eae9fa1"
87
+ "sha256": "d3e3cf149ce952230b7b3057dce3aaa97405ebc21b489f7c0e2b80ee0dc4d89b"
88
88
  },
89
89
  "structure": {
90
90
  "source": "src/views/structure.md",