Files
MetonaSqlark/tests/engine/aria-page.test.ts
T
thzxx d544501e1c
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
release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
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% 覆盖率
2026-08-08 10:41:30 +08:00

338 lines
12 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 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);
});
});