mongo-2-sql 0.1.4__tar.gz → 0.1.5__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.4 → mongo_2_sql-0.1.5}/LICENSE +2 -2
- {mongo_2_sql-0.1.4/src/mongo_2_sql.egg-info → mongo_2_sql-0.1.5}/PKG-INFO +23 -33
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/README.md +18 -32
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/pyproject.toml +7 -1
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expr_parser_base.py +5 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/stage_base.py +0 -1
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/main.py +1 -1
- mongo_2_sql-0.1.5/src/mongo_2_sql/stages/add_fields/expr.py +220 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/add_fields/stage.py +2 -2
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/group/expr.py +0 -7
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/project/expr.py +51 -51
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/project/stage.py +3 -8
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5/src/mongo_2_sql.egg-info}/PKG-INFO +23 -33
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/SOURCES.txt +2 -3
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/tests/test1.py +8 -4
- mongo_2_sql-0.1.4/tests/test_all_examples.py +0 -133
- mongo_2_sql-0.1.4/tests/test_complex_pipelines.py +0 -287
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/setup.cfg +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/converter.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/ast_base.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/cte_manager.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/cte_stage_base.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expr_base.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expr_registry.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expression_resolver.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/pipeline_runtime.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/stage_loader.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/renderer/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/renderer/sqlite.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/add_fields/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/group/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/group/stage.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/limit/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/limit/stage.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/lookup/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/lookup/stage.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/match/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/match/expr.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/match/stage.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/project/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/sort/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/sort/stage.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/unwind/__init__.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/unwind/stage.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/utils.py +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/dependency_links.txt +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/entry_points.txt +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/requires.txt +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/top_level.txt +0 -0
- {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/tests/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 baobingbo
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongo-2-sql
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A library to convert MongoDB aggregation pipelines to SQL parser
|
|
5
5
|
Author-email: Bao Bingbo <baob2@outlook.com>
|
|
6
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
|
|
7
11
|
Classifier: Development Status :: 4 - Beta
|
|
8
12
|
Classifier: Intended Audience :: Developers
|
|
9
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -24,42 +28,21 @@ Dynamic: license-file
|
|
|
24
28
|
|
|
25
29
|
Mongo to SQL 是一个将 MongoDB 聚合管道转换为 SQL 查询语句的工具。
|
|
26
30
|
|
|
27
|
-
##
|
|
31
|
+
## 项目仓库
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#### 1. CTE(公用表表达式)统一处理
|
|
32
|
-
- **$addFields、$project、$group 阶段现在使用 CTE 方式处理**
|
|
33
|
-
- 将复杂的聚合管道拆分为多个SQL片段,使用 WITH 子句拼接
|
|
34
|
-
- 自动管理 CTE 命名和依赖关系
|
|
35
|
-
- 适用于多次投影、多层分组等复杂场景
|
|
36
|
-
|
|
37
|
-
#### 2. 表达式解析器统一架构
|
|
38
|
-
- **ExpressionResolver**: 统一的表达式解析器入口,避免 stage 之间相互 import
|
|
39
|
-
- **BaseExpressionParser**: 基础表达式解析器基类,减少代码重复
|
|
40
|
-
- 提供通用操作符集合、字段引用解析、值转换等基础功能
|
|
41
|
-
|
|
42
|
-
#### 3. SQL 格式化输出
|
|
43
|
-
- 使用 sqlparse 库自动格式化 SQL
|
|
44
|
-
- SELECT、FROM、WHERE、GROUP BY 等关键字独占一行
|
|
45
|
-
- 字段列表缩进对齐,提高可读性
|
|
46
|
-
- WHERE 子句添加注释提示可扩展性
|
|
47
|
-
|
|
48
|
-
#### 4. Project 阶段增强
|
|
49
|
-
- 支持字段重命名语法: `{"new_name": "$old_name"}`
|
|
50
|
-
- 聚合后字段引用自动处理(无需添加表前缀)
|
|
51
|
-
- 支持包含模式和排除模式
|
|
33
|
+
- **GitHub**: [baobingbo/mongo-2-sql](https://github.com/baobingbo/mongo-2-sql)
|
|
52
34
|
|
|
53
|
-
|
|
54
|
-
- 各 stage 表达式解析器通过统一的 ExpressionResolver 进行交互
|
|
55
|
-
- 避免模块之间的循环依赖
|
|
56
|
-
- 更易于扩展和维护
|
|
35
|
+
## 版本更新 v0.1.5
|
|
57
36
|
|
|
58
|
-
###
|
|
37
|
+
### 新增功能
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
39
|
+
#### 1. $addFields 阶段表达式与字面量语义
|
|
40
|
+
- `$addFields` 使用独立的 `AddFieldsExpressionParser`,不再复用 `$project` 的解析逻辑,避免跨阶段耦合
|
|
41
|
+
- `$addFields` 支持三类值:
|
|
42
|
+
- **字面量值**: 如 `{"score": 1}`, `{ "flag": true }`, `{ "text": "hello" }`,会直接转换为 `1 AS score`、`TRUE AS flag`、`'hello' AS text`
|
|
43
|
+
- **字段引用**: 如 `{ "cat": "$category" }`,会转换为 `t.category AS cat`
|
|
44
|
+
- **表达式**: 如 `{ "total": { "$add": [1, "$price"] } }` 或 `{ "isBig": { "$gt": ["$amount", 100] } }`
|
|
45
|
+
- **仅 `$project` 阶段中**,`{"field": 1}` / `{"field": 0}` 仍然表示“包含/排除字段”;其他阶段(包括 `$addFields`)中,`1` 始终被视为字面量数值 1
|
|
63
46
|
|
|
64
47
|
## 功能特性
|
|
65
48
|
|
|
@@ -583,6 +566,13 @@ MIT License - 详见 [LICENSE](LICENSE) 文件
|
|
|
583
566
|
|
|
584
567
|
## 变更日志
|
|
585
568
|
|
|
569
|
+
### v0.1.5 (2025-02-23)
|
|
570
|
+
|
|
571
|
+
**新增功能:**
|
|
572
|
+
- addFields阶段独立解析器(AddFieldsExpressionParser),与 project 解耦
|
|
573
|
+
- 支持字面量值、字段引用、表达式三类语义
|
|
574
|
+
- 统一数值 1 为字面量(仅 $project 阶段保留包含/排除语义)
|
|
575
|
+
|
|
586
576
|
### v0.1.4 (2025-02-13)
|
|
587
577
|
|
|
588
578
|
**新增功能:**
|
|
@@ -2,42 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Mongo to SQL 是一个将 MongoDB 聚合管道转换为 SQL 查询语句的工具。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 项目仓库
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#### 1. CTE(公用表表达式)统一处理
|
|
10
|
-
- **$addFields、$project、$group 阶段现在使用 CTE 方式处理**
|
|
11
|
-
- 将复杂的聚合管道拆分为多个SQL片段,使用 WITH 子句拼接
|
|
12
|
-
- 自动管理 CTE 命名和依赖关系
|
|
13
|
-
- 适用于多次投影、多层分组等复杂场景
|
|
14
|
-
|
|
15
|
-
#### 2. 表达式解析器统一架构
|
|
16
|
-
- **ExpressionResolver**: 统一的表达式解析器入口,避免 stage 之间相互 import
|
|
17
|
-
- **BaseExpressionParser**: 基础表达式解析器基类,减少代码重复
|
|
18
|
-
- 提供通用操作符集合、字段引用解析、值转换等基础功能
|
|
19
|
-
|
|
20
|
-
#### 3. SQL 格式化输出
|
|
21
|
-
- 使用 sqlparse 库自动格式化 SQL
|
|
22
|
-
- SELECT、FROM、WHERE、GROUP BY 等关键字独占一行
|
|
23
|
-
- 字段列表缩进对齐,提高可读性
|
|
24
|
-
- WHERE 子句添加注释提示可扩展性
|
|
25
|
-
|
|
26
|
-
#### 4. Project 阶段增强
|
|
27
|
-
- 支持字段重命名语法: `{"new_name": "$old_name"}`
|
|
28
|
-
- 聚合后字段引用自动处理(无需添加表前缀)
|
|
29
|
-
- 支持包含模式和排除模式
|
|
7
|
+
- **GitHub**: [baobingbo/mongo-2-sql](https://github.com/baobingbo/mongo-2-sql)
|
|
30
8
|
|
|
31
|
-
|
|
32
|
-
- 各 stage 表达式解析器通过统一的 ExpressionResolver 进行交互
|
|
33
|
-
- 避免模块之间的循环依赖
|
|
34
|
-
- 更易于扩展和维护
|
|
9
|
+
## 版本更新 v0.1.5
|
|
35
10
|
|
|
36
|
-
###
|
|
11
|
+
### 新增功能
|
|
37
12
|
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
13
|
+
#### 1. $addFields 阶段表达式与字面量语义
|
|
14
|
+
- `$addFields` 使用独立的 `AddFieldsExpressionParser`,不再复用 `$project` 的解析逻辑,避免跨阶段耦合
|
|
15
|
+
- `$addFields` 支持三类值:
|
|
16
|
+
- **字面量值**: 如 `{"score": 1}`, `{ "flag": true }`, `{ "text": "hello" }`,会直接转换为 `1 AS score`、`TRUE AS flag`、`'hello' AS text`
|
|
17
|
+
- **字段引用**: 如 `{ "cat": "$category" }`,会转换为 `t.category AS cat`
|
|
18
|
+
- **表达式**: 如 `{ "total": { "$add": [1, "$price"] } }` 或 `{ "isBig": { "$gt": ["$amount", 100] } }`
|
|
19
|
+
- **仅 `$project` 阶段中**,`{"field": 1}` / `{"field": 0}` 仍然表示“包含/排除字段”;其他阶段(包括 `$addFields`)中,`1` 始终被视为字面量数值 1
|
|
41
20
|
|
|
42
21
|
## 功能特性
|
|
43
22
|
|
|
@@ -561,6 +540,13 @@ MIT License - 详见 [LICENSE](LICENSE) 文件
|
|
|
561
540
|
|
|
562
541
|
## 变更日志
|
|
563
542
|
|
|
543
|
+
### v0.1.5 (2025-02-23)
|
|
544
|
+
|
|
545
|
+
**新增功能:**
|
|
546
|
+
- addFields阶段独立解析器(AddFieldsExpressionParser),与 project 解耦
|
|
547
|
+
- 支持字面量值、字段引用、表达式三类语义
|
|
548
|
+
- 统一数值 1 为字面量(仅 $project 阶段保留包含/排除语义)
|
|
549
|
+
|
|
564
550
|
### v0.1.4 (2025-02-13)
|
|
565
551
|
|
|
566
552
|
**新增功能:**
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mongo-2-sql"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "A library to convert MongoDB aggregation pipelines to SQL parser"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT License"}
|
|
@@ -28,6 +28,12 @@ classifiers = [
|
|
|
28
28
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/baobingbo/mongo-2-sql"
|
|
33
|
+
Documentation = "https://github.com/baobingbo/mongo-2-sql#readme"
|
|
34
|
+
Repository = "https://github.com/baobingbo/mongo-2-sql.git"
|
|
35
|
+
Issues = "https://github.com/baobingbo/mongo-2-sql/issues"
|
|
36
|
+
|
|
31
37
|
# 配置 setuptools 特定的行为
|
|
32
38
|
[tool.setuptools]
|
|
33
39
|
package-dir = {"" = "src"}
|
|
@@ -50,6 +50,11 @@ class BaseExpressionParser:
|
|
|
50
50
|
'$concat', '$substr', '$toLower', '$toUpper', '$trim'
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
# 日期操作符
|
|
54
|
+
DATE_OPERATORS: Set[str] = {
|
|
55
|
+
'$year', '$month', '$dayOfMonth', '$hour', '$minute', '$second', '$dateToString'
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
# 条件操作符
|
|
54
59
|
CONDITIONAL_OPERATORS: Set[str] = {
|
|
55
60
|
'$cond', '$ifNull', '$switch'
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Add Fields Expression Parser Module
|
|
3
|
+
|
|
4
|
+
解析 MongoDB $addFields 阶段的表达式。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from ...core.expr_parser_base import BaseExpressionParser
|
|
10
|
+
from ...core.expr_base import (
|
|
11
|
+
BaseExpression,
|
|
12
|
+
BinaryOperation,
|
|
13
|
+
FieldReference,
|
|
14
|
+
FunctionCall,
|
|
15
|
+
LiteralValue,
|
|
16
|
+
)
|
|
17
|
+
from ...core.expr_registry import ExpressionRegistry
|
|
18
|
+
from ...core.stage_base import RenderContext
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AddFieldsExpressionParser(BaseExpressionParser):
|
|
22
|
+
"""
|
|
23
|
+
Add Fields 表达式解析器。
|
|
24
|
+
|
|
25
|
+
将 MongoDB $addFields 表达式解析为 SQL 计算字段。
|
|
26
|
+
专门用于处理 $addFields 阶段,不使用 $project 的包含/排除逻辑。
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def parse(cls, add_fields_spec: Dict[str, Any], context: RenderContext) -> List[str]:
|
|
31
|
+
"""
|
|
32
|
+
解析 $addFields 规范并生成 SQL SELECT 列。
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
add_fields_spec: MongoDB $addFields 规范
|
|
36
|
+
context: 渲染上下文
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
SQL SELECT 列列表
|
|
40
|
+
"""
|
|
41
|
+
columns = []
|
|
42
|
+
|
|
43
|
+
for field_name, field_value in add_fields_spec.items():
|
|
44
|
+
if isinstance(field_value, dict):
|
|
45
|
+
# 表达式:{"field": {"$add": ["$price", "$tax"]}}
|
|
46
|
+
column = cls._parse_expression_projection(field_name, field_value, context)
|
|
47
|
+
if column:
|
|
48
|
+
columns.append(column)
|
|
49
|
+
elif isinstance(field_value, str) and field_value.startswith('$'):
|
|
50
|
+
# 字段引用:{"new_field": "$old_field"}
|
|
51
|
+
column = cls._parse_field_rename(field_name, field_value, context)
|
|
52
|
+
if column:
|
|
53
|
+
columns.append(column)
|
|
54
|
+
else:
|
|
55
|
+
# 字面量值:{"field": 1} 或 {"field": "value"}
|
|
56
|
+
column = cls._parse_literal_value(field_name, field_value, context)
|
|
57
|
+
if column:
|
|
58
|
+
columns.append(column)
|
|
59
|
+
|
|
60
|
+
return columns
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def _parse_expression_projection(cls, alias: str, expr: Dict[str, Any], context: RenderContext) -> Optional[str]:
|
|
64
|
+
"""
|
|
65
|
+
解析表达式投影。
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
alias: 输出字段别名
|
|
69
|
+
expr: 表达式字典
|
|
70
|
+
context: 渲染上下文
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
SQL 列表达式,或 None
|
|
74
|
+
"""
|
|
75
|
+
if not expr:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# 获取操作符
|
|
79
|
+
operators = list(expr.keys())
|
|
80
|
+
if len(operators) != 1:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
op = operators[0]
|
|
84
|
+
value = expr[op]
|
|
85
|
+
|
|
86
|
+
# 解析表达式
|
|
87
|
+
sql_expr = cls._parse_expression_value(op, value, context)
|
|
88
|
+
|
|
89
|
+
if sql_expr:
|
|
90
|
+
return f"{sql_expr} AS {alias}"
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def _parse_field_rename(cls, alias: str, field_ref: str, context: RenderContext) -> Optional[str]:
|
|
96
|
+
"""
|
|
97
|
+
解析字段重命名投影。
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
alias: 新字段名
|
|
101
|
+
field_ref: 字段引用(如 "$old_name")
|
|
102
|
+
context: 渲染上下文
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
SQL 列表达式,或 None
|
|
106
|
+
"""
|
|
107
|
+
if not field_ref.startswith('$'):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# 提取字段名
|
|
111
|
+
field_name = field_ref[1:] # 移除开头的 $
|
|
112
|
+
|
|
113
|
+
# 添加表前缀
|
|
114
|
+
table_alias = context.from_alias or 't'
|
|
115
|
+
return f"{table_alias}.{field_name} AS {alias}"
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def _parse_literal_value(cls, alias: str, value: Any, context: RenderContext) -> Optional[str]:
|
|
119
|
+
"""
|
|
120
|
+
解析字面量值投影。
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
alias: 输出字段别名
|
|
124
|
+
value: 字面量值
|
|
125
|
+
context: 渲染上下文
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
SQL 列表达式,或 None
|
|
129
|
+
"""
|
|
130
|
+
sql_literal = cls._value_to_sql(value)
|
|
131
|
+
return f"{sql_literal} AS {alias}"
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def _parse_arithmetic_operator(cls, operator: str, value: Any, context: RenderContext) -> Optional[str]:
|
|
135
|
+
"""解析算术操作符,返回布尔表达式"""
|
|
136
|
+
op_map = {
|
|
137
|
+
'$add': '+',
|
|
138
|
+
'$subtract': '-',
|
|
139
|
+
'$multiply': '*',
|
|
140
|
+
'$divide': '/',
|
|
141
|
+
'$mod': '%',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
sql_op = op_map.get(operator)
|
|
145
|
+
if sql_op is None:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
if not isinstance(value, list) or len(value) < 2:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
operands = []
|
|
152
|
+
for item in value:
|
|
153
|
+
if isinstance(item, str) and item.startswith('$'):
|
|
154
|
+
field = item[1:]
|
|
155
|
+
table_alias = context.from_alias or 't'
|
|
156
|
+
operands.append(f'{table_alias}.{field}')
|
|
157
|
+
else:
|
|
158
|
+
operands.append(cls._value_to_sql(item))
|
|
159
|
+
|
|
160
|
+
return f"({f' {sql_op} '.join(operands)})"
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def _parse_comparison_operator(cls, operator: str, value: Any, context: RenderContext) -> Optional[str]:
|
|
164
|
+
"""解析比较操作符,返回布尔表达式"""
|
|
165
|
+
if not isinstance(value, list) or len(value) != 2:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
op_map = {
|
|
169
|
+
'$eq': '=',
|
|
170
|
+
'$ne': '!=',
|
|
171
|
+
'$gt': '>',
|
|
172
|
+
'$gte': '>=',
|
|
173
|
+
'$lt': '<',
|
|
174
|
+
'$lte': '<=',
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
sql_op = op_map.get(operator)
|
|
178
|
+
if sql_op is None:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def parse_operand(operand):
|
|
182
|
+
if isinstance(operand, str) and operand.startswith('$'):
|
|
183
|
+
field = operand[1:]
|
|
184
|
+
table_alias = context.from_alias or 't'
|
|
185
|
+
return f'{table_alias}.{field}'
|
|
186
|
+
else:
|
|
187
|
+
return cls._value_to_sql(operand)
|
|
188
|
+
|
|
189
|
+
left = parse_operand(value[0])
|
|
190
|
+
right = parse_operand(value[1])
|
|
191
|
+
|
|
192
|
+
return f"({left} {sql_op} {right})"
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def _parse_expression_value(cls, operator: str, value: Any, context: RenderContext) -> Optional[str]:
|
|
196
|
+
"""
|
|
197
|
+
解析表达式值。
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
operator: 操作符
|
|
201
|
+
value: 操作数值
|
|
202
|
+
context: 渲染上下文
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
SQL 表达式,或 None
|
|
206
|
+
"""
|
|
207
|
+
# 字面量
|
|
208
|
+
if operator == '$literal':
|
|
209
|
+
return cls._value_to_sql(value)
|
|
210
|
+
|
|
211
|
+
# 算术操作符 - 使用专门的处理逻辑
|
|
212
|
+
if operator in cls.ARITHMETIC_OPERATORS:
|
|
213
|
+
return cls._parse_arithmetic_operator(operator, value, context)
|
|
214
|
+
|
|
215
|
+
# 比较操作符 - 使用专门的处理逻辑
|
|
216
|
+
if operator in cls.COMPARISON_OPERATORS:
|
|
217
|
+
return cls._parse_comparison_operator(operator, value, context)
|
|
218
|
+
|
|
219
|
+
return None
|
|
220
|
+
|
|
@@ -76,10 +76,10 @@ class AddFieldsStage(CTEStageProcessor):
|
|
|
76
76
|
Returns:
|
|
77
77
|
CTE SQL 内容
|
|
78
78
|
"""
|
|
79
|
-
from ...stages.
|
|
79
|
+
from ...stages.add_fields.expr import AddFieldsExpressionParser
|
|
80
80
|
|
|
81
81
|
# 解析添加的字段表达式
|
|
82
|
-
columns =
|
|
82
|
+
columns = AddFieldsExpressionParser.parse(stage_value, context)
|
|
83
83
|
|
|
84
84
|
# 构建 CTE 的 SELECT 子句
|
|
85
85
|
select_parts = []
|
|
@@ -18,13 +18,6 @@ class GroupExpressionParser(BaseExpressionParser):
|
|
|
18
18
|
将 MongoDB $group 表达式解析为 SQL GROUP BY 和聚合函数。
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
# 累加器操作符
|
|
22
|
-
ACCUMULATORS = {
|
|
23
|
-
'$sum', '$avg', '$min', '$max', '$count',
|
|
24
|
-
'$first', '$last', '$push', '$addToSet',
|
|
25
|
-
'$stdDevPop', '$stdDevSamp'
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
@classmethod
|
|
29
22
|
def parse(cls, group_spec: Dict[str, Any], context: RenderContext) -> Tuple[List[str], List[str], List[str]]:
|
|
30
23
|
"""
|
|
@@ -25,27 +25,6 @@ class ProjectExpressionParser(BaseExpressionParser):
|
|
|
25
25
|
将 MongoDB 投影表达式解析为 SQL SELECT 列。
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
# 算术操作符
|
|
29
|
-
ARITHMETIC_OPERATORS = {'$add', '$subtract', '$multiply', '$divide', '$mod'}
|
|
30
|
-
|
|
31
|
-
# 字符串操作符
|
|
32
|
-
STRING_OPERATORS = {'$concat', '$substr', '$toLower', '$toUpper', '$trim'}
|
|
33
|
-
|
|
34
|
-
# 日期操作符
|
|
35
|
-
DATE_OPERATORS = {'$year', '$month', '$dayOfMonth', '$hour', '$minute', '$second', '$dateToString'}
|
|
36
|
-
|
|
37
|
-
# 条件操作符
|
|
38
|
-
CONDITIONAL_OPERATORS = {'$cond', '$ifNull', '$switch'}
|
|
39
|
-
|
|
40
|
-
# 比较操作符
|
|
41
|
-
COMPARISON_OPERATORS = {'$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin'}
|
|
42
|
-
|
|
43
|
-
# 数组操作符
|
|
44
|
-
ARRAY_OPERATORS = {'$size', '$slice', '$arrayElemAt'}
|
|
45
|
-
|
|
46
|
-
# 字面量操作符
|
|
47
|
-
LITERAL_OPERATOR = '$literal'
|
|
48
|
-
|
|
49
28
|
@classmethod
|
|
50
29
|
def parse(cls, projection: Dict[str, Any], context: RenderContext, is_post_aggregation: bool = False) -> List[str]:
|
|
51
30
|
"""
|
|
@@ -104,6 +83,11 @@ class ProjectExpressionParser(BaseExpressionParser):
|
|
|
104
83
|
column = cls._parse_field_rename(field, value, context, is_post_aggregation)
|
|
105
84
|
if column:
|
|
106
85
|
columns.append(column)
|
|
86
|
+
else:
|
|
87
|
+
# 字面量值(用于 $addFields):{"field": "literal_value"}
|
|
88
|
+
column = cls._parse_literal_value(field, value, context, is_post_aggregation)
|
|
89
|
+
if column:
|
|
90
|
+
columns.append(column)
|
|
107
91
|
else:
|
|
108
92
|
# 排除模式:value = 0 表示排除字段
|
|
109
93
|
# 表达式投影在两种模式下都应该工作
|
|
@@ -119,6 +103,11 @@ class ProjectExpressionParser(BaseExpressionParser):
|
|
|
119
103
|
column = cls._parse_field_rename(field, value, context, is_post_aggregation)
|
|
120
104
|
if column:
|
|
121
105
|
columns.append(column)
|
|
106
|
+
else:
|
|
107
|
+
# 字面量值(用于 $addFields):{"field": "literal_value"}
|
|
108
|
+
column = cls._parse_literal_value(field, value, context, is_post_aggregation)
|
|
109
|
+
if column:
|
|
110
|
+
columns.append(column)
|
|
122
111
|
|
|
123
112
|
return columns
|
|
124
113
|
|
|
@@ -241,6 +230,46 @@ class ProjectExpressionParser(BaseExpressionParser):
|
|
|
241
230
|
table_alias = context.from_alias or 't'
|
|
242
231
|
return f"{table_alias}.{field_name} AS {alias}"
|
|
243
232
|
|
|
233
|
+
@classmethod
|
|
234
|
+
def _parse_literal_value(cls, alias: str, value: Any, context: RenderContext, is_post_aggregation: bool = False) -> Optional[str]:
|
|
235
|
+
"""
|
|
236
|
+
解析字面量值投影(主要用于 $addFields)。
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
alias: 输出字段别名
|
|
240
|
+
value: 字面量值
|
|
241
|
+
context: 渲染上下文
|
|
242
|
+
is_post_aggregation: 是否在聚合之后进行投影
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
SQL 列表达式,或 None
|
|
246
|
+
"""
|
|
247
|
+
if value is None:
|
|
248
|
+
sql_literal = 'NULL'
|
|
249
|
+
elif isinstance(value, bool):
|
|
250
|
+
sql_literal = 'TRUE' if value else 'FALSE'
|
|
251
|
+
elif isinstance(value, (int, float)):
|
|
252
|
+
sql_literal = str(value)
|
|
253
|
+
elif isinstance(value, str):
|
|
254
|
+
escaped = value.replace("'", "''")
|
|
255
|
+
sql_literal = f"'{escaped}'"
|
|
256
|
+
elif isinstance(value, list):
|
|
257
|
+
import json
|
|
258
|
+
# 将数组转换为 JSON 字符串
|
|
259
|
+
json_str = json.dumps(value)
|
|
260
|
+
escaped = json_str.replace("'", "''")
|
|
261
|
+
sql_literal = f"'{escaped}'"
|
|
262
|
+
elif isinstance(value, dict):
|
|
263
|
+
import json
|
|
264
|
+
# 将字典转换为 JSON 字符串
|
|
265
|
+
json_str = json.dumps(value)
|
|
266
|
+
escaped = json_str.replace("'", "''")
|
|
267
|
+
sql_literal = f"'{escaped}'"
|
|
268
|
+
else:
|
|
269
|
+
sql_literal = f"'{str(value)}'"
|
|
270
|
+
|
|
271
|
+
return f"{sql_literal} AS {alias}"
|
|
272
|
+
|
|
244
273
|
@classmethod
|
|
245
274
|
def _parse_expression_value(cls, operator: str, value: Any, context: RenderContext) -> Optional[str]:
|
|
246
275
|
"""
|
|
@@ -596,35 +625,6 @@ class ProjectExpressionParser(BaseExpressionParser):
|
|
|
596
625
|
|
|
597
626
|
return cls._value_to_sql(value)
|
|
598
627
|
|
|
599
|
-
|
|
600
|
-
def _value_to_sql(cls, value: Any) -> str:
|
|
601
|
-
"""
|
|
602
|
-
将值转换为 SQL 表示。
|
|
603
|
-
|
|
604
|
-
Args:
|
|
605
|
-
value: 值
|
|
606
|
-
|
|
607
|
-
Returns:
|
|
608
|
-
SQL 字面量
|
|
609
|
-
"""
|
|
610
|
-
if value is None:
|
|
611
|
-
return 'NULL'
|
|
612
|
-
elif isinstance(value, bool):
|
|
613
|
-
return 'TRUE' if value else 'FALSE'
|
|
614
|
-
elif isinstance(value, (int, float)):
|
|
615
|
-
return str(value)
|
|
616
|
-
elif isinstance(value, str):
|
|
617
|
-
escaped = value.replace("'", "''")
|
|
618
|
-
return f"'{escaped}'"
|
|
619
|
-
elif isinstance(value, list):
|
|
620
|
-
items = [cls._value_to_sql(item) for item in value]
|
|
621
|
-
return f"({', '.join(items)})"
|
|
622
|
-
elif isinstance(value, dict):
|
|
623
|
-
import json
|
|
624
|
-
json_str = json.dumps(value)
|
|
625
|
-
escaped = json_str.replace("'", "''")
|
|
626
|
-
return f"'{escaped}'"
|
|
627
|
-
else:
|
|
628
|
-
return f"'{str(value)}'"
|
|
628
|
+
|
|
629
629
|
|
|
630
630
|
|
|
@@ -42,14 +42,9 @@ class ProjectStage(CTEStageProcessor):
|
|
|
42
42
|
|
|
43
43
|
# 检查是否在聚合后进行投影
|
|
44
44
|
is_post_aggregation = context.has_aggregation and len(context.group_by_columns) > 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
self.process_with_cte(stage_value, context, self.build_cte_sql)
|
|
49
|
-
else:
|
|
50
|
-
# 传统的处理方式
|
|
51
|
-
self._process_traditional(stage_value, context, is_post_aggregation)
|
|
52
|
-
|
|
45
|
+
|
|
46
|
+
self.process_with_cte(stage_value, context, self.build_cte_sql)
|
|
47
|
+
|
|
53
48
|
def should_use_cte(self, stage_value: Any, context: RenderContext) -> bool:
|
|
54
49
|
"""
|
|
55
50
|
判断是否应该使用 CTE 来处理此 $project 阶段。
|