Danilo Fragoso 6 달 전
부모
커밋
5cdb30e4a8
7개의 변경된 파일501개의 추가작업 그리고 10개의 파일을 삭제
  1. 4 1
      .gitignore
  2. 68 0
      command.zig
  3. 54 0
      hashing.zig
  4. 113 8
      index.zig
  5. 14 1
      makefile
  6. 141 0
      redis.zig
  7. 107 0
      storage.zig

+ 4 - 1
.gitignore

@@ -1,5 +1,8 @@
 main
+.claude
 tools/
 .db
 .dbb
-test_nov.js
+test_nov.js
+bin/
+.DS_Store

+ 68 - 0
command.zig

@@ -75,3 +75,71 @@ pub fn parse(msg: []const u8, allocator: std.mem.Allocator) ?[]const u8 {
 
     return null;
 }
+
+// -- Tests --
+
+const test_allocator = std.heap.page_allocator;
+
+test "parse write command" {
+    storage.init();
+    const result = parse("write mykey|myvalue\r\n", test_allocator) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("success", result);
+}
+
+test "parse read command" {
+    storage.init();
+    // Write first, then read
+    _ = parse("write cmd_rk|cmd_rv", test_allocator);
+    const result = parse("read cmd_rk", test_allocator) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("cmd_rv", result);
+}
+
+test "parse read nonexistent" {
+    storage.init();
+    const result = parse("read cmd_nonexistent_key", test_allocator);
+    try std.testing.expectEqualStrings("error", result.?);
+}
+
+test "parse delete command" {
+    storage.init();
+    _ = parse("write cmd_dk|cmd_dv", test_allocator);
+    const result = parse("delete cmd_dk", test_allocator) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("success", result);
+
+    // Verify deleted
+    const after = parse("read cmd_dk", test_allocator);
+    try std.testing.expectEqualStrings("error", after.?);
+}
+
+test "parse delete nonexistent" {
+    storage.init();
+    const result = parse("delete cmd_nonexistent_del", test_allocator);
+    try std.testing.expectEqualStrings("error", result.?);
+}
+
+test "parse status command" {
+    const result = parse("status", test_allocator) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("well going our operation", result);
+}
+
+test "parse unknown command returns null" {
+    try std.testing.expectEqual(@as(?[]const u8, null), parse("foobar", test_allocator));
+    try std.testing.expectEqual(@as(?[]const u8, null), parse("", test_allocator));
+}
+
+test "parse trims whitespace" {
+    const result = parse("  status \r\n", test_allocator) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("well going our operation", result);
+}
+
+test "parseKeyValue splits on pipe" {
+    const kv = parseKeyValue("hello|world") orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("hello", kv[0]);
+    try std.testing.expectEqualStrings("world", kv[1]);
+}
+
+test "parseKeyValue with multiple pipes" {
+    const kv = parseKeyValue("key|val|ue|extra") orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("key", kv[0]);
+    try std.testing.expectEqualStrings("val|ue|extra", kv[1]);
+}

+ 54 - 0
hashing.zig

@@ -35,3 +35,57 @@ pub fn djb2(key: []const u8) u32 {
 
     return hash;
 }
+
+test "fnv1a known values" {
+    // FNV-1a 32-bit test vectors
+    try std.testing.expectEqual(@as(u32, 2166136261), fnv1a(""));
+    try std.testing.expect(fnv1a("hello") != fnv1a("world"));
+    try std.testing.expect(fnv1a("hello") != fnv1a("Hello"));
+}
+
+test "fnv1a deterministic" {
+    const h1 = fnv1a("test_key");
+    const h2 = fnv1a("test_key");
+    try std.testing.expectEqual(h1, h2);
+}
+
+test "hashKey delegates to fnv1a" {
+    try std.testing.expectEqual(fnv1a("mykey"), hashKey("mykey"));
+}
+
+test "djb2 known values" {
+    try std.testing.expectEqual(@as(u32, 5381), djb2(""));
+    try std.testing.expect(djb2("hello") != djb2("world"));
+}
+
+test "djb2 deterministic" {
+    try std.testing.expectEqual(djb2("abc"), djb2("abc"));
+}
+
+test "xoramasrosas deterministic" {
+    try std.testing.expectEqual(xoramasrosas("key"), xoramasrosas("key"));
+    try std.testing.expect(xoramasrosas("a") != xoramasrosas("b"));
+}
+
+test "different hash functions produce different results" {
+    const key = "pizzakv";
+    const f = fnv1a(key);
+    const d = djb2(key);
+    const x = xoramasrosas(key);
+    // They should generally differ (not a guarantee but practically true)
+    try std.testing.expect(f != d or f != x or d != x);
+}
+
+test "hash distribution - no trivial collisions for short keys" {
+    const keys = [_][]const u8{ "a", "b", "c", "d", "e", "f", "g", "h" };
+    var hashes: [8]u32 = undefined;
+    for (keys, 0..) |k, i| {
+        hashes[i] = fnv1a(k);
+    }
+    // All hashes should be unique for single-char keys
+    for (0..8) |i| {
+        for (i + 1..8) |j| {
+            try std.testing.expect(hashes[i] != hashes[j]);
+        }
+    }
+}

+ 113 - 8
index.zig

@@ -81,19 +81,19 @@ pub fn insert(key: []const u8) void {
                     break;
                 } else {
                     const old_edge = child.edge;
-                    const common = old_edge[0..prefix_len];
-                    const child_suffix = old_edge[prefix_len..];
                     const key_suffix = remaining[prefix_len..];
 
-                    const intermediate = RadixNode.init(common);
+                    // Dupe before freeing old_edge
+                    const child_suffix = tree_allocator.dupe(u8, old_edge[prefix_len..]) catch unreachable;
+                    const intermediate = RadixNode.init(old_edge[0..prefix_len]);
 
-                    tree_allocator.free(child.edge);
-                    child.edge = tree_allocator.dupe(u8, child_suffix) catch unreachable;
+                    // Remove old entry before freeing
+                    _ = node.children.remove(old_edge);
+                    tree_allocator.free(old_edge);
 
+                    child.edge = child_suffix;
                     intermediate.children.put(child_suffix, child) catch unreachable;
-
-                    _ = node.children.remove(old_edge);
-                    node.children.put(common, intermediate) catch unreachable;
+                    node.children.put(intermediate.edge, intermediate) catch unreachable;
 
                     if (key_suffix.len == 0) {
                         intermediate.is_terminal = true;
@@ -298,3 +298,108 @@ pub fn getAllKeys(allocator: std.mem.Allocator) []const u8 {
     if (keys.len == 0) return "";
     return std.mem.join(allocator, "\n", keys) catch "";
 }
+
+// -- Tests --
+
+const test_allocator = std.heap.page_allocator;
+
+test "commonPrefixLen" {
+    try std.testing.expectEqual(@as(usize, 3), commonPrefixLen("abc", "abcdef"));
+    try std.testing.expectEqual(@as(usize, 3), commonPrefixLen("abcdef", "abc"));
+    try std.testing.expectEqual(@as(usize, 0), commonPrefixLen("abc", "xyz"));
+    try std.testing.expectEqual(@as(usize, 0), commonPrefixLen("", "abc"));
+    try std.testing.expectEqual(@as(usize, 0), commonPrefixLen("abc", ""));
+    try std.testing.expectEqual(@as(usize, 5), commonPrefixLen("hello", "hello"));
+}
+
+test "insert and searchByPrefix" {
+    insert("idx_apple");
+    insert("idx_app");
+    insert("idx_banana");
+
+    try std.testing.expect(searchByPrefix("idx_apple") != null);
+    try std.testing.expect(searchByPrefix("idx_banana") != null);
+    try std.testing.expect(searchByPrefix("idx_xyz") == null);
+}
+
+test "insert duplicate key does not crash" {
+    insert("idx_dup");
+    insert("idx_dup");
+    // Node should exist and be terminal
+    const node = searchByPrefix("idx_dup");
+    try std.testing.expect(node != null);
+    try std.testing.expect(node.?.is_terminal);
+}
+
+test "delete marks non-terminal" {
+    insert("idx_delme");
+    const node_before = searchByPrefix("idx_delme");
+    try std.testing.expect(node_before != null);
+    try std.testing.expect(node_before.?.is_terminal);
+
+    delete("idx_delme");
+
+    const node_after = searchByPrefix("idx_delme");
+    try std.testing.expect(node_after != null);
+    try std.testing.expect(!node_after.?.is_terminal);
+}
+
+test "getAllKeys returns inserted keys" {
+    insert("idx_all_a");
+    insert("idx_all_b");
+
+    const result = getAllKeys(test_allocator);
+
+    try std.testing.expect(result.len > 0);
+    try std.testing.expect(std.mem.indexOf(u8, result, "idx_all_a") != null);
+    try std.testing.expect(std.mem.indexOf(u8, result, "idx_all_b") != null);
+}
+
+test "insert empty key is no-op" {
+    insert("");
+}
+
+test "radix tree prefix splitting" {
+    insert("idx_test");
+    insert("idx_testing");
+    insert("idx_tested");
+    insert("idx_tester");
+
+    // All four keys should be findable
+    try std.testing.expect(searchByPrefix("idx_test") != null);
+    try std.testing.expect(searchByPrefix("idx_testing") != null);
+    try std.testing.expect(searchByPrefix("idx_tested") != null);
+    try std.testing.expect(searchByPrefix("idx_tester") != null);
+
+    // Verify terminals
+    const node_test = searchByPrefix("idx_test");
+    try std.testing.expect(node_test.?.is_terminal);
+    const node_testing = searchByPrefix("idx_testing");
+    try std.testing.expect(node_testing.?.is_terminal);
+}
+
+test "searchByPrefix returns null for missing prefix" {
+    try std.testing.expect(searchByPrefix("zzz_nonexistent") == null);
+}
+
+test "countKeys counts terminal nodes" {
+    ensureRoot();
+    insert("idx_cnt_a");
+    insert("idx_cnt_b");
+    insert("idx_cnt_c");
+
+    const node = searchByPrefix("idx_cnt") orelse return error.TestUnexpectedResult;
+    const count = countKeys(node);
+    try std.testing.expect(count >= 3);
+}
+
+test "getValuesByPrefix with storage" {
+    storage.init();
+    _ = storage.restore("idx_pv_key1", "val1");
+    _ = storage.restore("idx_pv_key2", "val2");
+
+    const result = getValuesByPrefix("idx_pv_key", test_allocator);
+    try std.testing.expect(result.len > 0);
+    try std.testing.expect(std.mem.indexOf(u8, result, "val1") != null);
+    try std.testing.expect(std.mem.indexOf(u8, result, "val2") != null);
+}

+ 14 - 1
makefile

@@ -26,4 +26,17 @@ clean:
 	rm -f pizzakv
 
 test:
-	node tools/test_nov.js
+	zig test hashing.zig
+	zig test redis.zig
+	zig test storage.zig
+	zig test index.zig
+	zig test command.zig
+
+bench:
+	node tools/test_nov.js
+	node tools/test_accuracy.js
+	node tools/test_comprehensive.js
+	node tools/test_concurrent.js
+	node tools/test_concurrent_reads.js
+	node tools/test_concurrent_reads_quick.js
+	node tools/test_reads_keys.js

+ 141 - 0
redis.zig

@@ -229,3 +229,144 @@ pub fn executeCommand(cmd: RedisCommand, response_buf: []u8) []const u8 {
         },
     }
 }
+
+// -- Tests --
+
+fn buildRedisArray(parts: []const []const u8) []u8 {
+    var buf: [4096]u8 = undefined;
+    var pos: usize = 0;
+
+    buf[pos] = '*';
+    pos += 1;
+    pos += formatInt(buf[pos..], parts.len);
+    buf[pos] = '\r';
+    buf[pos + 1] = '\n';
+    pos += 2;
+
+    for (parts) |part| {
+        buf[pos] = '$';
+        pos += 1;
+        pos += formatInt(buf[pos..], part.len);
+        buf[pos] = '\r';
+        buf[pos + 1] = '\n';
+        pos += 2;
+        @memcpy(buf[pos .. pos + part.len], part);
+        pos += part.len;
+        buf[pos] = '\r';
+        buf[pos + 1] = '\n';
+        pos += 2;
+    }
+
+    return buf[0..pos];
+}
+
+test "parseCommand SET" {
+    const input = buildRedisArray(&.{ "SET", "mykey", "myvalue" });
+    const result = parseCommand(input) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqual(CommandType.SET, result.cmd.cmd_type);
+    try std.testing.expectEqualStrings("mykey", result.cmd.key);
+    try std.testing.expectEqualStrings("myvalue", result.cmd.value);
+}
+
+test "parseCommand GET" {
+    const input = buildRedisArray(&.{ "GET", "mykey" });
+    const result = parseCommand(input) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqual(CommandType.GET, result.cmd.cmd_type);
+    try std.testing.expectEqualStrings("mykey", result.cmd.key);
+}
+
+test "parseCommand DEL" {
+    const input = buildRedisArray(&.{ "DEL", "mykey" });
+    const result = parseCommand(input) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqual(CommandType.DEL, result.cmd.cmd_type);
+    try std.testing.expectEqualStrings("mykey", result.cmd.key);
+}
+
+test "parseCommand case insensitive" {
+    const input = buildRedisArray(&.{ "set", "k", "v" });
+    const result = parseCommand(input) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqual(CommandType.SET, result.cmd.cmd_type);
+}
+
+test "parseCommand unknown command" {
+    const input = buildRedisArray(&.{ "FOO", "bar" });
+    const result = parseCommand(input) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqual(CommandType.UNKNOWN, result.cmd.cmd_type);
+}
+
+test "parseCommand empty input" {
+    try std.testing.expectEqual(@as(?ParseResult, null), parseCommand(""));
+}
+
+test "parseCommand malformed input" {
+    try std.testing.expectEqual(@as(?ParseResult, null), parseCommand("garbage"));
+    try std.testing.expectEqual(@as(?ParseResult, null), parseCommand("*"));
+    try std.testing.expectEqual(@as(?ParseResult, null), parseCommand("*1\r\n"));
+}
+
+test "parseCommand bytes_consumed" {
+    const input = buildRedisArray(&.{ "GET", "key1" });
+    const result = parseCommand(input) orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqual(input.len, result.bytes_consumed);
+}
+
+test "formatInt zero" {
+    var buf: [20]u8 = undefined;
+    const len = formatInt(&buf, 0);
+    try std.testing.expectEqualStrings("0", buf[0..len]);
+}
+
+test "formatInt positive" {
+    var buf: [20]u8 = undefined;
+    const len = formatInt(&buf, 12345);
+    try std.testing.expectEqualStrings("12345", buf[0..len]);
+}
+
+test "formatSimpleString" {
+    var buf: [64]u8 = undefined;
+    const result = formatSimpleString(&buf, "OK");
+    try std.testing.expectEqualStrings("+OK\r\n", result);
+}
+
+test "formatBulkString" {
+    var buf: [64]u8 = undefined;
+    const result = formatBulkString(&buf, "hello");
+    try std.testing.expectEqualStrings("$5\r\nhello\r\n", result);
+}
+
+test "formatNullBulkString" {
+    var buf: [64]u8 = undefined;
+    const result = formatNullBulkString(&buf);
+    try std.testing.expectEqualStrings("$-1\r\n", result);
+}
+
+test "formatError" {
+    var buf: [64]u8 = undefined;
+    const result = formatError(&buf, "ERR bad");
+    try std.testing.expectEqualStrings("-ERR bad\r\n", result);
+}
+
+test "formatInteger positive" {
+    var buf: [64]u8 = undefined;
+    const result = formatInteger(&buf, 42);
+    try std.testing.expectEqualStrings(":42\r\n", result);
+}
+
+test "formatInteger zero" {
+    var buf: [64]u8 = undefined;
+    const result = formatInteger(&buf, 0);
+    try std.testing.expectEqualStrings(":0\r\n", result);
+}
+
+test "formatInteger negative" {
+    var buf: [64]u8 = undefined;
+    const result = formatInteger(&buf, -7);
+    try std.testing.expectEqualStrings(":-7\r\n", result);
+}
+
+test "executeCommand UNKNOWN" {
+    var buf: [256]u8 = undefined;
+    const cmd = RedisCommand{ .cmd_type = .UNKNOWN, .key = "", .value = "" };
+    const result = executeCommand(cmd, &buf);
+    try std.testing.expectEqualStrings("-ERR unknown command\r\n", result);
+}

+ 107 - 0
storage.zig

@@ -199,3 +199,110 @@ pub fn delete(key: []const u8) bool {
 
     return deleted;
 }
+
+// -- Tests --
+
+test "writeVolatile and read basic" {
+    init();
+    const hash = hashing.hashKey("test_key");
+    const shard_idx = getShardIndex(hash);
+
+    shards[shard_idx].rwlock.lock();
+    _ = writeVolatile(hash, "test_key", "test_value");
+    shards[shard_idx].rwlock.unlock();
+
+    const val = read("test_key") orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("test_value", val);
+}
+
+test "writeVolatile overwrites existing key" {
+    init();
+    const hash = hashing.hashKey("overwrite_key");
+    const shard_idx = getShardIndex(hash);
+
+    shards[shard_idx].rwlock.lock();
+    _ = writeVolatile(hash, "overwrite_key", "first");
+    shards[shard_idx].rwlock.unlock();
+
+    shards[shard_idx].rwlock.lock();
+    _ = writeVolatile(hash, "overwrite_key", "second");
+    shards[shard_idx].rwlock.unlock();
+
+    const val = read("overwrite_key") orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("second", val);
+}
+
+test "read nonexistent key returns null" {
+    init();
+    try std.testing.expectEqual(@as(?[]const u8, null), read("no_such_key_xyz"));
+}
+
+test "deleteVolatile removes entry" {
+    init();
+    const hash = hashing.hashKey("del_key");
+    const shard_idx = getShardIndex(hash);
+
+    shards[shard_idx].rwlock.lock();
+    _ = writeVolatile(hash, "del_key", "val");
+    shards[shard_idx].rwlock.unlock();
+
+    try std.testing.expect(read("del_key") != null);
+
+    shards[shard_idx].rwlock.lock();
+    const deleted = deleteVolatile(hash, "del_key");
+    shards[shard_idx].rwlock.unlock();
+
+    try std.testing.expect(deleted);
+    try std.testing.expectEqual(@as(?[]const u8, null), read("del_key"));
+}
+
+test "deleteVolatile nonexistent key returns false" {
+    init();
+    const hash = hashing.hashKey("ghost_key");
+    const shard_idx = getShardIndex(hash);
+
+    shards[shard_idx].rwlock.lock();
+    const deleted = deleteVolatile(hash, "ghost_key");
+    shards[shard_idx].rwlock.unlock();
+
+    try std.testing.expect(!deleted);
+}
+
+test "multiple keys in same shard" {
+    init();
+    // Write several keys and verify they don't interfere
+    const keys = [_][]const u8{ "shard_a", "shard_b", "shard_c" };
+    const vals = [_][]const u8{ "val_a", "val_b", "val_c" };
+
+    for (keys, vals) |k, v| {
+        const hash = hashing.hashKey(k);
+        const shard_idx = getShardIndex(hash);
+        shards[shard_idx].rwlock.lock();
+        _ = writeVolatile(hash, k, v);
+        shards[shard_idx].rwlock.unlock();
+    }
+
+    for (keys, vals) |k, v| {
+        const val = read(k) orelse return error.TestUnexpectedResult;
+        try std.testing.expectEqualStrings(v, val);
+    }
+}
+
+test "empty key and value" {
+    init();
+    const hash = hashing.hashKey("");
+    const shard_idx = getShardIndex(hash);
+
+    shards[shard_idx].rwlock.lock();
+    _ = writeVolatile(hash, "", "");
+    shards[shard_idx].rwlock.unlock();
+
+    const val = read("") orelse return error.TestUnexpectedResult;
+    try std.testing.expectEqualStrings("", val);
+}
+
+test "getShardIndex stays in bounds" {
+    try std.testing.expect(getShardIndex(0) < NUM_SHARDS);
+    try std.testing.expect(getShardIndex(std.math.maxInt(u32)) < NUM_SHARDS);
+    try std.testing.expect(getShardIndex(12345) < NUM_SHARDS);
+}