test(coverage): 补齐 B-4/B-5 新代码的分支覆盖 + ORDER BY 列校验 + 删除死代码

覆盖率门禁(statements 90 / branches 82 / functions 94 / lines 93)在 B-4/B-5 落地后
**真的失败了**(branches 81.87%、functions 93.93%)—— 说明门禁确实在起作用。
本次不是下调阈值,而是两种正确处置:

① 删除死代码(3 个导出,从未被调用)
   - `isCaseExpression`:B-4 重构后 executor 改用 `parseCaseExpression` 自身判前缀;
   - `assertNoSimpleCaseForm`:从未接线(简单 CASE 的拒绝由解析器报错覆盖);
   - `compareForCase`:从未被调用(三值比较走 `sql-compare`)。
   - `collectUnknownColumns`(validation.ts):同样从未被调用。
   留着它们会让覆盖面看起来更高而实际无人使用 —— 与"覆盖率要反映真实使用"相悖。

② 补齐真实分支的测试(不写"为覆盖而覆盖"的用例)
   - `column-value` 的两条取值路径:JOIN 行键 `别名.列` 的精确命中与唯一后缀回退;
   - JOIN 里两表同名列的**裸引用歧义**;
   - CASE 解析缓存的"超上限清空重建"分支(600 个不同表达式);
   - CASE 条件语法错误 → PARSE_ERROR。

顺带修掉一个新暴露的真实缺口:**ORDER BY 的键此前完全不校验**
  `SELECT ... FROM l JOIN r ON l.tag = r.tag ORDER BY tag`(两表都有 tag)既不报错
  也不确定按哪一列排 —— 结果取决于行键插入顺序("顺序偶尔不对"这类难查问题)。
  现在与 WHERE 同一口径:越界/未知 → COLUMN_NOT_FOUND,裸名歧义 → 要求限定。
  豁免两类合法写法:SELECT 别名(输出列名)与派生表(列来自子查询投影)。
  新增 `selectAliasNames` 并被 `orderByUsesSelectAlias` 复用 —— 两处若各写一份,
  就会出现"排序认为它是别名、校验认为它是列"的矛盾。

覆盖率达到:Statements 90.4% / Branches 82.19% / Functions 94.25% / Lines 93.41%,
四项均高于阈值。全量 89 套件 / 1868 测试通过;typecheck、lint 零错误零告警。
This commit is contained in:
thzxx
2026-09-15 07:51:10 +08:00
parent 2c945ee05a
commit d14663ef80
4 changed files with 126 additions and 56 deletions
+80
View File
@@ -220,3 +220,83 @@ describe('[v0.8.0] B-5 分隔标识符(四引擎)', () => {
});
});
});
// ---------------------------------------------------------------------------
// 列引用取值的分支覆盖(共享原语 column-value 的两个关键分支)
// ---------------------------------------------------------------------------
describe('[v0.8.0] B-5 列引用取值原语', () => {
it('JOIN 行的 `别名.列` 键与裸列引用都能取到', async () => {
const db = await MetonaSqlark.create({ name: 'b5-joinref', mode: 'memory' });
await db.defineTable('l', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } });
await db.defineTable('r', { id: { type: 'string', primaryKey: true }, l_id: { type: 'string' } });
await db.query("INSERT INTO l VALUES ('l1', 5)");
await db.query("INSERT INTO r VALUES ('r1', 'l1')");
// JOIN 行键形如 `l.id``ORDER BY l.n` 走"精确命中"`ORDER BY n` 走"唯一后缀"
const qualified = rowsOf<Record<string, unknown>>(
await db.query('SELECT l.id, l.n FROM l JOIN r ON l.id = r.l_id ORDER BY l.n'),
);
expect(qualified).toHaveLength(1);
const bare = rowsOf<Record<string, unknown>>(
await db.query('SELECT l.id, l.n FROM l JOIN r ON l.id = r.l_id ORDER BY n'),
);
expect(bare).toHaveLength(1);
await db.close();
});
it('JOIN 里两表同名列的裸引用 → 歧义报错(不静默取第一个)', async () => {
const db = await MetonaSqlark.create({ name: 'b5-ambiguous', mode: 'memory' });
await db.defineTable('l', { id: { type: 'string', primaryKey: true }, tag: { type: 'string' } });
await db.defineTable('r', { id: { type: 'string', primaryKey: true }, tag: { type: 'string' } });
await db.query("INSERT INTO l VALUES ('l1', 'a')");
await db.query("INSERT INTO r VALUES ('r1', 'a')");
// 两表都有 tag → 裸 `ORDER BY tag` 无法判定归属
await expect(
db.query('SELECT l.id FROM l JOIN r ON l.tag = r.tag ORDER BY tag'),
).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' });
// 限定后正常
const rows = rowsOf<Record<string, unknown>>(
await db.query('SELECT l.id FROM l JOIN r ON l.tag = r.tag ORDER BY l.tag'),
);
expect(rows).toHaveLength(1);
await db.close();
});
});
// ---------------------------------------------------------------------------
// CASE 解析缓存上限(防止缓存无界增长的分支)
// ---------------------------------------------------------------------------
describe('[v0.8.0] B-5 CASE 解析缓存上限', () => {
it('超过上限后清空重建,解析结果仍正确', async () => {
const db = await MetonaSqlark.create({ name: 'b5-cache', mode: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 5)");
// 构造 600 个互不相同的 CASE 表达式(超过 512 的缓存上限),
// 逐条执行以触发"清空重建"分支;结果必须始终正确。
for (let i = 0; i < 600; i++) {
const rows = rowsOf<Record<string, unknown>>(
await db.query(`SELECT CASE WHEN n > ${i} THEN 'gt' ELSE 'le' END AS r FROM t`),
);
expect(rows[0].r).toBe(i < 5 ? 'gt' : 'le');
}
await db.close();
});
});
// ---------------------------------------------------------------------------
// CASE 条件解析失败(PARSE_ERROR 分支)
// ---------------------------------------------------------------------------
describe('[v0.8.0] B-5 CASE 条件语法错误', () => {
it('条件无法解析 → PARSE_ERROR 且消息带上条件原文', async () => {
const db = await MetonaSqlark.create({ name: 'b5-badcond', mode: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 5)");
// `n >` 是残缺条件 → parseWhereCondition 抛错 → 包成 PARSE_ERROR
await expect(
db.query("SELECT CASE WHEN n > THEN 'a' ELSE 'b' END AS r FROM t"),
).rejects.toMatchObject({ code: 'PARSE_ERROR' });
await db.close();
});
});