diff --git a/src/protocol.zig b/src/protocol.zig index cd94320..67f6da6 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -2670,3 +2670,162 @@ test "table_aggregate args round trip a groupField, several specs, and an expr" try std.testing.expectEqualStrings("city", args.specs[3].field); } +test "table_aggregate args round trip an empty groupField (global, no grouping) and empty expr" { + const allocator = std.testing.allocator; + const specs = [_]aggregatemod.AggSpec{.{ .field = "", .kind = .count }}; + const payload = try encodeTableAggregate(allocator, "orders", "", &specs, 0, 0, &.{}, "", ""); + defer allocator.free(payload); + const args = try decodeTableAggregate(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.orderBy); + + try std.testing.expectEqualStrings("", args.groupField); + try std.testing.expectEqualStrings("", args.exprText); + try std.testing.expectEqual(@as(usize, 1), args.specs.len); +} + +test "table_aggregate args round trip a nonzero offset and limit" { + const allocator = std.testing.allocator; + const specs = [_]aggregatemod.AggSpec{.{ .field = "", .kind = .count }}; + const payload = try encodeTableAggregate(allocator, "orders", "city", &specs, 2, 5, &.{}, "", ""); + defer allocator.free(payload); + const args = try decodeTableAggregate(allocator, payload); + defer allocator.free(args.specs); + 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_aggregate args round trip a non-empty havingText alongside a non-empty exprText" { + const allocator = std.testing.allocator; + const specs = [_]aggregatemod.AggSpec{ + .{ .field = "", .kind = .count }, + .{ .field = "total", .kind = .sum }, + }; + const payload = try encodeTableAggregate(allocator, "orders", "city", &specs, 0, 0, &.{}, "sum_total gt i:1000", "total gt i:10"); + defer allocator.free(payload); + const args = try decodeTableAggregate(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.orderBy); + + try std.testing.expectEqualStrings("sum_total gt i:1000", args.havingText); + try std.testing.expectEqualStrings("total gt i:10", args.exprText); +} + +test "table_aggregate args round trip a multi-column orderBy alongside havingText and exprText" { + const allocator = std.testing.allocator; + const specs = [_]aggregatemod.AggSpec{ + .{ .field = "", .kind = .count }, + .{ .field = "total", .kind = .sum }, + }; + const orderBy = [_]querymod.OrderBy{ + .{ .field = "sum_total", .descending = true }, + .{ .field = "city" }, + }; + const payload = try encodeTableAggregate(allocator, "orders", "city", &specs, 0, 0, &orderBy, "sum_total gt i:100", "total gt i:10"); + defer allocator.free(payload); + const args = try decodeTableAggregate(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 2), args.orderBy.len); + try std.testing.expectEqualStrings("sum_total", args.orderBy[0].field); + try std.testing.expect(args.orderBy[0].descending); + try std.testing.expectEqualStrings("city", args.orderBy[1].field); + try std.testing.expect(!args.orderBy[1].descending); + try std.testing.expectEqualStrings("sum_total gt i:100", args.havingText); + try std.testing.expectEqualStrings("total gt i:10", args.exprText); +} + +test "table_aggregate args round trip an empty orderBy the same as before this feature existed" { + const allocator = std.testing.allocator; + const specs = [_]aggregatemod.AggSpec{.{ .field = "", .kind = .count }}; + const payload = try encodeTableAggregate(allocator, "orders", "city", &specs, 0, 0, &.{}, "", ""); + defer allocator.free(payload); + const args = try decodeTableAggregate(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 0), args.orderBy.len); +} + +test "decodeTableAggregate rejects an unknown agg kind byte instead of corrupting a spec" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try putLenPrefixed(w, "orders"); + try putLenPrefixed(w, "city"); + try w.putU32(1); + try w.byte(200); // not a valid AggKind tag + try putLenPrefixed(w, "total"); + + try std.testing.expectError(error.Malformed, decodeTableAggregate(allocator, buf.items)); +} + +test "agg global result round trips count/sum/max values, including a none from an empty avg" { + const allocator = std.testing.allocator; + const values = [_]rowmod.Value{ + .{ .int = 4 }, + .{ .float = 10.5 }, + .{ .none = {} }, + .{ .text = "warsaw" }, + }; + const payload = try encodeAggGlobalResult(allocator, &values); + defer allocator.free(payload); + + const decoded = try decodeAggGlobalResult(allocator, payload); + defer freeAggValues(allocator, decoded); + + try std.testing.expectEqual(@as(usize, 4), decoded.len); + try std.testing.expectEqual(@as(i64, 4), decoded[0].int); + try std.testing.expectApproxEqAbs(@as(f64, 10.5), decoded[1].float, 1e-9); + try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, decoded[2])); + try std.testing.expectEqualStrings("warsaw", decoded[3].text); +} + +test "empty agg global result round trips" { + const allocator = std.testing.allocator; + const payload = try encodeAggGlobalResult(allocator, &[_]rowmod.Value{}); + defer allocator.free(payload); + const decoded = try decodeAggGlobalResult(allocator, payload); + defer freeAggValues(allocator, decoded); + try std.testing.expectEqual(@as(usize, 0), decoded.len); +} + +test "agg group rows round trip several groups with several values each" { + const allocator = std.testing.allocator; + const groups = [_]aggregatemod.GroupRow{ + .{ .group = .{ .text = "warsaw" }, .values = @constCast(&[_]rowmod.Value{ .{ .int = 2 }, .{ .int = 350 } }) }, + .{ .group = .{ .text = "krakow" }, .values = @constCast(&[_]rowmod.Value{ .{ .int = 1 }, .{ .int = 40 } }) }, + }; + const payload = try encodeAggGroupRows(allocator, &groups); + defer allocator.free(payload); + + const decoded = try decodeAggGroupRows(allocator, payload); + defer freeAggGroupRows(allocator, decoded); + + try std.testing.expectEqual(@as(usize, 2), decoded.len); + try std.testing.expectEqualStrings("warsaw", decoded[0].group.text); + try std.testing.expectEqual(@as(i64, 2), decoded[0].values[0].int); + try std.testing.expectEqual(@as(i64, 350), decoded[0].values[1].int); + try std.testing.expectEqualStrings("krakow", decoded[1].group.text); + try std.testing.expectEqual(@as(i64, 40), decoded[1].values[1].int); +} + +test "empty agg group rows list round trips" { + const allocator = std.testing.allocator; + const payload = try encodeAggGroupRows(allocator, &[_]aggregatemod.GroupRow{}); + defer allocator.free(payload); + const decoded = try decodeAggGroupRows(allocator, payload); + defer freeAggGroupRows(allocator, decoded); + try std.testing.expectEqual(@as(usize, 0), decoded.len); +} + +test "decodeAggGroupRows rejects a corrupted second group without leaking the first" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); +