feat: extend aggregate

This commit is contained in:
dualfroz committed 2026-09-01 14:42:34 +02:00
1 parent 96f2142f82
commit b8abb45b61
1 file changed
+152
+152
View File
@@ -658,3 +658,155 @@ test "sum stays exact integer until a float value forces it to switch" {
.{ .name = "x", .value = .{ .int = 4 } },
}));
const specs = [_]AggSpec{.{ .field = "x", .kind = .sum }};
{
const groups = try run(allocator, rows.items, "g", &specs);
defer freeGroupRows(allocator, groups);
// All-int input: exact int result, not a float with rounding risk.
try std.testing.expectEqual(rowmod.ValueTag.int, @as(rowmod.ValueTag, groups[0].values[0]));
try std.testing.expectEqual(@as(i64, 7), groups[0].values[0].int);
}
try rows.append(try makeRow(allocator, &.{
.{ .name = "g", .value = .{ .int = 1 } },
.{ .name = "x", .value = .{ .float = 0.5 } },
}));
{
const groups = try run(allocator, rows.items, "g", &specs);
defer freeGroupRows(allocator, groups);
try std.testing.expectEqual(rowmod.ValueTag.float, @as(rowmod.ValueTag, groups[0].values[0]));
try std.testing.expectApproxEqAbs(@as(f64, 7.5), groups[0].values[0].float, 1e-9);
}
}
test "sum reports overflow instead of silently wrapping" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
try rows.append(try makeRow(allocator, &.{
.{ .name = "g", .value = .{ .int = 1 } },
.{ .name = "x", .value = .{ .int = std.math.maxInt(i64) } },
}));
try rows.append(try makeRow(allocator, &.{
.{ .name = "g", .value = .{ .int = 1 } },
.{ .name = "x", .value = .{ .int = 1 } },
}));
const specs = [_]AggSpec{.{ .field = "x", .kind = .sum }};
var gb = GroupBy.init(allocator, "g", &specs);
defer gb.deinit();
try gb.observe(&rows.items[0]);
try std.testing.expectError(error.Overflow, gb.observe(&rows.items[1]));
}
test "sum on a non-numeric field errors without leaking accumulated state" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
try rows.append(try makeRow(allocator, &.{
.{ .name = "g", .value = .{ .int = 1 } },
.{ .name = "x", .value = .{ .text = "nope" } },
}));
const specs = [_]AggSpec{.{ .field = "x", .kind = .sum }};
var gb = GroupBy.init(allocator, "g", &specs);
defer gb.deinit();
try std.testing.expectError(error.NonNumericField, gb.observe(&rows.items[0]));
}
test "min and max use the total order across owned text values" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const names = [_][]const u8{ "delta", "alpha", "charlie", "bravo" };
for (names) |n| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "g", .value = .{ .int = 1 } },
.{ .name = "name", .value = .{ .text = n } },
}));
}
const specs = [_]AggSpec{
.{ .field = "name", .kind = .min },
.{ .field = "name", .kind = .max },
};
const groups = try run(allocator, rows.items, "g", &specs);
defer freeGroupRows(allocator, groups);
try std.testing.expectEqualStrings("alpha", groups[0].values[0].text);
try std.testing.expectEqualStrings("delta", groups[0].values[1].text);
}
test "count_distinct counts unique field values once per group, repeats collapsed" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const data = [_]struct { team: []const u8, tag: []const u8 }{
.{ .team = "a", .tag = "red" },
.{ .team = "a", .tag = "blue" },
.{ .team = "a", .tag = "red" }, // repeat, must not double count
.{ .team = "b", .tag = "green" },
};
for (data) |d| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "team", .value = .{ .text = d.team } },
.{ .name = "tag", .value = .{ .text = d.tag } },
}));
}
const specs = [_]AggSpec{.{ .field = "tag", .kind = .count_distinct }};
const groups = try run(allocator, rows.items, "team", &specs);
defer freeGroupRows(allocator, groups);
const a = findGroup(groups, .{ .text = "a" }).?;
try std.testing.expectEqual(@as(i64, 2), a.values[0].int);
const b = findGroup(groups, .{ .text = "b" }).?;
try std.testing.expectEqual(@as(i64, 1), b.values[0].int);
}
test "count_distinct ignores rows missing the field and never counts none as a value" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
try rows.append(try makeRow(allocator, &.{
.{ .name = "team", .value = .{ .text = "a" } },
.{ .name = "tag", .value = .{ .text = "red" } },
}));
// No "tag" field on this row - must not create a phantom distinct value.
try rows.append(try makeRow(allocator, &.{
.{ .name = "team", .value = .{ .text = "a" } },
}));
const specs = [_]AggSpec{.{ .field = "tag", .kind = .count_distinct }};
const groups = try run(allocator, rows.items, "team", &specs);
defer freeGroupRows(allocator, groups);
try std.testing.expectEqual(@as(usize, 1), groups.len);
try std.testing.expectEqual(@as(i64, 1), groups[0].values[0].int);
}
test "count_distinct on zero observed values reports zero, not none" {
const allocator = std.testing.allocator;
const rows: []const Row = &.{};
const specs = [_]AggSpec{.{ .field = "tag", .kind = .count_distinct }};
const values = try runGlobal(allocator, rows, &specs);
defer freeGlobalResult(allocator, values);
try std.testing.expectEqual(@as(i64, 0), values[0].int);
}