/** * v0.7.2 修复回归测试 * * 覆盖: * - UPDATE 语句级原子性(多行更新撞唯一约束 → 整句拒绝,四引擎) * - 批内唯一互查(两行同时更新到同一新唯一值 → 整句拒绝) * - 事务内 DDL 拒绝(ALTER TABLE / CREATE INDEX / DROP INDEX,Memory/KVStore 对齐 Aria) * - SET NULL 级联撞 required 列 → FOREIGN_KEY_VIOLATION(delete 与 update 主键变更,Memory/Aria) * - bindParameters 注释感知(行注释 / 块注释中的 ? 与引号不参与绑定) * - 未闭合字符串字面量 → PARSE_ERROR * - 未知 where 操作符 → QUERY_ERROR(不再静默全匹配) * - UPDATE undefined 值语义化(保留旧值,不再删除列) * - Hybrid 磁盘写失败补偿(内存对齐磁盘状态) */ import { MetonaSqlark } from '../src/core'; import { parse } from '../src/sql/parser'; import { bindParameters } from '../src/sql/params'; import { matchWhere } from '../src/query/where-matcher'; import { MemoryEngine } from '../src/engine/memory'; import { KVStoreEngine } from '../src/engine/kvstore_engine'; import { HybridEngine } from '../src/hybrid/index'; import { createSchema } from '../src/table/schema'; function uniqueDB(): string { return `v072-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } // =================================================================== // UPDATE 语句级原子性 // =================================================================== describe('v0.7.2 — UPDATE 语句级原子性', () => { const modes = ['memory', 'disk', 'hybrid', 'aria'] as const; for (const mode of modes) { it(`${mode}: 多行更新撞唯一约束 → 整句拒绝、数据不变`, async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode, diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, }); await db.table('t').insertMany([ { id: 'a', email: 'a@x.com' }, { id: 'b', email: 'b@x.com' }, { id: 'c', email: 'c@x.com' }, ]); await db.table('t').update({ email: 'z@x.com' }).where({ id: 'a' }).execute(); // 一条语句同时把 b、c 改成 a 已占用的 z@x.com:b 成功改到一半,c 撞唯一 await expect( db.table('t').update({ email: 'z@x.com' }).where({ id: 'c' }).execute(), ).rejects.toThrow(); // 单行冲突 // 关键场景:无 where 全表更新到同一值 → 第二行即冲突 await expect( db.table('t').update({ email: 'y@x.com' }).execute(), ).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); // 整句拒绝:任何行都不该被改成 y@x.com const rows = await db.query('SELECT id, email FROM t ORDER BY id') as Record[]; expect(rows).toEqual([ { id: 'a', email: 'z@x.com' }, { id: 'b', email: 'b@x.com' }, { id: 'c', email: 'c@x.com' }, ]); await db.close(); }); } it('memory: 校验失败(required 置 null)也整句拒绝', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string', required: true }, }); await db.table('t').insertMany([ { id: '1', name: 'A' }, { id: '2', name: 'B' }, ]); await expect( db.table('t').update({ name: null }).execute(), ).rejects.toMatchObject({ code: 'VALIDATION_ERROR' }); const rows = await db.query('SELECT * FROM t ORDER BY id') as Record[]; expect(rows).toEqual([{ id: '1', name: 'A' }, { id: '2', name: 'B' }]); await db.close(); }); it('aria: 主键变更撞已有主键整句拒绝(含 WAL 一致性)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' }, }); await db.table('t').insertMany([ { id: 'a', n: 1 }, { id: 'b', n: 2 }, { id: 'c', n: 3 }, ]); // a → c 撞已有 c;且 d 行的更新(同语句)也不得生效 await expect( db.table('t').update({ id: 'c' }).where({ id: 'a' }).execute(), ).rejects.toMatchObject({ code: 'DUPLICATE_KEY' }); const rows = await db.query('SELECT * FROM t ORDER BY id') as Record[]; expect(rows.map((r) => r.id)).toEqual(['a', 'b', 'c']); await db.close(); }); }); // =================================================================== // 批内唯一互查 // =================================================================== describe('v0.7.2 — 批内唯一互查', () => { it('两行同时更新到同一新唯一值 → 整句拒绝(memory)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, }); await db.table('t').insertMany([ { id: 'a', email: 'a@x.com' }, { id: 'b', email: 'b@x.com' }, ]); await expect( db.table('t').update({ email: 'z@x.com' }).execute(), ).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); const rows = await db.query('SELECT email FROM t ORDER BY id') as Record[]; expect(rows).toEqual([{ email: 'a@x.com' }, { email: 'b@x.com' }]); await db.close(); }); it('两行同时更新到同一新唯一值 → 整句拒绝(aria)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, }); await db.table('t').insertMany([ { id: 'a', email: 'a@x.com' }, { id: 'b', email: 'b@x.com' }, ]); await expect( db.table('t').update({ email: 'z@x.com' }).execute(), ).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); const rows = await db.query('SELECT email FROM t ORDER BY id') as Record[]; expect(rows).toEqual([{ email: 'a@x.com' }, { email: 'b@x.com' }]); await db.close(); }); it('唯一值不变时更新自身不误报(回归)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, name: { type: 'string' }, }); await db.table('t').insertMany([ { id: 'a', email: 'a@x.com', name: 'A' }, ]); const n = await db.table('t').update({ name: 'A2', email: 'a@x.com' }).execute(); expect(n).toBe(1); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows[0].name).toBe('A2'); await db.close(); }); }); // =================================================================== // 事务内 DDL 拒绝 // =================================================================== describe('v0.7.2 — 事务内 DDL 拒绝(三引擎一致)', () => { it('memory: 事务内 ALTER TABLE 抛 NOT_SUPPORTED,事务可继续提交', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, a: { type: 'number' } }); await db.query('BEGIN'); await db.query("INSERT INTO t VALUES ('1', 10)"); await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.query('COMMIT'); const schema = await db.getEngine().getTableSchema('t'); expect('b' in (schema!.columns)).toBe(false); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toEqual([{ id: '1', a: 10 }]); await db.close(); }); it('memory: ROLLBACK 后无 DDL 残留', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query('BEGIN'); await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.query('ROLLBACK'); const schema = await db.getEngine().getTableSchema('t'); expect(Object.keys(schema!.columns)).toEqual(['id']); await db.close(); }); it('kv: 事务内 ALTER TABLE 抛 NOT_SUPPORTED', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'disk', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query('BEGIN'); await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.query('ROLLBACK'); const schema = await db.getEngine().getTableSchema('t'); expect(Object.keys(schema!.columns)).toEqual(['id']); await db.close(); }); it('kv: 事务内 CREATE INDEX / DROP INDEX 抛 NOT_SUPPORTED', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'disk', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }); await db.query('BEGIN'); await expect(db.query('CREATE INDEX idx_v ON t (v)')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.query('ROLLBACK'); const schema = await db.getEngine().getTableSchema('t'); expect(schema!.columns.v.index).toBeFalsy(); await db.close(); }); it('memory: 事务内 createTable/dropTable 仍可回滚(既有行为保持)', async () => { const eng = new MemoryEngine(); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); await eng.beginTransaction(); await eng.createTable(createSchema('tmp', { id: { type: 'string', primaryKey: true } })); await eng.rollbackTransaction(); expect(await eng.hasTable('tmp')).toBe(false); expect(await eng.hasTable('t')).toBe(true); await eng.close(); }); it('hybrid: 事务内 ALTER TABLE 抛 NOT_SUPPORTED(内存层拒绝)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query('BEGIN'); await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.query('ROLLBACK'); const schema = await db.getEngine().getTableSchema('t'); expect(Object.keys(schema!.columns)).toEqual(['id']); await db.close(); }); }); // =================================================================== // SET NULL 级联撞 required 列 // =================================================================== describe('v0.7.2 — SET NULL 级联撞 required 列', () => { it('memory: delete 级联 SET NULL 到 required 列 → FOREIGN_KEY_VIOLATION', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('parent', { id: { type: 'string', primaryKey: true } }); await db.defineTable('child', { id: { type: 'string', primaryKey: true }, pid: { type: 'string', required: true, references: 'parent.id', onDelete: 'SET NULL' }, }); await db.query("INSERT INTO parent VALUES ('p1')"); await db.query("INSERT INTO child VALUES ('c1', 'p1')"); await expect(db.query("DELETE FROM parent WHERE id = 'p1'")).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' }); // 整体拒绝:父行仍在 const rows = await db.query('SELECT * FROM parent') as Record[]; expect(rows).toHaveLength(1); await db.close(); }); it('aria: delete 级联 SET NULL 到 required 列 → FOREIGN_KEY_VIOLATION', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('parent', { id: { type: 'string', primaryKey: true } }); await db.defineTable('child', { id: { type: 'string', primaryKey: true }, pid: { type: 'string', required: true, references: 'parent.id', onDelete: 'SET NULL' }, }); await db.query("INSERT INTO parent VALUES ('p1')"); await db.query("INSERT INTO child VALUES ('c1', 'p1')"); await expect(db.query("DELETE FROM parent WHERE id = 'p1'")).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' }); const rows = await db.query('SELECT * FROM parent') as Record[]; expect(rows).toHaveLength(1); await db.close(); }); it('memory: update 主键变更 SET NULL 到 required 列 → 整体拒绝', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('parent', { id: { type: 'string', primaryKey: true } }); await db.defineTable('child', { id: { type: 'string', primaryKey: true }, pid: { type: 'string', required: true, references: 'parent.id', onUpdate: 'SET NULL' }, }); await db.query("INSERT INTO parent VALUES ('p1')"); await db.query("INSERT INTO child VALUES ('c1', 'p1')"); await expect( db.table('parent').update({ id: 'p2' }).where({ id: 'p1' }).execute(), ).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' }); const rows = await db.query('SELECT * FROM parent') as Record[]; expect(rows).toEqual([{ id: 'p1' }]); await db.close(); }); it('aria: update 主键变更 SET NULL 到 required 列 → 整体拒绝', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' }); await db.defineTable('parent', { id: { type: 'string', primaryKey: true } }); await db.defineTable('child', { id: { type: 'string', primaryKey: true }, pid: { type: 'string', required: true, references: 'parent.id', onUpdate: 'SET NULL' }, }); await db.query("INSERT INTO parent VALUES ('p1')"); await db.query("INSERT INTO child VALUES ('c1', 'p1')"); await expect( db.table('parent').update({ id: 'p2' }).where({ id: 'p1' }).execute(), ).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' }); const rows = await db.query('SELECT * FROM parent') as Record[]; expect(rows).toEqual([{ id: 'p1' }]); await db.close(); }); it('SET NULL 到非 required 列仍正常(回归)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('parent', { id: { type: 'string', primaryKey: true } }); await db.defineTable('child', { id: { type: 'string', primaryKey: true }, pid: { type: 'string', references: 'parent.id', onDelete: 'SET NULL' }, }); await db.query("INSERT INTO parent VALUES ('p1')"); await db.query("INSERT INTO child VALUES ('c1', 'p1')"); await db.query("DELETE FROM parent WHERE id = 'p1'"); const rows = await db.query('SELECT * FROM child') as Record[]; expect(rows).toEqual([{ id: 'c1', pid: null }]); await db.close(); }); }); // =================================================================== // bindParameters 注释感知 // =================================================================== describe('v0.7.2 — bindParameters 注释感知', () => { it('行注释中的 ? 不参与占位符', () => { const sql = "SELECT * FROM t -- comment with ? inside\n WHERE id = ?"; const bound = bindParameters(sql, ['1']); expect(bound).toContain('comment with ? inside'); expect(bound).toContain("WHERE id = '1'"); }); it('块注释中的 ? 不参与占位符', () => { const sql = 'SELECT * FROM t /* c ? c */ WHERE id = ?'; const bound = bindParameters(sql, ['1']); expect(bound).toContain('/* c ? c */'); expect(bound).toContain("WHERE id = '1'"); }); it("行注释中的单引号不触发未闭合字符串(-- don't)", () => { const sql = "SELECT * FROM t WHERE id = ? -- don't touch"; const bound = bindParameters(sql, ['1']); expect(bound).toContain("-- don't touch"); }); it('块注释中的单引号不触发未闭合字符串', () => { const sql = "SELECT * FROM t /* it's fine */ WHERE id = ?"; const bound = bindParameters(sql, ['1']); expect(bound).toContain("/* it's fine */"); }); it('占位符数量按注释外计算(注释里两个 ? 不报参数不足)', () => { const sql = "SELECT * FROM t WHERE id = ? /* ? ? */ AND x = ? -- ?\n"; const bound = bindParameters(sql, ['1', '2']); expect(bound).toContain("'1'"); expect(bound).toContain("'2'"); }); it('SQL 注入载荷仍被安全编码(回归)', () => { const sql = 'SELECT * FROM t WHERE name = ?'; const bound = bindParameters(sql, ["O'Brien'; DROP TABLE t; --"]); expect(bound).toContain("'O''Brien''; DROP TABLE t; --'"); }); }); // =================================================================== // 未闭合字符串字面量 // =================================================================== describe('v0.7.2 — 未闭合字符串字面量', () => { it("SELECT 'abc → PARSE_ERROR", () => { expect(() => parse("SELECT 'abc")).toThrowError(/Unterminated string/); }); it('query API 同样抛 PARSE_ERROR', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await expect(db.query("SELECT 'abc")).rejects.toMatchObject({ code: 'PARSE_ERROR' }); await db.close(); }); }); // =================================================================== // 未知 where 操作符 // =================================================================== describe('v0.7.2 — 未知 where 操作符抛错', () => { it('$between 未实现 → QUERY_ERROR(不再静默全匹配)', () => { expect(() => matchWhere({ age: 3 }, { age: { $between: [1, 2] } } as never)).toThrowError(/Unknown where operator/); }); it('query API 路径同样报错', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO t VALUES ('1')"); await expect( db.getEngine().find('t', { table: 't', where: { id: { $betwen: '1' } } as never }), ).rejects.toThrowError(/Unknown where operator/); await db.close(); }); }); // =================================================================== // UPDATE undefined 语义化 // =================================================================== describe('v0.7.2 — UPDATE undefined 语义化', () => { const modes = ['memory', 'disk', 'hybrid', 'aria'] as const; for (const mode of modes) { it(`${mode}: update({ col: undefined }) 保留旧值`, async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode, diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, tag: { type: 'string', default: 'def' }, }); await db.query("INSERT INTO t VALUES ('1', 'x')"); await db.table('t').update({ tag: undefined }).where({ id: '1' }).execute(); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows[0]).toEqual({ id: '1', tag: 'x' }); await db.close(); }); it(`${mode}: update({ col: null }) 仍显式置空`, async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode, diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, tag: { type: 'string' }, }); await db.query("INSERT INTO t VALUES ('1', 'x')"); await db.table('t').update({ tag: null }).where({ id: '1' }).execute(); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows[0]).toEqual({ id: '1', tag: null }); await db.close(); }); } it('主键 undefined 不触发主键变更路径(kv 引擎)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'disk', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } }); await db.query("INSERT INTO t VALUES ('1', 1)"); const n = await db.table('t').update({ id: undefined, n: 2 }).where({ id: '1' }).execute(); expect(n).toBe(1); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows[0]).toEqual({ id: '1', n: 2 }); await db.close(); }); }); // =================================================================== // Hybrid 磁盘写失败补偿 // =================================================================== describe('v0.7.2 — Hybrid 磁盘写失败补偿', () => { it('insert 磁盘失败 → 错误抛出 + 内存与磁盘一致', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO t VALUES ('1')"); const eng = db.getEngine() as HybridEngine; const disk = (eng as unknown as { diskEngine: IStorageEngineMock }).diskEngine; const origInsert = disk.insert.bind(disk); let fail = true; disk.insert = async (table: string, rows: Record[]) => { if (fail) throw new Error('disk down'); return origInsert(table, rows); }; await expect(db.query("INSERT INTO t VALUES ('2')")).rejects.toThrow('disk down'); // 内存已对齐磁盘:'2' 不在(补偿后重载) const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toEqual([{ id: '1' }]); // 恢复后正常写入 fail = false; await db.query("INSERT INTO t VALUES ('3')"); const rows2 = await db.query('SELECT * FROM t ORDER BY id') as Record[]; expect(rows2).toEqual([{ id: '1' }, { id: '3' }]); await db.close(); }); it('update 磁盘失败 → 错误抛出 + 内存对齐磁盘', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } }); await db.query("INSERT INTO t VALUES ('1', 1)"); const eng = db.getEngine() as HybridEngine; const disk = (eng as unknown as { diskEngine: IStorageEngineMock }).diskEngine; disk.update = async () => { throw new Error('disk down'); }; await expect(db.table('t').update({ n: 99 }).where({ id: '1' }).execute()).rejects.toThrow('disk down'); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toEqual([{ id: '1', n: 1 }]); await db.close(); }); it('delete 磁盘失败 → 错误抛出 + 内存对齐磁盘', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO t VALUES ('1')"); const eng = db.getEngine() as HybridEngine; const disk = (eng as unknown as { diskEngine: IStorageEngineMock }).diskEngine; disk.delete = async () => { throw new Error('disk down'); }; await expect(db.table('t').delete().where({ id: '1' }).execute()).rejects.toThrow('disk down'); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toEqual([{ id: '1' }]); await db.close(); }); it('事务内写失败仍由快照回滚(补偿不影响事务路径)', async () => { const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' }); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO t VALUES ('1')"); await expect(db.transaction(async (trx) => { await trx.table('t').insert({ id: '2' }); throw new Error('tx fail'); })).rejects.toThrow('tx fail'); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows).toEqual([{ id: '1' }]); await db.close(); }); }); interface IStorageEngineMock { insert: (table: string, rows: Record[]) => Promise; update: (table: string, query: never, updates: Record) => Promise; delete: (table: string, query: never) => Promise; } // 确保 KVStoreEngine 也导出(引用用) void KVStoreEngine;