From d3d9c4e7e59c22569f5522ea3e50dd14b5ef5c1b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 14:17:49 +0200 Subject: [PATCH] feat: flesh out protocol --- src/protocol.zig | 154 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/protocol.zig b/src/protocol.zig index 67f6da6..16aeb96 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -2829,3 +2829,157 @@ test "decodeAggGroupRows rejects a corrupted second group without leaking the fi defer buf.deinit(); const w = encode.ByteWriter.init(&buf); + var goodGroup = rowmod.Row.init(allocator); + defer goodGroup.deinit(); + // Use rowmod.encodeValue directly for a real, well-formed first group so + // the corruption below is unambiguously in the SECOND group only. + const goodGroupValue = try rowmod.encodeValue(allocator, .{ .text = "warsaw" }); + defer allocator.free(goodGroupValue); + const goodMemberValue = try rowmod.encodeValue(allocator, .{ .int = 1 }); + defer allocator.free(goodMemberValue); + + try w.putU32(2); // claims two groups, only one is well-formed + try putLenPrefixed(w, goodGroupValue); + try w.putU32(1); + try putLenPrefixed(w, goodMemberValue); + // Second group: a length-prefixed blob whose declared value tag is + // invalid - `takeDecodedValue` must surface `error.Malformed` here + // rather than fabricating a bogus `Value`. + try putLenPrefixed(w, &[_]u8{99}); + + try std.testing.expectError(error.Malformed, decodeAggGroupRows(allocator, buf.items)); +} + +test "table_explain args round trip a table and an expr" { + const allocator = std.testing.allocator; + const payload = try encodeTableExplain(allocator, "users", "city eq s:warsaw"); + defer allocator.free(payload); + const args = try decodeTableExplain(payload); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("city eq s:warsaw", args.exprText); +} + +test "table_explain args round trip an empty expr" { + const allocator = std.testing.allocator; + const payload = try encodeTableExplain(allocator, "users", ""); + defer allocator.free(payload); + const args = try decodeTableExplain(payload); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("", args.exprText); +} + +test "table_update args round trip a table, set fields, and an expr" { + const allocator = std.testing.allocator; + const sets = [_]rowmod.Field{ + .{ .name = "active", .value = .{ .int = 1 } }, + .{ .name = "city", .value = .{ .text = "krakow" } }, + }; + const payload = try encodeTableUpdate(allocator, "users", &sets, "city eq s:warsaw"); + defer allocator.free(payload); + const args = try decodeTableUpdate(payload); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("city eq s:warsaw", args.exprText); + + var decoded = try rowmod.Row.decode(allocator, args.setsBytes); + defer decoded.deinit(); + try std.testing.expectEqual(rowmod.Value{ .int = 1 }, decoded.get("active").?); + try std.testing.expectEqualStrings("krakow", decoded.get("city").?.text); +} + +test "table_update args round trip an empty expr, updating every row" { + const allocator = std.testing.allocator; + const sets = [_]rowmod.Field{.{ .name = "seen", .value = .{ .int = 1 } }}; + const payload = try encodeTableUpdate(allocator, "users", &sets, ""); + defer allocator.free(payload); + const args = try decodeTableUpdate(payload); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("", args.exprText); + + var decoded = try rowmod.Row.decode(allocator, args.setsBytes); + defer decoded.deinit(); + try std.testing.expectEqual(@as(usize, 1), decoded.count()); +} + +test "table_delete args round trip a table and an expr" { + const allocator = std.testing.allocator; + const payload = try encodeTableDelete(allocator, "users", "city eq s:warsaw"); + defer allocator.free(payload); + const args = try decodeTableDelete(payload); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("city eq s:warsaw", args.exprText); +} + +test "table_delete args round trip an empty expr, deleting every row" { + const allocator = std.testing.allocator; + const payload = try encodeTableDelete(allocator, "users", ""); + defer allocator.free(payload); + const args = try decodeTableDelete(payload); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqualStrings("", args.exprText); +} + +test "table_export args round trip a table and each format" { + const allocator = std.testing.allocator; + + const csvPayload = try encodeTableExport(allocator, "users", .csv); + defer allocator.free(csvPayload); + const csvArgs = try decodeTableExport(csvPayload); + try std.testing.expectEqualStrings("users", csvArgs.table); + try std.testing.expectEqual(DumpFormat.csv, csvArgs.format); + + const jsonPayload = try encodeTableExport(allocator, "users", .json); + defer allocator.free(jsonPayload); + const jsonArgs = try decodeTableExport(jsonPayload); + try std.testing.expectEqualStrings("users", jsonArgs.table); + try std.testing.expectEqual(DumpFormat.json, jsonArgs.format); +} + +test "table_export rejects a format byte outside the DumpFormat enum" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try putLenPrefixed(w, "users"); + try w.byte(99); + try std.testing.expectError(error.Malformed, decodeTableExport(buf.items)); +} + +test "export result round trips a row count and content bytes" { + const allocator = std.testing.allocator; + const payload = try encodeExportResult(allocator, 3, "__pk\ni:1\ni:2\ni:3\n"); + defer allocator.free(payload); + const result = try decodeExportResult(payload); + try std.testing.expectEqual(@as(u64, 3), result.rowCount); + try std.testing.expectEqualStrings("__pk\ni:1\ni:2\ni:3\n", result.content); +} + +test "export result round trips a zero row count with empty content" { + const allocator = std.testing.allocator; + const payload = try encodeExportResult(allocator, 0, ""); + defer allocator.free(payload); + const result = try decodeExportResult(payload); + try std.testing.expectEqual(@as(u64, 0), result.rowCount); + try std.testing.expectEqualStrings("", result.content); +} + +test "table_import args round trip a table, format, and content" { + const allocator = std.testing.allocator; + const payload = try encodeTableImport(allocator, "users", .json, "[{\"__pk\": \"i:1\"}]"); + defer allocator.free(payload); + const args = try decodeTableImport(payload); + try std.testing.expectEqualStrings("users", args.table); + try std.testing.expectEqual(DumpFormat.json, args.format); + try std.testing.expectEqualStrings("[{\"__pk\": \"i:1\"}]", args.content); +} + +test "table_import rejects a format byte outside the DumpFormat enum" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try putLenPrefixed(w, "users"); + try w.byte(7); + try w.slice("ignored"); + try std.testing.expectError(error.Malformed, decodeTableImport(buf.items)); +} +