release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* migrateFromIndexedDB 迁移工具测试
|
||||
*
|
||||
* 用 fake-indexeddb 构造旧库(模拟 v0.5.x 及更早的 IndexedDB 引擎数据),
|
||||
* 验证迁移到自研 KV 引擎(KVStoreEngine)后数据/schema/索引完整。
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
import { MetonaSqlark } from '../src/core';
|
||||
import { migrateFromIndexedDB } from '../src/migration/index';
|
||||
|
||||
let counter = 0;
|
||||
function uniqueDB(): string {
|
||||
return `mig-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
}
|
||||
|
||||
/** 构造旧 disk 模式库(IndexedDBEngine 布局:每表一个 objectStore + __metona_schema) */
|
||||
async function createLegacyDiskDB(dbName: string): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.open(dbName, 2);
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
const users = db.createObjectStore('users', { keyPath: 'id' });
|
||||
users.createIndex('idx_email', 'email', { unique: true });
|
||||
db.createObjectStore('orders', { keyPath: 'id' });
|
||||
db.createObjectStore('__metona_schema', { keyPath: 'name' });
|
||||
};
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
const tx = db.transaction(['users', 'orders', '__metona_schema'], 'readwrite');
|
||||
const users = tx.objectStore('users');
|
||||
users.add({ id: '1', name: 'Alice', email: 'alice@x.com', age: 30 });
|
||||
users.add({ id: '2', name: 'Bob', email: 'bob@x.com', age: 25 });
|
||||
const orders = tx.objectStore('orders');
|
||||
orders.add({ id: 'o1', user_id: '1', amount: 100 });
|
||||
// 持久化 schema(IndexedDBEngine v0.3.2+ 布局)
|
||||
tx.objectStore('__metona_schema').put({
|
||||
name: 'users',
|
||||
schema: JSON.stringify({
|
||||
name: 'users',
|
||||
columns: {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string', required: true },
|
||||
email: { type: 'string', unique: true, index: true },
|
||||
age: { type: 'number', default: 0 },
|
||||
},
|
||||
}),
|
||||
});
|
||||
tx.objectStore('__metona_schema').put({
|
||||
name: 'orders',
|
||||
schema: JSON.stringify({
|
||||
name: 'orders',
|
||||
columns: {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
user_id: { type: 'string' },
|
||||
amount: { type: 'number' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
tx.oncomplete = () => { db.close(); resolve(); };
|
||||
tx.onerror = () => reject(tx.error);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/** 构造旧 aria 模式库(IndexedDBBackend 布局:单 data store + aria- 前缀) */
|
||||
async function createLegacyAriaDB(dbName: string): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.open(`aria-${dbName}`, 1);
|
||||
request.onupgradeneeded = () => {
|
||||
request.result.createObjectStore('data');
|
||||
};
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
const tx = db.transaction('data', 'readwrite');
|
||||
const store = tx.objectStore('data');
|
||||
store.put(new TextEncoder().encode('legacy').buffer, '__aria_keymeta_test');
|
||||
tx.oncomplete = () => { db.close(); resolve(); };
|
||||
tx.onerror = () => reject(tx.error);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
describe('migrateFromIndexedDB', () => {
|
||||
it('disk 模式旧库:schema + 数据完整迁移(含索引标记)', async () => {
|
||||
const legacyName = uniqueDB();
|
||||
await createLegacyDiskDB(legacyName);
|
||||
|
||||
const target = new MetonaSqlark({ name: uniqueDB(), mode: 'disk' });
|
||||
await target.init();
|
||||
|
||||
const result = await migrateFromIndexedDB({ dbName: legacyName, engine: 'disk', target });
|
||||
|
||||
expect(result.migratedTables.sort()).toEqual(['orders', 'users']);
|
||||
expect(result.rowCount).toBe(3);
|
||||
expect(await target.table('users').count()).toBe(2);
|
||||
expect(await target.table('orders').count()).toBe(1);
|
||||
|
||||
// schema 保留(索引/唯一标记)
|
||||
const schema = await target.getEngine().getTableSchema('users');
|
||||
expect(schema!.columns.email.unique).toBe(true);
|
||||
expect(schema!.columns.email.index).toBe(true);
|
||||
|
||||
// 数据正确
|
||||
const alice = await target.table('users').select().where({ id: '1' }).execute();
|
||||
expect(alice[0].email).toBe('alice@x.com');
|
||||
|
||||
// 重启后(持久化)数据保留
|
||||
await target.close();
|
||||
const target2 = new MetonaSqlark({ name: target.name, mode: 'disk' });
|
||||
await target2.init();
|
||||
expect(await target2.table('users').count()).toBe(2);
|
||||
await target2.close();
|
||||
});
|
||||
|
||||
it('无持久化 schema 的旧库:从数据推断', async () => {
|
||||
const legacyName = uniqueDB();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.open(legacyName, 1);
|
||||
request.onupgradeneeded = () => {
|
||||
request.result.createObjectStore('legacy_tbl', { keyPath: 'id' });
|
||||
};
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
const tx = db.transaction('legacy_tbl', 'readwrite');
|
||||
tx.objectStore('legacy_tbl').add({ id: 'a', name: 'X', age: 10 });
|
||||
tx.oncomplete = () => { db.close(); resolve(); };
|
||||
tx.onerror = () => reject(tx.error);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
const target = new MetonaSqlark({ name: uniqueDB(), mode: 'disk' });
|
||||
await target.init();
|
||||
const result = await migrateFromIndexedDB({ dbName: legacyName, engine: 'disk', target });
|
||||
|
||||
expect(result.migratedTables).toEqual(['legacy_tbl']);
|
||||
expect(result.rowCount).toBe(1);
|
||||
const rows = await target.table('legacy_tbl').select().execute();
|
||||
expect(rows[0].name).toBe('X');
|
||||
await target.close();
|
||||
});
|
||||
|
||||
it('目标库已存在的表跳过(不覆盖)', async () => {
|
||||
const legacyName = uniqueDB();
|
||||
await createLegacyDiskDB(legacyName);
|
||||
|
||||
const target = new MetonaSqlark({ name: uniqueDB(), mode: 'disk' });
|
||||
await target.init();
|
||||
await target.defineTable('users', { id: { type: 'string', primaryKey: true } });
|
||||
await target.table('users').insert({ id: 'keep' });
|
||||
|
||||
const result = await migrateFromIndexedDB({ dbName: legacyName, engine: 'disk', target });
|
||||
expect(result.skippedTables).toContain('users');
|
||||
// 已存在的表未被覆盖
|
||||
expect(await target.table('users').count()).toBe(1);
|
||||
await target.close();
|
||||
});
|
||||
|
||||
it('空旧库(无业务 store)→ 返回空迁移结果', async () => {
|
||||
const legacyName = uniqueDB();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.open(legacyName, 1);
|
||||
request.onsuccess = () => { request.result.close(); resolve(); };
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
const target = new MetonaSqlark({ name: uniqueDB(), mode: 'disk' });
|
||||
await target.init();
|
||||
const result = await migrateFromIndexedDB({ dbName: legacyName, engine: 'disk', target });
|
||||
expect(result.migratedTables).toEqual([]);
|
||||
expect(result.rowCount).toBe(0);
|
||||
await target.close();
|
||||
});
|
||||
|
||||
it('aria 模式旧库(引擎私有格式)→ 明确抛错说明不支持', async () => {
|
||||
const legacyName = uniqueDB();
|
||||
await createLegacyAriaDB(legacyName);
|
||||
|
||||
const target = new MetonaSqlark({ name: uniqueDB(), mode: 'disk' });
|
||||
await target.init();
|
||||
await expect(migrateFromIndexedDB({ dbName: legacyName, engine: 'aria' as never, target }))
|
||||
.rejects.toThrow('not supported');
|
||||
await target.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user