340 lines
10 KiB
Zig
340 lines
10 KiB
Zig
const std = @import("std");
|
|
const encode = @import("encode.zig");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const Kind = encode.Kind;
|
|
const Record = encode.Record;
|
|
|
|
const maxLevel = 16;
|
|
|
|
const Node = struct {
|
|
key: []u8,
|
|
value: []u8,
|
|
seq: u64,
|
|
kind: Kind,
|
|
forward: []?*Node,
|
|
backward: ?*Node,
|
|
};
|
|
|
|
pub const Memtable = struct {
|
|
allocator: Allocator,
|
|
head: *Node,
|
|
level: usize,
|
|
byteSize: usize,
|
|
count: usize,
|
|
rngState: u64,
|
|
|
|
pub fn init(allocator: Allocator) !Memtable {
|
|
const head = try makeNode(allocator, &.{}, &.{}, 0, .value, maxLevel);
|
|
return .{
|
|
.allocator = allocator,
|
|
.head = head,
|
|
.level = 1,
|
|
.byteSize = 0,
|
|
.count = 0,
|
|
.rngState = 0x243f6a8885a308d3,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Memtable) void {
|
|
var node = self.head.forward[0];
|
|
while (node) |current| {
|
|
const nextNode = current.forward[0];
|
|
self.freeNode(current);
|
|
node = nextNode;
|
|
}
|
|
self.allocator.free(self.head.forward);
|
|
self.allocator.destroy(self.head);
|
|
}
|
|
|
|
pub fn clear(self: *Memtable) void {
|
|
var node = self.head.forward[0];
|
|
while (node) |current| {
|
|
const nextNode = current.forward[0];
|
|
self.freeNode(current);
|
|
node = nextNode;
|
|
}
|
|
var i: usize = 0;
|
|
while (i < maxLevel) : (i += 1) self.head.forward[i] = null;
|
|
self.level = 1;
|
|
self.byteSize = 0;
|
|
self.count = 0;
|
|
}
|
|
|
|
pub fn upsert(self: *Memtable, kind: Kind, seq: u64, key: []const u8, value: []const u8) !void {
|
|
var update: [maxLevel]*Node = undefined;
|
|
var node = self.head;
|
|
var lvl: usize = self.level;
|
|
while (lvl > 0) {
|
|
lvl -= 1;
|
|
while (node.forward[lvl]) |next| {
|
|
if (std.mem.order(u8, next.key, key) == .lt) {
|
|
node = next;
|
|
} else break;
|
|
}
|
|
update[lvl] = node;
|
|
}
|
|
|
|
const candidate = node.forward[0];
|
|
if (candidate) |existing| {
|
|
if (std.mem.eql(u8, existing.key, key)) {
|
|
const newValue = try self.allocator.dupe(u8, value);
|
|
self.byteSize = self.byteSize - existing.value.len + newValue.len;
|
|
self.allocator.free(existing.value);
|
|
existing.value = newValue;
|
|
existing.seq = seq;
|
|
existing.kind = kind;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const newLevel = self.randomLevel();
|
|
if (newLevel > self.level) {
|
|
var i = self.level;
|
|
while (i < newLevel) : (i += 1) update[i] = self.head;
|
|
self.level = newLevel;
|
|
}
|
|
|
|
const created = try makeNode(self.allocator, key, value, seq, kind, newLevel);
|
|
var i: usize = 0;
|
|
while (i < newLevel) : (i += 1) {
|
|
created.forward[i] = update[i].forward[i];
|
|
update[i].forward[i] = created;
|
|
}
|
|
created.backward = if (update[0] == self.head) null else update[0];
|
|
if (created.forward[0]) |successor| successor.backward = created;
|
|
self.byteSize += key.len + value.len;
|
|
self.count += 1;
|
|
}
|
|
|
|
pub fn find(self: *const Memtable, key: []const u8) ?Record {
|
|
var node = self.head;
|
|
var lvl: usize = self.level;
|
|
while (lvl > 0) {
|
|
lvl -= 1;
|
|
while (node.forward[lvl]) |next| {
|
|
if (std.mem.order(u8, next.key, key) == .lt) {
|
|
node = next;
|
|
} else break;
|
|
}
|
|
}
|
|
const candidate = node.forward[0];
|
|
if (candidate) |existing| {
|
|
if (std.mem.eql(u8, existing.key, key)) return nodeRecord(existing);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn findAsOf(self: *const Memtable, key: []const u8, maxSeq: u64) ?Record {
|
|
const rec = self.find(key) orelse return null;
|
|
if (rec.seq > maxSeq) return null;
|
|
return rec;
|
|
}
|
|
|
|
pub fn cursor(self: *const Memtable, start: ?[]const u8) Cursor {
|
|
var node = self.head;
|
|
if (start) |startKey| {
|
|
var lvl: usize = self.level;
|
|
while (lvl > 0) {
|
|
lvl -= 1;
|
|
while (node.forward[lvl]) |next| {
|
|
if (std.mem.order(u8, next.key, startKey) == .lt) {
|
|
node = next;
|
|
} else break;
|
|
}
|
|
}
|
|
}
|
|
return .{ .current = node.forward[0] };
|
|
}
|
|
|
|
pub fn cursorRev(self: *const Memtable, end: ?[]const u8, endInclusive: bool) ReverseCursor {
|
|
var node = self.head;
|
|
var lvl: usize = self.level;
|
|
while (lvl > 0) {
|
|
lvl -= 1;
|
|
while (node.forward[lvl]) |next| {
|
|
const withinEnd = if (end) |bound|
|
|
(if (endInclusive) std.mem.order(u8, next.key, bound) != .gt else std.mem.order(u8, next.key, bound) == .lt)
|
|
else
|
|
true;
|
|
if (withinEnd) {
|
|
node = next;
|
|
} else break;
|
|
}
|
|
}
|
|
return .{ .current = if (node == self.head) null else node };
|
|
}
|
|
|
|
fn randomLevel(self: *Memtable) usize {
|
|
var lvl: usize = 1;
|
|
while (lvl < maxLevel and (self.nextRandom() & 0x3) == 0) : (lvl += 1) {}
|
|
return lvl;
|
|
}
|
|
|
|
fn nextRandom(self: *Memtable) u64 {
|
|
var x = self.rngState;
|
|
x ^= x << 13;
|
|
x ^= x >> 7;
|
|
x ^= x << 17;
|
|
self.rngState = x;
|
|
return x;
|
|
}
|
|
|
|
fn freeNode(self: *Memtable, node: *Node) void {
|
|
self.allocator.free(node.key);
|
|
self.allocator.free(node.value);
|
|
self.allocator.free(node.forward);
|
|
self.allocator.destroy(node);
|
|
}
|
|
};
|
|
|
|
pub const Cursor = struct {
|
|
current: ?*Node,
|
|
|
|
pub fn valid(self: *const Cursor) bool {
|
|
return self.current != null;
|
|
}
|
|
|
|
pub fn record(self: *const Cursor) Record {
|
|
return nodeRecord(self.current.?);
|
|
}
|
|
|
|
pub fn advance(self: *Cursor) void {
|
|
self.current = self.current.?.forward[0];
|
|
}
|
|
};
|
|
|
|
pub const ReverseCursor = struct {
|
|
current: ?*Node,
|
|
|
|
pub fn valid(self: *const ReverseCursor) bool {
|
|
return self.current != null;
|
|
}
|
|
|
|
pub fn record(self: *const ReverseCursor) Record {
|
|
return nodeRecord(self.current.?);
|
|
}
|
|
|
|
pub fn advance(self: *ReverseCursor) void {
|
|
self.current = self.current.?.backward;
|
|
}
|
|
};
|
|
|
|
fn nodeRecord(node: *Node) Record {
|
|
return .{ .key = node.key, .value = node.value, .seq = node.seq, .kind = node.kind };
|
|
}
|
|
|
|
fn makeNode(allocator: Allocator, key: []const u8, value: []const u8, seq: u64, kind: Kind, level: usize) !*Node {
|
|
const node = try allocator.create(Node);
|
|
errdefer allocator.destroy(node);
|
|
node.key = try allocator.dupe(u8, key);
|
|
errdefer allocator.free(node.key);
|
|
node.value = try allocator.dupe(u8, value);
|
|
errdefer allocator.free(node.value);
|
|
node.seq = seq;
|
|
node.kind = kind;
|
|
node.backward = null;
|
|
node.forward = try allocator.alloc(?*Node, level);
|
|
var i: usize = 0;
|
|
while (i < level) : (i += 1) node.forward[i] = null;
|
|
return node;
|
|
}
|
|
|
|
test "memtable put get" {
|
|
const allocator = std.testing.allocator;
|
|
var mt = try Memtable.init(allocator);
|
|
defer mt.deinit();
|
|
|
|
try mt.upsert(.value, 1, "banana", "yellow");
|
|
try mt.upsert(.value, 2, "apple", "red");
|
|
try mt.upsert(.value, 3, "cherry", "dark");
|
|
|
|
try std.testing.expectEqualStrings("red", mt.find("apple").?.value);
|
|
try std.testing.expectEqualStrings("yellow", mt.find("banana").?.value);
|
|
try std.testing.expectEqual(@as(?Record, null), mt.find("durian"));
|
|
try std.testing.expectEqual(@as(usize, 3), mt.count);
|
|
}
|
|
|
|
test "memtable overwrite and tombstone" {
|
|
const allocator = std.testing.allocator;
|
|
var mt = try Memtable.init(allocator);
|
|
defer mt.deinit();
|
|
|
|
try mt.upsert(.value, 1, "key", "first");
|
|
try mt.upsert(.value, 2, "key", "second");
|
|
try std.testing.expectEqualStrings("second", mt.find("key").?.value);
|
|
try std.testing.expectEqual(@as(usize, 1), mt.count);
|
|
|
|
try mt.upsert(.tombstone, 3, "key", "");
|
|
try std.testing.expectEqual(Kind.tombstone, mt.find("key").?.kind);
|
|
}
|
|
|
|
test "memtable sorted cursor" {
|
|
const allocator = std.testing.allocator;
|
|
var mt = try Memtable.init(allocator);
|
|
defer mt.deinit();
|
|
|
|
var buf: [16]u8 = undefined;
|
|
var i: usize = 200;
|
|
while (i > 0) : (i -= 1) {
|
|
const key = try std.fmt.bufPrint(&buf, "k{d:0>4}", .{i});
|
|
try mt.upsert(.value, i, key, "v");
|
|
}
|
|
|
|
var cur = mt.cursor(null);
|
|
var prev: [16]u8 = undefined;
|
|
var prevLen: usize = 0;
|
|
var seen: usize = 0;
|
|
while (cur.valid()) : (cur.advance()) {
|
|
const rec = cur.record();
|
|
if (prevLen > 0) try std.testing.expect(std.mem.order(u8, prev[0..prevLen], rec.key) == .lt);
|
|
@memcpy(prev[0..rec.key.len], rec.key);
|
|
prevLen = rec.key.len;
|
|
seen += 1;
|
|
}
|
|
try std.testing.expectEqual(@as(usize, 200), seen);
|
|
}
|
|
|
|
test "memtable cursor seek" {
|
|
const allocator = std.testing.allocator;
|
|
var mt = try Memtable.init(allocator);
|
|
defer mt.deinit();
|
|
|
|
try mt.upsert(.value, 1, "a", "1");
|
|
try mt.upsert(.value, 2, "c", "3");
|
|
try mt.upsert(.value, 3, "e", "5");
|
|
|
|
var cur = mt.cursor("b");
|
|
try std.testing.expectEqualStrings("c", cur.record().key);
|
|
}
|
|
|
|
test "memtable reverse cursor descends" {
|
|
const allocator = std.testing.allocator;
|
|
var mt = try Memtable.init(allocator);
|
|
defer mt.deinit();
|
|
|
|
var buf: [16]u8 = undefined;
|
|
var i: usize = 0;
|
|
while (i < 50) : (i += 1) {
|
|
const key = try std.fmt.bufPrint(&buf, "k{d:0>4}", .{i});
|
|
try mt.upsert(.value, i + 1, key, "v");
|
|
}
|
|
|
|
var cur = mt.cursorRev(null, false);
|
|
var expected: usize = 50;
|
|
var seen: usize = 0;
|
|
while (cur.valid()) : (cur.advance()) {
|
|
expected -= 1;
|
|
const key = try std.fmt.bufPrint(&buf, "k{d:0>4}", .{expected});
|
|
try std.testing.expectEqualStrings(key, cur.record().key);
|
|
seen += 1;
|
|
}
|
|
try std.testing.expectEqual(@as(usize, 50), seen);
|
|
|
|
var bounded = mt.cursorRev("k0010", false);
|
|
try std.testing.expectEqualStrings("k0009", bounded.record().key);
|
|
|
|
var inclusive = mt.cursorRev("k0010", true);
|
|
try std.testing.expectEqualStrings("k0010", inclusive.record().key);
|
|
}
|