release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
This commit is contained in:
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user