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% 覆盖率
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
/**
|
||
* AriaEngine Page 格式单元测试
|
||
* 覆盖: PageHeader / Slot / Tuple 编解码 + PageFormat 整合
|
||
*/
|
||
import {
|
||
PAGE_SIZE, PageType, PAGE_HEADER_SIZE, SLOT_ENTRY_SIZE,
|
||
} from '../../src/engine/aria/types';
|
||
import {
|
||
encodePageHeader, decodePageHeader, initPageHeader, getPageType,
|
||
getSlotCount, getFreeStart, setFreeStart, setFreeEnd,
|
||
} from '../../src/engine/aria/page/header';
|
||
import {
|
||
getSlotEntry, setSlotEntry, getSlotDirectorySize,
|
||
getFreeSpace, hasEnoughSpace, allocateSlot, readSlotData, freeSlot,
|
||
} from '../../src/engine/aria/page/slot';
|
||
import {
|
||
encodeTuple, decodeTuple, getColumnEncodingMap,
|
||
} from '../../src/engine/aria/page/tuple';
|
||
import { ColumnEncoding } from '../../src/engine/aria/types';
|
||
import {
|
||
createPage, pageFromBuffer, pageInsertRow, pageReadRow,
|
||
pageDeleteRow, pageUpdateRow, computeChecksum, verifyChecksum, updateChecksum,
|
||
} from '../../src/engine/aria/page/format';
|
||
|
||
// ===================================================================
|
||
// PageHeader
|
||
// ===================================================================
|
||
describe('AriaEngine Page — Header', () => {
|
||
let buf: ArrayBuffer;
|
||
|
||
beforeEach(() => {
|
||
buf = new ArrayBuffer(PAGE_SIZE);
|
||
});
|
||
|
||
it('initPageHeader 初始化头部字段', () => {
|
||
initPageHeader(buf, 42, PageType.DATA);
|
||
const h = decodePageHeader(buf);
|
||
expect(h.pageId).toBe(42);
|
||
expect(h.type).toBe(PageType.DATA);
|
||
expect(h.slotCount).toBe(0);
|
||
expect(h.freeStart).toBe(PAGE_HEADER_SIZE);
|
||
expect(h.freeEnd).toBe(PAGE_SIZE);
|
||
});
|
||
|
||
it('initPageHeader — INDEX 类型页面', () => {
|
||
initPageHeader(buf, 99, PageType.INDEX);
|
||
expect(getPageType(buf)).toBe(PageType.INDEX);
|
||
});
|
||
|
||
it('encodePageHeader + decodePageHeader 往返一致', () => {
|
||
const header = { pageId: 7, type: PageType.META, freeStart: 32, freeEnd: 4000, slotCount: 5, checksum: 0xdeadbeef };
|
||
encodePageHeader(header, buf);
|
||
const decoded = decodePageHeader(buf);
|
||
expect(decoded.pageId).toBe(7);
|
||
expect(decoded.type).toBe(PageType.META);
|
||
expect(decoded.freeStart).toBe(32);
|
||
expect(decoded.freeEnd).toBe(4000);
|
||
expect(decoded.slotCount).toBe(5);
|
||
});
|
||
|
||
it('不同 pageId 正确编解码', () => {
|
||
for (const id of [0, 1, 255, 65535, 0xffffffff]) {
|
||
initPageHeader(buf, id, PageType.DATA);
|
||
expect(decodePageHeader(buf).pageId).toBe(id >>> 0);
|
||
}
|
||
});
|
||
|
||
it('setFreeStart / setFreeEnd 修改字段', () => {
|
||
initPageHeader(buf, 1, PageType.DATA);
|
||
setFreeStart(buf, 100);
|
||
setFreeEnd(buf, 3000);
|
||
expect(getFreeStart(buf)).toBe(100);
|
||
expect(decodePageHeader(buf).freeEnd).toBe(3000);
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// Slot Directory
|
||
// ===================================================================
|
||
describe('AriaEngine Page — Slot', () => {
|
||
let buf: ArrayBuffer;
|
||
|
||
beforeEach(() => {
|
||
buf = new ArrayBuffer(PAGE_SIZE);
|
||
initPageHeader(buf, 1, PageType.DATA);
|
||
});
|
||
|
||
it('getSlotEntry — 空页面 slotCount 为 0', () => {
|
||
expect(getSlotCount(buf)).toBe(0);
|
||
});
|
||
|
||
it('setSlotEntry + getSlotEntry 往返', () => {
|
||
// 手动写一个 slot(不通过 allocateSlot)
|
||
new DataView(buf).setUint16(9, 1, false); // slotCount = 1
|
||
setSlotEntry(buf, 0, { offset: 1000, length: 50 });
|
||
const entry = getSlotEntry(buf, 0);
|
||
expect(entry.offset).toBe(1000);
|
||
expect(entry.length).toBe(50);
|
||
});
|
||
|
||
it('getSlotDirectorySize 计算正确', () => {
|
||
expect(getSlotDirectorySize(0)).toBe(0);
|
||
expect(getSlotDirectorySize(1)).toBe(SLOT_ENTRY_SIZE);
|
||
expect(getSlotDirectorySize(10)).toBe(10 * SLOT_ENTRY_SIZE);
|
||
});
|
||
|
||
it('getFreeSpace — 空页面有最大空闲空间', () => {
|
||
const free = getFreeSpace(buf);
|
||
expect(free).toBe(PAGE_SIZE - PAGE_HEADER_SIZE);
|
||
});
|
||
|
||
it('hasEnoughSpace — 小数据返回 true', () => {
|
||
expect(hasEnoughSpace(buf, 100)).toBe(true);
|
||
});
|
||
|
||
it('hasEnoughSpace — 超大数据返回 false', () => {
|
||
expect(hasEnoughSpace(buf, PAGE_SIZE * 2)).toBe(false);
|
||
});
|
||
|
||
it('allocateSlot 分配并写入数据', () => {
|
||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||
const idx = allocateSlot(buf, data);
|
||
expect(idx).toBe(0);
|
||
expect(getSlotCount(buf)).toBe(1);
|
||
|
||
const readBack = readSlotData(buf, 0);
|
||
expect(readBack).not.toBeNull();
|
||
expect(Array.from(readBack!)).toEqual([1, 2, 3, 4, 5]);
|
||
});
|
||
|
||
it('allocateSlot 多次分配', () => {
|
||
for (let i = 0; i < 10; i++) {
|
||
const data = new Uint8Array([i, i + 1]);
|
||
const idx = allocateSlot(buf, data);
|
||
expect(idx).toBe(i);
|
||
}
|
||
expect(getSlotCount(buf)).toBe(10);
|
||
|
||
// 验证每个 slot 数据正确
|
||
for (let i = 0; i < 10; i++) {
|
||
const data = readSlotData(buf, i);
|
||
expect(Array.from(data!)).toEqual([i, i + 1]);
|
||
}
|
||
});
|
||
|
||
it('freeSlot 标记为删除', () => {
|
||
const data = new Uint8Array([10, 20, 30]);
|
||
allocateSlot(buf, data);
|
||
freeSlot(buf, 0);
|
||
const entry = getSlotEntry(buf, 0);
|
||
expect(entry.offset).toBe(0);
|
||
expect(entry.length).toBe(0);
|
||
});
|
||
|
||
it('readSlotData — 已删除 slot 返回 null', () => {
|
||
allocateSlot(buf, new Uint8Array([1]));
|
||
freeSlot(buf, 0);
|
||
expect(readSlotData(buf, 0)).toBeNull();
|
||
});
|
||
|
||
it('allocateSlot — 空间不足返回 -1', () => {
|
||
// 填满页面
|
||
const big = new Uint8Array(PAGE_SIZE - PAGE_HEADER_SIZE - SLOT_ENTRY_SIZE);
|
||
const idx1 = allocateSlot(buf, big);
|
||
expect(idx1).toBe(0);
|
||
const idx2 = allocateSlot(buf, new Uint8Array([1]));
|
||
expect(idx2).toBe(-1);
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// Tuple Codec
|
||
// ===================================================================
|
||
describe('AriaEngine Page — Tuple', () => {
|
||
const colOrder = ['id', 'name', 'age', 'active', 'data'];
|
||
const colTypes: Record<string, string> = {
|
||
id: 'string', name: 'string', age: 'number', active: 'boolean', data: 'json',
|
||
};
|
||
|
||
it('encodeTuple + decodeTuple 完整往返', () => {
|
||
const row = { id: '1', name: 'Alice', age: 30, active: true, data: { x: 1 } };
|
||
const encoded = encodeTuple(row, colOrder, colTypes);
|
||
expect(encoded.byteLength).toBeGreaterThan(0);
|
||
|
||
const decoded = decodeTuple(encoded, colOrder, colTypes);
|
||
expect(decoded).not.toBeNull();
|
||
expect(decoded!.id).toBe('1');
|
||
expect(decoded!.name).toBe('Alice');
|
||
expect(decoded!.age).toBe(30);
|
||
expect(decoded!.active).toBe(true);
|
||
expect(decoded!.data).toEqual({ x: 1 });
|
||
});
|
||
|
||
it('encodeTuple — null 值正确处理', () => {
|
||
const row = { id: '2', name: null, age: 25, active: null, data: null };
|
||
const encoded = encodeTuple(row, colOrder, colTypes);
|
||
const decoded = decodeTuple(encoded, colOrder, colTypes);
|
||
expect(decoded!.name).toBeNull();
|
||
expect(decoded!.active).toBeNull();
|
||
expect(decoded!.data).toBeNull();
|
||
});
|
||
|
||
it('encodeTuple — undefined 值按 null 处理', () => {
|
||
const row = { id: '3', age: 30 } as any;
|
||
const encoded = encodeTuple(row, colOrder, colTypes);
|
||
const decoded = decodeTuple(encoded, colOrder, colTypes);
|
||
expect(decoded!.id).toBe('3');
|
||
expect(decoded!.name).toBeNull();
|
||
});
|
||
|
||
it('encodeTuple — date 类型', () => {
|
||
const order = ['ts'];
|
||
const types = { ts: 'date' };
|
||
const row = { ts: '2024-01-15T00:00:00.000Z' };
|
||
const encoded = encodeTuple(row, order, types);
|
||
const decoded = decodeTuple(encoded, order, types);
|
||
expect(decoded!.ts).toBe('2024-01-15T00:00:00.000Z');
|
||
});
|
||
|
||
it('encodeTuple — boolean false', () => {
|
||
const order = ['flag'];
|
||
const types = { flag: 'boolean' };
|
||
const encoded = encodeTuple({ flag: false }, order, types);
|
||
const decoded = decodeTuple(encoded, order, types);
|
||
expect(decoded!.flag).toBe(false);
|
||
});
|
||
|
||
it('encodeTuple — 负数', () => {
|
||
const order = ['val'];
|
||
const types = { val: 'number' };
|
||
const encoded = encodeTuple({ val: -42.5 }, order, types);
|
||
const decoded = decodeTuple(encoded, order, types);
|
||
expect(decoded!.val).toBe(-42.5);
|
||
});
|
||
|
||
it('encodeTuple — 空字符串', () => {
|
||
const order = ['s'];
|
||
const types = { s: 'string' };
|
||
const encoded = encodeTuple({ s: '' }, order, types);
|
||
const decoded = decodeTuple(encoded, order, types);
|
||
expect(decoded!.s).toBe('');
|
||
});
|
||
|
||
it('encodeTuple — 长字符串', () => {
|
||
const order = ['s'];
|
||
const types = { s: 'string' };
|
||
const long = 'x'.repeat(10000);
|
||
const encoded = encodeTuple({ s: long }, order, types);
|
||
const decoded = decodeTuple(encoded, order, types);
|
||
expect(decoded!.s).toBe(long);
|
||
});
|
||
|
||
it('getColumnEncodingMap 返回正确映射', () => {
|
||
const map = getColumnEncodingMap(colOrder, colTypes);
|
||
expect(map.get('id')).toBe(ColumnEncoding.STRING);
|
||
expect(map.get('age')).toBe(ColumnEncoding.NUMBER);
|
||
expect(map.get('active')).toBe(ColumnEncoding.BOOLEAN);
|
||
expect(map.get('data')).toBe(ColumnEncoding.JSON);
|
||
});
|
||
|
||
it('decodeTuple — 损坏数据返回 null', () => {
|
||
const broken = new Uint8Array([0xff, 0xff, 0xff]);
|
||
expect(decodeTuple(broken, colOrder, colTypes)).toBeNull();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// Page Format 整合
|
||
// ===================================================================
|
||
describe('AriaEngine Page — Format', () => {
|
||
const colOrder = ['id', 'name'];
|
||
const colTypes: Record<string, string> = { id: 'string', name: 'string' };
|
||
|
||
it('createPage 创建合法页面', () => {
|
||
const page = createPage(100, PageType.DATA);
|
||
expect(page.pageId).toBe(100);
|
||
expect(page.type).toBe(PageType.DATA);
|
||
expect(page.pins).toBe(0);
|
||
expect(page.dirty).toBe(true);
|
||
});
|
||
|
||
it('pageInsertRow + pageReadRow 往返', () => {
|
||
const page = createPage(1, PageType.DATA);
|
||
const row = { id: 'u1', name: 'Test' };
|
||
const idx = pageInsertRow(page, row, colOrder, colTypes);
|
||
expect(idx).toBe(0);
|
||
|
||
const read = pageReadRow(page, 0, colOrder, colTypes);
|
||
expect(read).not.toBeNull();
|
||
expect(read!.id).toBe('u1');
|
||
expect(read!.name).toBe('Test');
|
||
});
|
||
|
||
it('pageInsertRow — 多次插入', () => {
|
||
const page = createPage(1, PageType.DATA);
|
||
for (let i = 0; i < 50; i++) {
|
||
const idx = pageInsertRow(page, { id: `${i}`, name: `User${i}` }, colOrder, colTypes);
|
||
expect(idx).toBe(i);
|
||
}
|
||
for (let i = 0; i < 50; i++) {
|
||
const row = pageReadRow(page, i, colOrder, colTypes);
|
||
expect(row!.id).toBe(`${i}`);
|
||
}
|
||
});
|
||
|
||
it('pageDeleteRow 标记删除', () => {
|
||
const page = createPage(1, PageType.DATA);
|
||
pageInsertRow(page, { id: '1', name: 'A' }, colOrder, colTypes);
|
||
pageInsertRow(page, { id: '2', name: 'B' }, colOrder, colTypes);
|
||
pageDeleteRow(page, 0);
|
||
expect(page.dirty).toBe(true);
|
||
// 已删除的行读取失败
|
||
const row = pageReadRow(page, 0, colOrder, colTypes);
|
||
expect(row).toBeNull();
|
||
// 未删除的行仍然可读
|
||
const row2 = pageReadRow(page, 1, colOrder, colTypes);
|
||
expect(row2!.id).toBe('2');
|
||
});
|
||
|
||
it('pageFromBuffer 从 ArrayBuffer 恢复', () => {
|
||
const page = createPage(5, PageType.INDEX);
|
||
const restored = pageFromBuffer(5, page.data);
|
||
expect(restored.pageId).toBe(5);
|
||
expect(restored.type).toBe(PageType.INDEX);
|
||
expect(restored.dirty).toBe(false);
|
||
});
|
||
|
||
it('computeChecksum + verifyChecksum', () => {
|
||
const page = createPage(1, PageType.DATA);
|
||
updateChecksum(page);
|
||
expect(verifyChecksum(page)).toBe(true);
|
||
|
||
// 修改页面 → 校验和失效
|
||
new Uint8Array(page.data)[100] = 0xff;
|
||
expect(verifyChecksum(page)).toBe(false);
|
||
});
|
||
});
|