/** * v0.8.0 回归套件 —— A37 列引用:未限定操作数 + 存在性校验 * ============================================================================ * 两个此前互相纠缠的缺陷: * * ① **未限定列不能作比较操作数**(语法层) * parser 只把 `a.b` 形态当列引用,裸标识符一律走字面量解析,于是 * `WHERE x = y` → PARSE_ERROR: Expected value, got "y" * `ON k = k` → 同上 * 列对列比较被迫写成 `WHERE t.x = t.y` —— 而多表 JOIN 里未限定列恰恰是最 * 自然的写法(`ON user_id = id`)。 * * ② **`$col` 引用到不存在的列 → 静默空集**(语义层) * 投影侧早有"列必须存在"的校验,WHERE 侧一直**没有**:`$col` 取不到值时 * 三值求值器只能返回 UNKNOWN,而 UNKNOWN 在 WHERE 里表现为"不保留该行"。 * 实测修复前: * SELECT id FROM t WHERE id = oops → [](oops 是列引用) * SELECT id FROM t WHERE x = nope → [] * SELECT ... FROM l JOIN r ON l.k = r.nope → [](连接不上任何行) * 用户看到"没有数据",与"列名拼错"完全无法区分 —— 这正是 PLAN §3 根因 7 * (未解析引用静默变 false)的最后一处。 * * 契约变更:拼错的列名现在报 `COLUMN_NOT_FOUND`(而不是 PARSE_ERROR)—— * 问题不是语法错,而是"没有这一列"。 */ import { MetonaSqlark } from '../src/core'; import { rows as rowsOf } from './helpers/assertions'; import type { DatabaseConfig } from '../src/constants'; const ENGINES: Array<[string, DatabaseConfig['mode'], Partial]> = [ ['memory', 'memory', {}], ['disk', 'disk', {}], ['hybrid', 'hybrid', {}], ['aria', 'aria', { diskEngine: 'memory' }], ]; describe('[v0.8.0] A37 未限定列作操作数', () => { describe.each(ENGINES)('%s 引擎', (label, mode, extra) => { let db: MetonaSqlark; beforeEach(async () => { db = await MetonaSqlark.create({ name: `a37-${label}-${Math.random().toString(36).slice(2)}`, mode, ...extra, }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, x: { type: 'number' }, y: { type: 'number' }, }); await db.query("INSERT INTO t VALUES ('1',1,5),('2',2,2),('3',7,7),('4',NULL,9)"); }); afterEach(async () => { await db.close(); }); it('裸列比较与限定形态完全等价', async () => { // 数据:id=1 (x=1,y=5) / id=2 (2,2) / id=3 (7,7) / id=4 (x=NULL,y=9) // 期望值**逐个实测得出**(不是推导出来的):`x > y` 为空集, // 因为唯一满足的 id=1 是 1<5;id=4 的 x 是 NULL → 所有比较都是 UNKNOWN。 for (const [op, expected] of [ ['=', ['2', '3']], ['!=', ['1']], ['<', ['1']], ['>', []], ['>=', ['2', '3']], ['<=', ['1', '2', '3']], ] as Array<[string, string[]]>) { const bare = rowsOf<{ id: string }>(await db.query(`SELECT id FROM t WHERE x ${op} y ORDER BY id`)); const qualified = rowsOf<{ id: string }>( await db.query(`SELECT id FROM t WHERE t.x ${op} t.y ORDER BY id`), ); expect(bare.map((r) => r.id)).toEqual(expected); expect(bare).toEqual(qualified); } }); it('NULL 参与列比较时按三值逻辑处理(UNKNOWN 不保留)', async () => { // id=4 的 x 是 NULL → `x = y` 为 UNKNOWN → 不保留(上面 '=' 的期望里没有 4) const rows = rowsOf<{ id: string }>(await db.query('SELECT id FROM t WHERE x = y ORDER BY id')); expect(rows.map((r) => r.id)).not.toContain('4'); // 反向:IS NULL 能命中 const nullRows = rowsOf<{ id: string }>(await db.query('SELECT id FROM t WHERE x IS NULL')); expect(nullRows.map((r) => r.id)).toEqual(['4']); }); it('字面量操作数不受影响(NUMBER / STRING / TRUE / NULL 各有 token 类型)', async () => { expect(rowsOf(await db.query('SELECT id FROM t WHERE x = 2'))).toEqual([{ id: '2' }]); // x > 1 → id=2(2) 与 id=3(7);id=1 是 1,不满足 expect(rowsOf<{ id: string }>(await db.query('SELECT id FROM t WHERE x > 1 ORDER BY id')).map((r) => r.id)) .toEqual(['2', '3']); expect(rowsOf(await db.query('SELECT id FROM t WHERE y = 5'))).toEqual([{ id: '1' }]); // 字符串字面量 expect(rowsOf(await db.query("SELECT id FROM t WHERE id = '2'"))).toEqual([{ id: '2' }]); }); it('裸布尔列条件仍然工作(WHERE x 不因取消语法限制而改变)', async () => { await db.defineTable('b', { id: { type: 'string', primaryKey: true }, flag: { type: 'boolean' }, }); await db.query("INSERT INTO b VALUES ('1', TRUE), ('2', FALSE), ('3', NULL)"); const rows = rowsOf<{ id: string }>(await db.query('SELECT id FROM b WHERE flag')); expect(rows.map((r) => r.id)).toEqual(['1']); }); // ------------------------------------------------------------------- // ② 存在性校验 // ------------------------------------------------------------------- it('WHERE 中拼错的列名 → COLUMN_NOT_FOUND(此前静默空集)', async () => { await expect(db.query('SELECT id FROM t WHERE id = oops')).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND', }); await expect(db.query('SELECT id FROM t WHERE id = t.oops')).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND', }); await expect(db.query('SELECT id FROM t WHERE x = nope')).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND', }); // 字段键侧(原有行为,回归护栏) await expect(db.query('SELECT id FROM t WHERE nope = 1')).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND', }); }); it('JOIN ON 中拼错的列名 → COLUMN_NOT_FOUND', async () => { await db.defineTable('l', { id: { type: 'string', primaryKey: true }, k: { type: 'string' } }); await db.defineTable('r', { id: { type: 'string', primaryKey: true }, k: { type: 'string' } }); await db.query("INSERT INTO l VALUES ('l1','x')"); await db.query("INSERT INTO r VALUES ('r1','x')"); await expect( db.query('SELECT l.id AS lid, r.id AS rid FROM l JOIN r ON l.k = r.nope'), ).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' }); }); it('JOIN 里未限定且两表共有的列名 → 歧义报错(要求限定)', async () => { await db.defineTable('l', { id: { type: 'string', primaryKey: true }, k: { type: 'string' } }); await db.defineTable('r', { id: { type: 'string', primaryKey: true }, k: { type: 'string' } }); await db.query("INSERT INTO l VALUES ('l1','x')"); await db.query("INSERT INTO r VALUES ('r1','x')"); // WHERE 里裸写共有列 → 歧义(SQL 标准要求限定) await expect( db.query("SELECT l.id FROM l JOIN r ON l.k = r.k WHERE k = 'x'"), ).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' }); // 限定后正常。注意 JOIN 路径的行键带表别名前缀(既有行为,见 // executeJoinSelect 的行合并约定),因此输出键是 `l.id` 而非 `id`。 const rows = rowsOf>( await db.query("SELECT l.id FROM l JOIN r ON l.k = r.k WHERE l.k = 'x'"), ); expect(rows).toEqual([{ 'l.id': 'l1' }]); }); it('JOIN ON 用未限定列仍按主表列解释(不因两表同名而拒绝)', async () => { await db.defineTable('l', { id: { type: 'string', primaryKey: true }, k: { type: 'string' } }); await db.defineTable('r', { id: { type: 'string', primaryKey: true }, k: { type: 'string' } }); await db.query("INSERT INTO l VALUES ('l1','x'),('l2','y')"); await db.query("INSERT INTO r VALUES ('r1','x'),('r2','z')"); // `ON k = k` 是常见的简写写法:两侧都取主表列 → 恒真(仅受两表都有该列的约束), // 关键是**不报错**(列确实存在),结果由既有 joinRows 语义决定 const rows = rowsOf>( await db.query('SELECT l.id AS lid, r.id AS rid FROM l JOIN r ON k = k'), ); expect(rows.length).toBeGreaterThanOrEqual(0); }); }); });