|
| 1 | +# AstValidator WITH 语句支持修复 |
| 2 | + |
| 3 | +## 问题描述 |
| 4 | + |
| 5 | +测试 `TestAstValidator.testValidSelectForWithStatement()` 失败,因为 `AstValidator` 没有正确处理 SQL 中的 WITH 语句(CTE - Common Table Expression)。 |
| 6 | + |
| 7 | +## 根本原因 |
| 8 | + |
| 9 | +1. **CTE 表名被当作普通表校验**:WITH 语句中定义的临时表(如 `category_sales`、`total_sales`)被当作需要在白名单中的普通表 |
| 10 | +2. **分号解析错误**:Presto SQL Parser 不接受 SQL 末尾的分号 |
| 11 | +3. **CTE 之间的 JOIN 被校验**:CTE 定义的临时表之间的 JOIN 也被当作需要 linker 的 JOIN |
| 12 | +4. **CROSS JOIN 误报**:没有 ON 条件的 CROSS JOIN(包括逗号分隔的多表查询)被当作需要 linker 的 JOIN |
| 13 | + |
| 14 | +## 解决方案 |
| 15 | + |
| 16 | +### 1. 支持 WITH 语句中的 CTE(核心修复) |
| 17 | + |
| 18 | +**修改文件**:`AstValidator.java` |
| 19 | + |
| 20 | +在 `AstCollector` 类中添加: |
| 21 | +- 新增 `cteNames` 集合,用于存储 CTE 定义的表名 |
| 22 | +- 重写 `visitWith()` 方法,收集所有 CTE 名称 |
| 23 | +- 修改 `visitTable()` 方法,排除 CTE 表名的收集 |
| 24 | +- 修改 `visitJoin()` 方法,排除涉及 CTE 的 JOIN |
| 25 | + |
| 26 | +```java |
| 27 | +final Set<String> cteNames = new HashSet<>(); |
| 28 | + |
| 29 | +@Override |
| 30 | +protected Void visitWith(With node, Void context) { |
| 31 | + for (WithQuery query : node.getQueries()) { |
| 32 | + cteNames.add(query.getName().toString()); |
| 33 | + } |
| 34 | + return super.visitWith(node, context); |
| 35 | +} |
| 36 | + |
| 37 | +@Override |
| 38 | +protected Void visitTable(Table node, Void context) { |
| 39 | + String tableName = node.getName().toString(); |
| 40 | + if (!cteNames.contains(tableName)) { |
| 41 | + tables.add(tableName); |
| 42 | + } |
| 43 | + return super.visitTable(node, context); |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +### 2. 自动去除末尾分号 |
| 48 | + |
| 49 | +在 `validate()` 方法开头添加: |
| 50 | + |
| 51 | +```java |
| 52 | +// 去除末尾的分号(Presto Parser 不接受分号) |
| 53 | +sql = sql.trim(); |
| 54 | +if (sql.endsWith(";")) { |
| 55 | + sql = sql.substring(0, sql.length() - 1).trim(); |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +### 3. 跳过 CROSS JOIN 的 linker 校验 |
| 60 | + |
| 61 | +修改 `visitJoin()` 方法,只对有 ON 条件的 JOIN 进行 linker 校验: |
| 62 | + |
| 63 | +```java |
| 64 | +if (left != null && right != null && !cteNames.contains(left) && !cteNames.contains(right)) { |
| 65 | + // 如果有 JOIN 条件(不是 CROSS JOIN),才需要校验 linker |
| 66 | + if (node.getCriteria().isPresent()) { |
| 67 | + joins.add(new JoinPair(left, right)); |
| 68 | + } |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +### 4. 修复测试断言 |
| 73 | + |
| 74 | +**修改文件**:`TestAstValidator.java` |
| 75 | + |
| 76 | +- `testValidSelectForWithStatement`:添加必要的 linker(`toy_sales <-> toy_products`) |
| 77 | +- `testInvalidTableNotInWhitelist`:修改断言检查 `issues` 而不是 `reason` |
| 78 | + |
| 79 | +```java |
| 80 | +// 修改前 |
| 81 | +assertTrue("Error should mention invalid table", result.reason().contains("Invalid tables")); |
| 82 | + |
| 83 | +// 修改后 |
| 84 | +assertTrue("Error should mention invalid table", |
| 85 | + result.issues().stream().anyMatch(issue -> issue.contains("Invalid tables"))); |
| 86 | +``` |
| 87 | + |
| 88 | +## 测试结果 |
| 89 | + |
| 90 | +所有 12 个测试全部通过: |
| 91 | + |
| 92 | +``` |
| 93 | +[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 |
| 94 | +``` |
| 95 | + |
| 96 | +## 支持的 SQL 特性 |
| 97 | + |
| 98 | +修复后,`AstValidator` 现在支持: |
| 99 | + |
| 100 | +1. ✅ **WITH 语句(CTE)**:可以定义和使用多个 CTE |
| 101 | +2. ✅ **嵌套 CTE**:CTE 可以引用其他 CTE |
| 102 | +3. ✅ **CTE 与真实表的 JOIN**:CTE 中可以 JOIN 真实表 |
| 103 | +4. ✅ **CROSS JOIN**:不需要 linker 的笛卡尔积 JOIN |
| 104 | +5. ✅ **逗号分隔的多表查询**:隐式 CROSS JOIN |
| 105 | +6. ✅ **带分号的 SQL**:自动去除末尾分号 |
| 106 | + |
| 107 | +## 示例 |
| 108 | + |
| 109 | +### 成功案例:复杂 WITH 语句 |
| 110 | + |
| 111 | +```sql |
| 112 | +WITH category_sales AS ( |
| 113 | + SELECT |
| 114 | + p.Product_Category, |
| 115 | + SUM(CAST(p.Product_Price AS DECIMAL) * s.Units) AS category_total_sales |
| 116 | + FROM toy_sales s |
| 117 | + JOIN toy_products p ON s.Product_ID = p.Product_ID |
| 118 | + GROUP BY p.Product_Category |
| 119 | + HAVING SUM(CAST(p.Product_Price AS DECIMAL) * s.Units) > 50 |
| 120 | +), |
| 121 | +total_sales AS ( |
| 122 | + SELECT SUM(category_total_sales) AS grand_total |
| 123 | + FROM category_sales |
| 124 | +) |
| 125 | +SELECT |
| 126 | + cs.Product_Category, |
| 127 | + cs.category_total_sales, |
| 128 | + ROUND(cs.category_total_sales / ts.grand_total * 100, 2) AS percentage_of_total |
| 129 | +FROM category_sales cs |
| 130 | +CROSS JOIN total_sales ts; |
| 131 | +``` |
| 132 | + |
| 133 | +**校验结果**:✅ 通过 |
| 134 | +- CTE `category_sales` 和 `total_sales` 不在白名单校验范围内 |
| 135 | +- 真实表 `toy_sales` 和 `toy_products` 的 JOIN 通过 linker 校验 |
| 136 | +- CROSS JOIN 不需要 linker 校验 |
| 137 | + |
| 138 | +## 相关文件 |
| 139 | + |
| 140 | +- `AstValidator.java` - 核心校验逻辑 |
| 141 | +- `TestAstValidator.java` - 单元测试 |
| 142 | +- `ValidationResult.java` - 校验结果数据结构 |
| 143 | + |
| 144 | +## 日期 |
| 145 | + |
| 146 | +2026-06-02 |
0 commit comments