Files
MetonaSqlark/tests/v043-hardening.test.ts
T
thzxx 334067d89e
CI / test (18.x) (push) Successful in 10m10s
CI / test (20.x) (push) Successful in 10m10s
CI / test (22.x) (push) Successful in 10m6s
CI / e2e (push) Successful in 9m51s
CI / test (24.x) (push) Successful in 10m28s
release: v0.5.1 — 存储后端生产级硬化(CRC-32/全库加密/WAL分片/页面化存储/多标签页锁/e2e)+ 深度审查修复(假实现接线/死代码清理)
2026-08-10 12:07:00 +08:00

391 lines
15 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.4.2 生产就绪审计(第三轮)
* 覆盖:
* - P0: Aria 事务进行中 checkpoint 截断 WAL → 崩溃恢复丢事务数据
* - P1: Aria dropTable / ALTER DROP 索引列 的二级索引清理
* - P1: OPFS 并发写乱序丢更新 + 空表/schema/索引持久化
* - P2: Aria memtable 阈值衰减、红黑树随机压力、onUpdate 级联、事务 DDL 显式拒绝
*/
import { AriaEngine } from '../src/engine/aria/index';
import { createSchema } from '../src/table/schema';
import { OPFSEngine } from '../src/engine/opfs';
import { MemTable } from '../src/engine/aria/index/memtable';
import { MetonaSqlark } from '../src/core';
import 'fake-indexeddb/auto';
let idbCounter = 0;
function uniqueDB(): string {
return `prod-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===================================================================
// OPFS mock(模拟真实 I/OgetFileHandle 延迟 + 写入按内容差异化耗时)
// ===================================================================
function mockOPFS(
files?: Map<string, string>,
opts: { ioDelay?: number; writeDelayFor?: (data: string) => number } = {},
): Map<string, string> {
const store = files ?? new Map<string, string>();
const dirMock = {
getDirectoryHandle: async (_name: string, _opts?: any) => dirMock as any,
getFileHandle: async (name: string, fileOpts?: any) => {
if (fileOpts?.create) {
if (opts.ioDelay) {
await new Promise((r) => setTimeout(r, opts.ioDelay));
}
return {
createWritable: async () => ({
write: async (d: string) => {
const delay = opts.writeDelayFor ? opts.writeDelayFor(d) : 0;
if (delay > 0) {
await new Promise((r) => setTimeout(r, delay));
}
store.set(name, d);
},
close: async () => {},
}),
};
}
if (!store.has(name)) throw new Error('Not found');
return { getFile: async () => ({ text: async () => store.get(name)!, arrayBuffer: async () => new ArrayBuffer(0) }) };
},
removeEntry: async (name: string) => { store.delete(name); },
};
(dirMock as any).entries = () => ({
[Symbol.asyncIterator]: async function* () {
for (const [k] of store) yield [k];
},
});
const nav = (globalThis as any).navigator || {};
nav.storage = { getDirectory: async () => dirMock };
(globalThis as any).navigator = nav;
return store;
}
// ===================================================================
// P0: 事务与 checkpoint 冲突
// ===================================================================
describe('P0 — 事务进行中 checkpoint 不得截断 WAL', () => {
it('事务中触发 checkpoint → 崩溃恢复不丢事务数据', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'indexeddb',
checkpointInterval: 2, // 每 2 次操作即触发 checkpoint(事务中途)
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
}));
await engine.beginTransaction();
await engine.insert('t', [{ id: '1', v: 1 }]);
await engine.insert('t', [{ id: '2', v: 2 }]); // opCounter=2 → tick → checkpoint(修复前截断 WAL
await engine.commitTransaction();
// 模拟异常退出(不 close
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
// 修复前:checkpoint 截断了事务的 WAL 记录 → 恢复后数据丢失
expect(await engine2.count('t')).toBe(2);
await engine2.close();
});
it('事务回滚后再 checkpoint 正常截断', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'indexeddb',
checkpointInterval: 1,
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await engine.insert('t', [{ id: 'keep' }]);
await engine.beginTransaction();
await engine.insert('t', [{ id: 'tx1' }]);
await engine.rollbackTransaction();
// 事务结束后 checkpoint 可正常截断
await (engine as any).checkpointManager.checkpoint();
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
expect(await engine2.count('t')).toBe(1);
await engine2.close();
});
});
// ===================================================================
// P1: Aria DDL 的二级索引清理
// ===================================================================
describe('P1 — Aria DDL 索引清理', () => {
it('dropTable 清理二级索引(重建同名表索引不脏)', async () => {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'a@x.com' }]);
await engine.dropTable('t');
// 索引 LSM 必须清理(修复前残留)
expect((engine as any).secondaryIndexes.size).toBe(0);
// 重建同名表 + 相同 id:旧索引残留会返回错误行
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'b@x.com' }]);
// 旧索引残留场景:按旧 email 查询不得命中新行(主 LSM 有 t:1 → 修复前返回错误结果)
const byOld = await engine.find('t', { table: 't', where: { email: 'a@x.com' } });
expect(byOld).toHaveLength(0);
const byNew = await engine.find('t', { table: 't', where: { email: 'b@x.com' } });
expect(byNew).toHaveLength(1);
await engine.close();
});
it('ALTER TABLE DROP 索引列清理索引 LSM', async () => {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'a@x.com' }]);
await engine.alterTable('t', 'DROP', { name: 'email', type: 'string' });
expect((engine as any).secondaryIndexes.size).toBe(0);
await engine.close();
});
it('DROP_TABLE 崩溃恢复同样清理索引', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'a@x.com' }]);
await engine.dropTable('t');
// 模拟崩溃(不 closeWAL 含 DROP_TABLE
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
// 表不存在,且无索引残留
expect(await engine2.hasTable('t')).toBe(false);
expect((engine2 as any).secondaryIndexes.size).toBe(0);
await engine2.close();
});
});
// ===================================================================
// P1: OPFS 持久化与并发
// ===================================================================
describe('P1 — OPFS 生产加固', () => {
it('并发写不丢数据(内存快照总是最新,多写内容一致)', async () => {
// 模拟真实 I/O 延迟:内存写同步、持久化异步 → 并发写内容均基于最新内存快照
const files = mockOPFS(undefined, {
ioDelay: 10,
writeDelayFor: (data) => (data.length < 40 ? 30 : 5),
});
const engine = new OPFSEngine();
await engine.open('opfs-race', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
// 两个写并发:内存同步写保证两个快照一致 → 无论完成顺序文件内容完整
const p1 = engine.insert('users', [{ id: '1', name: 'A' }]);
const p2 = engine.insert('users', [{ id: '2', name: 'B' }]);
await Promise.all([p1, p2]);
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(2);
// 文件内容完整(重启不丢)
expect(files.get('users.json')).toContain('"B"');
await engine.close();
});
it('空表重启后保留(schema 持久化)', async () => {
mockOPFS();
const e1 = new OPFSEngine();
await e1.open('opfs-schema', 1);
await e1.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await e1.close();
// 重启:空表也应恢复(修复前空表消失)+ 索引标记恢复
const e2 = new OPFSEngine();
await e2.open('opfs-schema', 1);
expect(await e2.hasTable('users')).toBe(true);
const schema = await e2.getTableSchema('users');
expect(schema!.columns.email.index).toBe(true);
await e2.close();
});
it('有数据重启后索引查询可用', async () => {
mockOPFS();
const e1 = new OPFSEngine();
await e1.open('opfs-idx', 1);
await e1.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await e1.insert('users', [{ id: '1', email: 'a@x.com' }]);
await e1.close();
const e2 = new OPFSEngine();
await e2.open('opfs-idx', 1);
const byEmail = await e2.find('users', { table: 'users', where: { email: 'a@x.com' } });
expect(byEmail).toHaveLength(1);
await e2.close();
});
it('dropTable 后重启无幽灵表', async () => {
mockOPFS();
const e1 = new OPFSEngine();
await e1.open('opfs-drop', 1);
await e1.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await e1.insert('t', [{ id: '1' }]);
await e1.dropTable('t');
await e1.close();
const e2 = new OPFSEngine();
await e2.open('opfs-drop', 1);
expect(await e2.hasTable('t')).toBe(false);
expect(await e2.getTableNames()).toEqual([]);
await e2.close();
});
});
// ===================================================================
// P2: Aria 内部状态
// ===================================================================
describe('P2 — Aria 内部状态加固', () => {
it('freezeMemtable 后阈值不衰减', async () => {
const engine = new AriaEngine({
storageBackend: 'memory',
memtableSizeThreshold: 1024 * 1024,
});
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
// 少量写入 + 手动 flushfreezeMemtable 用旧表已用大小当新阈值 → 衰减)
for (let i = 0; i < 10; i++) await engine.insert('t', [{ id: `${i}` }]);
await (engine as any).lsm.flush();
// 修复前:新 memtable maxSize ≈ 已用字节(远小于配置)
const memtable = (engine as any).lsm.memtable;
expect((memtable as any).maxSize).toBeGreaterThanOrEqual(1024 * 1024);
await engine.close();
});
it('红黑树随机 insert/delete 5000 次保持有序且无丢失', () => {
const mem = new MemTable(1 << 30);
const reference = new Set<string>();
let seed = 42;
const rnd = () => {
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
return seed / 0x7fffffff;
};
for (let i = 0; i < 5000; i++) {
const k = `k${Math.floor(rnd() * 800)}`;
if (rnd() < 0.3) {
reference.delete(k);
mem.delete(k);
} else {
reference.add(k);
mem.put(k, { v: i });
}
}
const entries = mem.getAllEntries();
// 有序
for (let i = 1; i < entries.length; i++) {
expect(entries[i][0] > entries[i - 1][0]).toBe(true);
}
// 与引用集合完全一致(无丢失/无残留)
expect(entries.map(([k]) => k)).toEqual([...reference].sort());
expect(mem.getEntryCount()).toBe(reference.size);
});
it('onUpdate CASCADE:更新父表主键级联更新子表外键', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onUpdate: 'CASCADE' },
});
await db.table('users').insert({ id: '1' });
await db.table('orders').insert({ id: 'o1', user_id: '1' });
// 更新父表主键 1 → 2
await db.table('users').update({ id: '2' }).where({ id: '1' }).execute();
const orders = await db.table('orders').select().execute();
expect(orders[0].user_id).toBe('2');
// 旧 id 不可再被引用
expect(await db.table('orders').count({ user_id: '1' })).toBe(0);
await db.close();
});
it('onUpdate RESTRICT:存在引用行时禁止更新主键', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onUpdate: 'RESTRICT' },
});
await db.table('users').insert({ id: '1' });
await db.table('orders').insert({ id: 'o1', user_id: '1' });
await expect(
db.table('users').update({ id: '2' }).where({ id: '1' }).execute(),
).rejects.toThrow();
// 主键未被修改
const users = await db.table('users').select().execute();
expect(users[0].id).toBe('1');
await db.close();
});
it('onUpdate CASCADE:更新父表主键级联更新子表外键(Aria)', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onUpdate: 'CASCADE' },
});
await db.table('users').insert({ id: '1' });
await db.table('orders').insert({ id: 'o1', user_id: '1' });
await db.table('users').update({ id: '2' }).where({ id: '1' }).execute();
const orders = await db.table('orders').select().execute();
expect(orders[0].user_id).toBe('2');
expect(await db.table('orders').count({ user_id: '1' })).toBe(0);
await db.close();
});
it('Aria 事务中 DDL 显式拒绝(NOT_SUPPORTED)而非静默不一致', async () => {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(uniqueDB(), 1);
await engine.beginTransaction();
await expect(
engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })),
).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await engine.rollbackTransaction();
// 表未创建
expect(await engine.hasTable('t')).toBe(false);
await engine.close();
});
});