diff --git a/src/protocol.zig b/src/protocol.zig index dee7524..8b2610a 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -2355,3 +2355,162 @@ test "entries round trip and are independently owned" { const payload = try encodeEntries(allocator, &entries); defer allocator.free(payload); + const owned = try decodeEntries(allocator, payload); + defer freeOwnedEntries(allocator, owned); + + try std.testing.expectEqual(@as(usize, 2), owned.len); + try std.testing.expectEqualStrings("a", owned[0].key); + try std.testing.expectEqualStrings("1", owned[0].value); + try std.testing.expectEqualStrings("b", owned[1].key); + try std.testing.expectEqualStrings("2", owned[1].value); +} + +test "empty entry list round trips" { + const allocator = std.testing.allocator; + const payload = try encodeEntries(allocator, &[_]dbmod.Entry{}); + defer allocator.free(payload); + const owned = try decodeEntries(allocator, payload); + defer freeOwnedEntries(allocator, owned); + try std.testing.expectEqual(@as(usize, 0), owned.len); +} + +test "batch ops round trip put and delete" { + const allocator = std.testing.allocator; + const ops = [_]BatchOp{ + .{ .put = .{ .key = "a", .value = "1" } }, + .{ .del = "b" }, + }; + const payload = try encodeBatch(allocator, &ops); + defer allocator.free(payload); + + const decoded = try decodeBatch(allocator, payload); + defer allocator.free(decoded); + + try std.testing.expectEqual(@as(usize, 2), decoded.len); + try std.testing.expectEqualStrings("a", decoded[0].put.key); + try std.testing.expectEqualStrings("1", decoded[0].put.value); + try std.testing.expectEqualStrings("b", decoded[1].del); +} + +test "decodeBatch rejects an unknown op kind" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.putU32(1); + try w.byte(9); + try std.testing.expectError(error.Malformed, decodeBatch(allocator, buf.items)); +} + +test "stats round trip preserves all fields including per-level detail" { + const allocator = std.testing.allocator; + var stats = dbmod.Stats{}; + stats.memtableEntries = 3; + stats.memtableBytes = 128; + stats.sstableCount = 2; + stats.liveEntries = 100; + stats.physicalBytes = 4096; + stats.cacheHits = 10; + stats.cacheMisses = 4; + stats.nextSeq = 55; + stats.levels[0] = .{ .tables = 1, .entries = 50, .physicalBytes = 2048 }; + stats.levels[3] = .{ .tables = 2, .entries = 50, .physicalBytes = 2048 }; + + const payload = try encodeStats(allocator, stats); + defer allocator.free(payload); + const decoded = try decodeStats(payload); + + try std.testing.expectEqual(stats.memtableEntries, decoded.memtableEntries); + try std.testing.expectEqual(stats.nextSeq, decoded.nextSeq); + try std.testing.expectEqual(stats.levels[0].tables, decoded.levels[0].tables); + try std.testing.expectEqual(stats.levels[3].entries, decoded.levels[3].entries); + try std.testing.expectEqual(@as(f64, decoded.cacheHitRate()), decoded.cacheHitRate()); +} + +test "table select args round trip with a non-empty expr" { + const allocator = std.testing.allocator; + const payload = try encodeTableSelect(allocator, "users", "city eq s:warsaw", 0, 0, &.{}); + defer allocator.free(payload); + const args = try decodeTableSelect(allocator, payload); + defer allocator.free(args.orderBy); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("city eq s:warsaw", args.exprText); + try std.testing.expectEqual(@as(u32, 0), args.offset); + try std.testing.expectEqual(@as(u32, 0), args.limit); + try std.testing.expectEqual(@as(usize, 0), args.orderBy.len); +} + +test "table select args round trip with an empty expr means select all" { + const allocator = std.testing.allocator; + const payload = try encodeTableSelect(allocator, "users", "", 0, 0, &.{}); + defer allocator.free(payload); + const args = try decodeTableSelect(allocator, payload); + defer allocator.free(args.orderBy); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("", args.exprText); +} + +test "table select args round trip with a page window" { + const allocator = std.testing.allocator; + const payload = try encodeTableSelect(allocator, "users", "age gt i:10", 5, 3, &.{}); + defer allocator.free(payload); + const args = try decodeTableSelect(allocator, payload); + defer allocator.free(args.orderBy); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("age gt i:10", args.exprText); + try std.testing.expectEqual(@as(u32, 5), args.offset); + try std.testing.expectEqual(@as(u32, 3), args.limit); +} + +test "table select args round trip with an ascending order-by" { + const allocator = std.testing.allocator; + const payload = try encodeTableSelect(allocator, "users", "", 0, 0, &.{.{ .field = "age" }}); + defer allocator.free(payload); + const args = try decodeTableSelect(allocator, payload); + defer allocator.free(args.orderBy); + try std.testing.expectEqual(@as(usize, 1), args.orderBy.len); + try std.testing.expectEqualStrings("age", args.orderBy[0].field); + try std.testing.expectEqual(false, args.orderBy[0].descending); +} + +test "table select args round trip with a descending order-by, page, and expr all together" { + const allocator = std.testing.allocator; + const payload = try encodeTableSelect(allocator, "users", "city eq s:warsaw", 2, 5, &.{.{ .field = "score", .descending = true }}); + defer allocator.free(payload); + const args = try decodeTableSelect(allocator, payload); + defer allocator.free(args.orderBy); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("city eq s:warsaw", args.exprText); + try std.testing.expectEqual(@as(u32, 2), args.offset); + try std.testing.expectEqual(@as(u32, 5), args.limit); + try std.testing.expectEqualStrings("score", args.orderBy[0].field); + try std.testing.expectEqual(true, args.orderBy[0].descending); +} + +test "table select args round trip with two order-by columns in given order" { + const allocator = std.testing.allocator; + const payload = try encodeTableSelect(allocator, "users", "", 0, 0, &.{ .{ .field = "city" }, .{ .field = "score", .descending = true } }); + defer allocator.free(payload); + const args = try decodeTableSelect(allocator, payload); + defer allocator.free(args.orderBy); + try std.testing.expectEqual(@as(usize, 2), args.orderBy.len); + try std.testing.expectEqualStrings("city", args.orderBy[0].field); + try std.testing.expectEqual(false, args.orderBy[0].descending); + try std.testing.expectEqualStrings("score", args.orderBy[1].field); + try std.testing.expectEqual(true, args.orderBy[1].descending); +} + +test "decodeTableSelect rejects an order-by count claiming more columns than were actually encoded" { + const allocator = std.testing.allocator; + const payload = try encodeTableSelect(allocator, "users", "", 0, 0, &.{.{ .field = "age" }}); + defer allocator.free(payload); + // The count byte sits right after the two u32 page fields: 4 (table + // len prefix) + 5 ("users") + 4 (offset) + 4 (limit). + const countBytePos = 4 + 5 + 4 + 4; + try std.testing.expectEqual(@as(u8, 1), payload[countBytePos]); + var corrupted = try allocator.dupe(u8, payload); + defer allocator.free(corrupted); + corrupted[countBytePos] = 200; + try std.testing.expectError(error.Malformed, decodeTableSelect(allocator, corrupted)); +} +