/** * v0.7.4 回归测试 — 深度审计第七阶段修复 * * 1. UPDATE/DELETE WHERE 子查询静默 0 行(四引擎) * 2. 写语句关联引用($col / EXISTS)显式 NOT_SUPPORTED * 3. EXPLAIN UPDATE/DELETE 子查询 estimatedRows * 4. 主键 NULL/undefined 静默入库(四引擎) * 5. DROP INDEX 建表 UNIQUE 约束保护 / CREATE UNIQUE INDEX 可解除 * 6. GROUP BY null 与 'null' 字符串分离(DISTINCT/UNION 同类) * 7. UPDATE 未知列显式报错 * 8. queryStream 多语句显式报错 * 9. KVStore 后台错误跨 reopen 清理 * 10. Hybrid beginTransaction 部分成功补偿回滚 * 11. findStream 真惰性(limit 提前终止) * 12. reindex 单次扫描多索引列重建 */ import { MetonaSqlark } from '../src/core'; import { MemoryEngine } from '../src/engine/memory'; import { KVStore } from '../src/engine/kvstore/index'; import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium'; const uniqueName = (tag: string): string => `v074-${tag}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; async function makeDb(mode: 'memory' | 'disk' | 'aria' | 'hybrid') { const db = await MetonaSqlark.create({ name: uniqueName(mode), mode, diskEngine: 'memory', }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' }, }); return db; } // =================================================================== // 1. UPDATE / DELETE 子查询 // =================================================================== describe('v0.7.4: UPDATE/DELETE WHERE 子查询(此前静默 0 行)', () => { test.each([ ['memory', 'memory'], ['disk', 'disk'], ['aria', 'aria'], ['hybrid', 'hybrid'], ] as const)('%s: UPDATE IN (SELECT) 正确影响行', async (_label, mode) => { const db = await makeDb(mode); await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)"); const n = await db.query('UPDATE t SET v = 99 WHERE id IN (SELECT id FROM t WHERE v > 15)'); expect(n).toBe(1); const rows = await db.query('SELECT * FROM t ORDER BY id') as Record[]; expect(rows.map((r) => r.v)).toEqual([10, 99]); await db.close(); }); test.each([ ['memory', 'memory'], ['disk', 'disk'], ['aria', 'aria'], ['hybrid', 'hybrid'], ] as const)('%s: DELETE IN (SELECT) 正确影响行', async (_label, mode) => { const db = await makeDb(mode); await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)"); const n = await db.query('DELETE FROM t WHERE id IN (SELECT id FROM t WHERE v > 15)'); expect(n).toBe(1); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows.map((r) => r.id)).toEqual(['1']); await db.close(); }); test('memory: UPDATE 标量子查询(op (SELECT))', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)"); const n = await db.query('UPDATE t SET v = 1 WHERE v = (SELECT MAX(v) FROM t)'); expect(n).toBe(1); await db.close(); }); test('aria: UPDATE 子查询后持久化重开一致(kv 后端跨实例共享介质)', async () => { const name = uniqueName('aria-persist'); const db1 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' }); await db1.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' }, }); await db1.query("INSERT INTO t VALUES ('1', 10), ('2', 20)"); await db1.query('UPDATE t SET v = 99 WHERE id IN (SELECT id FROM t WHERE v > 15)'); await db1.close(); const db2 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' }); const rows = await db2.query('SELECT * FROM t ORDER BY id') as Record[]; expect(rows.map((r) => r.v)).toEqual([10, 99]); await db2.close(); }); }); // =================================================================== // 2. 写语句关联引用显式报错 // =================================================================== describe('v0.7.4: 写语句关联引用显式 NOT_SUPPORTED', () => { test('memory: SQL 列比较(a = b)解析层明确报错(此前静默或误解析)', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10)"); // parser 不支持列引用作为比较值 → 明确 PARSE_ERROR,绝不静默影响行 const err = await db.query('UPDATE t SET v = 1 WHERE id = id').catch((e: unknown) => e as { code?: string }); expect((err as { code?: string }).code).toBeDefined(); // 行未被修改(无静默副作用) const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows[0].v).toBe(10); await db.close(); }); test('memory: QueryBuilder 列引用更新 WHERE 显式 NOT_SUPPORTED', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect( db.table('t').update({ v: 1 }).where({ id: { $eq: { $col: 'id' } } }).execute(), ).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.close(); }); test('memory: DELETE WHERE EXISTS(关联)报错而非静默 0 行', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect( db.query('DELETE FROM t WHERE EXISTS (SELECT 1 FROM t t2 WHERE t2.id = t.id)'), ).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.close(); }); test('aria: UPDATE 关联 EXISTS 报错', async () => { const db = await makeDb('aria'); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect( db.query('UPDATE t SET v = 1 WHERE EXISTS (SELECT 1 FROM t t2 WHERE t2.id = t.id)'), ).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.close(); }); }); // =================================================================== // 3. EXPLAIN 子查询 estimatedRows // =================================================================== describe('v0.7.4: EXPLAIN UPDATE/DELETE 子查询估算', () => { test('EXPLAIN UPDATE IN (SELECT) estimatedRows 正确', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)"); const plan = await db.query('EXPLAIN UPDATE t SET v = 1 WHERE id IN (SELECT id FROM t WHERE v > 15)') as Record; expect(plan.estimatedRows).toBe(1); // EXPLAIN 不产生副作用 const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows.map((r) => r.v)).toEqual([10, 20]); await db.close(); }); }); // =================================================================== // 4. 主键 NULL/undefined 拒绝 // =================================================================== describe('v0.7.4: 主键 NULL/undefined 拒绝', () => { test.each([ ['memory', 'memory'], ['disk', 'disk'], ['aria', 'aria'], ['hybrid', 'hybrid'], ] as const)('%s: INSERT 主键 NULL 拒绝', async (_label, mode) => { const db = await makeDb(mode); await expect(db.query('INSERT INTO t VALUES (NULL, 1)')).rejects.toMatchObject({ code: 'VALIDATION_ERROR' }); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toHaveLength(0); await db.close(); }); test.each([ ['memory', 'memory'], ['disk', 'disk'], ['aria', 'aria'], ] as const)('%s: INSERT 省略主键(无 default)拒绝', async (_label, mode) => { const db = await makeDb(mode); await expect(db.query('INSERT INTO t (v) VALUES (5)')).rejects.toMatchObject({ code: 'VALIDATION_ERROR' }); await db.close(); }); test('memory: UPDATE 主键置 null 拒绝', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect(db.query("UPDATE t SET id = NULL WHERE id = '1'")).rejects.toMatchObject({ code: 'VALIDATION_ERROR' }); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows[0].id).toBe('1'); await db.close(); }); test('memory: 主键 default 生效时允许省略', async () => { const db = await MetonaSqlark.create({ name: uniqueName('pkdefault'), mode: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true, default: 'auto' }, v: { type: 'number' }, }); const pks = await db.query('INSERT INTO t (v) VALUES (5)') as string[]; expect(pks).toEqual(['auto']); await db.close(); }); test('aria: 主键 undefined(Table API)拒绝', async () => { const db = await makeDb('aria'); await expect( db.table('t').insert({ v: 3 } as Record), ).rejects.toMatchObject({ code: 'VALIDATION_ERROR' }); await db.close(); }); }); // =================================================================== // 5. DROP INDEX 与 UNIQUE 约束 // =================================================================== describe('v0.7.4: DROP INDEX UNIQUE 约束保护', () => { test.each([ ['memory', 'memory'], ['aria', 'aria'], ] as const)('%s: 建表 UNIQUE 列 DROP INDEX 拒绝且约束保留', async (_label, mode) => { const db = await MetonaSqlark.create({ name: uniqueName('uniq'), mode, diskEngine: 'memory' }); await db.defineTable('u', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, }); await db.query("INSERT INTO u VALUES ('1', 'a@x.com')"); await expect(db.query('DROP INDEX idx ON u (email)')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); // 约束仍生效 await expect(db.query("INSERT INTO u VALUES ('2', 'a@x.com')")).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); const schema = await db.getEngine().getTableSchema('u'); expect(schema!.columns.email.unique).toBe(true); await db.close(); }); test.each([ ['memory', 'memory'], ['aria', 'aria'], ] as const)('%s: CREATE UNIQUE INDEX 后 DROP 可解除(本会话)', async (_label, mode) => { const db = await MetonaSqlark.create({ name: uniqueName('uniq2'), mode, diskEngine: 'memory' }); await db.defineTable('u', { id: { type: 'string', primaryKey: true }, email: { type: 'string' }, }); await db.query("INSERT INTO u VALUES ('1', 'a@x.com')"); await db.query('CREATE UNIQUE INDEX idx_u ON u (email)'); await expect(db.query("INSERT INTO u VALUES ('2', 'a@x.com')")).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); await db.query('DROP INDEX idx_u ON u (email)'); // 约束已随索引解除 await db.query("INSERT INTO u VALUES ('2', 'a@x.com')"); const rows = await db.query('SELECT * FROM u') as Record[]; expect(rows).toHaveLength(2); await db.close(); }); test('aria: 建表 UNIQUE DROP INDEX 拒绝后重启约束仍在(kv 后端)', async () => { const name = uniqueName('uniq3'); const db1 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' }); await db1.defineTable('u', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, }); await db1.query("INSERT INTO u VALUES ('1', 'a@x.com')"); await expect(db1.query('DROP INDEX idx ON u (email)')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db1.close(); const db2 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' }); await expect(db2.query("INSERT INTO u VALUES ('2', 'a@x.com')")).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); await db2.close(); }); }); // =================================================================== // 6. GROUP BY / DISTINCT / UNION 键编码 // =================================================================== describe('v0.7.4: 分组/去重键类型安全编码', () => { test('memory: GROUP BY null 与 "null" 字符串分离', async () => { const db = await MetonaSqlark.create({ name: uniqueName('grp'), mode: 'memory' }); await db.defineTable('g', { id: { type: 'string', primaryKey: true }, grp: { type: 'string' }, }); await db.query("INSERT INTO g VALUES ('a', NULL), ('b', 'null'), ('c', NULL)"); const rows = await db.query('SELECT grp, COUNT(*) AS c FROM g GROUP BY grp') as Record[]; expect(rows).toHaveLength(2); const byKey = new Map(rows.map((r) => [r.grp, r.c])); expect(byKey.get(null)).toBe(2); expect(byKey.get('null')).toBe(1); await db.close(); }); test('aria: GROUP BY null 分离', async () => { const db = await MetonaSqlark.create({ name: uniqueName('grp-aria'), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('g', { id: { type: 'string', primaryKey: true }, grp: { type: 'string' }, }); await db.query("INSERT INTO g VALUES ('a', NULL), ('b', 'null')"); const rows = await db.query('SELECT grp, COUNT(*) AS c FROM g GROUP BY grp') as Record[]; expect(rows).toHaveLength(2); await db.close(); }); test('memory: DISTINCT null 与 "\\0" 字符串分离', async () => { const db = await MetonaSqlark.create({ name: uniqueName('dist'), mode: 'memory' }); await db.defineTable('g', { id: { type: 'string', primaryKey: true }, grp: { type: 'string' }, }); await db.query(`INSERT INTO g VALUES ('a', NULL), ('b', '\\0'), ('c', '\\0')`); const rows = await db.query('SELECT DISTINCT grp FROM g') as Record[]; expect(rows).toHaveLength(2); await db.close(); }); test('memory: UNION null 与字符串不吞并', async () => { const db = await MetonaSqlark.create({ name: uniqueName('union'), mode: 'memory' }); await db.defineTable('g', { id: { type: 'string', primaryKey: true }, grp: { type: 'string' }, }); await db.query("INSERT INTO g VALUES ('a', NULL), ('b', 'null')"); const rows = await db.query( "SELECT grp FROM g WHERE id = 'a' UNION SELECT grp FROM g WHERE id = 'b'", ) as Record[]; expect(rows).toHaveLength(2); await db.close(); }); }); // =================================================================== // 7. UPDATE 未知列报错 // =================================================================== describe('v0.7.4: UPDATE 未知列显式报错', () => { test.each([ ['memory', 'memory'], ['disk', 'disk'], ['aria', 'aria'], ['hybrid', 'hybrid'], ] as const)('%s: SQL UPDATE 未知列 COLUMN_NOT_FOUND', async (_label, mode) => { const db = await makeDb(mode); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect(db.query("UPDATE t SET nonexistent = 9 WHERE id = '1'")).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' }); const rows = await db.query('SELECT * FROM t') as Record[]; expect(JSON.stringify(rows[0])).not.toContain('nonexistent'); await db.close(); }); test('memory: Table API update 未知列报错', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect( db.table('t').update({ nonexistent: 1 }).where({ id: '1' }).execute(), ).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' }); await db.close(); }); test('memory: 更新既有列不受影响(回归护栏)', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10)"); const n = await db.query("UPDATE t SET v = 20 WHERE id = '1'"); expect(n).toBe(1); await db.close(); }); }); // =================================================================== // 8. queryStream 多语句 // =================================================================== describe('v0.7.4: queryStream 多语句显式报错', () => { test('queryStream 多语句抛 PARSE_ERROR(不静默忽略后续语句)', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect( db.queryStream('SELECT * FROM t; DELETE FROM t', () => {}), ).rejects.toMatchObject({ code: 'PARSE_ERROR' }); // 后续语句未执行 const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toHaveLength(1); await db.close(); }); test('queryStream 单语句正常流式(回归护栏)', async () => { const db = await makeDb('memory'); await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)"); let n = 0; const count = await db.queryStream('SELECT * FROM t', () => { n++; }); expect(count).toBe(2); expect(n).toBe(2); await db.close(); }); }); // =================================================================== // 9. KVStore 后台错误跨 reopen // =================================================================== describe('v0.7.4: KVStore 后台错误状态生命周期', () => { test('close/reopen 后 checkpoint 不抛旧 KV_BACKGROUND_ERROR', async () => { SharedMemoryBackend.clearRegistry(); const medium = new SharedMemoryBackend(); const kv1 = new KVStore(medium); await kv1.open('v074-kv-life'); // 制造一次失败写入:直接在介质层破坏 append(用只读 medium 模拟失败) await kv1.close(); // 用带失败注入的介质:append 抛错 → lastBackgroundError 记录 const failingMedium = { open: async (_n: string) => {}, close: async () => {}, isOpen: () => true, read: async () => null, write: async () => {}, append: async () => { throw new Error('disk full'); }, writeMany: async () => {}, delete: async () => {}, deleteMany: async () => {}, listKeys: async () => [], exists: async () => false, clear: async () => {}, }; const kv2 = new KVStore(failingMedium as never); await kv2.open('v074-kv-fail'); await expect(kv2.put('k', new TextEncoder().encode('v').buffer)).rejects.toThrow(); await kv2.close(); // 重开同一实例(同 medium 不再失败) (failingMedium.append as unknown) = async () => {}; await kv2.open('v074-kv-fail'); // 无残留后台错误:checkpoint 不抛 await expect(kv2.checkpoint()).resolves.not.toThrow(); await kv2.close(); }); }); // =================================================================== // 10. Hybrid beginTransaction 补偿 // =================================================================== describe('v0.7.4: Hybrid beginTransaction 部分成功补偿', () => { test('磁盘 begin 失败时内存快照回滚(后续事务可正常开始)', async () => { const db = await MetonaSqlark.create({ name: uniqueName('hybrid-begin'), mode: 'hybrid', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, }); const disk = (db.getEngine() as { getDiskEngineType?: () => string } & Record); // 让磁盘引擎 begin 抛错:先把磁盘引擎置于活跃事务 const diskEngine = (db.getEngine() as unknown as { getDiskEngine?: () => { beginTransaction(): Promise } }).getDiskEngine ? undefined : undefined; // HybridEngine 无 getDiskEngine 公共接口 → 直接通过 disk 事务路径制造失败: // 内存引擎先 begin 成功、磁盘引擎第二个 begin 报 TX_ACTIVE const hybridEngine = db.getEngine() as unknown as { beginTransaction: () => Promise; }; await hybridEngine.beginTransaction(); // 磁盘引擎现在活跃;再次 begin → 内存 begin 成功、磁盘抛 TX_ACTIVE → 补偿回滚 await expect(hybridEngine.beginTransaction()).rejects.toMatchObject({ code: 'TX_ACTIVE' }); await hybridEngine.rollbackTransaction(); // 补偿后事务可正常开始并提交 await db.transaction(async (trx) => { await trx.table('t').insert({ id: '1' }); }); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toHaveLength(1); expect(diskEngine).toBeUndefined(); await db.close(); }); }); // =================================================================== // 11. findStream 真惰性 // =================================================================== describe('v0.7.4: aria findStream 真惰性(limit 提前终止)', () => { test('大表 limit 1 流式只消费必要条目', async () => { const db = await MetonaSqlark.create({ name: uniqueName('lazy'), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('logs', { id: { type: 'string', primaryKey: true }, level: { type: 'string' }, }); const rows: Record[] = []; for (let i = 0; i < 2000; i++) rows.push({ id: `l${i}`, level: i % 2 ? 'error' : 'info' }); await db.table('logs').insertMany(rows); let called = 0; const count = await db.table('logs').stream( () => { called++; }, { limit: 5 }, ); expect(count).toBe(5); expect(called).toBe(5); // 语义护栏:无 limit 时全量 let all = 0; await db.table('logs').stream(() => { all++; }); expect(all).toBe(2000); await db.close(); }); test('queryStream limit 流式正确 + 与物化结果一致', async () => { const db = await MetonaSqlark.create({ name: uniqueName('lazy2'), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('logs', { id: { type: 'string', primaryKey: true }, level: { type: 'string' }, }); const rows: Record[] = []; for (let i = 0; i < 500; i++) rows.push({ id: `l${i}`, level: i % 2 ? 'error' : 'info' }); await db.table('logs').insertMany(rows); const streamed: string[] = []; const count = await db.queryStream( "SELECT id FROM logs WHERE level = 'error' LIMIT 7", (row) => { streamed.push((row as Record).id as string); }, ); expect(count).toBe(7); const materialized = await db.query( "SELECT id FROM logs WHERE level = 'error' LIMIT 7", ) as Record[]; expect(streamed).toEqual(materialized.map((r) => r.id)); await db.close(); }); }); // =================================================================== // 12. reindex 多索引列重建 // =================================================================== describe('v0.7.4: reindex 单次扫描重建多索引列', () => { test('aria: 多索引列 REINDEX 后查询完整', async () => { const db = await MetonaSqlark.create({ name: uniqueName('reidx'), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('u', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, city: { type: 'string', index: true }, }); const rows: Record[] = []; for (let i = 0; i < 300; i++) { rows.push({ id: `u${i}`, email: `e${i}@x.com`, city: i % 3 ? 'Beijing' : 'Shanghai' }); } await db.table('u').insertMany(rows); await db.query('REINDEX TABLE u'); const bj = await db.query("SELECT * FROM u WHERE city = 'Beijing'") as Record[]; expect(bj).toHaveLength(200); const email = await db.query("SELECT * FROM u WHERE email = 'e1@x.com'") as Record[]; expect(email).toHaveLength(1); // 唯一约束在重建后仍生效 await expect(db.table('u').insert({ id: 'uX', email: 'e1@x.com', city: 'Beijing' })).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); await db.close(); }); test('memory: reindex 路径不受影响(无实现护栏)', async () => { const db = await MetonaSqlark.create({ name: uniqueName('reidx-mem'), mode: 'memory' }); await db.defineTable('u', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, }); await db.query("INSERT INTO u VALUES ('1', 'a@x.com')"); await expect(db.query('REINDEX TABLE u')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.close(); }); });