Metadata-Version: 2.4
Name: mongo-2-sql
Version: 0.1.7
Summary: A library to convert MongoDB aggregation pipelines to SQL parser
Author-email: Bao Bingbo <baob2@outlook.com>
License: MIT License
Project-URL: Homepage, https://github.com/baobingbo/mongo-2-sql
Project-URL: Documentation, https://github.com/baobingbo/mongo-2-sql#readme
Project-URL: Repository, https://github.com/baobingbo/mongo-2-sql.git
Project-URL: Issues, https://github.com/baobingbo/mongo-2-sql/issues
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: sqlparse>=0.5.5
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5; extra == "dev"
Dynamic: license-file

# Mongo to SQL

将 MongoDB 聚合管道（aggregation pipeline）转换为 SQL 查询语句的 Python 工具，支持 **SQLite / MySQL / PostgreSQL** 三种方言。

- **GitHub**: [baobingbo/mongo-2-sql](https://github.com/baobingbo/mongo-2-sql)
- **版本**: v0.1.7（Python >= 3.10，当前在 3.13 验证）

## 功能特性

- **12 个聚合阶段**: `$match` `$project` `$group` `$sort` `$limit` `$skip` `$lookup` `$unwind` `$addFields`/`$set` `$count` `$sortByCount`
- **三方言支持**: 通过 `SQLDialect` 抽象层统一处理字符串拼接、TRIM、子串、日期、类型判断、标识符引用、LIMIT/OFFSET 等差异
- **复杂管道自动 CTE**: `$project`/`$group`/`$addFields`/`$unwind`/`$count` 以 CTE 串联；`$sort`/`$limit`/`$skip` 的位置语义会正确固化到 CTE 内层（含分组前派生表下推）
- **安全转义**: 字符串字面量、`$regex` → LIKE、表名/字段名（保留字智能引用）均按方言规则转义
- **显式错误**: 不支持的语法（`$text`、`$where`、`$first` 等）一律抛错，不静默丢字段
- **CLI**: `mongo2sql` 命令行工具（文件 / 参数 / stdin 三种输入）

## 安装

### 方式一：PyPI 安装

```bash
# 生产环境：仅安装运行时依赖（sqlparse）
pip install mongo-2-sql

# 开发/测试环境：额外安装 pytest + ruff + build + twine
pip install mongo-2-sql[dev]
```

### 方式二：uv（源码开发，推荐）

依赖已在 `pyproject.toml` 中区分：**生产依赖**（`dependencies`，仅 `sqlparse`）与
**测试/构建工具**（`[dev]` extra，pytest / ruff / build / twine）。锁定文件为 `uv.lock`：

```bash
git clone https://github.com/baobingbo/mongo-2-sql.git
cd mongo-2-sql

uv sync             # 生产环境：只装 mongo-2-sql(editable) + sqlparse
uv sync --extra dev # 测试/开发环境：额外安装 pytest、ruff、build、twine
```

两种环境可随时互相切换（`uv sync` 会自动增删到与锁定状态一致）：

| 命令 | 安装内容 |
|------|----------|
| `uv sync` | 项目本体（editable）+ `sqlparse` |
| `uv sync --extra dev` | 上述 + `pytest` `ruff` `build` `twine`（跑测试 / 打包装发布） |

非 uv 的 pip 用户等价命令：`pip install -e .` / `pip install -e ".[dev]"`。

## 快速开始

```python
from mongo_2_sql import convert_mongo_pipeline_to_sql

pipeline = [
    {"$match": {"status": "active", "age": {"$gte": 18}}},
    {"$group": {"_id": "$category", "count": {"$sum": 1}}},
    {"$sort": {"count": -1}},
    {"$limit": 10},
]

sql, metadata = convert_mongo_pipeline_to_sql(pipeline, "users")
print(sql)
```

输出（`$group` 等重塑结果集的阶段自动包裹为 CTE）：

```sql
WITH cte_1 AS
  (SELECT t.category AS _id,
          COUNT(*) AS count
   FROM users AS t
   WHERE 1=1
     AND (t.status = 'active')
     AND (t.age >= 18)
   GROUP BY t.category)
SELECT *
FROM cte_1 AS t
ORDER BY t.count DESC
LIMIT 10
```

`metadata` 同时给出结构信息（见 `元数据说明`），可用于检查与调试。

## 指定 SQL 方言

`dialect` 参数接受以下名称（大小写不敏感）：

| 方言 | 可用名称 |
|------|----------|
| SQLite（默认） | `sqlite` |
| MySQL / MariaDB | `mysql`、`mariadb` |
| PostgreSQL | `postgresql`、`postgres`、`pg` |

同一条管道在三种方言下的真实输出对比（注意 `CONCAT` vs ```、`TRIM` 写法、`$type` 判定、`$year` 实现、标识符引用差异）：

```python
pipeline = [
    {"$match": {"name": {"$type": "string"}}},
    {"$project": {
        "full": {"$concat": ["$first", " ", "$last"]},
        "nick": {"$trim": {"input": "$name", "chars": "x"}},
        "yr":   {"$year": "$birth"},
    }},
]

for dialect in ("sqlite", "mysql", "postgresql"):
    sql, meta = convert_mongo_pipeline_to_sql(pipeline, "users", dialect=dialect)
    print(f"--- {dialect} ---")
    print(sql)
```

```sql
--- sqlite ---
WITH cte_1 AS
  (SELECT (t.first || ' ' || t.last) AS "full",
          TRIM(t.name, 'x') AS nick,
          CAST(STRFTIME('%Y', t.birth) AS INTEGER) AS yr
   FROM users AS t
   WHERE 1=1
     AND (TYPEOF(t.name) = 'text'))
SELECT *
FROM cte_1 AS t

--- mysql ---
WITH cte_1 AS
  (SELECT CONCAT(t.first, ' ', t.last) AS `full`,
          TRIM(BOTH 'x'
               FROM t.name) AS nick,
          EXTRACT(YEAR
                  FROM t.birth) AS yr
   FROM users AS t
   WHERE 1=1
     AND (t.name IS NOT NULL
          AND t.name NOT REGEXP '^-?[0-9.]+$'))
SELECT *
FROM cte_1 AS t

--- postgresql ---
WITH cte_1 AS
  (SELECT (t.first || ' ' || t.last) AS "full",
          TRIM(BOTH 'x'
               FROM t.name) AS nick,
          CAST(EXTRACT(YEAR
                       FROM t.birth) AS INTEGER) AS yr
   FROM users AS t
   WHERE 1=1
     AND (pg_typeof(t.name) IN ('text'::regtype,
                                'character varying'::regtype,
                                'character'::regtype,
                                'name'::regtype)))
SELECT *
FROM cte_1 AS t
```

**方言覆盖现状：**

| 方言 | 关系型核心算子 | JSON / 数组算子 |
|------|----------------|-----------------|
| SQLite | `✅` 完整 | `✅` 完整（JSON1：`$unwind`/`$size`/`$slice`/`$isArray`/`$concatArrays` 等） |
| MySQL | `✅` 完整 | `🟡` `$all`（JSON_CONTAINS）、`$stdDevPop/Samp`（STDDEV_POP/SAMP）、`$push/$addToSet`（JSON_ARRAYAGG）为原生实现；`$unwind`/`$size`/`$slice` 等回退 SQLite 风格语法 + 警告 |
| PostgreSQL | `✅` 完整 | `🟡` `$all`（jsonb `@>`）、`$stdDevPop/Samp`（STDDEV）、`$push/$addToSet`（jsonb_agg）为原生实现；其余同上回退 + 警告 |

> 回退不会中断转换：产生明确警告（`metadata['warnings']` / CLI stderr），生成的 SQL 为 SQLite 风格，需目标库实际具备 JSON1 类函数才可执行。

## 命令行用法

`mongo2sql` 安装后由 `pip install mongo-2-sql` 自动注册（等价于 `python -m mongo_2_sql.main`）：

```bash
# 从 JSON 文件转换
mongo2sql -f pipeline.json -c users

# 从命令行参数转换（pipeline 为 JSON 字符串）
mongo2sql -p '[{"$match": {"status": "active"}}]' -c users

# 从标准输入转换
echo '[{"$match": {"status": "active"}}]' | mongo2sql -c users

# 指定方言（sqlite / mysql / mariadb / postgresql / postgres / pg）
mongo2sql -f pipeline.json -c users -d mysql

# 输出到文件（SQL 写 -o，状态与警告走 stderr）
mongo2sql -f pipeline.json -c users -o out.sql

# 附带元数据注释输出
mongo2sql -f pipeline.json -c users --metadata

# 跳过管道格式校验
mongo2sql -f pipeline.json -c users --no-validate

# 严格模式：转换产生警告时退出码为 1（便于 CI 拦截回退语法）
mongo2sql -f pipeline.json -c users -d mysql --strict

# 列出支持的阶段
mongo2sql --list-stages
```

`pipeline.json` 内容就是一个 MongoDB 聚合管道数组：

```json
[
    {"$match": {"status": "active", "age": {"$gte": 18}}},
    {"$group": {"_id": "$category", "totalAmount": {"$sum": "$amount"}, "count": {"$sum": 1}}},
    {"$project": {"_id": 1, "totalAmount": 1, "count": 1}},
    {"$sort": {"totalAmount": -1}},
    {"$limit": 5}
]
```

`-c` 为必填（源集合 / 表名）。错误与警告输出到 stderr，SQL 输出到 stdout（或 `-o` 指定文件）。

## API 文档

### 主要函数

```python
convert_mongo_pipeline_to_sql(
    pipeline,
    collection_name,
    dialect='sqlite',
    validate=True,
    stage_loader=None,
) -> tuple[str, dict]
```

| 参数 | 说明 |
|------|------|
| `pipeline` | 聚合管道（list[dict]），空列表生成 `SELECT * FROM <collection>` |
| `collection_name` | 集合名 / 源表名（自动智能引用，含特殊字符也安全） |
| `dialect` | `'sqlite'` / `'mysql'` / `'mariadb'` / `'postgresql'` / `'postgres'` / `'pg'` |
| `validate` | 是否先做管道结构校验（默认 True） |
| `stage_loader` | 可选的 `StageLoader` 实例（用于注入自定义阶段，见 `扩展`） |

返回 `(sql, metadata)`。异常：`ValueError`（管道结构无效 / 未知方言）、`RuntimeError`（阶段处理失败，错误信息含具体原因）。

### 元数据说明

`metadata` 字典的键：

| 键 | 说明 |
|----|------|
| `dialect` / `collection` | 回显输入 |
| `stages_processed` | 实际执行的阶段数 |
| `has_aggregation` | 是否包含 `$group` |
| `warnings` | 列表；方言回退等提示（空列表 = 纯原生转换） |
| `select_columns` / `where_conditions` / `group_by_columns` / `order_by_columns` | 外层查询的各子句成分 |
| `limit` / `offset` | 外层 `LIMIT`/`OFFSET`（上游位置语义固化进 CTE 后此处为 None） |

### 面向对象用法

```python
from mongo_2_sql import MongoToSQLConverter

converter = MongoToSQLConverter(dialect='sqlite')  # 可选 stage_loader 参数

sql, metadata = converter.convert(pipeline, "users")      # 转换整个管道
sql1 = converter.convert_single_stage({"$match": {"age": {"$gt": 18}}}, "users")  # 单阶段
print(converter.get_supported_stages())                   # 支持的阶段列表
print(converter.is_stage_supported('$match'))             # True
```

### 便捷函数（单阶段片段）

三个函数均支持 `dialect` 参数（默认 `'sqlite'`，影响转义 / ESCAPE / 保留字引用）：

```python
from mongo_2_sql import mongo_match_to_sql, mongo_project_to_sql, mongo_group_to_sql

# WHERE 片段（不含 WHERE 关键字）
mongo_match_to_sql({"status": "active", "age": {"$gte": 18}}, table_name='t')

# SELECT 列列表
mongo_project_to_sql({"name": 1, "full": {"$concat": ["$first", " ", "$last"]}})

# (GROUP BY 列, SELECT 列) 元组
mongo_group_to_sql({"_id": "$category", "count": {"$sum": 1}})
```

### 方言工厂

```python
from mongo_2_sql import get_dialect, SQLiteDialect, MySQLDialect, PostgreSQLDialect

dialect = get_dialect('mysql')          # 未知名称抛 ValueError
dialect.quote_string("a'b")             # 直接获取方言级 SQL 片段
```

## 支持的阶段

| 阶段 | SQL 对应 | 说明 |
|------|---------|------|
| `$match` | `WHERE` | 全算子支持（见下） |
| `$project` | `SELECT` / CTE | 包含模式 + 表达式（`\_id: 0` 可排除 _id）；全 0 排除模式需要 schema，**显式报错** |
| `$addFields` / `$set` | CTE（新列在前，同名字段覆盖旧值） | 字面量 / 字段引用 / 表达式三类值 |
| `$group` | CTE（`GROUP BY`） | `_id` 支持 `$field` / 常量 / `null` / 复合 dict / 表达式 |
| `$sortByCount` | `GROUP BY` + `ORDER BY` | 字符串或 dict 形式 |
| `$sort` | `ORDER BY` | `1`/`-1` 或 `'asc'`/`'desc'`；`0` 与 `$meta` 报错 |
| `$limit` / `$skip` | `LIMIT` / `OFFSET` | 多次 `$limit` 取最小值，多次 `$skip` 累加 |
| `$count` | `COUNT(*) AS name` | 上游过滤 / 截断先固化进 CTE 内层 |
| `$lookup` | `LEFT JOIN`（简单式）/ CTE 子查询（管道式） | 简单式为 1:1 语义，见 `限制` |
| `$unwind` | CTE + `CROSS JOIN JSON_EACH` | 支持 `preserveNullAndEmptyArrays`（LEFT JOIN）、`includeArrayIndex`；仅 JSON 字符串数组 |

## 支持的操作符

### `$match`

- **比较**: `=`、`$ne`、`$gt`、`$gte`、`$lt`、`$lte`、`$in`、`$nin`（`$in: []` → 永假；null 字面量 → `IS NULL` / `IS NOT NULL`）
- **逻辑**: `$and`、`$or`、`$nor`、`$not`（空数组语义与 MongoDB 一致：`$and: []` → `1=1`，`$or: []` → `1=0`）
- **元素**: `$exists`、`$type`、`$regex`（+`$options: 'i'`）、`$size`（仅整数）、`$all`（元素级匹配，非子串）
- `$regex` 转为 `LIKE ... ESCAPE`：支持字面字符、`.`（→`_`）、首尾 `^`/`$` 锚点；`* + ( ) [ ] { } |` 等特性**显式报错**
- 不支持（显式报错）：`$text`、`$where`、`$expr`、`$elemMatch`、`$mod`、点号路径 / 子文档匹配

### 聚合函数（`$group` 累加器）

- `$count`、`$sum`（`1` → `COUNT(*)`）、`$avg`、`$min`、`$max`
- `$push` / `$addToSet` → 方言原生 JSON 数组聚合
- `$stdDevPop` / `$stdDevSamp` → MySQL/PostgreSQL 原生；**SQLite 显式报错**
- `$first` / `$last` → **显式报错**（关系库无确定等价语义）

### 表达式（`$project` / `$addFields`）

- **条件**: `$cond`（文档式 + 数组式）、`$ifNull`、`$switch`
- **算术**: `$add` / `$subtract` / `$multiply` / `$divide`（方言除法）/ `$mod`（`%`）
- **字符串**: `$concat`、`$toLower`/`$toUpper`、`$substr`/`$substrBytes`/`$substrCP`（0-based → 1-based 自动转换）、`$trim`/`$ltrim`/`$rtrim`（`chars`）、`$strLenCP`/`$strLenBytes`、`$toString`
- **数组**: `$size`、`$arrayElemAt`、`$slice`（三种写法）、`$isArray`、`$concatArrays`
- **日期**: `$year`/`$month`/`$dayOfMonth`/`$hour`/`$minute`/`$second`、`$dateToString`
- **内联聚合**: 投影内的 `$sum`/`$avg`/`$min`/`$max`

## 语义与限制（重要）

1. **`$lookup` 简单式是 1:1 语义**：生成等值 `LEFT JOIN`，一个文档匹配多条对端记录时结果行数会相乘（MongoDB 是存入匹配数组）。多对多请使用**管道式 `$lookup`**（`let` + `$expr` 可推导关联条件），或先在对端管道内聚合。
2. **位置语义严格对应 MongoDB**：`$sort`/`$limit`/`$skip` 作用在随后 CTE 阶段（`$group`/`$project`/`$unwind`/`$count`）**之前**的行；单独 `$skip` 按方言生成（SQLite `LIMIT -1 OFFSET n`、MySQL `LIMIT 18446744073709551615 OFFSET n`、PostgreSQL 裸 `OFFSET n`）。
3. **`$unwind` 只支持 JSON 字符串数组列**（基表），展开后列与原列同名（SQLite 取首个出现 = 展开值）；仅 SQLite 为原生实现，MySQL/PG 回退 + 警告。
4. **`$project` 排除模式（所有字段均为 0）不支持**——需要 schema 元数据才能表达"除某列外全选"，显式报错。`_id: 0`（包含模式下排除 _id 字段）是合法用法，支持。
5. **不支持的语法显式报错**（不静默丢弃）：`$text`/`$where`/`$expr`/`$elemMatch`（`$match` 内）、`$first`/`$last`、`$sort: 0`、`$meta` 排序、未知操作符。
6. **字段引用**：仅支持平铺列（`t.field`），点号嵌套路径（`a.b`）报错。

## 示例

### 示例 1: 过滤

```python
pipeline = [{"$match": {"status": "active"}}]
```
```sql
SELECT *
FROM users AS t
WHERE 1=1
  AND (t.status = 'active')
```

### 示例 2: 多条件组合

```python
pipeline = [{"$match": {"age": {"$gte": 18, "$lt": 65}}}]
```

```sql
SELECT *
FROM users AS t
WHERE 1=1
  AND ((t.age >= 18)
       AND (t.age < 65))
```

### 示例 3: 分组聚合 + 排序 + 截断

```python
pipeline = [
    {"$group": {"_id": "$category", "count": {"$sum": 1}, "avgPrice": {"$avg": "$price"}}},
    {"$sort": {"count": -1}},
    {"$limit": 10},
]
sql, _ = convert_mongo_pipeline_to_sql(pipeline, "products")
```
```sql
WITH cte_1 AS
  (SELECT t.category AS _id,
          COUNT(*) AS count,
          AVG(t.price) AS avgPrice
   FROM products AS t
   GROUP BY t.category)
SELECT *
FROM cte_1 AS t
ORDER BY t.count DESC
LIMIT 10
```

### 示例 4: 字段重命名 + 计算字段

```python
pipeline = [
    {"$project": {
        "product_name": "$name",
        "total_value": {"$multiply": ["$price", "$quantity"]},
    }}
]
```
```sql
WITH cte_1 AS
  (SELECT t.name AS product_name,
          (t.price * t.quantity) AS total_value
   FROM products AS t)
SELECT *
FROM cte_1 AS t
```

### 示例 5: 字符串连接（方言差异见 `指定 SQL 方言`）

```python
pipeline = [{"$project": {"fullName": {"$concat": ["$firstName", " ", "$lastName"]}}}]
sql, _ = convert_mongo_pipeline_to_sql(pipeline, "users")
```

```sql
WITH cte_1 AS
  (SELECT (t.firstName || ' ' || t.lastName) AS fullName
   FROM users AS t)
SELECT *
FROM cte_1 AS t
```

### 示例 6: 关联查询（`$lookup`）

```python
pipeline = [
    {"$lookup": {
        "from": "orders",
        "localField": "_id",
        "foreignField": "customerId",
        "as": "orders",
    }}
]
```
```sql
SELECT *
FROM customers AS t
LEFT JOIN orders AS orders ON t._id = orders.customerId
```

### 示例 7: 条件表达式 + 分组（`$switch` / `$cond`）

```python
pipeline = [
    {"$addFields": {"grade": {"$switch": {
        "branches": [
            {"case": {"$gte": ["$score", 90]}, "then": "A"},
            {"case": {"$gte": ["$score", 60]}, "then": "B"},
        ],
        "default": "C",
    }}}},
    {"$group": {"_id": "$grade", "n": {"$sum": 1}}},
    {"$sort": {"_id": 1}},
]
```

生成 `CASE WHEN` 的 CTE 后按等级分组（完整输出参见 `tests/pipeline_suite.py` 中 `addfields_*` / `group_*` 用例的 golden 文件）。

## 高级特性

### 自定义阶段处理器

```python
from mongo_2_sql.core.stage_base import StageProcessor, RenderContext
from mongo_2_sql.core.stage_loader import StageLoader
from mongo_2_sql import convert_mongo_pipeline_to_sql

class CustomStage(StageProcessor):
    def __init__(self):
        super().__init__('$custom')

    def process(self, stage_value, context: RenderContext) -> None:
        # 自定义处理逻辑：向 context 写入 select_columns / where_conditions 等
        ...

    def validate(self, stage_value) -> bool:
        return True

loader = StageLoader()
loader.register_processor('$custom', CustomStage)

sql, _ = convert_mongo_pipeline_to_sql(
    [{"$custom": {"foo": 1}}], "users", stage_loader=loader
)
```

### 自定义方言

继承 `SQLDialect` 实现抽象方法（`quote_ident`/`quote_string`/`escape_char`/`limit_offset`/`concat`/`trim`/`substr`/`date_part` 等），再在 `mongo_2_sql/dialects/__init__.py` 的 `_DIALECT_REGISTRY` 注册名称即可——CLI 的 `-d` 选项自动跟随。

### 错误处理最佳实践

```python
try:
    sql, metadata = convert_mongo_pipeline_to_sql(pipeline, "users")
    if metadata['warnings']:
        for w in metadata['warnings']:
            print(f"警告: {w}")
    print(sql)
except ValueError as e:
    print(f"管道格式错误: {e}")
except RuntimeError as e:
    print(f"转换错误: {e}")   # 信息含具体阶段与原因
```

## 测试

```bash
# 全量测试（398 条 = 差异/硬化基线 72 + 三方言 pipeline 套件 326）
python -m pytest tests -q

# 三方言 pipeline 套件使用统一 golden 文件（tests/golden/<方言>/<case>.sql），
# 转换结果变化后重新生成：
PIPGEN=1 python -m pytest tests/test_sqlite_pipelines.py tests/test_mysql_pipelines.py tests/test_postgres_pipelines.py -q
（`PIPGEN=1` 为 Linux/macOS 前缀写法；Windows PowerShell 先执行 `$env:PIPGEN='1'`）

# 代码风格
python -m ruff check .
```

套件说明：`tests/pipeline_suite.py` 定义 64 个三方言逐字共用的 pipeline case（expected_rows 由 SQLite 参考引擎真实执行取得）；SQLite 脚本额外真实执行并严格断言行值，MySQL/PostgreSQL 脚本做 golden 对比与回退警告断言。

## 常见问题

### Q: 为什么生成的 SQL 中有 `1=1`？

A: 占位条件，便于多条件动态拼接（`WHERE 1=1 AND ...`），可安全忽略。

### Q: 支持哪些 SQL 方言？

A: SQLite（完整）、MySQL/MariaDB、PostgreSQL（关系算子完整 + 部分 JSON 算子原生，其余回退 + 警告，见 `方言覆盖现状`）。新增方言只需继承 `SQLDialect` 并注册。

### Q: `$lookup` 多对多怎么办？

A: 简单式 `$lookup` 是 1:1 的 LEFT JOIN 语义；多对多请用管道式 `$lookup`（let + `$expr`），或在对端 pipeline 中先 `$group` + `$push` 聚合。

### Q: 为什么某些语法直接报错而不是生成近似 SQL？

A: 设计原则是"宁错勿假"——排除投影、`$first/$last`、`$text` 等在没有 schema 或确定等价语义时生成 SQL 会悄悄给错结果，因此显式抛出带原因的错误。

## 贡献

1. Fork 项目
2. 创建功能分支（`git checkout -b feature/amazing-feature`）
3. 提交更改（`git commit -m 'Add amazing feature'`）
4. 推送并创建 Pull Request
5. 确保 `pytest tests -q` 与 `ruff check .` 全绿

## 许可证

MIT License - 详见 [LICENSE](LICENSE) 文件

## 变更日志

### v0.1.7

- **安全与正确性大重构**：字符串 / 正则 / 表名字段名全面按方言转义（`$regex` → `LIKE ... ESCAPE`）；标识符保留字智能引用；`$sort/$limit/$skip` 位置语义固化进 CTE 内层（含分组前派生表下推）
- **显式错误**：未知算子、`$first/$last`、排除投影、`$text/$where/$expr/$elemMatch`、`$sort: 0` 等一律报错，不再静默丢弃
- **多方言补齐**：MySQL/PostgreSQL 原生实现 `$all`、`$stdDevPop/Samp`、`$push/$addToSet`；`limit_offset` 三方言各自正确（含单独 `$skip`）
- **新阶段**: `$count`、`$sortByCount`、`$set`（`$addFields` 别名）；`$unwind` 重写为 CTE + `JSON_EACH`（`preserveNullAndEmptyArrays`/`includeArrayIndex`）
- **新操作符**: `$switch`、`$trim`/`$ltrim`/`$rtrim`、`$strLenCP/$strLenBytes`、`$substrBytes/$substrCP`、`$slice`、`$isArray`、`$concatArrays`、`$exists`
- **CLI**: `--strict`、`-o`、警告走 stderr、方言选项来自注册表
- **测试**: 398 条（三方言共享 64-case golden 套件 + 硬化回归）

### v0.1.5 (2025-02-23)

- addFields 阶段独立解析器（AddFieldsExpressionParser），与 project 解耦
- 支持字面量值、字段引用、表达式三类语义
- 统一数值 1 为字面量（仅 `$project` 阶段保留包含/排除语义）

### v0.1.4 (2025-02-13)

- CTE 统一处理框架，支持 `$addFields`、`$project`、`$group` 阶段切片
- ExpressionResolver 统一表达式解析器；BaseExpressionParser 基础解析器
- SQL 格式化输出（sqlparse）；`$project` 字段重命名

## 致谢

感谢所有贡献者和用户的支持！
