feat: add threeCityGroups to aggregate

This commit is contained in:
dualfroz committed 2026-09-01 14:58:43 +02:00
1 parent 289055c4e5
commit a3efd074a7
1 file changed
+163
+163
View File
@@ -971,3 +971,166 @@ fn firstLettersOf(rows: []const GroupRow) [4]u8 {
return out;
}
test "applyPageGroups with offset and limit keeps a middle window in first-seen order" {
const allocator = std.testing.allocator;
const groups = try fourGroupRows(allocator);
try std.testing.expectEqual(@as(usize, 4), groups.len);
const paged = try applyPageGroups(allocator, groups, .{ .offset = 1, .limit = 2 });
defer freeGroupRows(allocator, paged);
try std.testing.expectEqual(@as(usize, 2), paged.len);
try std.testing.expectEqualStrings("b", paged[0].group.text);
try std.testing.expectEqualStrings("c", paged[1].group.text);
}
test "applyPageGroups with limit 0 returns every group, matching unpaged run" {
const allocator = std.testing.allocator;
const groups = try fourGroupRows(allocator);
const paged = try applyPageGroups(allocator, groups, .{});
defer freeGroupRows(allocator, paged);
try std.testing.expectEqualSlices(u8, &.{ 'a', 'b', 'c', 'd' }, &firstLettersOf(paged));
}
test "applyPageGroups with offset past the end returns nothing" {
const allocator = std.testing.allocator;
const groups = try fourGroupRows(allocator);
const paged = try applyPageGroups(allocator, groups, .{ .offset = 50 });
defer freeGroupRows(allocator, paged);
try std.testing.expectEqual(@as(usize, 0), paged.len);
}
test "applyPageGroups with limit larger than the remaining groups stops at the end" {
const allocator = std.testing.allocator;
const groups = try fourGroupRows(allocator);
const paged = try applyPageGroups(allocator, groups, .{ .offset = 3, .limit = 100 });
defer freeGroupRows(allocator, paged);
try std.testing.expectEqual(@as(usize, 1), paged.len);
try std.testing.expectEqualStrings("d", paged[0].group.text);
}
test "applyPageGroups propagates OutOfMemory from its own result allocation without leaking any group" {
const allocator = std.testing.allocator;
const groups = try fourGroupRows(allocator);
try std.testing.expectEqual(@as(usize, 4), groups.len);
var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });
try std.testing.expectError(error.OutOfMemory, applyPageGroups(failing.allocator(), groups, .{ .offset = 1, .limit = 2 }));
// No explicit assertion beyond this: `testing.allocator`'s leak detector
// at process exit is the real check - every one of the 4 `GroupRow`s (2
// kept by the window, 2 trimmed outside it) must have been
// `.deinit(allocator)`-ed on the error path, and the `rows` backing
// slice itself freed, or this test fails on leak. Every `GroupRow` here
// was built by `run()` using the real `testing.allocator`, so the error
// path frees through that same allocator regardless of which allocator
// `applyPageGroups` was asked to allocate its OWN result slice with -
// the exact isolation `join.zig`'s analogous OOM test documents.
}
fn threeCityGroups(allocator: Allocator) ![]GroupRow {
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const data = [_]struct { city: []const u8, score: i64 }{
.{ .city = "warsaw", .score = 10 },
.{ .city = "warsaw", .score = 30 },
.{ .city = "krakow", .score = 5 },
.{ .city = "gdansk", .score = 100 },
.{ .city = "gdansk", .score = 100 },
.{ .city = "gdansk", .score = 100 },
};
for (data) |d| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "city", .value = .{ .text = d.city } },
.{ .name = "score", .value = .{ .int = d.score } },
}));
}
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
return run(allocator, rows.items, "city", &specs);
}
test "filterHaving keeps only groups whose aggregate result matches the predicate" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
// warsaw: count=2 sum=40, krakow: count=1 sum=5, gdansk: count=3 sum=300
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
const having = Predicate{ .gt = .{ .field = "count", .value = .{ .int = 1 } } };
const kept = try filterHaving(allocator, groups, "city", &specs, having);
defer freeGroupRows(allocator, kept);
try std.testing.expectEqual(@as(usize, 2), kept.len);
try std.testing.expectEqualStrings("warsaw", kept[0].group.text);
try std.testing.expectEqualStrings("gdansk", kept[1].group.text);
}
test "filterHaving can reference the underlying aggregate field name (sum_field)" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
const having = Predicate{ .gte = .{ .field = "sum_score", .value = .{ .int = 100 } } };
const kept = try filterHaving(allocator, groups, "city", &specs, having);
defer freeGroupRows(allocator, kept);
try std.testing.expectEqual(@as(usize, 1), kept.len);
try std.testing.expectEqualStrings("gdansk", kept[0].group.text);
}
test "filterHaving can also reference the group key itself, not just an aggregate result" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
const having = Predicate{ .ne = .{ .field = "city", .value = .{ .text = "krakow" } } };
const kept = try filterHaving(allocator, groups, "city", &specs, having);
defer freeGroupRows(allocator, kept);
try std.testing.expectEqual(@as(usize, 2), kept.len);
try std.testing.expectEqualStrings("warsaw", kept[0].group.text);
try std.testing.expectEqualStrings("gdansk", kept[1].group.text);
}
test "filterHaving that rejects every group returns an empty, still-valid slice" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
const having = Predicate{ .gt = .{ .field = "count", .value = .{ .int = 1000 } } };
const kept = try filterHaving(allocator, groups, "city", &specs, having);
defer freeGroupRows(allocator, kept);
try std.testing.expectEqual(@as(usize, 0), kept.len);
}
test "filterHaving composes with applyPageGroups: HAVING narrows before LIMIT/OFFSET windows" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
// Only warsaw and gdansk survive HAVING; a limit of 1 then keeps just
// the first of those two (warsaw, first-seen), not the first of all
// three original groups.
const having = Predicate{ .gt = .{ .field = "count", .value = .{ .int = 1 } } };
const filtered = try filterHaving(allocator, groups, "city", &specs, having);
const paged = try applyPageGroups(allocator, filtered, .{ .limit = 1 });
defer freeGroupRows(allocator, paged);