/** * v0.8.0 回归套件 —— B-3 单管线(SQL 只有一条执行路径) * ============================================================================ * 修复前每个 QueryBuilder 的 `execute()` 都有自己的执行动作: * - `SelectQueryBuilder`:**无 JOIN 时直接调 `engine.find`**,只有 JOIN 走 executor; * - `UpdateQueryBuilder` / `DeleteQueryBuilder`:直接调 `engine.update/delete`。 * * 于是 TABLE API(`db.table('t').select()...`)与 SQL API(`db.query()`)是两条管线, * 规则各写一份。本套件锁定"两条入口必须逐值等价"这一不变量 —— 任何一侧将来 * 自行加逻辑(如自己处理投影、自己下推 LIMIT),下面的断言就会失败。 */ 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] B-3 单管线:TABLE API 与 SQL API 逐值等价', () => { describe.each(ENGINES)('%s 引擎', (label, mode, extra) => { let db: MetonaSqlark; beforeEach(async () => { db = await MetonaSqlark.create({ name: `b3-${label}-${Math.random().toString(36).slice(2)}`, mode, ...extra, }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, g: { type: 'string' }, n: { type: 'number' }, }); await db.query("INSERT INTO t VALUES ('1','a',10),('2','a',20),('3','b',30),('4','b',40)"); }); afterEach(async () => { await db.close(); }); // ------------------------------------------------------------------- // SELECT:直通路径此前缺少的阶段 // ------------------------------------------------------------------- it('SELECT 带表前缀的列:两条入口输出键相同', async () => { // 修复前 builder 直通 engine.find,行键保留 `t.n`;SQL 路径归一化为 `n` const viaBuilder = rowsOf>( await db.table('t').select(['t.id', 't.n']).execute(), ); const viaSql = rowsOf>(await db.query('SELECT t.id, t.n FROM t')); expect(viaBuilder).toHaveLength(4); expect(Object.keys(viaBuilder[0]).sort()).toEqual(Object.keys(viaSql[0]).sort()); expect(viaBuilder).toEqual(expect.arrayContaining(viaSql)); }); it('SELECT 列别名:两条入口一致', async () => { const viaBuilder = rowsOf>( await db.table('t').select(['id AS ident']).execute(), ); const viaSql = rowsOf>(await db.query('SELECT id AS ident FROM t')); expect(viaBuilder).toEqual(viaSql); }); it('SELECT 未知列:两条入口都抛 COLUMN_NOT_FOUND', async () => { // 修复前 builder 直通引擎:引擎不校验列是否存在 → 静默产出 `[{}, {}, {}, {}]` await expect(db.table('t').select(['nope']).execute()).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND', }); await expect(db.query('SELECT nope FROM t')).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND', }); }); it('SELECT LIMIT/OFFSET:两条入口切片一致(不双重截断)', async () => { const viaBuilder = rowsOf>( await db.table('t').select(['id']).orderBy('id').limit(2).offset(1).execute(), ); const viaSql = rowsOf>( await db.query('SELECT id FROM t ORDER BY id LIMIT 2 OFFSET 1'), ); expect(viaBuilder).toEqual([{ id: '2' }, { id: '3' }]); expect(viaBuilder).toEqual(viaSql); }); it('SELECT 常量列:两条入口一致', async () => { const viaBuilder = rowsOf>( await db.table('t').select(['id', '1 AS one']).limit(1).execute(), ); expect(viaBuilder).toEqual([{ id: '1', one: 1 }]); }); // ------------------------------------------------------------------- // UPDATE / DELETE:此前完全绕过 Executor // ------------------------------------------------------------------- it('UPDATE 带真实子查询 WHERE:TABLE API 正确解析(此前静默 0 行)', async () => { // 修复前:builder 把 `{ $subquery: ... }` 直接交给 engine.update,引擎层无人 // 解析 → matchWhere 判 UNKNOWN → **返回 0 且不报错**(用户以为没有匹配行)。 await db.defineTable('u', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO u VALUES ('1'),('2')"); const affected = await db .table('t') .update({ g: 'z' }) .where({ id: { $in: { $subquery: { type: 'SELECT', columns: ['id'], from: 'u', where: {} } } } as never }) .execute(); expect(affected).toBe(2); const rows = rowsOf>(await db.query('SELECT id, g FROM t ORDER BY id')); expect(rows).toEqual([ { id: '1', g: 'z' }, { id: '2', g: 'z' }, { id: '3', g: 'b' }, { id: '4', g: 'b' }, ]); }); it('DELETE 带真实子查询 WHERE:TABLE API 正确解析(此前静默 0 行)', async () => { await db.defineTable('u', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO u VALUES ('1'),('2')"); const affected = await db .table('t') .delete() .where({ id: { $in: { $subquery: { type: 'SELECT', columns: ['id'], from: 'u', where: {} } } } as never }) .execute(); expect(affected).toBe(2); expect(rowsOf(await db.query('SELECT id FROM t ORDER BY id'))).toEqual([ { id: '3' }, { id: '4' }, ]); }); it('UPDATE 基础路径:两条入口影响行数一致', async () => { const viaSql = await db.query("UPDATE t SET g = 'x' WHERE n > 15"); const sb = rowsOf(await db.query("SELECT id FROM t WHERE g = 'x' ORDER BY id")); expect(viaSql).toBe(3); expect(sb).toEqual([{ id: '2' }, { id: '3' }, { id: '4' }]); const viaBuilder = await db.table('t').update({ g: 'y' }).where({ n: { $gt: 15 } }).execute(); expect(viaBuilder).toBe(3); expect(rowsOf(await db.query("SELECT id FROM t WHERE g = 'y' ORDER BY id"))).toEqual(sb); }); it('DELETE 基础路径:两条入口影响行数一致', async () => { const viaBuilder = await db.table('t').delete().where({ n: { $gt: 15 } }).execute(); expect(viaBuilder).toBe(3); // 剩余行按 SELECT id 投影(SQL 路径只返回 id —— 两条入口的投影也一致) expect(rowsOf(await db.query('SELECT id FROM t'))).toEqual([{ id: '1' }]); }); it('UPDATE 未知列:两条入口都抛 COLUMN_NOT_FOUND', async () => { await expect( db.table('t').update({ nope: 1 } as never).where({ id: '1' }).execute(), ).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' }); }); it('UPDATE 违反 maxLength:两条入口都抛 VALIDATION_ERROR', async () => { await db.defineTable('u', { id: { type: 'string', primaryKey: true }, name: { type: 'string', maxLength: 3 }, }); await db.query("INSERT INTO u VALUES ('1', 'abc')"); await expect( db.table('u').update({ name: 'abcd' } as never).where({ id: '1' }).execute(), ).rejects.toMatchObject({ code: 'VALIDATION_ERROR' }); }); // ------------------------------------------------------------------- // 生命周期钩子仍在(且拿到真实 where) // ------------------------------------------------------------------- it('TABLE API 写操作的钩子仍触发,且 before 拿到真实 where', async () => { const seen: string[] = []; db.on('beforeUpdate', (query) => { seen.push(`before:${JSON.stringify((query as { where: unknown }).where)}`); }); db.on('afterUpdate', (_query, _updates, count) => { seen.push(`after:${String(count)}`); }); await db.table('t').update({ g: 'q' }).where({ n: { $gt: 15 } }).execute(); // 关键:before 的 where 必须是 builder 上累积的条件, // 而不是构造 builder 时的空对象(否则钩子拿不到过滤条件) expect(seen).toEqual(['before:{"n":{"$gt":15}}', 'after:3']); }); it('TABLE API 删除的钩子仍触发,且 before 拿到真实 where', async () => { const seen: string[] = []; db.on('beforeDelete', (query) => { seen.push(`before:${JSON.stringify((query as { where: unknown }).where)}`); }); db.on('afterDelete', (_query, count) => { seen.push(`after:${String(count)}`); }); await db.table('t').delete().where({ id: '1' }).execute(); expect(seen).toEqual(['before:{"id":"1"}', 'after:1']); }); // ------------------------------------------------------------------- // maxRowsPerQuery 与列校验也覆盖 TABLE API // ------------------------------------------------------------------- it('TABLE API 的 SELECT 同样受 maxRowsPerQuery 约束', async () => { const limited = rowsOf(await db.table('t').select(['id']).execute()); // 本引擎默认不限(maxRowsPerQuery 0),此处只验证两条入口结果一致 const viaSql = rowsOf(await db.query('SELECT id FROM t')); expect(limited).toEqual(viaSql); }); }); }); describe('[v0.8.0] B-3 单管线:maxRowsPerQuery 对两条入口一致', () => { it('TABLE API 与 SQL 都被截断到同一行数', async () => { const db = await MetonaSqlark.create({ name: 'b3-maxrows', mode: 'memory', maxRowsPerQuery: 2 }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO t VALUES ('1'),('2')"); await db.query("INSERT INTO t VALUES ('3')"); const viaSql = rowsOf(await db.query('SELECT id FROM t')); const viaBuilder = rowsOf(await db.table('t').select(['id']).execute()); // 修复前 builder 直通引擎、不受 maxRowsPerQuery 约束 → 返回 3 行 expect(viaBuilder).toHaveLength(2); expect(viaBuilder).toEqual(viaSql); await db.close(); }); });