Files
MetonaSqlark/tests/v062-fixes.test.ts
T
thzxx ef1934a38c
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
fix(P0): v0.6.2 数据正确性专项 — 深度审计 6 项修复 + 22 回归
- 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
2026-08-13 09:51:26 +08:00

396 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});
});