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.
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/LICENSE +21 -21
- mongo_2_sql-0.1.7/PKG-INFO +612 -0
- mongo_2_sql-0.1.7/README.md +581 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/pyproject.toml +61 -46
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/__init__.py +25 -16
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/converter.py +59 -43
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/__init__.py +15 -23
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/cte_manager.py +5 -5
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/cte_stage_base.py +8 -4
- mongo_2_sql-0.1.7/src/mongo_2_sql/core/expr_parser_base.py +165 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/pipeline_runtime.py +175 -152
- mongo_2_sql-0.1.7/src/mongo_2_sql/core/query_block.py +176 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/core/stage_base.py +278 -313
- mongo_2_sql-0.1.7/src/mongo_2_sql/core/stage_loader.py +138 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/__init__.py +60 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/base.py +191 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/mysql.py +156 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/postgres.py +152 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/dialects/sqlite.py +167 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/main.py +274 -292
- mongo_2_sql-0.1.7/src/mongo_2_sql/renderer/__init__.py +27 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/renderer/sql_renderer.py +44 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/add_fields/__init__.py +9 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/add_fields/expr.py +143 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/add_fields/stage.py +38 -29
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/count/__init__.py +9 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/count/stage.py +116 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/group/__init__.py +10 -10
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/group/expr.py +202 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/group/stage.py +21 -27
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/limit/__init__.py +9 -9
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/limit/stage.py +20 -20
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/lookup/__init__.py +9 -9
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/lookup/stage.py +306 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/match/__init__.py +10 -10
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/match/expr.py +410 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/match/stage.py +67 -67
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/project/__init__.py +10 -10
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/project/expr.py +310 -159
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/project/stage.py +22 -36
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/sort/__init__.py +9 -9
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/sort/stage.py +32 -25
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/sort_by_count/__init__.py +9 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/sort_by_count/stage.py +79 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/unwind/__init__.py +9 -9
- mongo_2_sql-0.1.7/src/mongo_2_sql/stages/unwind/stage.py +168 -0
- mongo_2_sql-0.1.7/src/mongo_2_sql.egg-info/PKG-INFO +612 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql.egg-info/SOURCES.txt +16 -8
- mongo_2_sql-0.1.7/src/mongo_2_sql.egg-info/requires.txt +7 -0
- mongo_2_sql-0.1.7/tests/test_dialects.py +510 -0
- mongo_2_sql-0.1.7/tests/test_hardening.py +323 -0
- mongo_2_sql-0.1.7/tests/test_mysql_pipelines.py +157 -0
- mongo_2_sql-0.1.7/tests/test_postgres_pipelines.py +163 -0
- mongo_2_sql-0.1.7/tests/test_sqlite_pipelines.py +131 -0
- mongo_2_sql-0.1.6/PKG-INFO +0 -658
- mongo_2_sql-0.1.6/README.md +0 -632
- mongo_2_sql-0.1.6/src/mongo_2_sql/core/ast_base.py +0 -116
- mongo_2_sql-0.1.6/src/mongo_2_sql/core/expr_base.py +0 -483
- mongo_2_sql-0.1.6/src/mongo_2_sql/core/expr_parser_base.py +0 -358
- mongo_2_sql-0.1.6/src/mongo_2_sql/core/expr_registry.py +0 -447
- mongo_2_sql-0.1.6/src/mongo_2_sql/core/expression_resolver.py +0 -101
- mongo_2_sql-0.1.6/src/mongo_2_sql/core/stage_loader.py +0 -173
- mongo_2_sql-0.1.6/src/mongo_2_sql/renderer/__init__.py +0 -9
- mongo_2_sql-0.1.6/src/mongo_2_sql/renderer/sqlite.py +0 -234
- mongo_2_sql-0.1.6/src/mongo_2_sql/stages/add_fields/__init__.py +0 -9
- mongo_2_sql-0.1.6/src/mongo_2_sql/stages/add_fields/expr.py +0 -421
- mongo_2_sql-0.1.6/src/mongo_2_sql/stages/group/expr.py +0 -221
- mongo_2_sql-0.1.6/src/mongo_2_sql/stages/lookup/stage.py +0 -183
- mongo_2_sql-0.1.6/src/mongo_2_sql/stages/match/expr.py +0 -488
- mongo_2_sql-0.1.6/src/mongo_2_sql/stages/unwind/stage.py +0 -145
- mongo_2_sql-0.1.6/src/mongo_2_sql/utils.py +0 -529
- mongo_2_sql-0.1.6/src/mongo_2_sql.egg-info/PKG-INFO +0 -658
- mongo_2_sql-0.1.6/src/mongo_2_sql.egg-info/requires.txt +0 -4
- mongo_2_sql-0.1.6/tests/test.py +0 -412
- mongo_2_sql-0.1.6/tests/test1.py +0 -74
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/setup.cfg +0 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql/stages/__init__.py +0 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql.egg-info/dependency_links.txt +0 -0
- {mongo_2_sql-0.1.6 → mongo_2_sql-0.1.7}/src/mongo_2_sql.egg-info/entry_points.txt +0 -0
- {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
|
+
感谢所有贡献者和用户的支持!
|