fix: v0.7.3 数据正确性与边界窗口收尾 — INSERT 语句级原子(三引擎+Aria PK 批内重复)/ 索引列 IS NULL 恒空 / delete RESTRICT 破坏索引 / queryStream 子查询静默空结果 / ALTER DROP 索引残留 / UNIQUE INDEX 存量校验 / SELECT * 别名投影 / WAL BEGIN/ROLLBACK 事务边界 / aria $in 与级联重复扫描性能 / $and 等值下推 / ANALYZE 索引统计 / React-Vue hooks 生命周期 / 迁移主键兜底 + 58 回归
This commit is contained in:
@@ -16,6 +16,9 @@ jest.mock('react', () => {
|
||||
const stateStore: any[] = [];
|
||||
const registeredEffects = new Set<number>();
|
||||
const effectQueue: Array<() => void | Promise<void>> = [];
|
||||
// v0.7.3: 依赖数组指纹 + cleanup 支持(useDatabase config 变更重建测试用)
|
||||
const effectDeps = new Map<number, string>();
|
||||
const effectCleanups = new Map<number, () => void>();
|
||||
let cursor = 0;
|
||||
return {
|
||||
useState: (init: any) => {
|
||||
@@ -28,12 +31,26 @@ jest.mock('react', () => {
|
||||
},
|
||||
];
|
||||
},
|
||||
// 按 hook 调用位置去重:同一位置的 effect 只在首次 render 注册
|
||||
useEffect: (fn: any, _deps: any[]) => {
|
||||
// 按 hook 调用位置注册:同一位置首次注册入队;deps 指纹变化时
|
||||
// 先执行旧 cleanup 再重跑 effect(模拟 React 依赖更新语义)
|
||||
useEffect: (fn: any, _deps: any[] = []) => {
|
||||
const idx = cursor;
|
||||
const key = JSON.stringify(_deps ?? []);
|
||||
if (!registeredEffects.has(idx)) {
|
||||
registeredEffects.add(idx);
|
||||
effectQueue.push(fn);
|
||||
effectDeps.set(idx, key);
|
||||
effectQueue.push(() => {
|
||||
const cleanup = fn();
|
||||
if (typeof cleanup === 'function') effectCleanups.set(idx, cleanup);
|
||||
});
|
||||
} else if (effectDeps.get(idx) !== key) {
|
||||
effectDeps.set(idx, key);
|
||||
effectQueue.push(() => {
|
||||
const c = effectCleanups.get(idx);
|
||||
if (c) { c(); effectCleanups.delete(idx); }
|
||||
const cleanup = fn();
|
||||
if (typeof cleanup === 'function') effectCleanups.set(idx, cleanup);
|
||||
});
|
||||
}
|
||||
},
|
||||
useCallback: (fn: any) => fn,
|
||||
@@ -49,6 +66,8 @@ jest.mock('react', () => {
|
||||
cursor = 0;
|
||||
effectQueue.length = 0;
|
||||
registeredEffects.clear();
|
||||
effectDeps.clear();
|
||||
effectCleanups.clear();
|
||||
},
|
||||
};
|
||||
}, { virtual: true });
|
||||
@@ -197,4 +216,41 @@ describe('useDatabase', () => {
|
||||
|
||||
initSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('config 变更时关闭旧实例并重建(v0.7.3)', async () => {
|
||||
const closeSpy = jest.spyOn(MetonaSqlark.prototype, 'close').mockResolvedValue();
|
||||
|
||||
// 首次挂载(真实异步 init)
|
||||
const mount = (config: { name: string }) => useDatabase(config);
|
||||
render(() => mount({ name: 'hook-a' }));
|
||||
await flushEffects();
|
||||
// 轮询等待真实 init 完成(stateStore 经 re-render 读取最新值)
|
||||
for (let i = 0; i < 100 && render(() => mount({ name: 'hook-a' })).db === null; i++) {
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
}
|
||||
const first = render(() => mount({ name: 'hook-a' }));
|
||||
expect(first.db).not.toBeNull();
|
||||
const firstDb = first.db;
|
||||
|
||||
// 同名 config 再渲染 → 不重建(相同实例)
|
||||
const same = render(() => mount({ name: 'hook-a' }));
|
||||
expect(same.db).toBe(firstDb);
|
||||
|
||||
// config 变更渲染 → cleanup 关闭旧实例 + 重建新实例
|
||||
render(() => mount({ name: 'hook-b' }));
|
||||
await flushEffects();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const cur = render(() => mount({ name: 'hook-b' }));
|
||||
if (cur.db !== null && cur.db !== firstDb && cur.ready) break;
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
}
|
||||
const afterChange = render(() => mount({ name: 'hook-b' }));
|
||||
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
expect(afterChange.db).not.toBeNull();
|
||||
expect(afterChange.db).not.toBe(firstDb);
|
||||
expect(afterChange.ready).toBe(true);
|
||||
|
||||
closeSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// ---- 最小 Vue mock(自包含:jest.mock 工厂不能引用外部变量) ----
|
||||
jest.mock('vue', () => {
|
||||
const mountQueue: Array<() => void | Promise<void>> = [];
|
||||
const unmountQueue: Array<() => void | Promise<void>> = [];
|
||||
const watchList: Array<{ sources: any[]; cb: () => void | Promise<void> }> = [];
|
||||
return {
|
||||
ref: (init: any) => {
|
||||
@@ -25,10 +26,15 @@ jest.mock('vue', () => {
|
||||
onMounted: (fn: any) => {
|
||||
mountQueue.push(fn);
|
||||
},
|
||||
onUnmounted: (fn: any) => {
|
||||
unmountQueue.push(fn);
|
||||
},
|
||||
__mockMounted: mountQueue,
|
||||
__mockUnmounted: unmountQueue,
|
||||
__mockWatch: watchList,
|
||||
__mockReset: () => {
|
||||
mountQueue.length = 0;
|
||||
unmountQueue.length = 0;
|
||||
watchList.length = 0;
|
||||
},
|
||||
};
|
||||
@@ -39,6 +45,7 @@ import { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase } from '../../src/int
|
||||
/** mock 模块内的挂载/监听队列(自包含作用域) */
|
||||
const vueMock = jest.requireMock('vue') as {
|
||||
__mockMounted: Array<() => void | Promise<void>>;
|
||||
__mockUnmounted: Array<() => void | Promise<void>>;
|
||||
__mockWatch: Array<{ sources: any[]; cb: () => void | Promise<void> }>;
|
||||
__mockReset: () => void;
|
||||
};
|
||||
@@ -55,6 +62,14 @@ async function flushMounted(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 模拟组件卸载:执行 onUnmounted 注册的回调 */
|
||||
async function flushUnmounted(): Promise<void> {
|
||||
const fns = vueMock.__mockUnmounted.splice(0);
|
||||
for (const fn of fns) {
|
||||
await fn();
|
||||
}
|
||||
}
|
||||
|
||||
/** 触发 watch 回调 */
|
||||
async function flushWatch(): Promise<void> {
|
||||
const pairs = vueMock.__mockWatch.splice(0);
|
||||
@@ -163,4 +178,24 @@ describe('useSqlarkDatabase', () => {
|
||||
expect(hook.ready.value).toBe(false);
|
||||
expect(hook.error.value).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
test('组件卸载时关闭数据库实例(v0.7.3)', async () => {
|
||||
const hook = useSqlarkDatabase({ name: 'vue-unmount', mode: 'memory' });
|
||||
await flushMounted();
|
||||
expect(hook.db.value).not.toBeNull();
|
||||
const db = hook.db.value!;
|
||||
// 实例打开中:isReady
|
||||
expect(db.isReady()).toBe(true);
|
||||
|
||||
await flushUnmounted();
|
||||
// close 后实例不可再查询
|
||||
await expect(db.query('SELECT 1')).rejects.toMatchObject({ code: 'DB_NOT_READY' });
|
||||
});
|
||||
|
||||
test('卸载时实例为 null 不抛错(初始化失败场景)', async () => {
|
||||
const hook = useSqlarkDatabase({ name: 'vue-unmount-fail', mode: 'unknown-mode' as any });
|
||||
await flushMounted();
|
||||
await expect(flushUnmounted()).resolves.toBeUndefined();
|
||||
expect(hook.db.value).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user