release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
This commit is contained in:
@@ -13,7 +13,10 @@ import { createSchema } from '../src/table/schema';
|
||||
import { MemoryEngine } from '../src/engine/memory';
|
||||
import { HybridEngine } from '../src/hybrid/index';
|
||||
import { MetonaSqlark } from '../src/core';
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { installOPFSMock } from './helpers/opfs-mock';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
|
||||
let idbCounter = 0;
|
||||
function uniqueDB(): string {
|
||||
@@ -53,7 +56,7 @@ describe('P0-A — Compaction 缓存独立性', () => {
|
||||
it('多层级 compaction 后数据仍完整且可重开', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'indexeddb',
|
||||
storageBackend: 'opfs',
|
||||
bufferPoolPages: 8, // 32KB 缓存
|
||||
memtableSizeThreshold: 16 * 1024,
|
||||
checkpointInterval: 100000,
|
||||
@@ -74,7 +77,7 @@ describe('P0-A — Compaction 缓存独立性', () => {
|
||||
|
||||
// 重开:数据完整(meta/文件未被 compaction 破坏)
|
||||
const engine2 = new AriaEngine({
|
||||
storageBackend: 'indexeddb',
|
||||
storageBackend: 'opfs',
|
||||
bufferPoolPages: 8,
|
||||
memtableSizeThreshold: 16 * 1024,
|
||||
checkpointInterval: 100000,
|
||||
@@ -92,7 +95,7 @@ describe('P0-A — Compaction 缓存独立性', () => {
|
||||
describe('P1-A — 二级索引恢复', () => {
|
||||
it('Aria 重开后二级索引可用(索引 LSM 持久化恢复)', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine.open(dbName, 1);
|
||||
await engine.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
@@ -107,7 +110,7 @@ describe('P1-A — 二级索引恢复', () => {
|
||||
await engine.close();
|
||||
|
||||
// 重开:二级索引 LSM 应自动恢复(修复前为空 → 索引查询回退全表,createIndex 也静默跳过)
|
||||
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine2.open(dbName, 1);
|
||||
// 强断言:索引 LSM 真实存在且数据完整(防止"索引缺失静默回退全表扫描"的假通过)
|
||||
const idxLsm = (engine2 as any).secondaryIndexes.get('users:idx:email');
|
||||
@@ -124,7 +127,7 @@ describe('P1-A — 二级索引恢复', () => {
|
||||
|
||||
it('WAL 恢复后二级索引与主数据一致(崩溃前索引未更新)', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine.open(dbName, 1);
|
||||
await engine.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
@@ -138,7 +141,7 @@ describe('P1-A — 二级索引恢复', () => {
|
||||
await (engine as any).backend.close();
|
||||
(engine as any).opened = false;
|
||||
|
||||
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine2.open(dbName, 1);
|
||||
// 强断言:索引 LSM 已恢复且包含 WAL 回放的行(崩溃前索引未更新,恢复后必须重建)
|
||||
const idxLsm = (engine2 as any).secondaryIndexes.get('users:idx:city');
|
||||
@@ -154,7 +157,7 @@ describe('P1-A — 二级索引恢复', () => {
|
||||
|
||||
it('createIndex 重开后仍可新建(schema 标记恢复后不阻塞)', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine.open(dbName, 1);
|
||||
await engine.createTable(createSchema('t', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
@@ -163,7 +166,7 @@ describe('P1-A — 二级索引恢复', () => {
|
||||
await engine.insert('t', [{ id: '1', name: 'A' }]);
|
||||
await engine.close();
|
||||
|
||||
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine2.open(dbName, 1);
|
||||
await engine2.createIndex('t', 'name'); // 修复前 schema 无标记时正常;此处验证无标记场景
|
||||
const byName = await engine2.find('t', { table: 't', where: { name: 'A' } });
|
||||
@@ -177,9 +180,9 @@ describe('P1-A — 二级索引恢复', () => {
|
||||
// ===================================================================
|
||||
|
||||
describe('P1-B — ALTER TABLE 持久化', () => {
|
||||
it('IndexedDBEngine DROP COLUMN 后重启不复活', async () => {
|
||||
it('KVStoreEngine DROP COLUMN 后重启不复活', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'indexeddb' });
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'opfs' });
|
||||
await db.init();
|
||||
await db.defineTable('t', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
@@ -194,7 +197,7 @@ describe('P1-B — ALTER TABLE 持久化', () => {
|
||||
await db.close();
|
||||
|
||||
// 重启:schema 不复活,列定义已持久化
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'indexeddb' });
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'opfs' });
|
||||
await db2.init();
|
||||
const schema2 = await db2.getEngine().getTableSchema('t');
|
||||
expect(schema2!.columns.old_col).toBeUndefined();
|
||||
@@ -204,13 +207,13 @@ describe('P1-B — ALTER TABLE 持久化', () => {
|
||||
|
||||
it('HybridEngine ADD COLUMN 后重启保留', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
|
||||
await db.init();
|
||||
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
|
||||
await db.query('ALTER TABLE t ADD COLUMN phone STRING');
|
||||
await db.close();
|
||||
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
|
||||
await db2.init();
|
||||
const schema2 = await db2.getEngine().getTableSchema('t');
|
||||
expect(schema2!.columns.phone).toBeDefined();
|
||||
@@ -219,13 +222,13 @@ describe('P1-B — ALTER TABLE 持久化', () => {
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// P1-C: IndexedDB 事务内 DDL
|
||||
// P1-C: KVStore 事务内 DDL
|
||||
// ===================================================================
|
||||
|
||||
describe('P1-C — IndexedDB 事务内 DDL', () => {
|
||||
describe('P1-C — KVStore 事务内 DDL', () => {
|
||||
it('事务内建表 → commit 后磁盘一致(重启表存在且可查)', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
|
||||
await db.init();
|
||||
await db.defineTable('base', { id: { type: 'string', primaryKey: true } });
|
||||
await db.table('base').insert({ id: '1' });
|
||||
@@ -240,7 +243,7 @@ describe('P1-C — IndexedDB 事务内 DDL', () => {
|
||||
expect(await db.table('created_in_tx').count()).toBe(1);
|
||||
await db.close();
|
||||
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
|
||||
await db2.init();
|
||||
expect(await db2.table('created_in_tx').count()).toBe(1);
|
||||
expect(await db2.table('base').count()).toBe(2);
|
||||
@@ -249,7 +252,7 @@ describe('P1-C — IndexedDB 事务内 DDL', () => {
|
||||
|
||||
it('事务内删表 → commit 后磁盘一致(重启无幽灵表)', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
|
||||
await db.init();
|
||||
await db.defineTable('ghost', { id: { type: 'string', primaryKey: true } });
|
||||
await db.table('ghost').insert({ id: '1' });
|
||||
@@ -259,7 +262,7 @@ describe('P1-C — IndexedDB 事务内 DDL', () => {
|
||||
});
|
||||
|
||||
await db.close();
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
|
||||
await db2.init();
|
||||
const names = await db2.getTableNames();
|
||||
expect(names).not.toContain('ghost');
|
||||
@@ -300,7 +303,7 @@ describe('全模式抽查 — 生命周期与元数据', () => {
|
||||
|
||||
it('AriaEngine repair 幂等', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({ storageBackend: 'indexeddb' });
|
||||
const engine = new AriaEngine({ storageBackend: 'opfs' });
|
||||
await engine.open(dbName, 1);
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: '1' }]);
|
||||
|
||||
Reference in New Issue
Block a user