feat(B-5): 输出列序号 + 分隔标识符语义统一

两个相关缺陷:

① **输出列序号不被支持**(SQL 标准特性)
   `ORDER BY 1` / `GROUP BY 2` 此前直接 `PARSE_ERROR: Expected identifier, got "1"`
   (实测)。这在手写 SQL 与 UNION 里很常用 —— 各分支输出列名可能不同,只能按
   序号引用。序号含义依赖 SELECT 列表,故 parser 只保留数字文本,由 executor
   在拿到列表后解析。
   规则(与 SQLite/标准一致):裸数字在范围内 → 位置;越界 → QUERY_ERROR
   (不说"未知列",问题出在位置而非名字);`GROUP BY <序号>` 指向聚合表达式 →
   QUERY_ERROR(按聚合值分组语义上不成立);`ORDER BY 0` / `1.5` → PARSE_ERROR。

② **分隔标识符(`"1"`)在投影期被当成常量**
   `CREATE TABLE q ("1" STRING ...)` 后 `SELECT "1" FROM q` 返回 `{"1": 1}`
   (**字面量 1**),而 `SELECT *` 返回正确的 `{"1": "z"}` —— 同一列两种结论。
   根因:parser 把 `"1"` 的引号丢掉,投影期的"裸数字 = 常量列"子句就把它吃了。

修复的关键设计(一致性是这里唯一的难点,实测踩了四次漂移):
  **引号只在"解析 → 执行"的边界脱去,且必须在同一处、对同一批引用统一处理。**
  parser 必须保留引号才能区分"名为 1 的列"(`"1"`)与"常量 1"(`1`);
  而校验/取值/排序/分组必须用真实列名。此前"校验用裸名、投影用带引号名"两套规则
  导致 `1` 与 `"1"` 两个键并存。现在统一在 `normalizeUnprefixedReferences`
  与 `projectRow`/`projectColumns`/`resolveGroupKeyValue` 的对应分支脱引号,
  并新增可复用的 `unquoteIdentifier`。

同时修正 `projectColumns`(where-matcher,被四个引擎共用):此前"找不到列就不产出
键",现在支持分隔标识符与 `别名.列` 的唯一后缀匹配;"是否未知列"仍由 executor 的
`assertProjectionColumnsExist` 判定并报错,投影层不做静默兜底。

验证:新增 tests/v080-output-ordinals.test.ts(64 项:序号四引擎 40 + 分隔标识符
四引擎 24),覆盖 DESC/多键/LIMIT+OFFSET/UNION/别名共存/越界/非法序号/
WHERE 与 GROUP BY 引用分隔标识符/优先级。
全量 91 套件 / 1870 测试通过;typecheck、lint、build 零错误零告警;dist 已重建。
This commit is contained in:
thzxx
2026-09-15 07:39:03 +08:00
parent 3983aae426
commit 2c945ee05a
13 changed files with 1301 additions and 84 deletions
+14
View File
@@ -856,6 +856,20 @@ declare class QueryExecutor {
* 于是 `COUNT (n)`(函数名后有空格)在"是否聚合"判定与"如何求值"两处结论不同。
*/
private computeSingleAggregate;
/**
* v0.8.0B-5):把 ORDER BY / GROUP BY 里的**输出列序号**解析为输出列名。
*
* SQL 标准允许 `ORDER BY 1` / `GROUP BY 2` 按输出列位置引用(UNION 各分支
* 列名可能不同,只能按序号引用)。规则:
* 1. 若存在**同名的真实列**(如列名就是 `"1"`,用双引号建表),按列名优先 ——
* 显式标识符胜过位置简写;
* 2. 纯数字 → 第 N 个输出列的**表达式原文**`SELECT n AS num ... ORDER BY 1`
* 解析为 `num`,因为投影后行里只有 `num`);
* 3. 序号越界 → `QUERY_ERROR`(不说"未知列",因为问题出在位置而不是名字);
* 4. `GROUP BY <序号>` 指向聚合表达式 → `QUERY_ERROR`
* (按聚合值分组语义上不成立,SQL 标准同样禁止)。
*/
private resolveOutputOrdinals;
/**
* 归一化"行键不带前缀"的查询中的所有引用 —— 剥离表别名前缀。
*