- 新增 AriaEngine: LSM-Tree 页面式存储引擎,19 个模块,~3500 行 TS - page/: Slotted Page 格式 (header/slot/tuple/format) + CRC32 - buffer/: Buffer Pool (LRU 缓存 + 驱逐策略) - index/: LSM-Tree (MemTable 红黑树 + SSTable + Bloom Filter + Merge Iterator) - wal/: WAL 日志 (二进制格式) + Checkpoint 管理 - transaction/: MVCC 版本链 + 快照隔离 - store/: IndexedDB / Memory 双后端抽象 - compression/: LZ4 页面压缩 - 完整持久化: Schema 自动保存、SSTable 元数据管理、WAL 恢复 - 事务感知 CRUD: insert/update/delete 在事务中缓冲到 snapshot - mode: 'aria' 激活自研引擎 - 新增 7 个测试文件,测试数 318 → 524,套件 20 → 27 - aria-page.test.ts (32 tests): Page 格式单元测试 - aria-index.test.ts (26 tests): Bloom Filter + MemTable - aria-sstable.test.ts (9 tests): SSTable Builder + Reader - aria-buffer.test.ts (25 tests): LRU + Eviction + Buffer Pool - aria-wal-mvcc.test.ts (22 tests): WAL 编解码 + MVCC 事务 - aria-compress.test.ts (11 tests): LZ4 + Merge Iterator - aria.test.ts (80 tests): AriaEngine 集成 + 边界测试 - Bug 修复: LRUList size 跟踪、WAL 缓冲区越界、ColumnEncoding 导入 - 全面更新 README.md + site/ 站点文件 (index/docs/demo)
241 lines
7.8 KiB
TypeScript
241 lines
7.8 KiB
TypeScript
/**
|
|
* AriaEngine SSTable Builder — 构建有序字符串表
|
|
* @module engine/aria/index/sstable_builder
|
|
*
|
|
* 将排序后的 key-value 数据写入 SSTable 格式。
|
|
*
|
|
* SSTable 文件布局:
|
|
* ┌──────────────────────────────────────────────┐
|
|
* │ Data Block 0 │
|
|
* │ Data Block 1 │
|
|
* │ ... │
|
|
* │ Index Block (block offset → key range) │
|
|
* │ Bloom Filter │
|
|
* │ Footer (32 bytes) │
|
|
* │ - index_offset (u32) │
|
|
* │ - index_size (u32) │
|
|
* │ - bloom_offset (u32) │
|
|
* │ - bloom_size (u32) │
|
|
* │ - bloom_hash_count (u32) │
|
|
* │ - entry_count (u32) │
|
|
* │ - magic_number (u32, 0x53535442 ="SSTB")│
|
|
* │ - checksum (u32) │
|
|
* └──────────────────────────────────────────────┘
|
|
*/
|
|
|
|
import { BloomFilter } from './bloom';
|
|
import type { IndexEntry, DataBlock } from '../types';
|
|
|
|
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
|
|
const SSTABLE_FOOTER_SIZE = 32;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SSTableBuilder
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export class SSTableBuilder {
|
|
private entries: [string, Record<string, unknown>][] = [];
|
|
private currentBlock: [string, Record<string, unknown>][] = [];
|
|
private currentBlockStartKey = '';
|
|
private blockSizeLimit: number;
|
|
|
|
constructor(blockSizeLimit: number = 4096) {
|
|
this.blockSizeLimit = blockSizeLimit;
|
|
}
|
|
|
|
/** 添加一个 key-value 条目(必须按键排序添加) */
|
|
add(key: string, value: Record<string, unknown>): void {
|
|
if (this.currentBlock.length === 0) {
|
|
this.currentBlockStartKey = key;
|
|
}
|
|
|
|
this.currentBlock.push([key, value]);
|
|
this.entries.push([key, value]);
|
|
|
|
// 如果当前 Block 达到大小限制,切割
|
|
const estimated = this.estimateBlockSize();
|
|
if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) {
|
|
// 当前 block 结束(不在这里切割,在 build 时统一处理)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 构建 SSTable 文件的二进制数据。
|
|
* 返回 { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter }
|
|
*/
|
|
build(): { sstableData: Uint8Array; indexEntries: IndexEntry[] } {
|
|
const blocks = this.splitIntoBlocks();
|
|
const bloomFilter = new BloomFilter(this.entries.length);
|
|
|
|
// 预计算总大小
|
|
let totalSize = 0;
|
|
const blockOffsets: number[] = [];
|
|
|
|
for (const block of blocks) {
|
|
blockOffsets.push(totalSize);
|
|
const blockSize = this.computeBlockSize(block);
|
|
totalSize += blockSize;
|
|
}
|
|
|
|
// 索引块
|
|
const indexEntries: IndexEntry[] = [];
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
const block = blocks[i];
|
|
const lastKey = block[block.length - 1][0];
|
|
const blockSize = this.computeBlockSize(block);
|
|
indexEntries.push({
|
|
key: lastKey,
|
|
blockOffset: blockOffsets[i],
|
|
blockSize,
|
|
});
|
|
}
|
|
|
|
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
|
|
|
|
// 写入到 buffer
|
|
const finalSize = totalSize + indexBlockSize + SSTABLE_FOOTER_SIZE;
|
|
const buf = new ArrayBuffer(finalSize);
|
|
const view = new DataView(buf);
|
|
|
|
let offset = 0;
|
|
|
|
// ---- Data Blocks ----
|
|
for (const block of blocks) {
|
|
offset = this.writeDataBlock(view, offset, block, bloomFilter);
|
|
}
|
|
|
|
// ---- Index Block ----
|
|
const indexOffset = offset;
|
|
offset = this.writeIndexBlock(view, offset, indexEntries);
|
|
|
|
// ---- Footer ----
|
|
const footerOffset = offset;
|
|
view.setUint32(footerOffset, indexOffset, false); // index_offset
|
|
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
|
|
view.setUint32(footerOffset + 8, 0, false); // bloom_offset (embedded in footer)
|
|
view.setUint32(footerOffset + 12, 0, false); // bloom_size
|
|
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
|
|
view.setUint32(footerOffset + 20, this.entries.length, false);
|
|
view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false);
|
|
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
|
|
|
|
return {
|
|
sstableData: new Uint8Array(buf),
|
|
indexEntries,
|
|
};
|
|
}
|
|
|
|
/** 获取条目数 */
|
|
getEntryCount(): number {
|
|
return this.entries.length;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 内部
|
|
// -----------------------------------------------------------------------
|
|
|
|
private splitIntoBlocks(): [string, Record<string, unknown>][][] {
|
|
const blocks: [string, Record<string, unknown>][][] = [];
|
|
let current: [string, Record<string, unknown>][] = [];
|
|
|
|
for (const entry of this.entries) {
|
|
current.push(entry);
|
|
if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) {
|
|
blocks.push(current.slice(0, -1));
|
|
current = [entry];
|
|
}
|
|
}
|
|
if (current.length > 0) blocks.push(current);
|
|
|
|
return blocks;
|
|
}
|
|
|
|
private estimateBlockSize(): number {
|
|
return this.estimateBlockSizeFromEntries(this.currentBlock);
|
|
}
|
|
|
|
private estimateBlockSizeFromEntries(entries: [string, unknown][]): number {
|
|
let size = 0;
|
|
for (const [key, value] of entries) {
|
|
size += 4 + key.length + JSON.stringify(value).length;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
private computeBlockSize(block: [string, unknown][]): number {
|
|
// entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json
|
|
let size = 4;
|
|
for (const [key, value] of block) {
|
|
const json = JSON.stringify(value);
|
|
size += 2 + key.length + 2 + json.length;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
private writeDataBlock(
|
|
view: DataView,
|
|
offset: number,
|
|
block: [string, Record<string, unknown>][],
|
|
bloomFilter: BloomFilter,
|
|
): number {
|
|
const start = offset;
|
|
|
|
// entry count
|
|
view.setUint32(offset, block.length, false);
|
|
offset += 4;
|
|
|
|
for (const [key, value] of block) {
|
|
const encoder = new TextEncoder();
|
|
const keyBytes = encoder.encode(key);
|
|
const valueBytes = encoder.encode(JSON.stringify(value));
|
|
|
|
// key length
|
|
view.setUint16(offset, keyBytes.length, false);
|
|
offset += 2;
|
|
// key
|
|
new Uint8Array(view.buffer).set(keyBytes, offset);
|
|
offset += keyBytes.length;
|
|
// value length
|
|
view.setUint16(offset, valueBytes.length, false);
|
|
offset += 2;
|
|
// value
|
|
new Uint8Array(view.buffer).set(valueBytes, offset);
|
|
offset += valueBytes.length;
|
|
|
|
// 插入 bloom filter
|
|
bloomFilter.insert(key);
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
private estimateIndexBlockSize(entries: IndexEntry[]): number {
|
|
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32)
|
|
let size = 4;
|
|
for (const entry of entries) {
|
|
size += 2 + entry.key.length + 8;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
private writeIndexBlock(view: DataView, offset: number, entries: IndexEntry[]): number {
|
|
view.setUint32(offset, entries.length, false);
|
|
offset += 4;
|
|
|
|
for (const entry of entries) {
|
|
const encoder = new TextEncoder();
|
|
const keyBytes = encoder.encode(entry.key);
|
|
view.setUint16(offset, keyBytes.length, false);
|
|
offset += 2;
|
|
new Uint8Array(view.buffer).set(keyBytes, offset);
|
|
offset += keyBytes.length;
|
|
view.setUint32(offset, entry.blockOffset, false);
|
|
offset += 4;
|
|
view.setUint32(offset, entry.blockSize, false);
|
|
offset += 4;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
}
|