feat(cli): add command-line entry point
This commit is contained in:
1 parent
66a2a7d29f
commit
e649582f81
1 file changed
+191
+191
@@ -0,0 +1,191 @@
|
||||
const std = @import("std");
|
||||
const cobalt = @import("root.zig");
|
||||
|
||||
const usage =
|
||||
\\cobalt - LSM key/value store
|
||||
\\
|
||||
\\usage: cobalt <dir> <command> [args]
|
||||
\\
|
||||
\\commands:
|
||||
\\ put <key> <value> store a value
|
||||
\\ get <key> print a value or (nil)
|
||||
\\ del <key> delete a key
|
||||
\\ scan [start] [end] forward range scan, start inclusive, end exclusive
|
||||
\\ rscan [start] [end] reverse range scan
|
||||
\\ prefix <prefix> scan every key under a prefix
|
||||
\\ compact run full leveled compaction
|
||||
\\ stats print database statistics
|
||||
\\ verify <n> write n keys, reopen, check durability and scan order
|
||||
\\
|
||||
;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
|
||||
const argv = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, argv);
|
||||
|
||||
if (argv.len < 3) {
|
||||
try stderr.writeAll(usage);
|
||||
std.process.exit(2);
|
||||
}
|
||||
|
||||
const dir = argv[1];
|
||||
const command = argv[2];
|
||||
const rest = argv[3..];
|
||||
|
||||
if (std.mem.eql(u8, command, "verify")) {
|
||||
const count = if (rest.len >= 1) std.fmt.parseInt(usize, rest[0], 10) catch 1000 else 1000;
|
||||
try runVerify(allocator, stdout, dir, count);
|
||||
return;
|
||||
}
|
||||
|
||||
var db = try cobalt.Db.open(allocator, dir, .{});
|
||||
defer db.close();
|
||||
|
||||
if (std.mem.eql(u8, command, "put")) {
|
||||
if (rest.len < 2) return fail(stderr, "put needs <key> <value>");
|
||||
try db.put(rest[0], rest[1]);
|
||||
try db.maybeCompact();
|
||||
} else if (std.mem.eql(u8, command, "get")) {
|
||||
if (rest.len < 1) return fail(stderr, "get needs <key>");
|
||||
if (try db.get(allocator, rest[0])) |value| {
|
||||
defer allocator.free(value);
|
||||
try stdout.print("{s}\n", .{value});
|
||||
} else {
|
||||
try stdout.writeAll("(nil)\n");
|
||||
}
|
||||
} else if (std.mem.eql(u8, command, "del")) {
|
||||
if (rest.len < 1) return fail(stderr, "del needs <key>");
|
||||
try db.delete(rest[0]);
|
||||
try db.maybeCompact();
|
||||
} else if (std.mem.eql(u8, command, "scan")) {
|
||||
const start: ?[]const u8 = if (rest.len >= 1) rest[0] else null;
|
||||
const end: ?[]const u8 = if (rest.len >= 2) rest[1] else null;
|
||||
var iter = try db.scan(allocator, start, end);
|
||||
defer iter.deinit();
|
||||
while (try iter.next()) |entry| try stdout.print("{s}\t{s}\n", .{ entry.key, entry.value });
|
||||
} else if (std.mem.eql(u8, command, "rscan")) {
|
||||
const start: ?[]const u8 = if (rest.len >= 1) rest[0] else null;
|
||||
const end: ?[]const u8 = if (rest.len >= 2) rest[1] else null;
|
||||
var iter = try db.scanReverse(allocator, .{ .start = start, .end = end });
|
||||
defer iter.deinit();
|
||||
while (try iter.next()) |entry| try stdout.print("{s}\t{s}\n", .{ entry.key, entry.value });
|
||||
} else if (std.mem.eql(u8, command, "prefix")) {
|
||||
if (rest.len < 1) return fail(stderr, "prefix needs <prefix>");
|
||||
var iter = try db.scanPrefix(allocator, rest[0]);
|
||||
defer iter.deinit();
|
||||
while (try iter.next()) |entry| try stdout.print("{s}\t{s}\n", .{ entry.key, entry.value });
|
||||
} else if (std.mem.eql(u8, command, "compact")) {
|
||||
try db.compact();
|
||||
try stdout.print("sstables after compaction: {d}\n", .{db.sstables.items.len});
|
||||
} else if (std.mem.eql(u8, command, "stats")) {
|
||||
try db.stats().print(stdout);
|
||||
} else {
|
||||
try stderr.print("unknown command: {s}\n", .{command});
|
||||
try stderr.writeAll(usage);
|
||||
std.process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(stderr: anytype, message: []const u8) !void {
|
||||
try stderr.print("error: {s}\n", .{message});
|
||||
std.process.exit(2);
|
||||
}
|
||||
|
||||
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {
|
||||
std.fs.cwd().deleteTree(dir) catch {};
|
||||
|
||||
var reference = std.ArrayList([]u8).init(allocator);
|
||||
defer {
|
||||
for (reference.items) |value| allocator.free(value);
|
||||
reference.deinit();
|
||||
}
|
||||
|
||||
{
|
||||
var db = try cobalt.Db.open(allocator, dir, .{ .flushThreshold = 16 * 1024, .sstableTargetBytes = 8 * 1024 });
|
||||
defer db.close();
|
||||
var keyBuf: [24]u8 = undefined;
|
||||
var valBuf: [32]u8 = undefined;
|
||||
var i: usize = 0;
|
||||
while (i < count) : (i += 1) {
|
||||
const key = try std.fmt.bufPrint(&keyBuf, "key:{d:0>10}", .{i});
|
||||
const value = try std.fmt.bufPrint(&valBuf, "value-{d:0>10}-payload", .{i});
|
||||
try db.put(key, value);
|
||||
try reference.append(try allocator.dupe(u8, value));
|
||||
}
|
||||
try db.maybeCompact();
|
||||
try stdout.print("wrote {d} keys, sstables on disk: {d}\n", .{ count, db.sstables.items.len });
|
||||
}
|
||||
|
||||
try stdout.writeAll("process state dropped, reopening from disk...\n");
|
||||
|
||||
var db = try cobalt.Db.open(allocator, dir, .{ .flushThreshold = 16 * 1024, .sstableTargetBytes = 8 * 1024 });
|
||||
defer db.close();
|
||||
|
||||
var readOk: usize = 0;
|
||||
var keyBuf: [24]u8 = undefined;
|
||||
var i: usize = 0;
|
||||
while (i < count) : (i += 1) {
|
||||
const key = try std.fmt.bufPrint(&keyBuf, "key:{d:0>10}", .{i});
|
||||
const got = (try db.get(allocator, key)) orelse {
|
||||
try stdout.print("FAIL: missing key {s} after restart\n", .{key});
|
||||
std.process.exit(1);
|
||||
};
|
||||
defer allocator.free(got);
|
||||
if (!std.mem.eql(u8, got, reference.items[i])) {
|
||||
try stdout.print("FAIL: value mismatch for {s}\n", .{key});
|
||||
std.process.exit(1);
|
||||
}
|
||||
readOk += 1;
|
||||
}
|
||||
|
||||
var scanned: usize = 0;
|
||||
var prev: [24]u8 = undefined;
|
||||
var prevLen: usize = 0;
|
||||
var ordered = true;
|
||||
var iter = try db.scan(allocator, null, null);
|
||||
defer iter.deinit();
|
||||
while (try iter.next()) |entry| {
|
||||
if (prevLen > 0 and !std.mem.lessThan(u8, prev[0..prevLen], entry.key)) ordered = false;
|
||||
@memcpy(prev[0..entry.key.len], entry.key);
|
||||
prevLen = entry.key.len;
|
||||
scanned += 1;
|
||||
}
|
||||
|
||||
var rev = try db.scanReverse(allocator, .{});
|
||||
defer rev.deinit();
|
||||
var reverseCount: usize = 0;
|
||||
var revPrevLen: usize = 0;
|
||||
var revPrev: [24]u8 = undefined;
|
||||
var reverseOrdered = true;
|
||||
while (try rev.next()) |entry| {
|
||||
if (revPrevLen > 0 and !std.mem.lessThan(u8, entry.key, revPrev[0..revPrevLen])) reverseOrdered = false;
|
||||
@memcpy(revPrev[0..entry.key.len], entry.key);
|
||||
revPrevLen = entry.key.len;
|
||||
reverseCount += 1;
|
||||
}
|
||||
|
||||
const rangeLow = "key:0000000100";
|
||||
const rangeHigh = "key:0000000110";
|
||||
var rangeCount: usize = 0;
|
||||
if (count > 110) {
|
||||
var range = try db.scan(allocator, rangeLow, rangeHigh);
|
||||
defer range.deinit();
|
||||
while (try range.next()) |_| rangeCount += 1;
|
||||
}
|
||||
|
||||
try stdout.print("point reads ok: {d}/{d}\n", .{ readOk, count });
|
||||
try stdout.print("forward scan: {d} entries, sortedOrder={}\n", .{ scanned, ordered });
|
||||
try stdout.print("reverse scan: {d} entries, descendingOrder={}\n", .{ reverseCount, reverseOrdered });
|
||||
if (count > 110) try stdout.print("range [100,110): {d} entries (expected 10)\n", .{rangeCount});
|
||||
|
||||
const pass = readOk == count and scanned == count and reverseCount == count and ordered and reverseOrdered and (count <= 110 or rangeCount == 10);
|
||||
try stdout.print("\nRESULT: {s}\n", .{if (pass) "PASS - all keys durable, scans consistent with reference" else "FAIL"});
|
||||
if (!pass) std.process.exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user