release: v0.4.2 — 生产就绪与崩溃自愈 + 问题清单修复 + 版本迭代
CI / test (18.x) (push) Successful in 10m7s
CI / test (20.x) (push) Successful in 10m6s
CI / test (22.x) (push) Successful in 10m2s
CI / test (24.x) (push) Successful in 10m0s

This commit is contained in:
thzxx
2026-08-09 19:15:37 +08:00
parent a1e4f5071c
commit 22b0b1fad4
34 changed files with 6626 additions and 565 deletions
+390
View File
@@ -0,0 +1,390 @@
/**
* 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.forceCheckpoint();
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();
});
});