diff --git a/src/protocol.zig b/src/protocol.zig index ec2fb1d..237f58f 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -3452,3 +3452,164 @@ test "empty txn_op entry list round trips" { try std.testing.expectEqual(@as(usize, 0), decoded.len); } +test "decodeTxnOp rejects an unknown verb byte" { + 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, decodeTxnOp(allocator, buf.items)); +} + +test "txn_op result round trips the applied count" { + const allocator = std.testing.allocator; + const payload = try encodeTxnResult(allocator, 7); + defer allocator.free(payload); + try std.testing.expectEqual(@as(u32, 7), try decodeTxnResult(payload)); +} + +test "table_window args round trip a ranking spec with a partition, an order-by, and an expr" { + const allocator = std.testing.allocator; + const orderBy = [_]querymod.OrderBy{.{ .field = "day", .descending = false }}; + const partitionBy = [_][]const u8{"city"}; + const specs = [_]windowmod.WindowSpec{.{ .field = "", .kind = .row_number }}; + const payload = try encodeTableWindow(allocator, "orders", &specs, &partitionBy, &orderBy, 0, 0, "total gt i:10", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqualStrings("orders", args.table); + try std.testing.expectEqual(@as(usize, 1), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.row_number, args.specs[0].kind); + try std.testing.expectEqualStrings("", args.specs[0].field); + try std.testing.expectEqual(@as(usize, 1), args.partitionBy.len); + try std.testing.expectEqualStrings("city", args.partitionBy[0]); + try std.testing.expectEqual(@as(usize, 1), args.orderBy.len); + try std.testing.expectEqualStrings("day", args.orderBy[0].field); + try std.testing.expect(!args.orderBy[0].descending); + try std.testing.expectEqualStrings("total gt i:10", args.exprText); +} + +test "table_window args round trip a sum spec with no partition, no order-by, and an empty expr" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{.{ .field = "total", .kind = .sum }}; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 1), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.sum, args.specs[0].kind); + try std.testing.expectEqualStrings("total", args.specs[0].field); + try std.testing.expectEqual(@as(usize, 0), args.partitionBy.len); + try std.testing.expectEqual(@as(usize, 0), args.orderBy.len); + try std.testing.expectEqualStrings("", args.exprText); +} + +test "table_window args round trip a count_distinct spec alongside sum, no extra wire bytes needed" { + const allocator = std.testing.allocator; + const orderBy = [_]querymod.OrderBy{.{ .field = "day" }}; + const specs = [_]windowmod.WindowSpec{ + .{ .field = "tag", .kind = .count_distinct }, + .{ .field = "amount", .kind = .sum }, + }; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 2), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.count_distinct, args.specs[0].kind); + try std.testing.expectEqualStrings("tag", args.specs[0].field); + try std.testing.expectEqual(windowmod.WindowKind.sum, args.specs[1].kind); +} + +test "table_window args round trip first_value/last_value specs alongside a partition/order/RANGE frame" { + const allocator = std.testing.allocator; + const orderBy = [_]querymod.OrderBy{.{ .field = "day" }}; + const partitionBy = [_][]const u8{"city"}; + const specs = [_]windowmod.WindowSpec{ + .{ .field = "amount", .kind = .first_value }, + .{ .field = "amount", .kind = .last_value }, + }; + const payload = try encodeTableWindow(allocator, "orders", &specs, &partitionBy, &orderBy, 0, 0, "", .range); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 2), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.first_value, args.specs[0].kind); + try std.testing.expectEqualStrings("amount", args.specs[0].field); + try std.testing.expectEqual(windowmod.WindowKind.last_value, args.specs[1].kind); + try std.testing.expectEqualStrings("amount", args.specs[1].field); + try std.testing.expectEqual(windowmod.FrameMode.range, args.frame); +} + +test "table_window args round trip MULTIPLE specs sharing one partition/order/frame" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{ + .{ .field = "total", .kind = .sum }, + .{ .field = "total", .kind = .avg }, + .{ .field = "", .kind = .rank }, + }; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 3), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.sum, args.specs[0].kind); + try std.testing.expectEqualStrings("total", args.specs[0].field); + try std.testing.expectEqual(windowmod.WindowKind.avg, args.specs[1].kind); + try std.testing.expectEqualStrings("total", args.specs[1].field); + try std.testing.expectEqual(windowmod.WindowKind.rank, args.specs[2].kind); + try std.testing.expectEqualStrings("", args.specs[2].field); +} + +test "table_window args round trip a nonzero offset and limit" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{.{ .field = "", .kind = .rank }}; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 2, 5, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(u32, 2), args.offset); + try std.testing.expectEqual(@as(u32, 5), args.limit); +} + +test "table_window args round trip a RANGE frame, and default to ROWS when the byte is 0" { + const allocator = std.testing.allocator; + const order = [_]querymod.OrderBy{.{ .field = "day", .descending = false }}; + const specs = [_]windowmod.WindowSpec{.{ .field = "amount", .kind = .sum }}; + + const rangePayload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &order, 0, 0, "", .range); + defer allocator.free(rangePayload); + const rangeArgs = try decodeTableWindow(allocator, rangePayload); + defer allocator.free(rangeArgs.specs); + defer allocator.free(rangeArgs.partitionBy); + defer allocator.free(rangeArgs.orderBy); + try std.testing.expectEqual(windowmod.FrameMode.range, rangeArgs.frame); + + const rowsPayload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &order, 0, 0, "", .rows); + defer allocator.free(rowsPayload); + const rowsArgs = try decodeTableWindow(allocator, rowsPayload); + defer allocator.free(rowsArgs.specs); + defer allocator.free(rowsArgs.partitionBy); + defer allocator.free(rowsArgs.orderBy); + try std.testing.expectEqual(windowmod.FrameMode.rows, rowsArgs.frame); +} +