release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
CI / test (18.x) (push) Failing after 5m11s
CI / test (20.x) (push) Failing after 5m8s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m56s

v0.2.6 质量加固:
- 修复 AriaEngine 二级索引 SSTable 互相覆盖(命名空间隔离)
- 修复 LSM 多版本读取顺序错误 + MergeIterator 取最新来源
- 重写 LZ4 压缩器(往返一致性 + 缓冲区溢出)
- sstableCache LRU 上限 + 预加载兜底(BufferPool 配置生效)
- 修复 React/Vue 集成 import type 运行时 bug + exports 子路径
- 新增 38 个测试(LZ4往返/Crypto/集成), 删除伪测试

v0.3.0 SQL 功能扩展:
- 多语句 parseAll + 事务语句 BEGIN/COMMIT/ROLLBACK
- INSERT INTO ... SELECT + UNION/UNION ALL + EXISTS 关联子查询
- CREATE/DROP INDEX 五引擎实现 + 别名 WHERE 修复
- benchmark 页面 + 36 个新测试

v0.3.1 表达式与性能:
- CASE WHEN 表达式(SELECT 列/WHERE/聚合)
- JOIN + 关联子查询逐行绑定
- WAL 批量组提交(写放大 O(N)→O(1))
- 修复 pending frozen 可见性 + flush 缓存竞争

v0.3.2 并发:
- CASE WHEN 用于 WHERE/聚合 + JOIN 哈希连接
- 多标签页同步(multiTabSync + BroadcastChannel)
- IndexedDB schema 持久化(reopen 后表结构恢复)
- 修复 where-matcher 顶层 $not
- 修复 CJS 产物 .js 被 ESM 解析(exports 空) — .cjs 后缀 + exports 修正
- 836 测试 / 44 套件 / 81.0% 覆盖率
This commit is contained in:
thzxx
2026-08-08 10:41:30 +08:00
parent 3ae7d6e8fb
commit d544501e1c
77 changed files with 29007 additions and 20395 deletions
+200
View File
@@ -0,0 +1,200 @@
/**
* React 集成 hooks 测试(v0.2.6 补强)
* @module tests/integrations/react
*
* 项目零运行时依赖(react 为 peer dependency),
* 使用最小 React mock 验证 hooks 的真实逻辑:
* - mount 时执行查询并更新状态
* - 错误路径
* - refresh 重新执行
* - 表名校验(SQL 注入防护)
*/
import { MetonaSqlark } from '../../src/core';
// ---- 最小 React mock(自包含:jest.mock 工厂不能引用外部变量) ----
jest.mock('react', () => {
const stateStore: any[] = [];
const registeredEffects = new Set<number>();
const effectQueue: Array<() => void | Promise<void>> = [];
let cursor = 0;
return {
useState: (init: any) => {
const idx = cursor++;
if (!(idx in stateStore)) stateStore[idx] = init;
return [
stateStore[idx],
(v: any) => {
stateStore[idx] = typeof v === 'function' ? v(stateStore[idx]) : v;
},
];
},
// 按 hook 调用位置去重:同一位置的 effect 只在首次 render 注册
useEffect: (fn: any, _deps: any[]) => {
const idx = cursor;
if (!registeredEffects.has(idx)) {
registeredEffects.add(idx);
effectQueue.push(fn);
}
},
useCallback: (fn: any) => fn,
useRef: (init: any) => ({ current: init }),
/** 模拟一次组件渲染:cursor 归零后执行 hook 函数 */
__mockRender: (fn: () => any): any => {
cursor = 0;
return fn();
},
__mockEffects: effectQueue,
__mockReset: () => {
stateStore.length = 0;
cursor = 0;
effectQueue.length = 0;
registeredEffects.clear();
},
};
}, { virtual: true });
import { useQuery, useTable, useDatabase } from '../../src/integrations/react';
/** mock 模块内的 effect 队列与渲染控制(自包含作用域) */
const reactMock = jest.requireMock('react') as {
__mockEffects: Array<() => void | Promise<void>>;
__mockRender: (fn: () => any) => any;
__mockReset: () => void;
};
beforeEach(() => {
reactMock.__mockReset();
});
/** 模拟组件挂载:执行 useEffect 中注册的回调 */
async function flushEffects(): Promise<void> {
const fns = reactMock.__mockEffects.splice(0);
for (const fn of fns) {
await fn();
}
}
/** 模拟组件渲染(cursor 归零,读取最新状态) */
function render<T>(fn: () => T): T {
return reactMock.__mockRender(fn);
}
describe('useQuery', () => {
test('mount 时执行 SQL 查询并更新 data/loading', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: '1', name: 'Alice' }]) } as any;
const mount = () => useQuery(db, 'SELECT * FROM users');
const hook = render(mount);
expect(hook.loading).toBe(true);
expect(db.query).not.toHaveBeenCalled();
await flushEffects();
const after = render(mount); // 模拟重渲染读取最新状态
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(after.loading).toBe(false);
expect(after.data).toEqual([{ id: '1', name: 'Alice' }]);
expect(after.error).toBeNull();
});
test('查询失败时设置 error 且 data 保持空', async () => {
const db = { query: jest.fn().mockRejectedValue(new Error('query boom')) } as any;
const mount = () => useQuery(db, 'SELECT * FROM users');
render(mount);
await flushEffects();
const after = render(mount);
expect(after.error).toBeInstanceOf(Error);
expect((after.error as Error).message).toBe('query boom');
expect(after.data).toEqual([]);
expect(after.loading).toBe(false);
});
test('refresh 可重新执行查询', async () => {
let call = 0;
const db = { query: jest.fn().mockImplementation(async () => [{ n: ++call }]) } as any;
const mount = () => useQuery(db, 'SELECT * FROM users');
const hook = render(mount);
await flushEffects();
expect(render(mount).data).toEqual([{ n: 1 }]);
await hook.refresh();
await flushEffects();
expect(db.query).toHaveBeenCalledTimes(2);
expect(render(mount).data).toEqual([{ n: 2 }]);
});
test('不同 SQL 使用各自独立的 hook 状态', async () => {
const db = { query: jest.fn().mockResolvedValue([]) } as any;
const hook1 = useQuery(db, 'SELECT * FROM a');
const hook2 = useQuery(db, 'SELECT * FROM b');
await flushEffects();
expect(hook1).not.toBe(hook2);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM a');
expect(db.query).toHaveBeenCalledWith('SELECT * FROM b');
});
});
describe('useTable', () => {
test('合法表名执行全表查询', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: 1 }]) } as any;
const mount = () => useTable(db, 'users');
render(mount);
await flushEffects();
const after = render(mount);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(after.data).toEqual([{ id: 1 }]);
expect(after.loading).toBe(false);
});
test('非法表名抛出校验错误(SQL 注入防护)', () => {
const db = { query: jest.fn() } as any;
expect(() => useTable(db, 'users; DROP TABLE orders')).toThrow(/Invalid table name/);
expect(() => useTable(db, "users' OR '1'='1")).toThrow(/Invalid table name/);
expect(() => useTable(db, '1users')).toThrow(/Invalid table name/);
expect(db.query).not.toHaveBeenCalled();
});
});
describe('useDatabase', () => {
test('创建数据库实例并初始化', async () => {
const initSpy = jest.spyOn(MetonaSqlark.prototype, 'init').mockResolvedValue();
const closeSpy = jest.spyOn(MetonaSqlark.prototype, 'close').mockResolvedValue();
const mount = () => useDatabase({ name: 'hook-test' });
const hook = render(mount);
expect(hook.ready).toBe(false);
await flushEffects();
const after = render(mount);
expect(initSpy).toHaveBeenCalled();
expect(after.db).toBeInstanceOf(MetonaSqlark);
expect(after.ready).toBe(true);
expect(after.error).toBeNull();
initSpy.mockRestore();
closeSpy.mockRestore();
});
test('初始化失败时设置 error', async () => {
const initSpy = jest.spyOn(MetonaSqlark.prototype, 'init').mockRejectedValue(new Error('init fail'));
const mount = () => useDatabase({ name: 'hook-fail' });
render(mount);
await flushEffects();
const after = render(mount);
expect(after.db).toBeNull();
expect(after.ready).toBe(false);
expect(after.error).toBeInstanceOf(Error);
expect((after.error as Error).message).toBe('init fail');
initSpy.mockRestore();
});
});
+167
View File
@@ -0,0 +1,167 @@
/**
* Vue 集成 composables 测试(v0.2.6 补强)
* @module tests/integrations/vue
*
* 项目零运行时依赖(vue 为 peer dependency),
* 使用最小 Vue mock 验证 composables 的真实逻辑:
* - onMounted 时执行查询
* - watch sql/deps 变化重新执行
* - refresh 手动刷新
* - 表名校验(SQL 注入防护)
*/
import 'fake-indexeddb/auto';
// ---- 最小 Vue mock(自包含:jest.mock 工厂不能引用外部变量) ----
jest.mock('vue', () => {
const mountQueue: Array<() => void | Promise<void>> = [];
const watchList: Array<{ sources: any[]; cb: () => void | Promise<void> }> = [];
return {
ref: (init: any) => {
const box: { value: any } = { value: init };
return box;
},
watch: (sources: any[], cb: any) => {
watchList.push({ sources, cb });
},
onMounted: (fn: any) => {
mountQueue.push(fn);
},
__mockMounted: mountQueue,
__mockWatch: watchList,
__mockReset: () => {
mountQueue.length = 0;
watchList.length = 0;
},
};
}, { virtual: true });
import { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase } from '../../src/integrations/vue';
/** mock 模块内的挂载/监听队列(自包含作用域) */
const vueMock = jest.requireMock('vue') as {
__mockMounted: Array<() => void | Promise<void>>;
__mockWatch: Array<{ sources: any[]; cb: () => void | Promise<void> }>;
__mockReset: () => void;
};
beforeEach(() => {
vueMock.__mockReset();
});
/** 模拟组件挂载:执行 onMounted 注册的回调 */
async function flushMounted(): Promise<void> {
const fns = vueMock.__mockMounted.splice(0);
for (const fn of fns) {
await fn();
}
}
/** 触发 watch 回调 */
async function flushWatch(): Promise<void> {
const pairs = vueMock.__mockWatch.splice(0);
for (const { cb } of pairs) {
await cb();
}
}
/** 构造最小响应式引用(与 vue.mock 的 ref 等价) */
function makeRef<T>(init: T): { value: T } {
return { value: init };
}
describe('useSqlarkQuery', () => {
test('onMounted 时执行 SQL 查询并更新响应式 data', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: '1' }]) } as any;
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
expect(hook.loading.value).toBe(true);
expect(db.query).not.toHaveBeenCalled();
await flushMounted();
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(hook.loading.value).toBe(false);
expect(hook.data.value).toEqual([{ id: '1' }]);
expect(hook.error.value).toBeNull();
});
test('查询失败时设置 error', async () => {
const db = { query: jest.fn().mockRejectedValue(new Error('vue boom')) } as any;
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
await flushMounted();
expect(hook.error.value).toBeInstanceOf(Error);
expect((hook.error.value as Error).message).toBe('vue boom');
expect(hook.data.value).toEqual([]);
});
test('refresh 重新执行查询', async () => {
let call = 0;
const db = { query: jest.fn().mockImplementation(async () => [{ n: ++call }]) } as any;
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
await flushMounted();
expect(hook.data.value).toEqual([{ n: 1 }]);
await hook.refresh();
expect(db.query).toHaveBeenCalledTimes(2);
expect(hook.data.value).toEqual([{ n: 2 }]);
});
test('watch 注册在 sql 与 deps 上', async () => {
const db = { query: jest.fn().mockResolvedValue([]) } as any;
const sqlRef = makeRef('SELECT * FROM users');
useSqlarkQuery(db, sqlRef.value, [sqlRef]);
expect(vueMock.__mockWatch).toHaveLength(1);
expect(vueMock.__mockWatch[0].sources).toHaveLength(2); // [() => sql, ...deps]
// 模拟依赖变化触发 watch
sqlRef.value = 'SELECT * FROM orders';
await flushWatch();
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
});
});
describe('useSqlarkTable', () => {
test('合法表名执行全表查询', async () => {
const db = { query: jest.fn().mockResolvedValue([{ id: 1 }]) } as any;
const hook = useSqlarkTable(db, 'users');
await flushMounted();
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
expect(hook.data.value).toEqual([{ id: 1 }]);
});
test('非法表名抛出校验错误(SQL 注入防护)', () => {
const db = { query: jest.fn() } as any;
expect(() => useSqlarkTable(db, 'users; DELETE FROM orders')).toThrow(/Invalid table name/);
expect(() => useSqlarkTable(db, 'users--')).toThrow(/Invalid table name/);
expect(db.query).not.toHaveBeenCalled();
});
});
describe('useSqlarkDatabase', () => {
test('onMounted 创建并初始化数据库实例', async () => {
const hook = useSqlarkDatabase({ name: 'vue-hook' });
expect(hook.ready.value).toBe(false);
await flushMounted();
expect(hook.db.value).not.toBeNull();
expect(hook.ready.value).toBe(true);
expect(hook.error.value).toBeNull();
});
test('初始化失败时设置 error', async () => {
// 使用非法配置触发初始化错误(mode 未知)
const hook = useSqlarkDatabase({ name: 'vue-fail', mode: 'unknown-mode' as any });
await flushMounted();
expect(hook.db.value).toBeNull();
expect(hook.ready.value).toBe(false);
expect(hook.error.value).toBeInstanceOf(Error);
});
});