From ffc4fadc47c1e5d26685622e6c8d3350f16f96d1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 09:00:00 +0200 Subject: [PATCH] test(store): add point, flush, recovery, and batch tests --- src/db.zig | 263 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) diff --git a/src/db.zig b/src/db.zig index 2ac1d26..d734ab9 100644 --- a/src/db.zig +++ b/src/db.zig @@ -885,3 +885,266 @@ fn parseSstId(name: []const u8) ?u64 { return std.fmt.parseInt(u64, digits, 10) catch null; } +fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator, options: Options) !Db { + var pathBuf: [std.fs.max_path_bytes]u8 = undefined; + const base = try tmp.dir.realpath(".", &pathBuf); + var dbPathBuf: [std.fs.max_path_bytes]u8 = undefined; + const dbPath = try std.fmt.bufPrint(&dbPathBuf, "{s}/store", .{base}); + return Db.open(allocator, dbPath, options); +} + +test "put get delete" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + var db = try openTemp(&tmp, allocator, .{}); + defer db.close(); + + try db.put("apple", "red"); + try db.put("banana", "yellow"); + + const apple = (try db.get(allocator, "apple")).?; + defer allocator.free(apple); + try std.testing.expectEqualStrings("red", apple); + + try db.put("apple", "green"); + const apple2 = (try db.get(allocator, "apple")).?; + defer allocator.free(apple2); + try std.testing.expectEqualStrings("green", apple2); + + try db.delete("apple"); + try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "apple")); + + const banana = (try db.get(allocator, "banana")).?; + defer allocator.free(banana); + try std.testing.expectEqualStrings("yellow", banana); +} + +test "wal recovery after reopen" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + { + var db = try openTemp(&tmp, allocator, .{}); + defer db.close(); + try db.put("city", "warsaw"); + try db.put("river", "vistula"); + try db.delete("city"); + try std.testing.expectEqual(@as(usize, 0), db.sstables.items.len); + } + + var db = try openTemp(&tmp, allocator, .{}); + defer db.close(); + try std.testing.expectEqual(@as(usize, 0), db.sstables.items.len); + try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "city")); + const river = (try db.get(allocator, "river")).?; + defer allocator.free(river); + try std.testing.expectEqualStrings("vistula", river); +} + +test "flush to sstable and reopen" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + { + var db = try openTemp(&tmp, allocator, .{ .flushThreshold = 64 }); + defer db.close(); + var buf: [16]u8 = undefined; + var i: usize = 0; + while (i < 200) : (i += 1) { + const key = try std.fmt.bufPrint(&buf, "k{d:0>5}", .{i}); + try db.put(key, "value-payload"); + } + try std.testing.expect(db.sstables.items.len > 0); + } + + var db = try openTemp(&tmp, allocator, .{ .flushThreshold = 64 }); + defer db.close(); + var buf: [16]u8 = undefined; + var i: usize = 0; + while (i < 200) : (i += 1) { + const key = try std.fmt.bufPrint(&buf, "k{d:0>5}", .{i}); + const got = (try db.get(allocator, key)).?; + defer allocator.free(got); + try std.testing.expectEqualStrings("value-payload", got); + } +} + +test "compaction dedups and drops tombstones" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + var db = try openTemp(&tmp, allocator, .{ .flushThreshold = 1 }); + defer db.close(); + + try db.put("a", "1"); + try db.put("b", "2"); + try db.put("c", "3"); + try db.put("d", "4"); + try db.put("a", "11"); + try db.delete("c"); + try db.flush(); + + try std.testing.expect(db.sstables.items.len > 1); + try db.compact(); + try std.testing.expectEqual(@as(usize, 1), db.sstables.items.len); + + const a = (try db.get(allocator, "a")).?; + defer allocator.free(a); + try std.testing.expectEqualStrings("11", a); + + const b = (try db.get(allocator, "b")).?; + defer allocator.free(b); + try std.testing.expectEqualStrings("2", b); + + try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "c")); + + const d = (try db.get(allocator, "d")).?; + defer allocator.free(d); + try std.testing.expectEqualStrings("4", d); +} + +test "scan merges memtable and sstable with tombstones" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + var db = try openTemp(&tmp, allocator, .{}); + defer db.close(); + + try db.put("a", "1"); + try db.put("b", "2"); + try db.put("c", "3"); + try db.put("d", "4"); + try db.put("e", "5"); + try db.flush(); + + try db.put("c", "cNew"); + try db.delete("b"); + + var iter = try db.scan(allocator, null, null); + defer iter.deinit(); + + const expectedKeys = [_][]const u8{ "a", "c", "d", "e" }; + const expectedVals = [_][]const u8{ "1", "cNew", "4", "5" }; + var idx: usize = 0; + while (try iter.next()) |item| { + try std.testing.expect(idx < expectedKeys.len); + try std.testing.expectEqualStrings(expectedKeys[idx], item.key); + try std.testing.expectEqualStrings(expectedVals[idx], item.value); + idx += 1; + } + try std.testing.expectEqual(expectedKeys.len, idx); +} + +test "scan honors range bounds" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + var db = try openTemp(&tmp, allocator, .{ .flushThreshold = 32 }); + defer db.close(); + + var keyBuf: [8]u8 = undefined; + var i: usize = 0; + while (i < 26) : (i += 1) { + const key = try std.fmt.bufPrint(&keyBuf, "{c}", .{@as(u8, 'a') + @as(u8, @intCast(i))}); + try db.put(key, "x"); + } + + var iter = try db.scan(allocator, "d", "h"); + defer iter.deinit(); + const expected = [_][]const u8{ "d", "e", "f", "g" }; + var idx: usize = 0; + while (try iter.next()) |item| { + try std.testing.expect(idx < expected.len); + try std.testing.expectEqualStrings(expected[idx], item.key); + idx += 1; + } + try std.testing.expectEqual(expected.len, idx); +} + +test "snapshot reads pre-overwrite value" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + var db = try openTemp(&tmp, allocator, .{}); + defer db.close(); + + try db.put("color", "red"); + try db.put("shape", "circle"); + + var snap = try db.snapshot(); + + try db.put("color", "green"); + try db.delete("shape"); + try db.put("color", "blue"); + + const snapColor = (try snap.get(allocator, "color")).?; + defer allocator.free(snapColor); + try std.testing.expectEqualStrings("red", snapColor); + + const snapShape = (try snap.get(allocator, "shape")).?; + defer allocator.free(snapShape); + try std.testing.expectEqualStrings("circle", snapShape); + + const liveColor = (try db.get(allocator, "color")).?; + defer allocator.free(liveColor); + try std.testing.expectEqualStrings("blue", liveColor); + + try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "shape")); + + var snapIter = try snap.scan(allocator, null, null); + defer snapIter.deinit(); + var snapCount: usize = 0; + while (try snapIter.next()) |entry| { + if (std.mem.eql(u8, entry.key, "color")) try std.testing.expectEqualStrings("red", entry.value); + snapCount += 1; + } + try std.testing.expectEqual(@as(usize, 2), snapCount); +} + +test "write batch applies all or nothing" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + { + var db = try openTemp(&tmp, allocator, .{}); + defer db.close(); + + try db.put("existing", "keep"); + const seqBefore = db.nextSeq; + + var batch = WriteBatch.init(allocator); + defer batch.deinit(); + try batch.put("one", "1"); + try batch.put("two", "2"); + try batch.put("three", "3"); + try batch.delete("existing"); + try db.apply(&batch); + + try std.testing.expectEqual(seqBefore + 4, db.nextSeq); + + const one = (try db.get(allocator, "one")).?; + defer allocator.free(one); + try std.testing.expectEqualStrings("1", one); + const three = (try db.get(allocator, "three")).?; + defer allocator.free(three); + try std.testing.expectEqualStrings("3", three); + try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "existing")); + } + + var db = try openTemp(&tmp, allocator, .{}); + defer db.close(); + const two = (try db.get(allocator, "two")).?; + defer allocator.free(two); + try std.testing.expectEqualStrings("2", two); + try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "existing")); +} +