mongo-2-sql 0.1.6__tar.gz → 0.1.7__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 (80) hide show
  1. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/LICENSE +21 -21
  2. mongo_2_sql-0.1.7/PKG-INFO +612 -0
  3. mongo_2_sql-0.1.7/README.md +581 -0
  4. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/pyproject.toml +61 -46
  5. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/__init__.py +25 -16
  6. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/converter.py +59 -43
  7. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/__init__.py +15 -23
  8. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/cte_manager.py +5 -5
  9. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/cte_stage_base.py +8 -4
  10. mongo_2_sql-0.1.7/src/mongo_2_sql/core/expr_parser_base.py +165 -0
  11. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/pipeline_runtime.py +175 -152
  12. mongo_2_sql-0.1.7/src/mongo_2_sql/core/query_block.py +176 -0
  13. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/stage_base.py +278 -313
  14. mongo_2_sql-0.1.7/src/mongo_2_sql/core/stage_loader.py +138 -0
  15. mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/__init__.py +60 -0
  16. mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/base.py +191 -0
  17. mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/mysql.py +156 -0
  18. mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/postgres.py +152 -0
  19. mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/sqlite.py +167 -0
  20. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/main.py +274 -292
  21. mongo_2_sql-0.1.7/src/mongo_2_sql/renderer/__init__.py +27 -0
  22. mongo_2_sql-0.1.7/src/mongo_2_sql/renderer/sql_renderer.py +44 -0
  23. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/add_fields/__init__.py +9 -0
  24. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/add_fields/expr.py +143 -0
  25. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/add_fields/stage.py +38 -29
  26. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/count/__init__.py +9 -0
  27. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/count/stage.py +116 -0
  28. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/group/__init__.py +10 -10
  29. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/group/expr.py +202 -0
  30. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/group/stage.py +21 -27
  31. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/limit/__init__.py +9 -9
  32. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/limit/stage.py +20 -20
  33. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/lookup/__init__.py +9 -9
  34. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/lookup/stage.py +306 -0
  35. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/match/__init__.py +10 -10
  36. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/match/expr.py +410 -0
  37. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/match/stage.py +67 -67
  38. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/project/__init__.py +10 -10
  39. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/project/expr.py +310 -159
  40. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/project/stage.py +22 -36
  41. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/sort/__init__.py +9 -9
  42. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/sort/stage.py +32 -25
  43. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/sort_by_count/__init__.py +9 -0
  44. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/sort_by_count/stage.py +79 -0
  45. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/unwind/__init__.py +9 -9
  46. mongo_2_sql-0.1.7/src/mongo_2_sql/stages/unwind/stage.py +168 -0
  47. mongo_2_sql-0.1.7/src/mongo_2_sql.egg-info/PKG-INFO +612 -0
  48. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql.egg-info/SOURCES.txt +16 -8
  49. mongo_2_sql-0.1.7/src/mongo_2_sql.egg-info/requires.txt +7 -0
  50. mongo_2_sql-0.1.7/tests/test_dialects.py +510 -0
  51. mongo_2_sql-0.1.7/tests/test_hardening.py +323 -0
  52. mongo_2_sql-0.1.7/tests/test_mysql_pipelines.py +157 -0
  53. mongo_2_sql-0.1.7/tests/test_postgres_pipelines.py +163 -0
  54. mongo_2_sql-0.1.7/tests/test_sqlite_pipelines.py +131 -0
  55. mongo_2_sql-0.1.6/PKG-INFO +0 -658
  56. mongo_2_sql-0.1.6/README.md +0 -632
  57. mongo_2_sql-0.1.6/src/mongo_2_sql/core/ast_base.py +0 -116
  58. mongo_2_sql-0.1.6/src/mongo_2_sql/core/expr_base.py +0 -483
  59. mongo_2_sql-0.1.6/src/mongo_2_sql/core/expr_parser_base.py +0 -358
  60. mongo_2_sql-0.1.6/src/mongo_2_sql/core/expr_registry.py +0 -447
  61. mongo_2_sql-0.1.6/src/mongo_2_sql/core/expression_resolver.py +0 -101
  62. mongo_2_sql-0.1.6/src/mongo_2_sql/core/stage_loader.py +0 -173
  63. mongo_2_sql-0.1.6/src/mongo_2_sql/renderer/__init__.py +0 -9
  64. mongo_2_sql-0.1.6/src/mongo_2_sql/renderer/sqlite.py +0 -234
  65. mongo_2_sql-0.1.6/src/mongo_2_sql/stages/add_fields/__init__.py +0 -9
  66. mongo_2_sql-0.1.6/src/mongo_2_sql/stages/add_fields/expr.py +0 -421
  67. mongo_2_sql-0.1.6/src/mongo_2_sql/stages/group/expr.py +0 -221
  68. mongo_2_sql-0.1.6/src/mongo_2_sql/stages/lookup/stage.py +0 -183
  69. mongo_2_sql-0.1.6/src/mongo_2_sql/stages/match/expr.py +0 -488
  70. mongo_2_sql-0.1.6/src/mongo_2_sql/stages/unwind/stage.py +0 -145
  71. mongo_2_sql-0.1.6/src/mongo_2_sql/utils.py +0 -529
  72. mongo_2_sql-0.1.6/src/mongo_2_sql.egg-info/PKG-INFO +0 -658
  73. mongo_2_sql-0.1.6/src/mongo_2_sql.egg-info/requires.txt +0 -4
  74. mongo_2_sql-0.1.6/tests/test.py +0 -412
  75. mongo_2_sql-0.1.6/tests/test1.py +0 -74
  76. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/setup.cfg +0 -0
  77. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/__init__.py +0 -0
  78. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql.egg-info/dependency_links.txt +0 -0
  79. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql.egg-info/entry_points.txt +0 -0
  80. {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql.egg-info/top_level.txt +0 -0
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 baobingbo
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 baobingbo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,612 @@
1
+ Metadata-Version: 2.4
2
+ Name: mongo-2-sql
3
+ Version: 0.1.7
4
+ Summary: A library to convert MongoDB aggregation pipelines to SQL parser
5
+ Author-email: Bao Bingbo <baob2@outlook.com>
6
+ License: MIT License
7
+ Project-URL: Homepage, https://github.com/baobingbo/mongo-2-sql
8
+ Project-URL: Documentation, https://github.com/baobingbo/mongo-2-sql#readme
9
+ Project-URL: Repository, https://github.com/baobingbo/mongo-2-sql.git
10
+ Project-URL: Issues, https://github.com/baobingbo/mongo-2-sql/issues
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Database
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: sqlparse>=0.5.5
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: ruff>=0.4; extra == "dev"
28
+ Requires-Dist: build>=1.0; extra == "dev"
29
+ Requires-Dist: twine>=5; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # Mongo to SQL
33
+
34
+ 将 MongoDB 聚合管道(aggregation pipeline)转换为 SQL 查询语句的 Python 工具,支持 **SQLite / MySQL / PostgreSQL** 三种方言。
35
+
36
+ - **GitHub**: [baobingbo/mongo-2-sql](https://github.com/baobingbo/mongo-2-sql)
37
+ - **版本**: v0.1.7(Python >= 3.10,当前在 3.13 验证)
38
+
39
+ ## 功能特性
40
+
41
+ - **12 个聚合阶段**: `$match` `$project` `$group` `$sort` `$limit` `$skip` `$lookup` `$unwind` `$addFields`/`$set` `$count` `$sortByCount`
42
+ - **三方言支持**: 通过 `SQLDialect` 抽象层统一处理字符串拼接、TRIM、子串、日期、类型判断、标识符引用、LIMIT/OFFSET 等差异
43
+ - **复杂管道自动 CTE**: `$project`/`$group`/`$addFields`/`$unwind`/`$count` 以 CTE 串联;`$sort`/`$limit`/`$skip` 的位置语义会正确固化到 CTE 内层(含分组前派生表下推)
44
+ - **安全转义**: 字符串字面量、`$regex` → LIKE、表名/字段名(保留字智能引用)均按方言规则转义
45
+ - **显式错误**: 不支持的语法(`$text`、`$where`、`$first` 等)一律抛错,不静默丢字段
46
+ - **CLI**: `mongo2sql` 命令行工具(文件 / 参数 / stdin 三种输入)
47
+
48
+ ## 安装
49
+
50
+ ### 方式一:PyPI 安装
51
+
52
+ ```bash
53
+ # 生产环境:仅安装运行时依赖(sqlparse)
54
+ pip install mongo-2-sql
55
+
56
+ # 开发/测试环境:额外安装 pytest + ruff + build + twine
57
+ pip install mongo-2-sql[dev]
58
+ ```
59
+
60
+ ### 方式二:uv(源码开发,推荐)
61
+
62
+ 依赖已在 `pyproject.toml` 中区分:**生产依赖**(`dependencies`,仅 `sqlparse`)与
63
+ **测试/构建工具**(`[dev]` extra,pytest / ruff / build / twine)。锁定文件为 `uv.lock`:
64
+
65
+ ```bash
66
+ git clone https://github.com/baobingbo/mongo-2-sql.git
67
+ cd mongo-2-sql
68
+
69
+ uv sync # 生产环境:只装 mongo-2-sql(editable) + sqlparse
70
+ uv sync --extra dev # 测试/开发环境:额外安装 pytest、ruff、build、twine
71
+ ```
72
+
73
+ 两种环境可随时互相切换(`uv sync` 会自动增删到与锁定状态一致):
74
+
75
+ | 命令 | 安装内容 |
76
+ |------|----------|
77
+ | `uv sync` | 项目本体(editable)+ `sqlparse` |
78
+ | `uv sync --extra dev` | 上述 + `pytest` `ruff` `build` `twine`(跑测试 / 打包装发布) |
79
+
80
+ 非 uv 的 pip 用户等价命令:`pip install -e .` / `pip install -e ".[dev]"`。
81
+
82
+ ## 快速开始
83
+
84
+ ```python
85
+ from mongo_2_sql import convert_mongo_pipeline_to_sql
86
+
87
+ pipeline = [
88
+ {"$match": {"status": "active", "age": {"$gte": 18}}},
89
+ {"$group": {"_id": "$category", "count": {"$sum": 1}}},
90
+ {"$sort": {"count": -1}},
91
+ {"$limit": 10},
92
+ ]
93
+
94
+ sql, metadata = convert_mongo_pipeline_to_sql(pipeline, "users")
95
+ print(sql)
96
+ ```
97
+
98
+ 输出(`$group` 等重塑结果集的阶段自动包裹为 CTE):
99
+
100
+ ```sql
101
+ WITH cte_1 AS
102
+ (SELECT t.category AS _id,
103
+ COUNT(*) AS count
104
+ FROM users AS t
105
+ WHERE 1=1
106
+ AND (t.status = 'active')
107
+ AND (t.age >= 18)
108
+ GROUP BY t.category)
109
+ SELECT *
110
+ FROM cte_1 AS t
111
+ ORDER BY t.count DESC
112
+ LIMIT 10
113
+ ```
114
+
115
+ `metadata` 同时给出结构信息(见 `元数据说明`),可用于检查与调试。
116
+
117
+ ## 指定 SQL 方言
118
+
119
+ `dialect` 参数接受以下名称(大小写不敏感):
120
+
121
+ | 方言 | 可用名称 |
122
+ |------|----------|
123
+ | SQLite(默认) | `sqlite` |
124
+ | MySQL / MariaDB | `mysql`、`mariadb` |
125
+ | PostgreSQL | `postgresql`、`postgres`、`pg` |
126
+
127
+ 同一条管道在三种方言下的真实输出对比(注意 `CONCAT` vs ```、`TRIM` 写法、`$type` 判定、`$year` 实现、标识符引用差异):
128
+
129
+ ```python
130
+ pipeline = [
131
+ {"$match": {"name": {"$type": "string"}}},
132
+ {"$project": {
133
+ "full": {"$concat": ["$first", " ", "$last"]},
134
+ "nick": {"$trim": {"input": "$name", "chars": "x"}},
135
+ "yr": {"$year": "$birth"},
136
+ }},
137
+ ]
138
+
139
+ for dialect in ("sqlite", "mysql", "postgresql"):
140
+ sql, meta = convert_mongo_pipeline_to_sql(pipeline, "users", dialect=dialect)
141
+ print(f"--- {dialect} ---")
142
+ print(sql)
143
+ ```
144
+
145
+ ```sql
146
+ --- sqlite ---
147
+ WITH cte_1 AS
148
+ (SELECT (t.first || ' ' || t.last) AS "full",
149
+ TRIM(t.name, 'x') AS nick,
150
+ CAST(STRFTIME('%Y', t.birth) AS INTEGER) AS yr
151
+ FROM users AS t
152
+ WHERE 1=1
153
+ AND (TYPEOF(t.name) = 'text'))
154
+ SELECT *
155
+ FROM cte_1 AS t
156
+
157
+ --- mysql ---
158
+ WITH cte_1 AS
159
+ (SELECT CONCAT(t.first, ' ', t.last) AS `full`,
160
+ TRIM(BOTH 'x'
161
+ FROM t.name) AS nick,
162
+ EXTRACT(YEAR
163
+ FROM t.birth) AS yr
164
+ FROM users AS t
165
+ WHERE 1=1
166
+ AND (t.name IS NOT NULL
167
+ AND t.name NOT REGEXP '^-?[0-9.]+$'))
168
+ SELECT *
169
+ FROM cte_1 AS t
170
+
171
+ --- postgresql ---
172
+ WITH cte_1 AS
173
+ (SELECT (t.first || ' ' || t.last) AS "full",
174
+ TRIM(BOTH 'x'
175
+ FROM t.name) AS nick,
176
+ CAST(EXTRACT(YEAR
177
+ FROM t.birth) AS INTEGER) AS yr
178
+ FROM users AS t
179
+ WHERE 1=1
180
+ AND (pg_typeof(t.name) IN ('text'::regtype,
181
+ 'character varying'::regtype,
182
+ 'character'::regtype,
183
+ 'name'::regtype)))
184
+ SELECT *
185
+ FROM cte_1 AS t
186
+ ```
187
+
188
+ **方言覆盖现状:**
189
+
190
+ | 方言 | 关系型核心算子 | JSON / 数组算子 |
191
+ |------|----------------|-----------------|
192
+ | SQLite | `✅` 完整 | `✅` 完整(JSON1:`$unwind`/`$size`/`$slice`/`$isArray`/`$concatArrays` 等) |
193
+ | MySQL | `✅` 完整 | `🟡` `$all`(JSON_CONTAINS)、`$stdDevPop/Samp`(STDDEV_POP/SAMP)、`$push/$addToSet`(JSON_ARRAYAGG)为原生实现;`$unwind`/`$size`/`$slice` 等回退 SQLite 风格语法 + 警告 |
194
+ | PostgreSQL | `✅` 完整 | `🟡` `$all`(jsonb `@>`)、`$stdDevPop/Samp`(STDDEV)、`$push/$addToSet`(jsonb_agg)为原生实现;其余同上回退 + 警告 |
195
+
196
+ > 回退不会中断转换:产生明确警告(`metadata['warnings']` / CLI stderr),生成的 SQL 为 SQLite 风格,需目标库实际具备 JSON1 类函数才可执行。
197
+
198
+ ## 命令行用法
199
+
200
+ `mongo2sql` 安装后由 `pip install mongo-2-sql` 自动注册(等价于 `python -m mongo_2_sql.main`):
201
+
202
+ ```bash
203
+ # 从 JSON 文件转换
204
+ mongo2sql -f pipeline.json -c users
205
+
206
+ # 从命令行参数转换(pipeline 为 JSON 字符串)
207
+ mongo2sql -p '[{"$match": {"status": "active"}}]' -c users
208
+
209
+ # 从标准输入转换
210
+ echo '[{"$match": {"status": "active"}}]' | mongo2sql -c users
211
+
212
+ # 指定方言(sqlite / mysql / mariadb / postgresql / postgres / pg)
213
+ mongo2sql -f pipeline.json -c users -d mysql
214
+
215
+ # 输出到文件(SQL 写 -o,状态与警告走 stderr)
216
+ mongo2sql -f pipeline.json -c users -o out.sql
217
+
218
+ # 附带元数据注释输出
219
+ mongo2sql -f pipeline.json -c users --metadata
220
+
221
+ # 跳过管道格式校验
222
+ mongo2sql -f pipeline.json -c users --no-validate
223
+
224
+ # 严格模式:转换产生警告时退出码为 1(便于 CI 拦截回退语法)
225
+ mongo2sql -f pipeline.json -c users -d mysql --strict
226
+
227
+ # 列出支持的阶段
228
+ mongo2sql --list-stages
229
+ ```
230
+
231
+ `pipeline.json` 内容就是一个 MongoDB 聚合管道数组:
232
+
233
+ ```json
234
+ [
235
+ {"$match": {"status": "active", "age": {"$gte": 18}}},
236
+ {"$group": {"_id": "$category", "totalAmount": {"$sum": "$amount"}, "count": {"$sum": 1}}},
237
+ {"$project": {"_id": 1, "totalAmount": 1, "count": 1}},
238
+ {"$sort": {"totalAmount": -1}},
239
+ {"$limit": 5}
240
+ ]
241
+ ```
242
+
243
+ `-c` 为必填(源集合 / 表名)。错误与警告输出到 stderr,SQL 输出到 stdout(或 `-o` 指定文件)。
244
+
245
+ ## API 文档
246
+
247
+ ### 主要函数
248
+
249
+ ```python
250
+ convert_mongo_pipeline_to_sql(
251
+ pipeline,
252
+ collection_name,
253
+ dialect='sqlite',
254
+ validate=True,
255
+ stage_loader=None,
256
+ ) -> tuple[str, dict]
257
+ ```
258
+
259
+ | 参数 | 说明 |
260
+ |------|------|
261
+ | `pipeline` | 聚合管道(list[dict]),空列表生成 `SELECT * FROM <collection>` |
262
+ | `collection_name` | 集合名 / 源表名(自动智能引用,含特殊字符也安全) |
263
+ | `dialect` | `'sqlite'` / `'mysql'` / `'mariadb'` / `'postgresql'` / `'postgres'` / `'pg'` |
264
+ | `validate` | 是否先做管道结构校验(默认 True) |
265
+ | `stage_loader` | 可选的 `StageLoader` 实例(用于注入自定义阶段,见 `扩展`) |
266
+
267
+ 返回 `(sql, metadata)`。异常:`ValueError`(管道结构无效 / 未知方言)、`RuntimeError`(阶段处理失败,错误信息含具体原因)。
268
+
269
+ ### 元数据说明
270
+
271
+ `metadata` 字典的键:
272
+
273
+ | 键 | 说明 |
274
+ |----|------|
275
+ | `dialect` / `collection` | 回显输入 |
276
+ | `stages_processed` | 实际执行的阶段数 |
277
+ | `has_aggregation` | 是否包含 `$group` |
278
+ | `warnings` | 列表;方言回退等提示(空列表 = 纯原生转换) |
279
+ | `select_columns` / `where_conditions` / `group_by_columns` / `order_by_columns` | 外层查询的各子句成分 |
280
+ | `limit` / `offset` | 外层 `LIMIT`/`OFFSET`(上游位置语义固化进 CTE 后此处为 None) |
281
+
282
+ ### 面向对象用法
283
+
284
+ ```python
285
+ from mongo_2_sql import MongoToSQLConverter
286
+
287
+ converter = MongoToSQLConverter(dialect='sqlite') # 可选 stage_loader 参数
288
+
289
+ sql, metadata = converter.convert(pipeline, "users") # 转换整个管道
290
+ sql1 = converter.convert_single_stage({"$match": {"age": {"$gt": 18}}}, "users") # 单阶段
291
+ print(converter.get_supported_stages()) # 支持的阶段列表
292
+ print(converter.is_stage_supported('$match')) # True
293
+ ```
294
+
295
+ ### 便捷函数(单阶段片段)
296
+
297
+ 三个函数均支持 `dialect` 参数(默认 `'sqlite'`,影响转义 / ESCAPE / 保留字引用):
298
+
299
+ ```python
300
+ from mongo_2_sql import mongo_match_to_sql, mongo_project_to_sql, mongo_group_to_sql
301
+
302
+ # WHERE 片段(不含 WHERE 关键字)
303
+ mongo_match_to_sql({"status": "active", "age": {"$gte": 18}}, table_name='t')
304
+
305
+ # SELECT 列列表
306
+ mongo_project_to_sql({"name": 1, "full": {"$concat": ["$first", " ", "$last"]}})
307
+
308
+ # (GROUP BY 列, SELECT 列) 元组
309
+ mongo_group_to_sql({"_id": "$category", "count": {"$sum": 1}})
310
+ ```
311
+
312
+ ### 方言工厂
313
+
314
+ ```python
315
+ from mongo_2_sql import get_dialect, SQLiteDialect, MySQLDialect, PostgreSQLDialect
316
+
317
+ dialect = get_dialect('mysql') # 未知名称抛 ValueError
318
+ dialect.quote_string("a'b") # 直接获取方言级 SQL 片段
319
+ ```
320
+
321
+ ## 支持的阶段
322
+
323
+ | 阶段 | SQL 对应 | 说明 |
324
+ |------|---------|------|
325
+ | `$match` | `WHERE` | 全算子支持(见下) |
326
+ | `$project` | `SELECT` / CTE | 包含模式 + 表达式(`\_id: 0` 可排除 _id);全 0 排除模式需要 schema,**显式报错** |
327
+ | `$addFields` / `$set` | CTE(新列在前,同名字段覆盖旧值) | 字面量 / 字段引用 / 表达式三类值 |
328
+ | `$group` | CTE(`GROUP BY`) | `_id` 支持 `$field` / 常量 / `null` / 复合 dict / 表达式 |
329
+ | `$sortByCount` | `GROUP BY` + `ORDER BY` | 字符串或 dict 形式 |
330
+ | `$sort` | `ORDER BY` | `1`/`-1` 或 `'asc'`/`'desc'`;`0` 与 `$meta` 报错 |
331
+ | `$limit` / `$skip` | `LIMIT` / `OFFSET` | 多次 `$limit` 取最小值,多次 `$skip` 累加 |
332
+ | `$count` | `COUNT(*) AS name` | 上游过滤 / 截断先固化进 CTE 内层 |
333
+ | `$lookup` | `LEFT JOIN`(简单式)/ CTE 子查询(管道式) | 简单式为 1:1 语义,见 `限制` |
334
+ | `$unwind` | CTE + `CROSS JOIN JSON_EACH` | 支持 `preserveNullAndEmptyArrays`(LEFT JOIN)、`includeArrayIndex`;仅 JSON 字符串数组 |
335
+
336
+ ## 支持的操作符
337
+
338
+ ### `$match`
339
+
340
+ - **比较**: `=`、`$ne`、`$gt`、`$gte`、`$lt`、`$lte`、`$in`、`$nin`(`$in: []` → 永假;null 字面量 → `IS NULL` / `IS NOT NULL`)
341
+ - **逻辑**: `$and`、`$or`、`$nor`、`$not`(空数组语义与 MongoDB 一致:`$and: []` → `1=1`,`$or: []` → `1=0`)
342
+ - **元素**: `$exists`、`$type`、`$regex`(+`$options: 'i'`)、`$size`(仅整数)、`$all`(元素级匹配,非子串)
343
+ - `$regex` 转为 `LIKE ... ESCAPE`:支持字面字符、`.`(→`_`)、首尾 `^`/`$` 锚点;`* + ( ) [ ] { } |` 等特性**显式报错**
344
+ - 不支持(显式报错):`$text`、`$where`、`$expr`、`$elemMatch`、`$mod`、点号路径 / 子文档匹配
345
+
346
+ ### 聚合函数(`$group` 累加器)
347
+
348
+ - `$count`、`$sum`(`1` → `COUNT(*)`)、`$avg`、`$min`、`$max`
349
+ - `$push` / `$addToSet` → 方言原生 JSON 数组聚合
350
+ - `$stdDevPop` / `$stdDevSamp` → MySQL/PostgreSQL 原生;**SQLite 显式报错**
351
+ - `$first` / `$last` → **显式报错**(关系库无确定等价语义)
352
+
353
+ ### 表达式(`$project` / `$addFields`)
354
+
355
+ - **条件**: `$cond`(文档式 + 数组式)、`$ifNull`、`$switch`
356
+ - **算术**: `$add` / `$subtract` / `$multiply` / `$divide`(方言除法)/ `$mod`(`%`)
357
+ - **字符串**: `$concat`、`$toLower`/`$toUpper`、`$substr`/`$substrBytes`/`$substrCP`(0-based → 1-based 自动转换)、`$trim`/`$ltrim`/`$rtrim`(`chars`)、`$strLenCP`/`$strLenBytes`、`$toString`
358
+ - **数组**: `$size`、`$arrayElemAt`、`$slice`(三种写法)、`$isArray`、`$concatArrays`
359
+ - **日期**: `$year`/`$month`/`$dayOfMonth`/`$hour`/`$minute`/`$second`、`$dateToString`
360
+ - **内联聚合**: 投影内的 `$sum`/`$avg`/`$min`/`$max`
361
+
362
+ ## 语义与限制(重要)
363
+
364
+ 1. **`$lookup` 简单式是 1:1 语义**:生成等值 `LEFT JOIN`,一个文档匹配多条对端记录时结果行数会相乘(MongoDB 是存入匹配数组)。多对多请使用**管道式 `$lookup`**(`let` + `$expr` 可推导关联条件),或先在对端管道内聚合。
365
+ 2. **位置语义严格对应 MongoDB**:`$sort`/`$limit`/`$skip` 作用在随后 CTE 阶段(`$group`/`$project`/`$unwind`/`$count`)**之前**的行;单独 `$skip` 按方言生成(SQLite `LIMIT -1 OFFSET n`、MySQL `LIMIT 18446744073709551615 OFFSET n`、PostgreSQL 裸 `OFFSET n`)。
366
+ 3. **`$unwind` 只支持 JSON 字符串数组列**(基表),展开后列与原列同名(SQLite 取首个出现 = 展开值);仅 SQLite 为原生实现,MySQL/PG 回退 + 警告。
367
+ 4. **`$project` 排除模式(所有字段均为 0)不支持**——需要 schema 元数据才能表达"除某列外全选",显式报错。`_id: 0`(包含模式下排除 _id 字段)是合法用法,支持。
368
+ 5. **不支持的语法显式报错**(不静默丢弃):`$text`/`$where`/`$expr`/`$elemMatch`(`$match` 内)、`$first`/`$last`、`$sort: 0`、`$meta` 排序、未知操作符。
369
+ 6. **字段引用**:仅支持平铺列(`t.field`),点号嵌套路径(`a.b`)报错。
370
+
371
+ ## 示例
372
+
373
+ ### 示例 1: 过滤
374
+
375
+ ```python
376
+ pipeline = [{"$match": {"status": "active"}}]
377
+ ```
378
+ ```sql
379
+ SELECT *
380
+ FROM users AS t
381
+ WHERE 1=1
382
+ AND (t.status = 'active')
383
+ ```
384
+
385
+ ### 示例 2: 多条件组合
386
+
387
+ ```python
388
+ pipeline = [{"$match": {"age": {"$gte": 18, "$lt": 65}}}]
389
+ ```
390
+
391
+ ```sql
392
+ SELECT *
393
+ FROM users AS t
394
+ WHERE 1=1
395
+ AND ((t.age >= 18)
396
+ AND (t.age < 65))
397
+ ```
398
+
399
+ ### 示例 3: 分组聚合 + 排序 + 截断
400
+
401
+ ```python
402
+ pipeline = [
403
+ {"$group": {"_id": "$category", "count": {"$sum": 1}, "avgPrice": {"$avg": "$price"}}},
404
+ {"$sort": {"count": -1}},
405
+ {"$limit": 10},
406
+ ]
407
+ sql, _ = convert_mongo_pipeline_to_sql(pipeline, "products")
408
+ ```
409
+ ```sql
410
+ WITH cte_1 AS
411
+ (SELECT t.category AS _id,
412
+ COUNT(*) AS count,
413
+ AVG(t.price) AS avgPrice
414
+ FROM products AS t
415
+ GROUP BY t.category)
416
+ SELECT *
417
+ FROM cte_1 AS t
418
+ ORDER BY t.count DESC
419
+ LIMIT 10
420
+ ```
421
+
422
+ ### 示例 4: 字段重命名 + 计算字段
423
+
424
+ ```python
425
+ pipeline = [
426
+ {"$project": {
427
+ "product_name": "$name",
428
+ "total_value": {"$multiply": ["$price", "$quantity"]},
429
+ }}
430
+ ]
431
+ ```
432
+ ```sql
433
+ WITH cte_1 AS
434
+ (SELECT t.name AS product_name,
435
+ (t.price * t.quantity) AS total_value
436
+ FROM products AS t)
437
+ SELECT *
438
+ FROM cte_1 AS t
439
+ ```
440
+
441
+ ### 示例 5: 字符串连接(方言差异见 `指定 SQL 方言`)
442
+
443
+ ```python
444
+ pipeline = [{"$project": {"fullName": {"$concat": ["$firstName", " ", "$lastName"]}}}]
445
+ sql, _ = convert_mongo_pipeline_to_sql(pipeline, "users")
446
+ ```
447
+
448
+ ```sql
449
+ WITH cte_1 AS
450
+ (SELECT (t.firstName || ' ' || t.lastName) AS fullName
451
+ FROM users AS t)
452
+ SELECT *
453
+ FROM cte_1 AS t
454
+ ```
455
+
456
+ ### 示例 6: 关联查询(`$lookup`)
457
+
458
+ ```python
459
+ pipeline = [
460
+ {"$lookup": {
461
+ "from": "orders",
462
+ "localField": "_id",
463
+ "foreignField": "customerId",
464
+ "as": "orders",
465
+ }}
466
+ ]
467
+ ```
468
+ ```sql
469
+ SELECT *
470
+ FROM customers AS t
471
+ LEFT JOIN orders AS orders ON t._id = orders.customerId
472
+ ```
473
+
474
+ ### 示例 7: 条件表达式 + 分组(`$switch` / `$cond`)
475
+
476
+ ```python
477
+ pipeline = [
478
+ {"$addFields": {"grade": {"$switch": {
479
+ "branches": [
480
+ {"case": {"$gte": ["$score", 90]}, "then": "A"},
481
+ {"case": {"$gte": ["$score", 60]}, "then": "B"},
482
+ ],
483
+ "default": "C",
484
+ }}}},
485
+ {"$group": {"_id": "$grade", "n": {"$sum": 1}}},
486
+ {"$sort": {"_id": 1}},
487
+ ]
488
+ ```
489
+
490
+ 生成 `CASE WHEN` 的 CTE 后按等级分组(完整输出参见 `tests/pipeline_suite.py` 中 `addfields_*` / `group_*` 用例的 golden 文件)。
491
+
492
+ ## 高级特性
493
+
494
+ ### 自定义阶段处理器
495
+
496
+ ```python
497
+ from mongo_2_sql.core.stage_base import StageProcessor, RenderContext
498
+ from mongo_2_sql.core.stage_loader import StageLoader
499
+ from mongo_2_sql import convert_mongo_pipeline_to_sql
500
+
501
+ class CustomStage(StageProcessor):
502
+ def __init__(self):
503
+ super().__init__('$custom')
504
+
505
+ def process(self, stage_value, context: RenderContext) -> None:
506
+ # 自定义处理逻辑:向 context 写入 select_columns / where_conditions 等
507
+ ...
508
+
509
+ def validate(self, stage_value) -> bool:
510
+ return True
511
+
512
+ loader = StageLoader()
513
+ loader.register_processor('$custom', CustomStage)
514
+
515
+ sql, _ = convert_mongo_pipeline_to_sql(
516
+ [{"$custom": {"foo": 1}}], "users", stage_loader=loader
517
+ )
518
+ ```
519
+
520
+ ### 自定义方言
521
+
522
+ 继承 `SQLDialect` 实现抽象方法(`quote_ident`/`quote_string`/`escape_char`/`limit_offset`/`concat`/`trim`/`substr`/`date_part` 等),再在 `mongo_2_sql/dialects/__init__.py` 的 `_DIALECT_REGISTRY` 注册名称即可——CLI 的 `-d` 选项自动跟随。
523
+
524
+ ### 错误处理最佳实践
525
+
526
+ ```python
527
+ try:
528
+ sql, metadata = convert_mongo_pipeline_to_sql(pipeline, "users")
529
+ if metadata['warnings']:
530
+ for w in metadata['warnings']:
531
+ print(f"警告: {w}")
532
+ print(sql)
533
+ except ValueError as e:
534
+ print(f"管道格式错误: {e}")
535
+ except RuntimeError as e:
536
+ print(f"转换错误: {e}") # 信息含具体阶段与原因
537
+ ```
538
+
539
+ ## 测试
540
+
541
+ ```bash
542
+ # 全量测试(398 条 = 差异/硬化基线 72 + 三方言 pipeline 套件 326)
543
+ python -m pytest tests -q
544
+
545
+ # 三方言 pipeline 套件使用统一 golden 文件(tests/golden/<方言>/<case>.sql),
546
+ # 转换结果变化后重新生成:
547
+ PIPGEN=1 python -m pytest tests/test_sqlite_pipelines.py tests/test_mysql_pipelines.py tests/test_postgres_pipelines.py -q
548
+ (`PIPGEN=1` 为 Linux/macOS 前缀写法;Windows PowerShell 先执行 `$env:PIPGEN='1'`)
549
+
550
+ # 代码风格
551
+ python -m ruff check .
552
+ ```
553
+
554
+ 套件说明:`tests/pipeline_suite.py` 定义 64 个三方言逐字共用的 pipeline case(expected_rows 由 SQLite 参考引擎真实执行取得);SQLite 脚本额外真实执行并严格断言行值,MySQL/PostgreSQL 脚本做 golden 对比与回退警告断言。
555
+
556
+ ## 常见问题
557
+
558
+ ### Q: 为什么生成的 SQL 中有 `1=1`?
559
+
560
+ A: 占位条件,便于多条件动态拼接(`WHERE 1=1 AND ...`),可安全忽略。
561
+
562
+ ### Q: 支持哪些 SQL 方言?
563
+
564
+ A: SQLite(完整)、MySQL/MariaDB、PostgreSQL(关系算子完整 + 部分 JSON 算子原生,其余回退 + 警告,见 `方言覆盖现状`)。新增方言只需继承 `SQLDialect` 并注册。
565
+
566
+ ### Q: `$lookup` 多对多怎么办?
567
+
568
+ A: 简单式 `$lookup` 是 1:1 的 LEFT JOIN 语义;多对多请用管道式 `$lookup`(let + `$expr`),或在对端 pipeline 中先 `$group` + `$push` 聚合。
569
+
570
+ ### Q: 为什么某些语法直接报错而不是生成近似 SQL?
571
+
572
+ A: 设计原则是"宁错勿假"——排除投影、`$first/$last`、`$text` 等在没有 schema 或确定等价语义时生成 SQL 会悄悄给错结果,因此显式抛出带原因的错误。
573
+
574
+ ## 贡献
575
+
576
+ 1. Fork 项目
577
+ 2. 创建功能分支(`git checkout -b feature/amazing-feature`)
578
+ 3. 提交更改(`git commit -m 'Add amazing feature'`)
579
+ 4. 推送并创建 Pull Request
580
+ 5. 确保 `pytest tests -q` 与 `ruff check .` 全绿
581
+
582
+ ## 许可证
583
+
584
+ MIT License - 详见 [LICENSE](LICENSE) 文件
585
+
586
+ ## 变更日志
587
+
588
+ ### v0.1.7
589
+
590
+ - **安全与正确性大重构**:字符串 / 正则 / 表名字段名全面按方言转义(`$regex` → `LIKE ... ESCAPE`);标识符保留字智能引用;`$sort/$limit/$skip` 位置语义固化进 CTE 内层(含分组前派生表下推)
591
+ - **显式错误**:未知算子、`$first/$last`、排除投影、`$text/$where/$expr/$elemMatch`、`$sort: 0` 等一律报错,不再静默丢弃
592
+ - **多方言补齐**:MySQL/PostgreSQL 原生实现 `$all`、`$stdDevPop/Samp`、`$push/$addToSet`;`limit_offset` 三方言各自正确(含单独 `$skip`)
593
+ - **新阶段**: `$count`、`$sortByCount`、`$set`(`$addFields` 别名);`$unwind` 重写为 CTE + `JSON_EACH`(`preserveNullAndEmptyArrays`/`includeArrayIndex`)
594
+ - **新操作符**: `$switch`、`$trim`/`$ltrim`/`$rtrim`、`$strLenCP/$strLenBytes`、`$substrBytes/$substrCP`、`$slice`、`$isArray`、`$concatArrays`、`$exists`
595
+ - **CLI**: `--strict`、`-o`、警告走 stderr、方言选项来自注册表
596
+ - **测试**: 398 条(三方言共享 64-case golden 套件 + 硬化回归)
597
+
598
+ ### v0.1.5 (2025-02-23)
599
+
600
+ - addFields 阶段独立解析器(AddFieldsExpressionParser),与 project 解耦
601
+ - 支持字面量值、字段引用、表达式三类语义
602
+ - 统一数值 1 为字面量(仅 `$project` 阶段保留包含/排除语义)
603
+
604
+ ### v0.1.4 (2025-02-13)
605
+
606
+ - CTE 统一处理框架,支持 `$addFields`、`$project`、`$group` 阶段切片
607
+ - ExpressionResolver 统一表达式解析器;BaseExpressionParser 基础解析器
608
+ - SQL 格式化输出(sqlparse);`$project` 字段重命名
609
+
610
+ ## 致谢
611
+
612
+ 感谢所有贡献者和用户的支持!