fix(P0): v0.6.2 数据正确性专项 — 深度审计 6 项修复 + 22 回归
CI / test (22.x) (push) Successful in 17m6s
CI / test (18.x) (push) Failing after 17m45s
CI / test (20.x) (push) Successful in 18m7s
CI / test (24.x) (push) Failing after 14m40s
CI / e2e (push) Successful in 9m54s

- KVStoreEngine 数值主键 update 丢行(P0):String 化主键回查不命中 → 误删 KV 行
  (重启丢数据);改为单次全表扫描 + 受影响集合过滤(兼消 O(N×M) 回查开销)
- update 主键撞已有主键静默覆盖(P0,Memory/Aria):抛 DUPLICATE_KEY,事务路径同拦截
- Aria 二级索引范围查询边界算法错误(P1):Number(v)±1 构造 key 漏小数/字符串数据;
  改全索引扫描 + matchWhere 过滤,边界语义统一
- Aria 索引列 IS NULL 返回空(P1):null 等值/含 null 的 IN 不走索引(回退全表)
- AriaEngine unique 约束未强制(P1):insert 整批预检(批内互查+索引扫描,失败整批
  不落库)+ update 排除自身旧条目检查;新增 LSM.prefetchPrefixRanges 批量预加载
- 非主键 update 索引旧值残留:统一传旧行清理(消除唯一性误报与索引膨胀)
- EXPLAIN 写语句产生真实副作用(P2):仅 SELECT 执行,UPDATE/DELETE 用 count 估算

测试 1092 → 1114(70 套件);行覆盖率 89.6%;版本 0.6.2
This commit is contained in:
thzxx
2026-08-13 09:51:26 +08:00
parent a91ac368e2
commit ef1934a38c
20 changed files with 1284 additions and 118 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ beforeEach(() => { installOPFSMock(new Map()); });
describe('[v0.2.5] P0-1: 版本号统一', () => {
test('VERSION 常量为当前版本(0.6.0', () => {
expect(VERSION).toBe('0.6.1');
expect(VERSION).toBe('0.6.2');
});
});
+1 -1
View File
@@ -403,7 +403,7 @@ describe('[v0.3.3] P1-9: Savepoint + MVCC 一致性', () => {
describe('[v0.3.3] 端到端', () => {
test('全部修复点可共存于 MetonaSqlark API', async () => {
expect(VERSION).toBe('0.6.1');
expect(VERSION).toBe('0.6.2');
const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
await db.init();
await db.defineTable('users', {
+395
View File
@@ -0,0 +1,395 @@
/**
* 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<unknown>, code: string): Promise<void> {
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('高层 APIdisk 模式)数值主键 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<string, unknown>[];
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<AriaEngine> {
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<string, unknown>[];
expect(rows.map((r) => r.id).sort()).toEqual(['2', '3']);
await db.close();
});
});
describe('v0.6.2 — Aria 索引列 IS NULL', () => {
it('引擎层 $eq: null 返回 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' }]);
const rows = await eng.find('t', { table: 't', where: { email: { $eq: null } } });
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('2');
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<string, unknown>[];
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<string, unknown>[];
expect(notNull).toHaveLength(1);
expect(notNull[0].id).toBe('1');
await db.close();
});
it('IN 列表含 null 不走索引(不漏 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 }]);
const rows = await eng.find('t', { table: 't', where: { email: { $in: [null, 'a@b.c'] } } });
expect(rows).toHaveLength(2);
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<string, unknown>[];
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<string, unknown>[];
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<string, unknown>;
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<string, unknown>[];
expect(rows).toHaveLength(0);
await db.close();
});
});