release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
CI / test (20.x) (push) Successful in 10m53s
CI / test (22.x) (push) Successful in 10m47s
CI / test (24.x) (push) Successful in 10m42s
CI / e2e (push) Successful in 10m27s
CI / test (18.x) (push) Successful in 11m1s

This commit is contained in:
thzxx
2026-08-10 13:56:04 +08:00
parent 2668bd47cf
commit 24b3ca1079
74 changed files with 8016 additions and 7688 deletions
+508
View File
@@ -0,0 +1,508 @@
/**
* 介质:SharedMemoryBackend(跨实例共享,模拟磁盘持久化)
*/
import { KVStoreEngine } from '../../src/engine/kvstore_engine';
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
import { createSchema } from '../../src/table/schema';
let dbCounter = 0;
describe('KVStoreEngine', () => {
let engine: KVStoreEngine;
let dbName: string;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
email: { type: 'string', unique: true },
});
beforeEach(async () => {
dbName = `test-kv-${++dbCounter}-${Math.random().toString(36).slice(2, 6)}`;
engine = new KVStoreEngine(new SharedMemoryBackend());
await engine.open(dbName, 1);
});
afterEach(async () => {
if (engine.isOpen()) {
await engine.close();
}
});
// ---- 生命周期 ----
describe('生命周期', () => {
it('打开和关闭', async () => {
expect(engine.isOpen()).toBe(true);
await engine.close();
expect(engine.isOpen()).toBe(false);
});
it('未打开时操作抛出错误', async () => {
await engine.close();
await expect(engine.count('users')).rejects.toThrow('not opened');
});
it('可以重新打开', async () => {
await engine.close();
await engine.open(dbName, 2);
expect(engine.isOpen()).toBe(true);
});
});
// ---- 表管理 ----
describe('表管理', () => {
it('创建表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('重复创建表抛错', async () => {
await engine.createTable(userSchema);
await expect(engine.createTable(userSchema)).rejects.toThrow('already exists');
});
it('删除表', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
await expect(engine.count('users')).rejects.toThrow('does not exist');
});
it('获取表结构', async () => {
await engine.createTable(userSchema);
const schema = await engine.getTableSchema('users');
expect(schema).not.toBeNull();
expect(schema!.columns.id.primaryKey).toBe(true);
});
it('schema 持久化:重启后表结构保留', async () => {
await engine.createTable(userSchema);
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.hasTable('users')).toBe(true);
const schema = await engine2.getTableSchema('users');
expect(schema!.columns.email.unique).toBe(true);
await engine2.close();
});
});
// ---- 插入 ----
describe('插入', () => {
beforeEach(async () => { await engine.createTable(userSchema); });
it('插入单行并返回主键', async () => {
const pks = await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
expect(pks).toEqual(['1']);
expect(await engine.count('users')).toBe(1);
});
it('默认值生效', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
const rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(0);
});
it('重复主键抛错', async () => {
await engine.insert('users', [{ id: '1', name: 'A' }]);
await expect(engine.insert('users', [{ id: '1', name: 'B' }])).rejects.toThrow('Duplicate primary key');
});
it('唯一约束冲突抛错', async () => {
await engine.insert('users', [{ id: '1', name: 'A', email: 'a@x.com' }]);
await expect(engine.insert('users', [{ id: '2', name: 'B', email: 'a@x.com' }]))
.rejects.toThrow('Unique constraint violation');
});
it('必填字段校验', async () => {
await expect(engine.insert('users', [{ id: '1' }])).rejects.toThrow('is required');
});
it('批量插入', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'A' }, { id: '2', name: 'B' }, { id: '3', name: 'C' },
]);
expect(pks).toEqual(['1', '2', '3']);
});
});
// ---- 查询 ----
describe('查询', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
{ id: '3', name: 'Carol', age: 35 },
]);
});
it('全表查询', async () => {
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(3);
});
it('WHERE 等值过滤', async () => {
const rows = await engine.find('users', { table: 'users', where: { name: 'Bob' } });
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('2');
});
it('WHERE 操作符', async () => {
const rows = await engine.find('users', { table: 'users', where: { age: { $gt: 26 } } });
expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']);
});
it('ORDER BY', async () => {
const rows = await engine.find('users', { table: 'users', orderBy: [{ column: 'age', direction: 'desc' }] });
expect(rows[0].id).toBe('3');
expect(rows[2].id).toBe('2');
});
it('LIMIT / OFFSET', async () => {
const rows = await engine.find('users', { table: 'users', limit: 2, offset: 1 });
expect(rows).toHaveLength(2);
});
it('列投影', async () => {
const rows = await engine.find('users', { table: 'users', columns: ['name'] });
expect(Object.keys(rows[0])).toEqual(['name']);
});
});
// ---- 更新 ----
describe('更新', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
});
it('按条件更新', async () => {
const count = await engine.update('users', { table: 'users', where: { id: '1' } }, { age: 31 });
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].age).toBe(31);
});
it('无匹配行影响 0 行', async () => {
const count = await engine.update('users', { table: 'users', where: { id: 'nope' } }, { age: 1 });
expect(count).toBe(0);
});
it('更新主键(旧键删除 + 新键写入)', async () => {
const count = await engine.update('users', { table: 'users', where: { id: '1' } }, { id: '10', age: 99 });
expect(count).toBe(1);
expect(await engine.count('users')).toBe(2);
const rows = await engine.find('users', { table: 'users', where: { id: '10' } });
expect(rows[0].age).toBe(99);
expect(await engine.find('users', { table: 'users', where: { id: '1' } })).toHaveLength(0);
});
it('更新持久化:重启后保留', async () => {
await engine.update('users', { table: 'users', where: { id: '1' } }, { age: 99 });
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
const rows = await engine2.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].age).toBe(99);
await engine2.close();
});
});
// ---- 删除 ----
describe('删除', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }, { id: '3', name: 'Carol' },
]);
});
it('按条件删除', async () => {
const count = await engine.delete('users', { table: 'users', where: { id: '1' } });
expect(count).toBe(1);
expect(await engine.count('users')).toBe(2);
});
it('删除持久化:重启后保留', async () => {
await engine.delete('users', { table: 'users', where: { id: '1' } });
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.count('users')).toBe(2);
await engine2.close();
});
it('级联删除(CASCADE', async () => {
const ordersSchema = createSchema('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' },
});
await engine.createTable(ordersSchema);
await engine.insert('orders', [{ id: 'o1', user_id: '1' }, { id: 'o2', user_id: '2' }]);
await engine.delete('users', { table: 'users', where: { id: '1' } });
// 级联:orders 中 user_id=1 的行被删
expect(await engine.count('orders')).toBe(1);
// 重启后级联结果持久化
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.count('users')).toBe(2);
expect(await engine2.count('orders')).toBe(1);
await engine2.close();
});
});
// ---- Count ----
describe('Count', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'A', age: 10 }, { id: '2', name: 'B', age: 20 }, { id: '3', name: 'C', age: 30 },
]);
});
it('总数', async () => {
expect(await engine.count('users')).toBe(3);
});
it('条件计数', async () => {
expect(await engine.count('users', { table: 'users', where: { age: { $gte: 20 } } })).toBe(2);
});
});
// ---- 事务 ----
describe('事务', () => {
beforeEach(async () => { await engine.createTable(userSchema); });
it('commit 后数据可见且持久化', async () => {
await engine.beginTransaction();
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
await engine.commitTransaction();
expect(await engine.count('users')).toBe(1);
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.count('users')).toBe(1);
await engine2.close();
});
it('rollback 后数据消失', async () => {
await engine.beginTransaction();
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
await engine.rollbackTransaction();
expect(await engine.count('users')).toBe(0);
});
it('事务内读到自己写入的行', async () => {
await engine.beginTransaction();
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(1);
await engine.rollbackTransaction();
});
it('commit 多表原子(全部生效或全部不生效)', async () => {
await engine.createTable(createSchema('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
}));
await engine.beginTransaction();
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
await engine.insert('orders', [{ id: 'o1', user_id: '1' }]);
await engine.commitTransaction();
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.count('users')).toBe(1);
expect(await engine2.count('orders')).toBe(1);
await engine2.close();
});
it('事务中 DDL 支持(create/drop 随 commit 生效)', async () => {
const txSchema = createSchema('tx_users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
});
await engine.beginTransaction();
await engine.createTable(txSchema);
await engine.insert('tx_users', [{ id: '1', name: 'A' }]);
await engine.commitTransaction();
expect(await engine.hasTable('tx_users')).toBe(true);
expect(await engine.count('tx_users')).toBe(1);
// 事务中 drop 表
await engine.beginTransaction();
await engine.dropTable('tx_users');
await engine.commitTransaction();
expect(await engine.hasTable('tx_users')).toBe(false);
// 重启后 drop 生效(KV 残留清理)
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.hasTable('tx_users')).toBe(false);
await engine2.close();
});
});
// ---- 持久化与崩溃恢复 ----
describe('持久化与崩溃恢复', () => {
it('未 checkpoint 数据重启后经日志重放恢复', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
await engine.close(); // 模拟"崩溃后重开"(未 checkpoint,数据在 KVStore 日志)
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.count('users')).toBe(2);
const rows = await engine2.find('users', { table: 'users' });
expect(rows.map((r) => r.id).sort()).toEqual(['1', '2']);
await engine2.close();
});
it('checkpoint 后重启(快照恢复)', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
await (engine as any).kv.checkpoint();
await engine.insert('users', [{ id: '2', name: 'Bob' }]);
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.count('users')).toBe(2);
await engine2.close();
});
it('索引列跨重启恢复(查询走索引)', async () => {
const idxSchema = createSchema('items', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
});
await engine.createTable(idxSchema);
await engine.insert('items', [
{ id: '1', tag: 'a' }, { id: '2', tag: 'b' }, { id: '3', tag: 'a' },
]);
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
const rows = await engine2.find('items', { table: 'items', where: { tag: 'a' } });
expect(rows).toHaveLength(2);
await engine2.close();
});
it('CREATE INDEX 持久化(重启后索引保留)', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
await engine.createIndex('users', 'name');
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
const schema = await engine2.getTableSchema('users');
expect(schema!.columns.name.index).toBe(true);
const rows = await engine2.find('users', { table: 'users', where: { name: 'Alice' } });
expect(rows).toHaveLength(1);
await engine2.close();
});
it('ALTER TABLE ADD/DROP 持久化', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
await engine.alterTable('users', 'ADD', { name: 'phone', type: 'string' });
await engine.alterTable('users', 'DROP', { name: 'age', type: 'number' });
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
const schema = await engine2.getTableSchema('users');
expect(schema!.columns.phone).toBeDefined();
expect(schema!.columns.age).toBeUndefined();
const rows = await engine2.find('users', { table: 'users' });
expect(rows[0].phone).toBeUndefined();
expect(rows[0].age).toBeUndefined();
await engine2.close();
});
it('元数据 getMeta/setMeta 持久化', async () => {
await engine.setMeta('__metona_version', '3');
await engine.close();
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1);
expect(await engine2.getMeta('__metona_version')).toBe('3');
await engine2.close();
});
it('数据损坏自愈:日志损坏 → open 不崩,repair 截断', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
await engine.close();
// 篡改日志尾部 CRC(模拟最后一条记录写入中断)
const medium = new SharedMemoryBackend();
await medium.open(dbName);
const log = await medium.read('__kv_log');
expect(log).not.toBeNull();
const corrupted = new Uint8Array(log as ArrayBuffer);
corrupted[corrupted.byteLength - 5] ^= 0xff;
await medium.write('__kv_log', corrupted.buffer as ArrayBuffer);
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
await engine2.open(dbName, 1); // 损坏尾部截断,打开不崩
expect(await engine2.hasTable('users')).toBe(true);
await engine2.repair();
expect(await engine2.hasTable('users')).toBe(true);
// repair 后新写入正常
await engine2.insert('users', [{ id: '2', name: 'Bob' }]);
expect(await engine2.count('users')).toBeGreaterThanOrEqual(1);
await engine2.close();
});
});
// ---- 流式查询 ----
describe('流式查询', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'A' }, { id: '2', name: 'B' }, { id: '3', name: 'C' },
]);
});
it('findStream 逐行回调', async () => {
const rows: Record<string, unknown>[] = [];
const count = await engine.findStream!('users', { table: 'users' }, (r) => rows.push(r));
expect(count).toBe(3);
expect(rows.map((r) => r.id).sort()).toEqual(['1', '2', '3']);
});
});
});