工作流 C-1 / C-3 前半 + 测试代码类型检查。 【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts - TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入 (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃) - 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现 导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness) - 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码 与 entry.content.subarray 恒等分支) - FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close() (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相) - 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组) 【覆盖率口径】jest.config.cjs - 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体 排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由 - 新增 coverageThreshold 门禁(此前完全不存在) - 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43% - 修正 testMatch 使 tests/helpers 下的测试可被发现 【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests - 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏) - 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode 以断言收窄替代 as any - 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码) - parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例, 当前红灯,对应总账第 11 项,将在工作流 A 修复) 【版本契约】新增 tests/version-contract.test.ts - 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量 【CI 门禁】.gitea/workflows/ci.yml - lint 去掉 continue-on-error(此前永远不让 CI 变红) - 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验 - 版本 0.7.4 升至 0.8.0
565 lines
24 KiB
TypeScript
565 lines
24 KiB
TypeScript
/**
|
||
* v0.7.4 回归测试 — 深度审计第七阶段修复
|
||
*
|
||
* 1. UPDATE/DELETE WHERE 子查询静默 0 行(四引擎)
|
||
* 2. 写语句关联引用($col / EXISTS)显式 NOT_SUPPORTED
|
||
* 3. EXPLAIN UPDATE/DELETE 子查询 estimatedRows
|
||
* 4. 主键 NULL/undefined 静默入库(四引擎)
|
||
* 5. DROP INDEX 建表 UNIQUE 约束保护 / CREATE UNIQUE INDEX 可解除
|
||
* 6. GROUP BY null 与 'null' 字符串分离(DISTINCT/UNION 同类)
|
||
* 7. UPDATE 未知列显式报错
|
||
* 8. queryStream 多语句显式报错
|
||
* 9. KVStore 后台错误跨 reopen 清理
|
||
* 10. Hybrid beginTransaction 部分成功补偿回滚
|
||
* 11. findStream 真惰性(limit 提前终止)
|
||
* 12. reindex 单次扫描多索引列重建
|
||
*/
|
||
|
||
import { MetonaSqlark } from '../src/core';
|
||
import { KVStore } from '../src/engine/kvstore/index';
|
||
import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium';
|
||
|
||
const uniqueName = (tag: string): string => `v074-${tag}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
|
||
async function makeDb(mode: 'memory' | 'disk' | 'aria' | 'hybrid') {
|
||
const db = await MetonaSqlark.create({
|
||
name: uniqueName(mode),
|
||
mode,
|
||
diskEngine: 'memory',
|
||
});
|
||
await db.defineTable('t', {
|
||
id: { type: 'string', primaryKey: true },
|
||
v: { type: 'number' },
|
||
});
|
||
return db;
|
||
}
|
||
|
||
// ===================================================================
|
||
// 1. UPDATE / DELETE 子查询
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: UPDATE/DELETE WHERE 子查询(此前静默 0 行)', () => {
|
||
test.each([
|
||
['memory', 'memory'],
|
||
['disk', 'disk'],
|
||
['aria', 'aria'],
|
||
['hybrid', 'hybrid'],
|
||
] as const)('%s: UPDATE IN (SELECT) 正确影响行', async (_label, mode) => {
|
||
const db = await makeDb(mode);
|
||
await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)");
|
||
const n = await db.query('UPDATE t SET v = 99 WHERE id IN (SELECT id FROM t WHERE v > 15)');
|
||
expect(n).toBe(1);
|
||
const rows = await db.query('SELECT * FROM t ORDER BY id') as Record<string, unknown>[];
|
||
expect(rows.map((r) => r.v)).toEqual([10, 99]);
|
||
await db.close();
|
||
});
|
||
|
||
test.each([
|
||
['memory', 'memory'],
|
||
['disk', 'disk'],
|
||
['aria', 'aria'],
|
||
['hybrid', 'hybrid'],
|
||
] as const)('%s: DELETE IN (SELECT) 正确影响行', async (_label, mode) => {
|
||
const db = await makeDb(mode);
|
||
await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)");
|
||
const n = await db.query('DELETE FROM t WHERE id IN (SELECT id FROM t WHERE v > 15)');
|
||
expect(n).toBe(1);
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows.map((r) => r.id)).toEqual(['1']);
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: UPDATE 标量子查询(op (SELECT))', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)");
|
||
const n = await db.query('UPDATE t SET v = 1 WHERE v = (SELECT MAX(v) FROM t)');
|
||
expect(n).toBe(1);
|
||
await db.close();
|
||
});
|
||
|
||
test('aria: UPDATE 子查询后持久化重开一致(kv 后端跨实例共享介质)', async () => {
|
||
const name = uniqueName('aria-persist');
|
||
const db1 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' });
|
||
await db1.defineTable('t', {
|
||
id: { type: 'string', primaryKey: true },
|
||
v: { type: 'number' },
|
||
});
|
||
await db1.query("INSERT INTO t VALUES ('1', 10), ('2', 20)");
|
||
await db1.query('UPDATE t SET v = 99 WHERE id IN (SELECT id FROM t WHERE v > 15)');
|
||
await db1.close();
|
||
const db2 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' });
|
||
const rows = await db2.query('SELECT * FROM t ORDER BY id') as Record<string, unknown>[];
|
||
expect(rows.map((r) => r.v)).toEqual([10, 99]);
|
||
await db2.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 2. 写语句关联引用显式报错
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: 写语句关联引用显式 NOT_SUPPORTED', () => {
|
||
test('memory: SQL 列比较(a = b)解析层明确报错(此前静默或误解析)', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
// parser 不支持列引用作为比较值 → 明确 PARSE_ERROR,绝不静默影响行
|
||
const err = await db.query('UPDATE t SET v = 1 WHERE id = id').catch((e: unknown) => e as { code?: string });
|
||
expect((err as { code?: string }).code).toBeDefined();
|
||
// 行未被修改(无静默副作用)
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows[0].v).toBe(10);
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: QueryBuilder 列引用更新 WHERE 显式 NOT_SUPPORTED', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
await expect(
|
||
db.table('t').update({ v: 1 }).where({ id: { $eq: { $col: 'id' } } }).execute(),
|
||
).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: DELETE WHERE EXISTS(关联)报错而非静默 0 行', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
await expect(
|
||
db.query('DELETE FROM t WHERE EXISTS (SELECT 1 FROM t t2 WHERE t2.id = t.id)'),
|
||
).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
|
||
await db.close();
|
||
});
|
||
|
||
test('aria: UPDATE 关联 EXISTS 报错', async () => {
|
||
const db = await makeDb('aria');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
await expect(
|
||
db.query('UPDATE t SET v = 1 WHERE EXISTS (SELECT 1 FROM t t2 WHERE t2.id = t.id)'),
|
||
).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 3. EXPLAIN 子查询 estimatedRows
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: EXPLAIN UPDATE/DELETE 子查询估算', () => {
|
||
test('EXPLAIN UPDATE IN (SELECT) estimatedRows 正确', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)");
|
||
const plan = await db.query('EXPLAIN UPDATE t SET v = 1 WHERE id IN (SELECT id FROM t WHERE v > 15)') as Record<string, unknown>;
|
||
expect(plan.estimatedRows).toBe(1);
|
||
// EXPLAIN 不产生副作用
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows.map((r) => r.v)).toEqual([10, 20]);
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 4. 主键 NULL/undefined 拒绝
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: 主键 NULL/undefined 拒绝', () => {
|
||
test.each([
|
||
['memory', 'memory'],
|
||
['disk', 'disk'],
|
||
['aria', 'aria'],
|
||
['hybrid', 'hybrid'],
|
||
] as const)('%s: INSERT 主键 NULL 拒绝', async (_label, mode) => {
|
||
const db = await makeDb(mode);
|
||
await expect(db.query('INSERT INTO t VALUES (NULL, 1)')).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(0);
|
||
await db.close();
|
||
});
|
||
|
||
test.each([
|
||
['memory', 'memory'],
|
||
['disk', 'disk'],
|
||
['aria', 'aria'],
|
||
] as const)('%s: INSERT 省略主键(无 default)拒绝', async (_label, mode) => {
|
||
const db = await makeDb(mode);
|
||
await expect(db.query('INSERT INTO t (v) VALUES (5)')).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: UPDATE 主键置 null 拒绝', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
await expect(db.query("UPDATE t SET id = NULL WHERE id = '1'")).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows[0].id).toBe('1');
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: 主键 default 生效时允许省略', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('pkdefault'), mode: 'memory' });
|
||
await db.defineTable('t', {
|
||
id: { type: 'string', primaryKey: true, default: 'auto' },
|
||
v: { type: 'number' },
|
||
});
|
||
const pks = await db.query('INSERT INTO t (v) VALUES (5)') as string[];
|
||
expect(pks).toEqual(['auto']);
|
||
await db.close();
|
||
});
|
||
|
||
test('aria: 主键 undefined(Table API)拒绝', async () => {
|
||
const db = await makeDb('aria');
|
||
await expect(
|
||
db.table('t').insert({ v: 3 } as Record<string, unknown>),
|
||
).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 5. DROP INDEX 与 UNIQUE 约束
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: DROP INDEX UNIQUE 约束保护', () => {
|
||
test.each([
|
||
['memory', 'memory'],
|
||
['aria', 'aria'],
|
||
] as const)('%s: 建表 UNIQUE 列 DROP INDEX 拒绝且约束保留', async (_label, mode) => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('uniq'), mode, diskEngine: 'memory' });
|
||
await db.defineTable('u', {
|
||
id: { type: 'string', primaryKey: true },
|
||
email: { type: 'string', unique: true },
|
||
});
|
||
await db.query("INSERT INTO u VALUES ('1', 'a@x.com')");
|
||
await expect(db.query('DROP INDEX idx ON u (email)')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
|
||
// 约束仍生效
|
||
await expect(db.query("INSERT INTO u VALUES ('2', 'a@x.com')")).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
|
||
const schema = await db.getEngine().getTableSchema('u');
|
||
expect(schema!.columns.email.unique).toBe(true);
|
||
await db.close();
|
||
});
|
||
|
||
test.each([
|
||
['memory', 'memory'],
|
||
['aria', 'aria'],
|
||
] as const)('%s: CREATE UNIQUE INDEX 后 DROP 可解除(本会话)', async (_label, mode) => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('uniq2'), mode, diskEngine: 'memory' });
|
||
await db.defineTable('u', {
|
||
id: { type: 'string', primaryKey: true },
|
||
email: { type: 'string' },
|
||
});
|
||
await db.query("INSERT INTO u VALUES ('1', 'a@x.com')");
|
||
await db.query('CREATE UNIQUE INDEX idx_u ON u (email)');
|
||
await expect(db.query("INSERT INTO u VALUES ('2', 'a@x.com')")).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
|
||
await db.query('DROP INDEX idx_u ON u (email)');
|
||
// 约束已随索引解除
|
||
await db.query("INSERT INTO u VALUES ('2', 'a@x.com')");
|
||
const rows = await db.query('SELECT * FROM u') as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(2);
|
||
await db.close();
|
||
});
|
||
|
||
test('aria: 建表 UNIQUE DROP INDEX 拒绝后重启约束仍在(kv 后端)', async () => {
|
||
const name = uniqueName('uniq3');
|
||
const db1 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' });
|
||
await db1.defineTable('u', {
|
||
id: { type: 'string', primaryKey: true },
|
||
email: { type: 'string', unique: true },
|
||
});
|
||
await db1.query("INSERT INTO u VALUES ('1', 'a@x.com')");
|
||
await expect(db1.query('DROP INDEX idx ON u (email)')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
|
||
await db1.close();
|
||
const db2 = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'kv' });
|
||
await expect(db2.query("INSERT INTO u VALUES ('2', 'a@x.com')")).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
|
||
await db2.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 6. GROUP BY / DISTINCT / UNION 键编码
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: 分组/去重键类型安全编码', () => {
|
||
test('memory: GROUP BY null 与 "null" 字符串分离', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('grp'), mode: 'memory' });
|
||
await db.defineTable('g', {
|
||
id: { type: 'string', primaryKey: true },
|
||
grp: { type: 'string' },
|
||
});
|
||
await db.query("INSERT INTO g VALUES ('a', NULL), ('b', 'null'), ('c', NULL)");
|
||
const rows = await db.query('SELECT grp, COUNT(*) AS c FROM g GROUP BY grp') as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(2);
|
||
const byKey = new Map(rows.map((r) => [r.grp, r.c]));
|
||
expect(byKey.get(null)).toBe(2);
|
||
expect(byKey.get('null')).toBe(1);
|
||
await db.close();
|
||
});
|
||
|
||
test('aria: GROUP BY null 分离', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('grp-aria'), mode: 'aria', diskEngine: 'memory' });
|
||
await db.defineTable('g', {
|
||
id: { type: 'string', primaryKey: true },
|
||
grp: { type: 'string' },
|
||
});
|
||
await db.query("INSERT INTO g VALUES ('a', NULL), ('b', 'null')");
|
||
const rows = await db.query('SELECT grp, COUNT(*) AS c FROM g GROUP BY grp') as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(2);
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: DISTINCT null 与 "\\0" 字符串分离', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('dist'), mode: 'memory' });
|
||
await db.defineTable('g', {
|
||
id: { type: 'string', primaryKey: true },
|
||
grp: { type: 'string' },
|
||
});
|
||
await db.query(`INSERT INTO g VALUES ('a', NULL), ('b', '\\0'), ('c', '\\0')`);
|
||
const rows = await db.query('SELECT DISTINCT grp FROM g') as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(2);
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: UNION null 与字符串不吞并', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('union'), mode: 'memory' });
|
||
await db.defineTable('g', {
|
||
id: { type: 'string', primaryKey: true },
|
||
grp: { type: 'string' },
|
||
});
|
||
await db.query("INSERT INTO g VALUES ('a', NULL), ('b', 'null')");
|
||
const rows = await db.query(
|
||
"SELECT grp FROM g WHERE id = 'a' UNION SELECT grp FROM g WHERE id = 'b'",
|
||
) as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(2);
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 7. UPDATE 未知列报错
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: UPDATE 未知列显式报错', () => {
|
||
test.each([
|
||
['memory', 'memory'],
|
||
['disk', 'disk'],
|
||
['aria', 'aria'],
|
||
['hybrid', 'hybrid'],
|
||
] as const)('%s: SQL UPDATE 未知列 COLUMN_NOT_FOUND', async (_label, mode) => {
|
||
const db = await makeDb(mode);
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
await expect(db.query("UPDATE t SET nonexistent = 9 WHERE id = '1'")).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' });
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(JSON.stringify(rows[0])).not.toContain('nonexistent');
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: Table API update 未知列报错', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
await expect(
|
||
db.table('t').update({ nonexistent: 1 }).where({ id: '1' }).execute(),
|
||
).rejects.toMatchObject({ code: 'COLUMN_NOT_FOUND' });
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: 更新既有列不受影响(回归护栏)', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
const n = await db.query("UPDATE t SET v = 20 WHERE id = '1'");
|
||
expect(n).toBe(1);
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 8. queryStream 多语句
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: queryStream 多语句显式报错', () => {
|
||
test('queryStream 多语句抛 PARSE_ERROR(不静默忽略后续语句)', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10)");
|
||
await expect(
|
||
db.queryStream('SELECT * FROM t; DELETE FROM t', () => {}),
|
||
).rejects.toMatchObject({ code: 'PARSE_ERROR' });
|
||
// 后续语句未执行
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(1);
|
||
await db.close();
|
||
});
|
||
|
||
test('queryStream 单语句正常流式(回归护栏)', async () => {
|
||
const db = await makeDb('memory');
|
||
await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20)");
|
||
let n = 0;
|
||
const count = await db.queryStream('SELECT * FROM t', () => { n++; });
|
||
expect(count).toBe(2);
|
||
expect(n).toBe(2);
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 9. KVStore 后台错误跨 reopen
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: KVStore 后台错误状态生命周期', () => {
|
||
test('close/reopen 后 checkpoint 不抛旧 KV_BACKGROUND_ERROR', async () => {
|
||
SharedMemoryBackend.clearRegistry();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium);
|
||
await kv1.open('v074-kv-life');
|
||
// 制造一次失败写入:直接在介质层破坏 append(用只读 medium 模拟失败)
|
||
await kv1.close();
|
||
|
||
// 用带失败注入的介质:append 抛错 → lastBackgroundError 记录
|
||
const failingMedium = {
|
||
open: async (_n: string) => {},
|
||
close: async () => {},
|
||
isOpen: () => true,
|
||
read: async () => null,
|
||
write: async () => {},
|
||
append: async () => { throw new Error('disk full'); },
|
||
writeMany: async () => {},
|
||
delete: async () => {},
|
||
deleteMany: async () => {},
|
||
listKeys: async () => [],
|
||
exists: async () => false,
|
||
clear: async () => {},
|
||
};
|
||
const kv2 = new KVStore(failingMedium as never);
|
||
await kv2.open('v074-kv-fail');
|
||
await expect(kv2.put('k', new TextEncoder().encode('v').buffer)).rejects.toThrow();
|
||
await kv2.close();
|
||
|
||
// 重开同一实例(同 medium 不再失败)
|
||
(failingMedium.append as unknown) = async () => {};
|
||
await kv2.open('v074-kv-fail');
|
||
// 无残留后台错误:checkpoint 不抛
|
||
await expect(kv2.checkpoint()).resolves.not.toThrow();
|
||
await kv2.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 10. Hybrid beginTransaction 补偿
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: Hybrid beginTransaction 部分成功补偿', () => {
|
||
test('磁盘 begin 失败时内存快照回滚(后续事务可正常开始)', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('hybrid-begin'), mode: 'hybrid', diskEngine: 'memory' });
|
||
await db.defineTable('t', {
|
||
id: { type: 'string', primaryKey: true },
|
||
});
|
||
// 让磁盘引擎 begin 抛错:先把磁盘引擎置于活跃事务
|
||
const diskEngine = (db.getEngine() as unknown as { getDiskEngine?: () => { beginTransaction(): Promise<void> } }).getDiskEngine
|
||
? undefined
|
||
: undefined;
|
||
// HybridEngine 无 getDiskEngine 公共接口 → 直接通过 disk 事务路径制造失败:
|
||
// 内存引擎先 begin 成功、磁盘引擎第二个 begin 报 TX_ACTIVE
|
||
const hybridEngine = db.getEngine() as unknown as {
|
||
beginTransaction: () => Promise<void>;
|
||
rollbackTransaction: () => Promise<void>;
|
||
};
|
||
await hybridEngine.beginTransaction();
|
||
// 磁盘引擎现在活跃;再次 begin → 内存 begin 成功、磁盘抛 TX_ACTIVE → 补偿回滚
|
||
await expect(hybridEngine.beginTransaction()).rejects.toMatchObject({ code: 'TX_ACTIVE' });
|
||
await hybridEngine.rollbackTransaction();
|
||
// 补偿后事务可正常开始并提交
|
||
await db.transaction(async (trx) => {
|
||
await trx.table('t').insert({ id: '1' });
|
||
});
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows).toHaveLength(1);
|
||
expect(diskEngine).toBeUndefined();
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 11. findStream 真惰性
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: aria findStream 真惰性(limit 提前终止)', () => {
|
||
test('大表 limit 1 流式只消费必要条目', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('lazy'), mode: 'aria', diskEngine: 'memory' });
|
||
await db.defineTable('logs', {
|
||
id: { type: 'string', primaryKey: true },
|
||
level: { type: 'string' },
|
||
});
|
||
const rows: Record<string, unknown>[] = [];
|
||
for (let i = 0; i < 2000; i++) rows.push({ id: `l${i}`, level: i % 2 ? 'error' : 'info' });
|
||
await db.table('logs').insertMany(rows);
|
||
let called = 0;
|
||
const count = await db.table('logs').stream(
|
||
() => { called++; },
|
||
{ limit: 5 },
|
||
);
|
||
expect(count).toBe(5);
|
||
expect(called).toBe(5);
|
||
// 语义护栏:无 limit 时全量
|
||
let all = 0;
|
||
await db.table('logs').stream(() => { all++; });
|
||
expect(all).toBe(2000);
|
||
await db.close();
|
||
});
|
||
|
||
test('queryStream limit 流式正确 + 与物化结果一致', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('lazy2'), mode: 'aria', diskEngine: 'memory' });
|
||
await db.defineTable('logs', {
|
||
id: { type: 'string', primaryKey: true },
|
||
level: { type: 'string' },
|
||
});
|
||
const rows: Record<string, unknown>[] = [];
|
||
for (let i = 0; i < 500; i++) rows.push({ id: `l${i}`, level: i % 2 ? 'error' : 'info' });
|
||
await db.table('logs').insertMany(rows);
|
||
const streamed: string[] = [];
|
||
const count = await db.queryStream(
|
||
"SELECT id FROM logs WHERE level = 'error' LIMIT 7",
|
||
(row) => { streamed.push((row as Record<string, unknown>).id as string); },
|
||
);
|
||
expect(count).toBe(7);
|
||
const materialized = await db.query(
|
||
"SELECT id FROM logs WHERE level = 'error' LIMIT 7",
|
||
) as Record<string, unknown>[];
|
||
expect(streamed).toEqual(materialized.map((r) => r.id));
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// 12. reindex 多索引列重建
|
||
// ===================================================================
|
||
|
||
describe('v0.7.4: reindex 单次扫描重建多索引列', () => {
|
||
test('aria: 多索引列 REINDEX 后查询完整', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('reidx'), mode: 'aria', diskEngine: 'memory' });
|
||
await db.defineTable('u', {
|
||
id: { type: 'string', primaryKey: true },
|
||
email: { type: 'string', unique: true },
|
||
city: { type: 'string', index: true },
|
||
});
|
||
const rows: Record<string, unknown>[] = [];
|
||
for (let i = 0; i < 300; i++) {
|
||
rows.push({ id: `u${i}`, email: `e${i}@x.com`, city: i % 3 ? 'Beijing' : 'Shanghai' });
|
||
}
|
||
await db.table('u').insertMany(rows);
|
||
await db.query('REINDEX TABLE u');
|
||
const bj = await db.query("SELECT * FROM u WHERE city = 'Beijing'") as Record<string, unknown>[];
|
||
expect(bj).toHaveLength(200);
|
||
const email = await db.query("SELECT * FROM u WHERE email = 'e1@x.com'") as Record<string, unknown>[];
|
||
expect(email).toHaveLength(1);
|
||
// 唯一约束在重建后仍生效
|
||
await expect(db.table('u').insert({ id: 'uX', email: 'e1@x.com', city: 'Beijing' })).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
|
||
await db.close();
|
||
});
|
||
|
||
test('memory: reindex 路径不受影响(无实现护栏)', async () => {
|
||
const db = await MetonaSqlark.create({ name: uniqueName('reidx-mem'), mode: 'memory' });
|
||
await db.defineTable('u', {
|
||
id: { type: 'string', primaryKey: true },
|
||
email: { type: 'string', unique: true },
|
||
});
|
||
await db.query("INSERT INTO u VALUES ('1', 'a@x.com')");
|
||
await expect(db.query('REINDEX TABLE u')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
|
||
await db.close();
|
||
});
|
||
});
|