Skip to content

Commit

Permalink
btree -- rough draft
Browse files Browse the repository at this point in the history
  • Loading branch information
friendlymatthew committed Jan 12, 2024
1 parent bc7d5c0 commit 8c9c51c
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 0 deletions.
135 changes: 135 additions & 0 deletions src/btree/btree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { BPTreeNode, MemoryPointer, compareBytes } from "./node";

// taken from `buffer.go`
interface ReadWriteSeekTruncater {
write(buffer: Uint8Array): Promise<number>;
seek(offset: number, whence: "start" | "current" | "end"): Promise<number>;
read(buffer: Uint8Array, offset: number): Promise<number>;
truncate(size: number): Promise<void>;
}

interface MetaPage {
root(): Promise<MemoryPointer>;
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<BPTreeNode | null> {
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<TraversalRecord[] | null> {
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;
}
}
171 changes: 171 additions & 0 deletions src/btree/node.ts
Original file line number Diff line number Diff line change
@@ -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<number>;
}

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<number> {
return await this.dataHandler.read(buffer, offset);
}

async leaf(): Promise<boolean> {
return this.pointers.length === this.keys.length;
}

async readFrom(buffer: Buffer): Promise<number> {
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;
}

0 comments on commit 8c9c51c

Please sign in to comment.