- 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
396 lines
16 KiB
TypeScript
396 lines
16 KiB
TypeScript
/**
|
||
* 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('高层 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<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();
|
||
});
|
||
});
|