/** * v0.6.2 修复回归测试 — 深度审计 P0/P1 修复锁定 * * 覆盖: * - P0: KVStoreEngine 数值主键 update 后行被误删(重启丢数据) * - P0: Memory/Aria update 主键变更撞已有主键静默覆盖 * - P1: Aria 二级索引范围查询边界算法(小数/字符串漏数据) * - P1: Aria 索引列 IS NULL 返回空 * - P1: Aria unique 约束未强制 * - P2: EXPLAIN 写语句产生真实副作用 * - 索引旧值残留(非主键 update 不清理旧索引条目) */ import { MetonaSqlark } from '../src/core'; import { MemoryEngine } from '../src/engine/memory'; import { KVStoreEngine } from '../src/engine/kvstore_engine'; import { AriaEngine } from '../src/engine/aria/index'; import { createSchema } from '../src/table/schema'; function uniqueDB(): string { return `v062-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } /** 断言 Promise 以指定错误码拒绝 */ async function expectCode(promise: Promise, code: string): Promise { try { await promise; } catch (e) { expect((e as { code?: string }).code).toBe(code); return; } throw new Error(`Expected rejection with code "${code}", but promise resolved`); } describe('v0.6.2 — 数值主键 update 数据丢失(KVStoreEngine)', () => { it('数值主键 update 后重开行不丢失', async () => { const name = uniqueDB(); const eng = new KVStoreEngine(); await eng.open(name, 1); await eng.createTable(createSchema('t', { id: { type: 'number', primaryKey: true }, v: { type: 'string' }, })); await eng.insert('t', [{ id: 1, v: 'a' }, { id: 2, v: 'b' }]); await eng.update('t', { table: 't', where: { id: 1 } }, { v: 'A' }); await eng.close(); const eng2 = new KVStoreEngine(); await eng2.open(name, 1); const rows = await eng2.find('t', { table: 't' }); expect(rows).toHaveLength(2); const row1 = rows.find((r) => r.id === 1); expect(row1).toBeDefined(); expect(row1!.v).toBe('A'); await eng2.close(); }); it('数值主键 delete 后重开不残留', async () => { const name = uniqueDB(); const eng = new KVStoreEngine(); await eng.open(name, 1); await eng.createTable(createSchema('t', { id: { type: 'number', primaryKey: true }, })); await eng.insert('t', [{ id: 1 }, { id: 2 }]); await eng.delete('t', { table: 't', where: { id: 1 } }); await eng.close(); const eng2 = new KVStoreEngine(); await eng2.open(name, 1); const rows = await eng2.find('t', { table: 't' }); expect(rows).toHaveLength(1); expect(rows[0].id).toBe(2); await eng2.close(); }); it('高层 API(disk 模式)数值主键 update 持久化正确', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'disk' }); await db.init(); await db.defineTable('t', { id: { type: 'number', primaryKey: true }, name: { type: 'string' }, }); await db.query('INSERT INTO t VALUES (1, \'a\'), (2, \'b\')'); await db.query('UPDATE t SET name = \'A\' WHERE id = 1'); await db.close(); const db2 = new MetonaSqlark({ name: db.name, mode: 'disk' }); await db2.init(); const rows = await db2.query('SELECT * FROM t') as Record[]; expect(rows).toHaveLength(2); expect(rows.find((r) => r.id === 1)!.name).toBe('A'); await db2.close(); }); }); describe('v0.6.2 — update 主键撞已有主键(DUPLICATE_KEY)', () => { it('MemoryEngine 主键碰撞更新抛 DUPLICATE_KEY,数据不丢', async () => { const eng = new MemoryEngine(); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); await eng.insert('t', [{ id: '1' }, { id: '2' }]); await expectCode(eng.update('t', { table: 't', where: { id: '1' } }, { id: '2' }), 'DUPLICATE_KEY'); const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(2); }); it('AriaEngine 主键碰撞更新抛 DUPLICATE_KEY,数据不丢', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); await eng.insert('t', [{ id: '1' }, { id: '2' }]); await expectCode(eng.update('t', { table: 't', where: { id: '1' } }, { id: '2' }), 'DUPLICATE_KEY'); const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(2); await eng.close(); }); it('事务内主键碰撞更新抛 DUPLICATE_KEY', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); await eng.insert('t', [{ id: '1' }, { id: '2' }]); await eng.beginTransaction(); await expectCode(eng.update('t', { table: 't', where: { id: '1' } }, { id: '2' }), 'DUPLICATE_KEY'); await eng.rollbackTransaction(); const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(2); await eng.close(); }); }); describe('v0.6.2 — Aria 二级索引范围查询边界', () => { async function makeEngine(): Promise { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, score: { type: 'number', index: true }, tag: { type: 'string', index: true }, })); return eng; } it('小数数值范围查询($gt/$lt)不漏行', async () => { const eng = await makeEngine(); await eng.insert('t', [ { id: '1', score: 1.5, tag: 'a' }, { id: '2', score: 2.5, tag: 'b' }, { id: '3', score: 3.5, tag: 'c' }, { id: '4', score: 2, tag: 'd' }, ]); const gt = await eng.find('t', { table: 't', where: { score: { $gt: 2 } } }); expect(gt.map((r) => r.id).sort()).toEqual(['2', '3']); const lt = await eng.find('t', { table: 't', where: { score: { $lt: 2 } } }); expect(lt.map((r) => r.id).sort()).toEqual(['1']); const gte = await eng.find('t', { table: 't', where: { score: { $gte: 2 } } }); expect(gte.map((r) => r.id).sort()).toEqual(['2', '3', '4']); const lte = await eng.find('t', { table: 't', where: { score: { $lte: 2.5 } } }); expect(lte.map((r) => r.id).sort()).toEqual(['1', '2', '4']); await eng.close(); }); it('字符串范围查询(小写/大写/数字开头)不漏行', async () => { const eng = await makeEngine(); await eng.insert('t', [ { id: '1', score: 1, tag: 'apple' }, { id: '2', score: 2, tag: 'Banana' }, { id: '3', score: 3, tag: 'cherry' }, { id: '4', score: 4, tag: '1start' }, { id: '5', score: 5, tag: 'Zebra' }, ]); const gt = await eng.find('t', { table: 't', where: { tag: { $gt: 'apple' } } }); // JS 字符串比较:'Banana' > 'apple'(大写 < 小写)→ 只有 cherry expect(gt.map((r) => r.id).sort()).toEqual(['3']); const lt = await eng.find('t', { table: 't', where: { tag: { $lt: 'Banana' } } }); expect(lt.map((r) => r.id).sort()).toEqual(['4']); const gteDigit = await eng.find('t', { table: 't', where: { tag: { $gte: '1start' } } }); // JS 字符串比较:数字开头最小,全部字母值都 >= '1start' expect(gteDigit.map((r) => r.id).sort()).toEqual(['1', '2', '3', '4', '5']); await eng.close(); }); it('整数范围查询结果与全表扫描一致(flush 前后)', async () => { const eng = await makeEngine(); await eng.insert('t', Array.from({ length: 50 }, (_, i) => ({ id: `${i}`, score: i * 10, tag: `t${i % 5}` }))); const byIndex = await eng.find('t', { table: 't', where: { score: { $gt: 200, $lte: 350 } } }); const fullScan = (await eng.find('t', { table: 't' })) .filter((r) => (r.score as number) > 200 && (r.score as number) <= 350); expect(byIndex.map((r) => r.id).sort()).toEqual(fullScan.map((r) => r.id).sort()); // flush 后(索引进 SSTable)结果一致 await (eng as any).lsm.flush(); await (eng as any).secondaryIndexes.get('t:idx:score').flush(); const after = await eng.find('t', { table: 't', where: { score: { $gt: 200, $lte: 350 } } }); expect(after.map((r) => r.id).sort()).toEqual(fullScan.map((r) => r.id).sort()); await eng.close(); }); it('SQL 层索引范围查询(GROUP BY 前过滤)一致性', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' }); await db.init(); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, score: { type: 'number', index: true }, }); await db.query('INSERT INTO t VALUES (\'1\', 1.5), (\'2\', 2.5), (\'3\', 3.5)'); const rows = await db.query('SELECT * FROM t WHERE score > 2') as Record[]; expect(rows.map((r) => r.id).sort()).toEqual(['2', '3']); await db.close(); }); }); describe('v0.6.2 — Aria 索引列 IS NULL', () => { it('引擎层 $isNull 命中 null 行', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }, { id: '3', email: 'd@e.f' }]); // v0.8.0(A15):此断言此前写作 `{ $eq: null }` 并期望命中 NULL 行 —— 那是 // 错误的 SQL 语义(`= NULL` 恒为 UNKNOWN)。索引列 IS NULL 的**目的** //("NULL 行不能被索引漏掉")不变,改为用正确的谓词表达。 const rows = await eng.find('t', { table: 't', where: { email: { $isNull: true } } }); expect(rows).toHaveLength(1); expect(rows[0].id).toBe('2'); await eng.close(); }); it('引擎层 $eq: null 不再命中 null 行(SQL 标准)', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }]); const rows = await eng.find('t', { table: 't', where: { email: { $eq: null } } }); expect(rows).toHaveLength(0); await eng.close(); }); it('SQL 层 IS NULL / IS NOT NULL 正确', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' }); await db.init(); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, }); await db.query('INSERT INTO t VALUES (\'1\', \'a@b.c\'), (\'2\', NULL)'); const nullRows = await db.query('SELECT * FROM t WHERE email IS NULL') as Record[]; expect(nullRows).toHaveLength(1); expect(nullRows[0].id).toBe('2'); const notNull = await db.query('SELECT * FROM t WHERE email IS NOT NULL') as Record[]; expect(notNull).toHaveLength(1); expect(notNull[0].id).toBe('1'); await db.close(); }); it('IN 列表含 null 不走索引(不漏可匹配行)', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }]); // v0.8.0(A15):`x IN (NULL, 'a@b.c')` 等价于 `x = NULL OR x = 'a@b.c'`: // `null = NULL` 是 UNKNOWN(不成立),所以只有 id=1 命中。 // 此前的期望值 2 正是"NULL 与 NULL 相等"这一错误语义的产物。 const rows = await eng.find('t', { table: 't', where: { email: { $in: [null, 'a@b.c'] } } }); expect(rows.map((r) => r.id)).toEqual(['1']); await eng.close(); }); it('IN 列表含 null 时不可匹配的行仍不返回(UNKNOWN 不保留)', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }, { id: '3', email: 'z@z.z' }]); // id=3 既不在列表里,列表又含 NULL → 结果 UNKNOWN → 不保留 const rows = await eng.find('t', { table: 't', where: { email: { $in: [null, 'a@b.c'] } } }); expect(rows.map((r) => r.id)).toEqual(['1']); await eng.close(); }); }); describe('v0.6.2 — Aria unique 约束强制', () => { it('同批重复唯一值被拦截', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, })); await expectCode(eng.insert('t', [ { id: '1', email: 'x@y' }, { id: '2', email: 'x@y' }, ]), 'UNIQUE_VIOLATION'); // 验证失败整批不落库 const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(0); await eng.close(); }); it('跨批重复唯一值被拦截', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, })); await eng.insert('t', [{ id: '1', email: 'x@y' }]); await expectCode(eng.insert('t', [{ id: '2', email: 'x@y' }]), 'UNIQUE_VIOLATION'); await eng.close(); }); it('flush 后(索引进 SSTable)重复唯一值仍被拦截', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, })); await eng.insert('t', [{ id: '1', email: 'x@y' }]); await (eng as any).secondaryIndexes.get('t:idx:email').flush(); await (eng as any).lsm.flush(); await expectCode(eng.insert('t', [{ id: '2', email: 'x@y' }]), 'UNIQUE_VIOLATION'); await eng.close(); }); it('update 改为已存在唯一值被拦截;改为自身旧值放行', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, })); await eng.insert('t', [{ id: '1', email: 'a@y' }, { id: '2', email: 'b@y' }]); await expectCode(eng.update('t', { table: 't', where: { id: '1' } }, { email: 'b@y' }), 'UNIQUE_VIOLATION'); // 更新为自身旧值(未变化)放行 await eng.update('t', { table: 't', where: { id: '1' } }, { email: 'a@y' }); const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(2); await eng.close(); }); it('非主键 update 清理旧索引条目:旧值可被新行复用', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, })); await eng.insert('t', [{ id: '1', email: 'a@y' }]); await eng.update('t', { table: 't', where: { id: '1' } }, { email: 'b@y' }); // 旧值 a@y 应可被复用(残留索引条目否则会误报 UNIQUE_VIOLATION) await eng.insert('t', [{ id: '2', email: 'a@y' }]); const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(2); await eng.close(); }); it('null 值不受唯一约束', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true }, })); await eng.insert('t', [{ id: '1' }, { id: '2' }]); const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(2); await eng.close(); }); it('数值唯一列 + 事务内重复拦截', async () => { const eng = new AriaEngine({ storageBackend: 'memory' }); await eng.open(uniqueDB(), 1); await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, code: { type: 'number', unique: true }, })); await eng.insert('t', [{ id: '1', code: 100 }]); await eng.beginTransaction(); await expectCode(eng.insert('t', [{ id: '2', code: 100 }]), 'UNIQUE_VIOLATION'); await eng.rollbackTransaction(); const rows = await eng.find('t', { table: 't' }); expect(rows).toHaveLength(1); await eng.close(); }); }); describe('v0.6.2 — EXPLAIN 无副作用', () => { it('EXPLAIN DELETE/UPDATE 不修改数据', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, }); await db.query('INSERT INTO users VALUES (\'1\', \'Alice\'), (\'2\', \'Bob\')'); await db.query('EXPLAIN DELETE FROM users WHERE id = \'1\''); let rows = await db.query('SELECT * FROM users') as Record[]; expect(rows).toHaveLength(2); await db.query('EXPLAIN UPDATE users SET name = \'X\' WHERE id = \'1\''); rows = await db.query('SELECT * FROM users') as Record[]; expect(rows).toHaveLength(2); expect(rows.find((r) => r.id === '1')!.name).toBe('Alice'); // EXPLAIN DELETE 给出估算行数(无副作用) const plan = await db.query('EXPLAIN DELETE FROM users WHERE id = \'1\'') as Record; expect(plan.estimatedRows).toBe(1); await db.close(); }); it('EXPLAIN INSERT 不写入数据', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true } }); await db.query('EXPLAIN INSERT INTO users VALUES (\'1\')'); const rows = await db.query('SELECT * FROM users') as Record[]; expect(rows).toHaveLength(0); await db.close(); }); });