Files
MetonaSqlark/tests/engine/kvstore-engine.test.ts
T
thzxx 24b3ca1079
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
release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
2026-08-10 13:56:04 +08:00

509 lines
18 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.
/**
* 介质: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']);
});
});
});