feat: add fourGroupRows and firstLettersOf to aggregate

This commit is contained in:
dualfroz committed 2026-09-01 14:53:00 +02:00
1 parent b8abb45b61
commit 289055c4e5
1 file changed
+161
+161
View File
@@ -810,3 +810,164 @@ test "count_distinct on zero observed values reports zero, not none" {
try std.testing.expectEqual(@as(i64, 0), values[0].int); try std.testing.expectEqual(@as(i64, 0), values[0].int);
} }
test "count_distinct propagates OutOfMemory without leaking already-tracked distinct values" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const tags = [_][]const u8{ "red", "blue", "green" };
for (tags) |t| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "g", .value = .{ .text = "x" } },
.{ .name = "tag", .value = .{ .text = t } },
}));
}
var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 6 });
const specs = [_]AggSpec{.{ .field = "tag", .kind = .count_distinct }};
var gb = GroupBy.init(failing.allocator(), "g", &specs);
defer gb.deinit();
var sawError = false;
for (rows.items) |*r| {
gb.observe(r) catch {
sawError = true;
break;
};
}
try std.testing.expect(sawError);
}
test "rows missing the group field are excluded from every group" {
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" } }}));
try rows.append(try makeRow(allocator, &.{})); // no "team" field at all
const specs = [_]AggSpec{.{ .field = "", .kind = .count }};
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 "group order is first-seen, not sorted" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const order = [_][]const u8{ "z", "a", "m" };
for (order) |g| {
try rows.append(try makeRow(allocator, &.{.{ .name = "g", .value = .{ .text = g } }}));
}
const specs = [_]AggSpec{.{ .field = "", .kind = .count }};
const groups = try run(allocator, rows.items, "g", &specs);
defer freeGroupRows(allocator, groups);
try std.testing.expectEqual(@as(usize, 3), groups.len);
try std.testing.expectEqualStrings("z", groups[0].group.text);
try std.testing.expectEqualStrings("a", groups[1].group.text);
try std.testing.expectEqualStrings("m", groups[2].group.text);
}
test "runGlobal aggregates every row into a single result with no grouping" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
var i: i64 = 1;
while (i <= 4) : (i += 1) {
try rows.append(try makeRow(allocator, &.{.{ .name = "x", .value = .{ .int = i } }}));
}
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "x", .kind = .sum },
.{ .field = "x", .kind = .max },
};
const values = try runGlobal(allocator, rows.items, &specs);
defer freeGlobalResult(allocator, values);
try std.testing.expectEqual(@as(i64, 4), values[0].int);
try std.testing.expectEqual(@as(i64, 10), values[1].int);
try std.testing.expectEqual(@as(i64, 4), values[2].int);
}
test "runGlobal on zero rows still reports identity results, not an error" {
const allocator = std.testing.allocator;
const rows: []const Row = &.{};
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "x", .kind = .sum },
.{ .field = "x", .kind = .max },
};
const values = try runGlobal(allocator, rows, &specs);
defer freeGlobalResult(allocator, values);
try std.testing.expectEqual(@as(i64, 0), values[0].int);
try std.testing.expectEqual(@as(i64, 0), values[1].int);
try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, values[2]));
}
test "GroupBy propagates OutOfMemory without leaking any accumulated group" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const groupNames = [_][]const u8{ "a", "b", "c" };
for (groupNames) |g| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "g", .value = .{ .text = g } },
.{ .name = "x", .value = .{ .int = 1 } },
}));
}
var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 6 });
const specs = [_]AggSpec{.{ .field = "x", .kind = .sum }};
var gb = GroupBy.init(failing.allocator(), "g", &specs);
defer gb.deinit();
var sawError = false;
for (rows.items) |*r| {
gb.observe(r) catch {
sawError = true;
break;
};
}
try std.testing.expect(sawError);
}
fn fourGroupRows(allocator: Allocator) ![]GroupRow {
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const groupNames = [_][]const u8{ "a", "b", "c", "d" };
for (groupNames) |g| {
try rows.append(try makeRow(allocator, &.{.{ .name = "g", .value = .{ .text = g } }}));
}
const specs = [_]AggSpec{.{ .field = "", .kind = .count }};
return run(allocator, rows.items, "g", &specs);
}
fn firstLettersOf(rows: []const GroupRow) [4]u8 {
var out: [4]u8 = undefined;
for (rows, 0..) |r, i| out[i] = r.group.text[0];
return out;
}