Files
MetonaSqlark/tests/migration.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

188 lines
7.3 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.
/**
* 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 });
// 持久化 schemaIndexedDBEngine 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();
});
});