From 8c9c51c929f7fb41dbae0692a1458e3e586f75c3 Mon Sep 17 00:00:00 2001 From: Matthew <38759997+friendlymatthew@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:46:54 -0500 Subject: [PATCH] btree -- rough draft --- src/btree/btree.ts | 135 +++++++++++++++++++++++++++++++++++ src/btree/node.ts | 171 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 src/btree/btree.ts create mode 100644 src/btree/node.ts diff --git a/src/btree/btree.ts b/src/btree/btree.ts new file mode 100644 index 00000000..c03a341d --- /dev/null +++ b/src/btree/btree.ts @@ -0,0 +1,135 @@ +import { BPTreeNode, MemoryPointer, compareBytes } from "./node"; + +// taken from `buffer.go` +interface ReadWriteSeekTruncater { + write(buffer: Uint8Array): Promise; + seek(offset: number, whence: "start" | "current" | "end"): Promise; + read(buffer: Uint8Array, offset: number): Promise; + truncate(size: number): Promise; +} + +interface MetaPage { + root(): Promise; + setRoot(pointer: MemoryPointer): void; +} + +class BPTree { + private tree: ReadWriteSeekTruncater; + private meta: MetaPage; + private maxPageSize: number; + + constructor( + tree: ReadWriteSeekTruncater, + meta: MetaPage, + maxPageSize: number + ) { + this.tree = tree; + this.meta = meta; + this.maxPageSize = maxPageSize; + } + + private async root(): Promise<[BPTreeNode | null, MemoryPointer]> { + const mp = await this.meta.root(); + if (!mp || mp.length === 0) { + return [null, mp]; + } + + const root = await this.readNode(mp); + if (!root) { + return [null, mp]; + } + + return [root, mp]; + } + + private async readNode(ptr: MemoryPointer): Promise { + const pos = await this.tree.seek(ptr.offset, "start"); + if (!pos || pos !== ptr.offset) { + return null; + } + + const buffer = Buffer.alloc(ptr.length); + await this.tree.read(buffer, 0); + + const node = new BPTreeNode(this.tree, [], []); + + const bytesRead = await node.readFrom(buffer); + + if (!bytesRead || bytesRead !== ptr.length) { + return null; + } + + return node; + } + + private async traverse( + key: Uint8Array, + node: BPTreeNode + ): Promise { + if (await node.leaf()) { + return [{ node: node, index: 0 }]; + } + + for (const [i, k] of node.keys.entries()) { + if (compareBytes(key, k.value) < 0) { + const child = await this.readNode(node.pointers[i]); + if (!child) { + return null; + } + + const path = await this.traverse(key, child); + if (!path) { + return null; + } + + return [...path, { node: node, index: i }]; + } + } + + const child = await this.readNode(node.pointers[node.pointers.length - 1]); + + if (!child) { + return null; + } + + const path = await this.traverse(key, child); + if (!path) { + return null; + } + + return [...path, { node: node, index: node.keys.length }]; + } + + public async find(key: Uint8Array): Promise<[MemoryPointer, boolean]> { + let [rootNode, _] = await this.root(); + + if (!rootNode) { + return [new MemoryPointer(0, 0), false]; + } + + const path = await this.traverse(key, rootNode); + if (!path) { + return [new MemoryPointer(0, 0), false]; + } + + const n = path[0].node; + + const [i, found] = await n.bsearch(key); + + if (found) { + return [n.pointers[i], true]; + } + + return [new MemoryPointer(0, 0), false]; + } +} + +class TraversalRecord { + public node: BPTreeNode; + public index: number; + + constructor(node: BPTreeNode, index: number) { + this.node = node; + this.index = index; + } +} diff --git a/src/btree/node.ts b/src/btree/node.ts new file mode 100644 index 00000000..0668c828 --- /dev/null +++ b/src/btree/node.ts @@ -0,0 +1,171 @@ +class ReferencedValue { + private dataPointer: MemoryPointer; + public value: Buffer; + + constructor(dataPointer: MemoryPointer, value: Buffer) { + this.dataPointer = dataPointer; + this.value = value; + } +} + +export class MemoryPointer { + offset: number; + length: number; + + constructor(offset: number, length: number) { + this.offset = offset; + this.length = length; + } +} + +interface DataHandler { + read(buffer: Uint8Array, offset: number): Promise; +} + +export class BPTreeNode { + public dataHandler: DataHandler; + public pointers: MemoryPointer[]; + public keys: ReferencedValue[]; + + constructor( + dataHandler: DataHandler, + pointers: MemoryPointer[], + keys: ReferencedValue[] + ) { + this.dataHandler = dataHandler; + this.pointers = pointers; + this.keys = keys; + } + + addPointer(pointer: MemoryPointer) { + this.pointers.push(pointer); + } + + addKey(key: ReferencedValue) { + this.keys.push(key); + } + + async readAtOffset(buffer: Uint8Array, offset: number): Promise { + return await this.dataHandler.read(buffer, offset); + } + + async leaf(): Promise { + return this.pointers.length === this.keys.length; + } + + async readFrom(buffer: Buffer): Promise { + let offset = 0; + let size: number; + + let m = 4; + + try { + // since we are reading a 32-bit integer, we move by 4 bytes + size = buffer.readInt32BE(offset); + offset += 4; + + const leaf = size < 0; + const absSize = Math.abs(size); + + this.pointers = new Array(absSize + (leaf ? 0 : 1)) + .fill(null) + .map(() => new MemoryPointer(0, 0)); + + this.keys = new Array(absSize) + .fill(null) + .map( + () => new ReferencedValue(new MemoryPointer(0, 0), Buffer.alloc(0)) + ); + + for (let idx = 0; idx <= this.keys.length - 1; idx++) { + const l = buffer.readUInt32BE(offset); + + offset += 4; + m += 4; + + if (l == 0) { + const dpOffset = buffer.readUInt32BE(offset); + offset += 4; + const dpLength = buffer.readUInt32BE(offset); + offset += 4; + + const dataPointer = new MemoryPointer(dpOffset, dpLength); + const keyValue = Buffer.alloc(dpLength); + + await this.dataHandler.read(keyValue, dpOffset); + this.keys[idx] = new ReferencedValue(dataPointer, keyValue); + m += 12; + } else { + const keyValue = buffer.slice(offset, offset + l); + this.keys[idx] = new ReferencedValue( + new MemoryPointer(0, 0), + keyValue + ); + + offset += l; + m += l; + } + } + + for (let idx = 0; idx <= this.pointers.length - 1; idx++) { + const pointerOffset = buffer.readUint32BE(offset); + offset += 4; + + const pointerLength = buffer.readUint32BE(offset); + offset += 4; + + this.pointers[idx] = new MemoryPointer(pointerOffset, pointerLength); + + m += 8; + } + } catch (error) { + return 0; + } + return m; + } + + async bsearch(key: Uint8Array): Promise<[number, boolean]> { + let lo = 0; + let hi = this.keys.length - 1; + + while (lo <= hi) { + const mid = (lo + hi) / 2; + const cmp = compareBytes(key, this.keys[mid].value); + + switch (cmp) { + case 0: + return [mid, true]; + case -1: + hi = mid - 1; + case 1: + lo = mid + 1; + } + } + + return [lo, false]; + } +} + +// https://pkg.go.dev/internal/bytealg#Compare +export function compareBytes(a: Uint8Array, b: Uint8Array): number { + const len = Math.min(a.length, b.length); + + for (let idx = 0; idx <= len - 1; idx++) { + if (a[idx] !== b[idx]) { + return -1; + } + + if (a[idx] > b[idx]) { + return 1; + } + } + + if (a.length < b.length) { + return -1; + } + if (a.length > b.length) { + return 1; + } + + return 0; +}