Files
MetonaSqlark/tests/engine/aria-matrix-audit.test.ts
T
thzxx 05e6823bf1
CI / test (22.x) (push) Successful in 24m26s
CI / e2e (push) Successful in 10m0s
CI / test (18.x) (push) Successful in 27m14s
CI / test (20.x) (push) Failing after 1h19m9s
CI / test (24.x) (push) Successful in 37m49s
fix: v0.7.2 语句级原子性 + 事务 DDL 拒绝 + 约束/绑定硬化 — 6 项修复 + 43 回归 + CI 重型套件串行
- UPDATE 语句级部分提交(P1,四引擎):两阶段全量预检后执行,批内唯一互查,
  任何一行失败整句不执行(aria 场景 WAL 与内存不再错位)
- 事务内 ALTER/CREATE INDEX/DROP INDEX 残留(P1):Memory/KVStore 显式拒绝
  (对齐 Aria),createTable/dropTable 保持可回滚
- SET NULL 级联绕过 required 约束(P1):预检阶段整体拒绝 FOREIGN_KEY_VIOLATION
- bindParameters 注释误判(P2):行注释/块注释中的 ? 与引号不再参与绑定
- 未闭合字符串静默接受 → lexer 抛 PARSE_ERROR;未知 where 操作符抛 QUERY_ERROR
- UPDATE undefined 覆盖列值 → 语义化为不更新(null 仍置空)
- Hybrid 写穿透非原子(P1):磁盘失败自动重载内存对齐磁盘再抛原错误
- CI:Run tests 拆常规并行 + 重型串行(runInBand),重型测试超时余量提升,
  性能护栏 kv 120→240s / opfs 150→300s(仍拦截悬崖回归)
- 测试 1155 → 1198(74 套件),覆盖率 89.82% 保持
2026-08-13 15:31:16 +08:00

318 lines
13 KiB
TypeScript
Raw 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.
/**
* v0.6.1 生产矩阵审计 — 模式 × 功能全组合验收
*
* 后端(opfs/kv/memory)× 特性(页面化/加密/压缩/WAL开关/同步模式)×
* 功能(CRUD/二级索引/事务/崩溃恢复/主键变更一致性)
*
* 目标:全部组合生产级可用。任一组合同等断言 —— 数据完整、索引一致、崩溃恢复不丢。
*/
import { AriaEngine } from '../../src/engine/aria/index';
import { createSchema } from '../../src/table/schema';
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
import { installOPFSMock } from '../helpers/opfs-mock';
let counter = 0;
function uniqueDB(): string {
return `mx-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
}
const SCHEMA = () => createSchema('big', {
id: { type: 'string', primaryKey: true },
val: { type: 'number' },
tag: { type: 'string', index: true },
grp: { type: 'number', index: true },
name: { type: 'string' },
});
/** 写入 total 行(tag/grp 双索引) */
async function seed(engine: AriaEngine, table: string, total: number): Promise<void> {
for (let batch = 0; batch < total / 1000; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 1000; i++) {
const idx = batch * 1000 + i;
rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, grp: idx % 5, name: `U${idx}` });
}
await engine.insert(table, rows);
}
}
/** 双索引完整性断言 */
async function assertIndexes(engine: AriaEngine, table: string, total: number): Promise<void> {
expect(await engine.count(table)).toBe(total);
for (let t = 0; t < 10; t++) {
expect(await engine.find(table, { table, where: { tag: `t${t}` } })).toHaveLength(total / 10);
}
for (let g = 0; g < 5; g++) {
expect(await engine.find(table, { table, where: { grp: g } })).toHaveLength(total / 5);
}
}
/** 崩溃恢复:关闭后端 → 新引擎重开同库 */
async function reopen(dbName: string, config: Record<string, unknown>): Promise<AriaEngine> {
const engine2 = new AriaEngine(config as never);
await engine2.open(dbName, 1);
return engine2;
}
beforeEach(() => {
SharedMemoryBackend.clearRegistry();
installOPFSMock(new Map());
});
describe('生产矩阵审计 — 后端 × 核心功能', () => {
const coreConfig = (backend: string): Record<string, unknown> => ({
storageBackend: backend,
memtableSizeThreshold: 256 * 1024,
checkpointInterval: 5000,
walSyncMode: 'full',
});
it('kv × 3 万行 + 双索引 + 事务 + 崩溃恢复', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine(coreConfig('kv') as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 30000);
// 事务:提交一批更新
await engine.beginTransaction();
for (let i = 0; i < 100; i++) {
await engine.update('big', { table: 'big', where: { id: `k${i}` } }, { name: `Tx${i}` });
}
await engine.commitTransaction();
await assertIndexes(engine, 'big', 30000);
const row = await engine.find('big', { table: 'big', where: { id: 'k50' } });
expect(row[0].name).toBe('Tx50');
// 事务回滚不产生残留
await engine.beginTransaction();
for (let i = 1000; i < 1100; i++) {
await engine.update('big', { table: 'big', where: { id: `k${i}` } }, { name: `Rx${i}` });
}
await engine.rollbackTransaction();
expect((await engine.find('big', { table: 'big', where: { id: 'k1050' } }))[0].name).toBe('U1050');
// 崩溃恢复
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = await reopen(dbName, coreConfig('kv'));
await assertIndexes(engine2, 'big', 30000);
expect((await engine2.find('big', { table: 'big', where: { id: 'k50' } }))[0].name).toBe('Tx50');
await engine2.close();
}, 600000);
it('opfs × 3 万行 + 双索引 + 崩溃恢复', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine(coreConfig('opfs') as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 30000);
await assertIndexes(engine, 'big', 30000);
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = await reopen(dbName, coreConfig('opfs'));
await assertIndexes(engine2, 'big', 30000);
await engine2.close();
}, 600000);
it('memory × 2 万行 + 双索引 + 事务回滚', async () => {
const engine = new AriaEngine(coreConfig('memory') as never);
await engine.open(uniqueDB(), 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await engine.beginTransaction();
await engine.update('big', { table: 'big', where: { id: 'k7' } }, { tag: 't9' });
await engine.rollbackTransaction();
await assertIndexes(engine, 'big', 20000);
// 更新后索引一致(无脏数据)——k7 从 t7 移到 t9
await engine.update('big', { table: 'big', where: { id: 'k7' } }, { tag: 't9' });
expect(await engine.find('big', { table: 'big', where: { tag: 't9' } })).toHaveLength(2001);
expect(await engine.find('big', { table: 'big', where: { tag: 't7' } })).toHaveLength(1999);
await engine.close();
}, 600000);
});
describe('生产矩阵审计 — 特性组合', () => {
it('kv × 加密 × 压缩 × 页面化(默认开)→ 2 万行 + 崩溃恢复', async () => {
const dbName = uniqueDB();
const cfg = {
storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 5000,
walSyncMode: 'full', compression: true, encryption: { password: 'matrix-secret' },
};
const engine = new AriaEngine(cfg as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await assertIndexes(engine, 'big', 20000);
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = await reopen(dbName, cfg);
await assertIndexes(engine2, 'big', 20000);
await engine2.close();
// 错误密码必须拒绝打开
const bad = new AriaEngine({ ...cfg, encryption: { password: 'wrong' } } as never);
await expect(bad.open(dbName, 1)).rejects.toThrow();
}, 600000);
it('kv × pageStorage:false(整 value)→ 2 万行 + 崩溃恢复', async () => {
const dbName = uniqueDB();
const cfg = {
storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 5000,
walSyncMode: 'full', pageStorage: false,
};
const engine = new AriaEngine(cfg as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await assertIndexes(engine, 'big', 20000);
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = await reopen(dbName, cfg);
await assertIndexes(engine2, 'big', 20000);
await engine2.close();
}, 600000);
it('opfs × 加密 × 压缩 × 页面化全开 → 2 万行 + 崩溃恢复', async () => {
const dbName = uniqueDB();
const cfg = {
storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 5000,
walSyncMode: 'full', compression: true, encryption: { password: 'matrix-secret' },
pageStorage: true,
};
const engine = new AriaEngine(cfg as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await assertIndexes(engine, 'big', 20000);
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = await reopen(dbName, cfg);
await assertIndexes(engine2, 'big', 20000);
await engine2.close();
}, 600000);
it('opfs × walEnabled:false → 写入 + 优雅关闭后重开完整', async () => {
const dbName = uniqueDB();
const cfg = {
storageBackend: 'opfs', memtableSizeThreshold: 128 * 1024, checkpointInterval: 2000,
walEnabled: false, pageStorage: false,
};
const engine = new AriaEngine(cfg as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await assertIndexes(engine, 'big', 20000);
// 优雅关闭:close 等待后台链 + checkpoint 落盘 → 重开完整
await engine.close();
const engine2 = await reopen(dbName, cfg);
await assertIndexes(engine2, 'big', 20000);
await engine2.close();
}, 600000);
it('kv × walEnabled:false → 写入 + 优雅关闭后重开完整', async () => {
const dbName = uniqueDB();
const cfg = {
storageBackend: 'kv', memtableSizeThreshold: 128 * 1024, checkpointInterval: 2000,
walEnabled: false, pageStorage: false,
};
const engine = new AriaEngine(cfg as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await assertIndexes(engine, 'big', 20000);
await engine.close();
const engine2 = await reopen(dbName, cfg);
await assertIndexes(engine2, 'big', 20000);
await engine2.close();
}, 600000);
it('kv × walSyncMode:batch → 优雅关闭后重开完整(崩溃保底已 checkpoint 数据)', async () => {
const dbName = uniqueDB();
const cfg = {
storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 3000,
walSyncMode: 'batch',
};
const engine = new AriaEngine(cfg as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await assertIndexes(engine, 'big', 20000);
await engine.close();
const engine2 = await reopen(dbName, cfg);
await assertIndexes(engine2, 'big', 20000);
await engine2.close();
}, 600000);
it('opfs × walSyncMode:none → 优雅关闭后重开完整(checkpoint 兜底)', async () => {
const dbName = uniqueDB();
const cfg = {
storageBackend: 'opfs', memtableSizeThreshold: 128 * 1024, checkpointInterval: 3000,
walSyncMode: 'none', pageStorage: false,
};
const engine = new AriaEngine(cfg as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await seed(engine, 'big', 20000);
await assertIndexes(engine, 'big', 20000);
await engine.close();
const engine2 = await reopen(dbName, cfg);
await assertIndexes(engine2, 'big', 20000);
await engine2.close();
}, 600000);
});
describe('生产矩阵审计 — 主键变更索引一致性(三后端)', () => {
it('kv × 主键变更:新旧索引无脏数据', async () => {
const engine = new AriaEngine({ storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 100000 } as never);
await engine.open(uniqueDB(), 1);
await engine.createTable(SCHEMA());
await engine.insert('big', [{ id: 'a1', val: 1, tag: 'x', grp: 1, name: 'A' }]);
// 主键 a1 → b1tag/grp 不变)
await engine.update('big', { table: 'big', where: { id: 'a1' } }, { id: 'b1' });
expect(await engine.count('big')).toBe(1);
expect(await engine.find('big', { table: 'big', where: { tag: 'x' } })).toHaveLength(1);
expect(await engine.find('big', { table: 'big', where: { id: 'a1' } })).toHaveLength(0);
await engine.close();
}, 300000);
it('opfs × 主键变更 + 崩溃恢复:索引一致', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 100000, pageStorage: false } as never);
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
for (let i = 0; i < 5000; i++) {
await engine.insert('big', [{ id: `k${i}`, val: i, tag: `t${i % 10}`, grp: i % 5, name: `U${i}` }]);
}
for (let i = 0; i < 5000; i += 10) {
await engine.update('big', { table: 'big', where: { id: `k${i}` } }, { id: `p${i}` });
}
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 100000, pageStorage: false } as never);
await engine2.open(dbName, 1);
expect(await engine2.count('big')).toBe(5000);
for (let t = 0; t < 10; t++) {
expect(await engine2.find('big', { table: 'big', where: { tag: `t${t}` } })).toHaveLength(500);
}
await engine2.close();
}, 600000);
it('memory × 级联删除 + 索引清理', async () => {
const engine = new AriaEngine({ storageBackend: 'memory', memtableSizeThreshold: 256 * 1024 } as never);
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('parent', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
}));
await engine.createTable(createSchema('child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', references: 'parent.id', onDelete: 'CASCADE', index: true },
}));
for (let i = 0; i < 2000; i++) {
await engine.insert('parent', [{ id: `p${i}`, tag: `t${i % 10}` }]);
await engine.insert('child', [{ id: `c${i}`, pid: `p${i}` }]);
}
await engine.delete('parent', { table: 'parent', where: { id: 'p5' } });
expect(await engine.count('child')).toBe(1999);
expect(await engine.find('child', { table: 'child', where: { pid: 'p5' } })).toHaveLength(0);
await engine.close();
}, 300000);
});