Files
MetonaSqlark/tests/engine/aria.test.ts
T
thzxx c5694b1d23 feat(B-6): 存储层单一提交点(__aria_manifest)+ LSM 结构根治
按 PLAN-v0.7.5.md §B-6 的**完整规格**实施(此前只落地了"降级选项"里的五处止血):
B-6 要求的是 `__aria_manifest` 单一提交点 + LSM 单项改造。完整记录见方案附录 H。

一、单一提交点
  - 新增 `src/engine/aria/store/manifest.ts`:`__aria_manifest_<generation>`
    (magic + formatVersion + generation + 头部 CRC + 载荷 CRC;先写后验;保留两代)。
    载荷 = 页面水位 + 各命名空间 SSTable 元数据 + 表结构 + WAL 起始位置 + 待落盘冻结表意图。
  - 顺序固定:**数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件 / 删除旧 SSTable**。
  - 恢复只认最后一份 CRC 通过的世代;全部世代无效 → `ARIA_MANIFEST_CORRUPT`
    (修复前:裸 JSON meta 解析失败 → `[]` → 静默空库,随后 repair 还会删光活页)。
  - 旧格式(__aria_lsm_meta/__aria_schemas/__aria_meta)首次打开自动迁移,旧键保留;
    迁移遇到损坏 → `ARIA_LEGACY_META_CORRUPT`。
  - 陈旧实例保护(STALE_INSTANCE):认领时一次跨过 MANIFEST_TAKEOVER_STRIDE 个世代,
    杜绝"旧实例在途提交落在同一世代号上"(实测第二个实例 open 直接失败)。

二、LSM
  - 44 冻结表成为一等状态:失败保留 + 可重试(修复前失败即永久失去落盘机会)。
  - 45 `flush()` 先入链再报告后台错误(修复前一次后台失败会让之后每次 flush 直接抛错、
       数据永远等不到落盘);被重试修复的失败进 `getBackgroundWarnings()`(可见但不误报失败)。
  - 47 `MergeIterator` 胜出来源的补充推迟到下一次 `next()`:提前终止不再多算一条。
  - 49 `compacting` 由单 boolean 改为按层集合(跨层触发不再被静默丢弃)。
  - 50 compaction 不再"先 splice 整层再合并"(窗口内该层对读者可见);
       被取代的 SSTable 进"退休表" + 读者 epoch,等更早读者退出才物理删除。
  - 51 底部层原地合并回收墓碑(删除密集场景空间不再无界增长);"整层只剩墓碑" 有专门分支
       (修复前会读 `merged[0][0]` 抛 TypeError,compaction 永久失败)。
  - 55 flush 与 compaction 拆成两条链,checkpoint 只落 memtable;删除引擎层全部
       `prefetch*`/`drainChain` 依赖,改为"快照 + 结构版本乐观重试"
       (版本号同时覆盖 levels 与前台 memtable/frozen 的变化)。
  - 读路径自洽:介质读故障抛 `ARIA_SSTABLE_READ_FAILED`,不再折叠成"文件不存在"误删元数据。

三、WAL
  - LSN 全库单调(manifest 记高水位);按水位删除旧分片(`planKeepFrom` → 提交 → 再删除)。
  - **分片号只增不减**:修复前全量截断后重置为 0,会与 manifest 记录的 startSegment 错位,
    实测造成两个方向的损坏(删掉的行复活 / 已确认写入丢失,见随机压力套件)。
  - 分片空洞(含前缀缺失)显式报 `ARIA_WAL_GAP`,不再静默丢弃尾部。

四、其它
  - `sstable.ts` 三份解析循环合并为 `iterEntries()`,越界策略统一。
  - `vacuum()` 返回真实压缩层数(修复前硬编码 6 且底部层永不压缩)。
  - `close()` 加 try/finally(落盘失败也必须释放后端/锁并复位状态)。
  - `getRecoveryReport()`:{droppedSSTables, dataLossSuspected, walGaps, legacyImported,
    manifestFallback} —— "自愈了什么、有没有真丢数据"成为可读返回值。

五、验证
  - 新增 `tests/v080-b6-single-commit-point.test.ts`(63 项,含 manifest 严格校验表驱动 25 例)。
  - 新增 `scripts/mutation-b6.py`:22 项变异验证(把每个修复回退到修复前行为,对应用例必须失败),
    全部被拦住 —— 这批用例不是陪跑。
  - 常规套件 1935 通过 / 91 套件;覆盖率 90.34 / 82.16 / 94.06 / 93.23(阈值 90/82/94/93);
    e2e 14/14;重型套件 4 套件 27 项全绿。
2026-09-15 10:29:03 +08:00

937 lines
30 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.
/**
* AriaEngine 完整测试套件 (v0.2.0)
*
* 覆盖:
* 生命周期 · 表管理 · CRUD · 事务 · 持久化 · 查询 · 边界
*/
import { AriaEngine } from '../../src/engine/aria/index';
import { createSchema } from '../../src/table/schema';
import { MetonaSqlark } from '../../src/core';
import { resetOPFSMock, readManifestState } from '../helpers/storage-harness';
beforeEach(() => { resetOPFSMock(); });
// ===================================================================
// AriaEngine 引擎级测试 (Memory Backend)
// ===================================================================
describe('AriaEngine — Memory Backend', () => {
let engine: AriaEngine;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
email: { type: 'string', unique: true },
active: { type: 'boolean', default: true },
});
beforeEach(async () => {
engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open('test-aria', 1);
});
afterEach(async () => {
await engine.close();
});
// ---- 生命周期 ----
describe('生命周期', () => {
it('打开后 isOpen 返回 true', () => {
expect(engine.isOpen()).toBe(true);
});
it('关闭后 isOpen 返回 false', async () => {
await engine.close();
expect(engine.isOpen()).toBe(false);
});
it('重复 open 不报错(幂等)', async () => {
await engine.open('test-aria', 1);
expect(engine.isOpen()).toBe(true);
});
it('未打开时操作抛出错误', () => {
const e = new AriaEngine({ storageBackend: 'memory' });
return expect(e.createTable(userSchema)).rejects.toThrow('not opened');
});
});
// ---- 表管理 ----
describe('表管理', () => {
it('创建表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
});
it('重复创建表抛出错误', async () => {
await engine.createTable(userSchema);
await expect(engine.createTable(userSchema)).rejects.toThrow('already exists');
});
it('获取所有表名', async () => {
await engine.createTable(userSchema);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('获取表结构', async () => {
await engine.createTable(userSchema);
const schema = await engine.getTableSchema('users');
expect(schema).not.toBeNull();
expect(schema!.name).toBe('users');
expect(schema!.columns.id.primaryKey).toBe(true);
});
it('获取不存在表的 schema 返回 null', async () => {
expect(await engine.getTableSchema('nonexistent')).toBeNull();
});
it('删除表', async () => {
await engine.createTable(userSchema);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
});
it('hasTable 返回 false', async () => {
expect(await engine.hasTable('nope')).toBe(false);
});
});
// ---- 插入 ----
describe('插入', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
});
it('插入单行返回主键', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
]);
expect(pks).toEqual(['1']);
});
it('插入多行', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', email: 'a@t.com' },
{ id: '2', name: 'Bob', email: 'b@t.com' },
]);
expect(pks).toEqual(['1', '2']);
});
it('重复主键抛出错误', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'a@t.com' }]);
await expect(
engine.insert('users', [{ id: '1', name: 'Dup', email: 'd@t.com' }]),
).rejects.toThrow('Duplicate');
});
it('必填字段缺失抛出错误', async () => {
await expect(
engine.insert('users', [{ id: '1' }]),
).rejects.toThrow('required');
});
it('默认值填充', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'a@t.com' }]);
const rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(0);
expect(rows[0].active).toBe(true);
});
it('类型错误抛出异常', async () => {
await expect(
engine.insert('users', [{ id: '1', name: 'Alice', age: 'not-a-number' as any, email: 'a@t.com' }]),
).rejects.toThrow('expects number');
});
it('插入不存在的表抛出错误', async () => {
await expect(
engine.insert('ghosts', [{ id: '1' }]),
).rejects.toThrow('does not exist');
});
});
// ---- 查询 ----
describe('查询', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
{ id: '3', name: 'Charlie', age: 35, email: 'charlie@test.com' },
]);
});
it('查询所有行', async () => {
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(3);
});
it('WHERE $gt', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gt: 28 } },
});
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']);
});
it('WHERE $eq(等值)', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: 'Alice' },
});
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
});
it('WHERE $in', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $in: [25, 35] } },
});
expect(rows).toHaveLength(2);
});
it('WHERE $like', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: { $like: 'A%' } },
});
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('Alice');
});
it('WHERE $and', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { $and: [{ age: { $gt: 20 } }, { age: { $lt: 35 } }] },
});
expect(rows).toHaveLength(2);
});
it('WHERE $or', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { $or: [{ name: 'Alice' }, { name: 'Charlie' }] },
});
expect(rows).toHaveLength(2);
});
it('WHERE $ne', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $ne: 30 } },
});
expect(rows).toHaveLength(2);
});
it('WHERE $gte + $lte', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gte: 25, $lte: 30 } },
});
expect(rows).toHaveLength(2);
});
it('ORDER BY asc', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'asc' }],
});
expect(rows.map((r) => r.age)).toEqual([25, 30, 35]);
});
it('ORDER BY desc', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'desc' }],
});
expect(rows.map((r) => r.age)).toEqual([35, 30, 25]);
});
it('LIMIT', async () => {
const rows = await engine.find('users', {
table: 'users',
limit: 2,
});
expect(rows).toHaveLength(2);
});
it('OFFSET + LIMIT', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'id', direction: 'asc' }],
offset: 1,
limit: 1,
});
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('2');
});
it('列投影', async () => {
const rows = await engine.find('users', {
table: 'users',
columns: ['id', 'name'],
where: { id: '1' },
});
expect(Object.keys(rows[0]).sort()).toEqual(['id', 'name']);
});
it('空结果查询', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gt: 999 } },
});
expect(rows).toHaveLength(0);
});
});
// ---- 更新 ----
describe('更新', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
});
it('条件更新', async () => {
const count = await engine.update('users',
{ table: 'users', where: { id: '1' } },
{ age: 31 },
);
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].age).toBe(31);
});
it('更新所有行(无 where', async () => {
const count = await engine.update('users',
{ table: 'users' },
{ age: 100 },
);
expect(count).toBe(2);
});
it('更新不存在的表抛出错误', async () => {
await expect(
engine.update('ghosts', { table: 'ghosts' }, { x: 1 }),
).rejects.toThrow();
});
});
// ---- 删除 ----
describe('删除', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
});
it('条件删除', async () => {
const count = await engine.delete('users', { table: 'users', where: { id: '1' } });
expect(count).toBe(1);
expect(await engine.count('users')).toBe(1);
});
it('删除所有行(无 where', async () => {
const count = await engine.delete('users', { table: 'users' });
expect(count).toBe(2);
expect(await engine.count('users')).toBe(0);
});
it('清空表', async () => {
await engine.clear('users');
expect(await engine.count('users')).toBe(0);
});
});
// ---- Count ----
describe('Count', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
});
it('count 全部', async () => {
expect(await engine.count('users')).toBe(2);
});
it('count with where', async () => {
expect(await engine.count('users', { table: 'users', where: { age: { $gt: 27 } } })).toBe(1);
});
});
});
// ===================================================================
// AriaEngine 持久化测试 (Memory Backend, within-session)
// ===================================================================
describe('AriaEngine — 持久化 (Memory Backend)', () => {
it('创建表 → 不关闭 → 多次操作后数据一致', async () => {
const e = new AriaEngine({ storageBackend: 'memory' });
await e.open('test-persist-1', 1);
await e.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
}));
await e.insert('users', [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]);
// 多次查询验证
expect(await e.count('users')).toBe(2);
expect(await e.count('users')).toBe(2);
await e.close();
});
it('CRUD 操作 → count 验证', async () => {
const e = new AriaEngine({ storageBackend: 'memory' });
await e.open('test-persist-2', 1);
await e.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
val: { type: 'number', default: 0 },
}));
await e.insert('items', [
{ id: 'a', val: 1 },
{ id: 'b', val: 2 },
{ id: 'c', val: 3 },
]);
await e.update('items', { table: 'items', where: { id: 'b' } }, { val: 20 });
await e.delete('items', { table: 'items', where: { id: 'c' } });
const rows = await e.find('items', { table: 'items', orderBy: [{ column: 'id', direction: 'asc' }] });
expect(rows).toHaveLength(2);
expect(rows[0]).toMatchObject({ id: 'a', val: 1 });
expect(rows[1]).toMatchObject({ id: 'b', val: 20 });
await e.close();
});
it('多表操作', async () => {
const e = new AriaEngine({ storageBackend: 'memory' });
await e.open('test-persist-3', 1);
await e.createTable(createSchema('t1', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
await e.createTable(createSchema('t2', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
await e.insert('t1', [{ id: 'x', v: 1 }]);
await e.insert('t2', [{ id: 'y', v: 2 }]);
const names = await e.getTableNames();
expect(names.sort()).toEqual(['t1', 't2']);
expect(await e.count('t1')).toBe(1);
expect(await e.count('t2')).toBe(1);
await e.close();
});
});
// ===================================================================
// AriaEngine Schema/数据持久化测试 (Memory Backend, 安全无 IDB)
// ===================================================================
// 注:原 IndexedDB 持久化测试使用 fake-indexeddb 会导致 CI 卡死。
// 改为使用 Memory Backend 验证持久化流程:Schema 写入 → 读取 → 数据保持。
describe('AriaEngine — Schema 持久化 (Memory Backend)', () => {
it('Schema 持久化写入后再读取保持一致', async () => {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open('test-schema-persist', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
}));
// v0.8.0B-6):表结构不再是独立的裸 JSON__aria_schemas),
// 而是随 manifest 一起**原子提交**(单一提交点)。测试改读 manifest ——
// 它才是落盘结构的权威来源(带 CRC 与世代号)。
// 旧布局的问题:坏 JSON 会被 `readMetaList()` 当成 `[]`,元数据损坏 = 静默空库。
const manifest = await readManifestState((engine as any).backend);
expect(manifest).not.toBeNull();
expect(manifest!.schemas.users).toBeDefined();
expect(manifest!.schemas.users.id.primaryKey).toBe(true);
await engine.close();
});
it('close 后 Schema 可重新 load', async () => {
const engine1 = new AriaEngine({ storageBackend: 'memory' });
await engine1.open('test-reload', 1);
await engine1.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
val: { type: 'number', default: 0 },
}));
// 数据写入后 close(触发 schema 持久化 + flush
await engine1.insert('items', [{ id: 'a', val: 1 }, { id: 'b', val: 2 }]);
await engine1.close();
// 重新 open 并验证数据仍在(Memory Backend 的 close 会清空,但验证 loadSchemas 流程)
const engine2 = new AriaEngine({ storageBackend: 'memory' });
await engine2.open('test-reload', 1);
// Memory backend close 会清空 store,所以数据不保留
// 但我们可以验证引擎能正常重新初始化
expect(engine2.isOpen()).toBe(true);
await engine2.close();
});
});
// ===================================================================
// AriaEngine 事务测试
// ===================================================================
describe('AriaEngine — 事务', () => {
let engine: AriaEngine;
beforeEach(async () => {
engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open('test-aria-tx', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
balance: { type: 'number', default: 0 },
}));
});
afterEach(async () => {
await engine.close();
});
it('begin + commit: 事务中插入的数据最终可见', async () => {
await engine.beginTransaction();
await engine.insert('users', [{ id: '1', name: 'Alice', balance: 100 }]);
await engine.insert('users', [{ id: '2', name: 'Bob', balance: 200 }]);
await engine.commitTransaction();
expect(await engine.count('users')).toBe(2);
});
it('rollback: 事务中的数据不被持久化', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', balance: 100 }]);
await engine.beginTransaction();
await engine.insert('users', [{ id: '2', name: 'Bob', balance: 200 }]);
await engine.rollbackTransaction();
expect(await engine.count('users')).toBe(1);
const rows = await engine.find('users', { table: 'users' });
expect(rows[0].name).toBe('Alice');
});
it('双重 beginTransaction 抛出错误', async () => {
await engine.beginTransaction();
await expect(engine.beginTransaction()).rejects.toThrow('already in progress');
await engine.rollbackTransaction();
});
it('未开始事务时 commit 抛出错误', async () => {
await expect(engine.commitTransaction()).rejects.toThrow('No active transaction');
});
it('未开始事务时 rollback 抛出错误', async () => {
await expect(engine.rollbackTransaction()).rejects.toThrow('No active transaction');
});
});
// ===================================================================
// AriaEngine 通过 MetonaSqlark (mode: 'aria') 集成测试
// ===================================================================
describe('MetonaSqlark with mode=aria (Memory)', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: `ms-aria-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'aria', diskEngine: 'opfs' });
await db.init();
});
afterEach(async () => {
await db.close();
});
it('创建数据库并初始化', () => {
expect(db.isReady()).toBe(true);
expect(db.mode).toBe('aria');
});
it('定义表 + 基本 CRUD', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
});
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice', age: 30 });
await table.insert({ id: '2', name: 'Bob', age: 25 });
expect(await table.count()).toBe(2);
const rows = await table.select().where({ age: { $gt: 20 } }).execute();
expect(rows).toHaveLength(2);
});
it('SQL INSERT + SELECT', async () => {
await db.query('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)');
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
const result = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Alice');
});
it('SQL UPDATE + DELETE', async () => {
await db.query('CREATE TABLE users (id STRING PRIMARY KEY, name STRING)');
await db.query("INSERT INTO users VALUES ('1', 'Alice')");
await db.query("UPDATE users SET name = 'Alicia' WHERE id = '1'");
const rows = await db.query("SELECT * FROM users WHERE id = '1'") as Record<string, unknown>[];
expect(rows[0].name).toBe('Alicia');
await db.query("DELETE FROM users WHERE id = '1'");
expect(await db.table('users').count()).toBe(0);
});
it('SQL ORDER BY + LIMIT', async () => {
await db.query('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)');
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
await db.query("INSERT INTO users VALUES ('3', 'Charlie', 35)");
const result = await db.query('SELECT * FROM users ORDER BY age DESC LIMIT 2') as Record<string, unknown>[];
expect(result).toHaveLength(2);
expect(result[0].name).toBe('Charlie');
expect(result[1].name).toBe('Alice');
});
it('SQL GROUP BY + 聚合', async () => {
await db.query('CREATE TABLE emp (id STRING PRIMARY KEY, dept STRING, salary NUMBER)');
await db.query("INSERT INTO emp VALUES ('1', 'Eng', 1000)");
await db.query("INSERT INTO emp VALUES ('2', 'Eng', 1200)");
await db.query("INSERT INTO emp VALUES ('3', 'Sales', 900)");
const result = await db.query(
'SELECT dept, SUM(salary) AS total FROM emp GROUP BY dept',
) as Record<string, unknown>[];
expect(result).toHaveLength(2);
});
it('导出生效', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.table('users').insert({ id: '1', name: 'Alice' });
const exported = await db.exportTable('users');
expect(exported).toHaveLength(1);
expect(exported[0].name).toBe('Alice');
});
});
// ===================================================================
// AriaEngine 边界与错误处理测试
// ===================================================================
describe('AriaEngine — 边界与错误处理', () => {
let engine: AriaEngine;
beforeEach(async () => {
engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open('test-edge', 1);
});
afterEach(async () => {
await engine.close();
});
it('操作不存在的表抛出 TABLE_NOT_FOUND', async () => {
await expect(engine.find('ghosts', { table: 'ghosts' })).rejects.toThrow('does not exist');
await expect(engine.insert('ghosts', [{ id: '1' }])).rejects.toThrow('does not exist');
await expect(engine.update('ghosts', { table: 'ghosts' }, {})).rejects.toThrow('does not exist');
await expect(engine.delete('ghosts', { table: 'ghosts' })).rejects.toThrow('does not exist');
});
it('dropTable 删除不存在的表抛出错误', async () => {
await expect(engine.dropTable('nope')).rejects.toThrow('does not exist');
});
it('close 后操作抛出错误', async () => {
await engine.close();
await expect(engine.find('users', { table: 'users' })).rejects.toThrow('not opened');
});
it('空表 count 返回 0', async () => {
await engine.createTable(createSchema('empty', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
}));
expect(await engine.count('empty')).toBe(0);
});
it('空表 find 返回空数组', async () => {
await engine.createTable(createSchema('empty', {
id: { type: 'string', primaryKey: true },
}));
const rows = await engine.find('empty', { table: 'empty' });
expect(rows).toHaveLength(0);
});
it('clear 空表不报错', async () => {
await engine.createTable(createSchema('empty', {
id: { type: 'string', primaryKey: true },
}));
await engine.clear('empty');
expect(await engine.count('empty')).toBe(0);
});
it('update 无匹配行返回 0', async () => {
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
const cnt = await engine.update('users', { table: 'users', where: { id: 'x' } }, { name: 'X' });
expect(cnt).toBe(0);
});
it('delete 无匹配行返回 0', async () => {
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
}));
await engine.insert('users', [{ id: '1' }]);
const cnt = await engine.delete('users', { table: 'users', where: { id: 'x' } });
expect(cnt).toBe(0);
});
it('大量数据插入与查询 (100 行)', async () => {
await engine.createTable(createSchema('big', {
id: { type: 'string', primaryKey: true },
val: { type: 'number' },
}));
const rows = [];
for (let i = 0; i < 100; i++) {
rows.push({ id: `${i}`, val: i * 10 });
}
await engine.insert('big', rows);
expect(await engine.count('big')).toBe(100);
const result = await engine.find('big', {
table: 'big',
orderBy: [{ column: 'val', direction: 'asc' }],
limit: 5,
});
expect(result).toHaveLength(5);
expect(result[0].val).toBe(0);
});
it('多条件 WHERE 组合', async () => {
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
cat: { type: 'string' },
price: { type: 'number' },
}));
await engine.insert('items', [
{ id: '1', cat: 'A', price: 10 },
{ id: '2', cat: 'A', price: 20 },
{ id: '3', cat: 'B', price: 30 },
{ id: '4', cat: 'A', price: 40 },
]);
const rows = await engine.find('items', {
table: 'items',
where: { $and: [{ cat: 'A' }, { price: { $gt: 15 } }] },
});
expect(rows).toHaveLength(2);
});
it('WHERE $not 条件', async () => {
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
val: { type: 'number' },
}));
for (let i = 0; i < 5; i++) {
await engine.insert('items', [{ id: `${i}`, val: i }]);
}
const rows = await engine.find('items', {
table: 'items',
where: { val: { $not: { $eq: 3 } } },
});
expect(rows).toHaveLength(4);
});
it('count with $or', async () => {
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
age: { type: 'number' },
}));
await engine.insert('users', [
{ id: '1', age: 20 },
{ id: '2', age: 25 },
{ id: '3', age: 30 },
]);
expect(await engine.count('users', {
table: 'users',
where: { $or: [{ age: 20 }, { age: 30 }] },
})).toBe(2);
});
it('主键索引加速 — $eq 查询', async () => {
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
for (let i = 0; i < 50; i++) {
await engine.insert('users', [{ id: `${i}`, name: `User${i}` }]);
}
// PK 等值查询应直接通过索引
const rows = await engine.find('users', {
table: 'users',
where: { id: '25' },
});
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('User25');
});
it('多列 ORDER BY', async () => {
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
cat: { type: 'string' },
price: { type: 'number' },
}));
await engine.insert('items', [
{ id: '1', cat: 'A', price: 30 },
{ id: '2', cat: 'B', price: 10 },
{ id: '3', cat: 'A', price: 20 },
]);
const rows = await engine.find('items', {
table: 'items',
orderBy: [{ column: 'cat', direction: 'asc' }, { column: 'price', direction: 'asc' }],
});
expect(rows[0]).toMatchObject({ cat: 'A', price: 20 });
expect(rows[1]).toMatchObject({ cat: 'A', price: 30 });
expect(rows[2]).toMatchObject({ cat: 'B', price: 10 });
});
it('列投影 — 跨表格式列名', async () => {
await engine.createTable(createSchema('test', {
id: { type: 'string', primaryKey: true },
a: { type: 'number' },
b: { type: 'number' },
c: { type: 'number' },
}));
await engine.insert('test', [{ id: '1', a: 1, b: 2, c: 3 }]);
const rows = await engine.find('test', {
table: 'test',
columns: ['a', 'c'],
});
expect(Object.keys(rows[0])).toEqual(['a', 'c']);
expect(rows[0].a).toBe(1);
expect(rows[0].c).toBe(3);
});
it('boolean 类型正确存储和查询', async () => {
await engine.createTable(createSchema('flags', {
id: { type: 'string', primaryKey: true },
active: { type: 'boolean' },
}));
await engine.insert('flags', [
{ id: '1', active: true },
{ id: '2', active: false },
]);
const active = await engine.find('flags', { table: 'flags', where: { active: true } });
expect(active).toHaveLength(1);
expect(active[0].id).toBe('1');
});
it('date 类型存储和显示', async () => {
await engine.createTable(createSchema('events', {
id: { type: 'string', primaryKey: true },
at: { type: 'date' },
}));
const ts = '2026-01-01T00:00:00.000Z';
await engine.insert('events', [{ id: 'e1', at: ts }]);
const rows = await engine.find('events', { table: 'events' });
expect(rows[0].at).toBe(ts);
});
it('json 类型存储', async () => {
await engine.createTable(createSchema('docs', {
id: { type: 'string', primaryKey: true },
meta: { type: 'json' },
}));
await engine.insert('docs', [{ id: 'd1', meta: { tags: ['a', 'b'], count: 5 } }]);
const rows = await engine.find('docs', { table: 'docs' });
expect(rows[0].meta).toEqual({ tags: ['a', 'b'], count: 5 });
});
it('$like 模糊查询', async () => {
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
await engine.insert('users', [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Alicia' },
{ id: '3', name: 'Bob' },
]);
const rows = await engine.find('users', {
table: 'users',
where: { name: { $like: 'Ali%' } },
});
expect(rows).toHaveLength(2);
});
it('$in 查询', async () => {
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
await engine.insert('users', [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
{ id: '3', name: 'Charlie' },
]);
const rows = await engine.find('users', {
table: 'users',
where: { name: { $in: ['Alice', 'Charlie'] } },
});
expect(rows).toHaveLength(2);
});
it('dropTable 后重新创建同名表', async () => {
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
await engine.dropTable('users');
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
}));
const schema = await engine.getTableSchema('users');
expect(schema!.columns.v).toBeDefined();
expect(schema!.columns.name).toBeUndefined();
});
it('insert 后可立即 find 同一行', async () => {
await engine.createTable(createSchema('test', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
}));
await engine.insert('test', [{ id: '1', v: 42 }]);
const rows = await engine.find('test', { table: 'test', where: { id: '1' } });
expect(rows[0].v).toBe(42);
});
});