Files
MetonaSqlark/tests
thzxx 3983aae426 feat(B-4): 统一表达式求值 —— CASE 结构化解析(消除正则切分整类缺陷)
修复前 `parseCaseExpression` 用正则切分 WHEN/THEN/ELSE,不认字符串字面量与嵌套,
实测出三类错误结果:

① **嵌套 CASE 返回字符串残片**
   `CASE WHEN n>10 THEN CASE WHEN n>25 THEN 'huge' ELSE 'big' END ELSE 'small' END`
   → 实测返回 `"big' END ELSE 'small"`(正则把嵌套 CASE 的 ELSE 当成自己的分支
   边界,残片被原样返回给用户)。修复后正确返回 huge/big/small。

② **条件引用不存在的列时静默错值**
   `CASE WHEN nope > 1 THEN 'x' ELSE 'y' END` → 每行都是 'y' 且**无任何报错**
   (`cond = null` 表示"解析失败"→ 静默跳过分支),而同一个 nope 写在 WHERE 里
   会正常抛 COLUMN_NOT_FOUND。修复后统一报 COLUMN_NOT_FOUND。

③ **`GROUP BY CASE ... END` 完全不可用**
   → `COLUMN_NOT_FOUND Unknown column "CASE WHEN n>10 THEN 'big' ELSE 'small' END"`
   (分组把 CASE 原文当成列名)。而"按条件分组"是 SQL 最常见的分析写法之一。
   修复后正常输出 `[{band:'big',c:2},{band:'small',c:2}]`。

实现(新增 `src/query/expression.ts`):
- 复用 `sql/lexer` 的 token 流做**递归下降**(天然支持嵌套;字符串里的
  WHEN/ELSE/THEN 由词法层天然隔离,不可能被当作切分点);
- 条件按源码切片后交给与 WHERE **完全相同**的 `parseWhereCondition` —— 语义同源;
- 结果表达式显式支持字面量/列引用/嵌套 CASE,无法识别的**抛 NOT_SUPPORTED**
  (不再把原文当字符串返回);
- 解析结果记忆化(同一表达式在 N 行上只解析一次);
- 新增 `assertCaseColumnsExist`,在**分组/聚合之前**按 schema 校验 CASE 里引用的列
  (时机很关键:分组会把行替换为"分组键+聚合值",此后任何基于行的校验都会
  误报未知列 —— 这一点在实现中踩到并已修正)。

顺带修掉的**词法层**缺陷:`Token.position` 语义按 token 类型不一致 ——
`readString` 用 `position + 1`(指向引号**之内**),`readIdentifier`/`readNumber`
用 `position - len`(指向首字符)。任何"按 position 切片"的调用方都会对字符串
切错一个字符(实测 `'big'` 被切成 `"big'"`,导致 CASE 全部报 NOT_SUPPORTED)。
现统一为"token 首字符在源码中的下标",并用"从 position 重新词法化应得到同一
token"作为可判定判据加入回归测试。

验证:新增 tests/v080-case-expression.test.ts(54 项:词法层 2 + 解析层 8 +
求值层 5 + 列校验 2 + 四引擎端到端 37),并做**变异验证**:去掉嵌套 CASE 感知后
7 项立即失败(含四引擎的端到端断言),恢复后全绿。
全量 91 套件 / 1807 测试通过;typecheck、lint、build 零错误零告警;dist 已重建。
2026-09-15 07:26:20 +08:00
..