Files
MetonaSqlark/tests/migration.test.ts

245 lines
9.8 KiB
TypeScript
Raw Permalink 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();
});
it('无 id 列的旧库:第一个非 json 列兜底为主键(v0.7.3', async () => {
const legacyName = uniqueDB();
await new Promise<void>((resolve, reject) => {
const request = indexedDB.open(legacyName, 1);
request.onupgradeneeded = () => {
request.result.createObjectStore('nokey_tbl', { autoIncrement: true });
};
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction('nokey_tbl', 'readwrite');
tx.objectStore('nokey_tbl').add({ code: 'c1', name: 'X' });
tx.objectStore('nokey_tbl').add({ code: 'c2', name: 'Y' });
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();
// 此前无主键 → createSchema 抛 SCHEMA_ERROR 中断整个迁移;现在 code 列兜底为主键
const result = await migrateFromIndexedDB({ dbName: legacyName, engine: 'disk', target });
expect(result.migratedTables).toEqual(['nokey_tbl']);
expect(result.rowCount).toBe(2);
const rows = await target.table('nokey_tbl').select().execute();
expect(rows).toHaveLength(2);
await target.close();
});
it('全 json 列的旧库:无可用主键 → 跳过该表不中断迁移(v0.7.3)', async () => {
const legacyName = uniqueDB();
await new Promise<void>((resolve, reject) => {
const request = indexedDB.open(legacyName, 1);
request.onupgradeneeded = () => {
request.result.createObjectStore('json_tbl', { autoIncrement: true });
request.result.createObjectStore('ok_tbl', { keyPath: 'id' });
};
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction(['json_tbl', 'ok_tbl'], 'readwrite');
tx.objectStore('json_tbl').add({ payload: { a: 1 } });
tx.objectStore('ok_tbl').add({ id: '1', name: 'Z' });
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 });
// json_tbl 无可用主键被跳过;ok_tbl 正常迁移
expect(result.skippedTables).toContain('json_tbl');
expect(result.migratedTables).toEqual(['ok_tbl']);
await target.close();
});
});