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.
Files changed (53) hide show
  1. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/LICENSE +2 -2
  2. {mongo_2_sql-0.1.4/src/mongo_2_sql.egg-info → mongo_2_sql-0.1.5}/PKG-INFO +23 -33
  3. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/README.md +18 -32
  4. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/pyproject.toml +7 -1
  5. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expr_parser_base.py +5 -0
  6. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/stage_base.py +0 -1
  7. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/main.py +1 -1
  8. mongo_2_sql-0.1.5/src/mongo_2_sql/stages/add_fields/expr.py +220 -0
  9. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/add_fields/stage.py +2 -2
  10. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/group/expr.py +0 -7
  11. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/project/expr.py +51 -51
  12. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/project/stage.py +3 -8
  13. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5/src/mongo_2_sql.egg-info}/PKG-INFO +23 -33
  14. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/SOURCES.txt +2 -3
  15. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/tests/test1.py +8 -4
  16. mongo_2_sql-0.1.4/tests/test_all_examples.py +0 -133
  17. mongo_2_sql-0.1.4/tests/test_complex_pipelines.py +0 -287
  18. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/setup.cfg +0 -0
  19. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/__init__.py +0 -0
  20. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/converter.py +0 -0
  21. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/__init__.py +0 -0
  22. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/ast_base.py +0 -0
  23. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/cte_manager.py +0 -0
  24. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/cte_stage_base.py +0 -0
  25. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expr_base.py +0 -0
  26. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expr_registry.py +0 -0
  27. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/expression_resolver.py +0 -0
  28. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/pipeline_runtime.py +0 -0
  29. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/core/stage_loader.py +0 -0
  30. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/renderer/__init__.py +0 -0
  31. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/renderer/sqlite.py +0 -0
  32. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/__init__.py +0 -0
  33. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/add_fields/__init__.py +0 -0
  34. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/group/__init__.py +0 -0
  35. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/group/stage.py +0 -0
  36. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/limit/__init__.py +0 -0
  37. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/limit/stage.py +0 -0
  38. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/lookup/__init__.py +0 -0
  39. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/lookup/stage.py +0 -0
  40. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/match/__init__.py +0 -0
  41. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/match/expr.py +0 -0
  42. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/match/stage.py +0 -0
  43. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/project/__init__.py +0 -0
  44. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/sort/__init__.py +0 -0
  45. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/sort/stage.py +0 -0
  46. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/unwind/__init__.py +0 -0
  47. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/stages/unwind/stage.py +0 -0
  48. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql/utils.py +0 -0
  49. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/dependency_links.txt +0 -0
  50. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/entry_points.txt +0 -0
  51. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/requires.txt +0 -0
  52. {mongo_2_sql-0.1.4 → mongo_2_sql-0.1.5}/src/mongo_2_sql.egg-info/top_level.txt +0 -0
  53. {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 Bao Bingbo
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.4
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
- ## 版本更新 v0.1.4
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
- #### 5. Stage 解耦架构
54
- - 各 stage 表达式解析器通过统一的 ExpressionResolver 进行交互
55
- - 避免模块之间的循环依赖
56
- - 更易于扩展和维护
35
+ ## 版本更新 v0.1.5
57
36
 
58
- ### 问题修复
37
+ ### 新增功能
59
38
 
60
- - **修复 `_id` 字段双引号问题**: 不再使用 `"_id"`,改为 `_id`
61
- - **修复聚合后字段引用**: 聚合后的字段不再错误添加表前缀
62
- - **修复模块导入路径**: 统一使用相对导入,避免 "attempted relative import beyond top-level package" 错误
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
- ## 版本更新 v0.1.4
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
- #### 5. Stage 解耦架构
32
- - 各 stage 表达式解析器通过统一的 ExpressionResolver 进行交互
33
- - 避免模块之间的循环依赖
34
- - 更易于扩展和维护
9
+ ## 版本更新 v0.1.5
35
10
 
36
- ### 问题修复
11
+ ### 新增功能
37
12
 
38
- - **修复 `_id` 字段双引号问题**: 不再使用 `"_id"`,改为 `_id`
39
- - **修复聚合后字段引用**: 聚合后的字段不再错误添加表前缀
40
- - **修复模块导入路径**: 统一使用相对导入,避免 "attempted relative import beyond top-level package" 错误
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.4"
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'
@@ -197,7 +197,6 @@ class RenderContext:
197
197
 
198
198
  # CTE - 使用新的 CTE 管理器
199
199
  with_clause = self.cte_manager.build_with_clause()
200
- print(with_clause)
201
200
  if with_clause:
202
201
  parts.append(with_clause)
203
202
 
@@ -261,7 +261,7 @@ def main() -> int:
261
261
  try:
262
262
  # 加载管道
263
263
  pipeline = load_pipeline(args)
264
-
264
+ print(pipeline)
265
265
  # 转换
266
266
  sql, metadata = convert_mongo_pipeline_to_sql(
267
267
  pipeline=pipeline,
@@ -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.project.expr import ProjectExpressionParser
79
+ from ...stages.add_fields.expr import AddFieldsExpressionParser
80
80
 
81
81
  # 解析添加的字段表达式
82
- columns = ProjectExpressionParser.parse(stage_value, context, is_post_aggregation=False)
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
- @classmethod
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
- # 对于会改变字段结构的 $project,使用 CTE
47
- if self.should_use_cte(stage_value, context):
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 阶段。