feat: expand aggregate

This commit is contained in:
dualfroz committed 2026-09-01 15:04:26 +02:00
1 parent a3efd074a7
commit 4bb136858e
1 file changed
+202
+202
View File
@@ -1134,3 +1134,205 @@ test "filterHaving composes with applyPageGroups: HAVING narrows before LIMIT/OF
const paged = try applyPageGroups(allocator, filtered, .{ .limit = 1 });
defer freeGroupRows(allocator, paged);
try std.testing.expectEqual(@as(usize, 1), paged.len);
try std.testing.expectEqualStrings("warsaw", paged[0].group.text);
}
test "filterHaving propagates OutOfMemory without leaking any group, kept or rejected" {
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 = 1 } } };
var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });
try std.testing.expectError(error.OutOfMemory, filterHaving(failing.allocator(), groups, "city", &specs, having));
// Leak detector on `std.testing.allocator` at process exit is the real
// check here (same pattern as `applyPageGroups`'s analogous OOM test):
// every one of the 3 groups `threeCityGroups` built with the real
// allocator must still get freed on this early failure, even though
// `filterHaving` itself was asked to allocate its OWN bookkeeping
// (`kept`) through the failing allocator.
}
test "globalMatchesHaving reports true or false without freeing the caller's values" {
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 <= 5) : (i += 1) {
try rows.append(try makeRow(allocator, &.{.{ .name = "x", .value = .{ .int = i } }}));
}
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "x", .kind = .sum },
};
const values = try runGlobal(allocator, rows.items, &specs);
defer freeGlobalResult(allocator, values);
const passing = Predicate{ .eq = .{ .field = "count", .value = .{ .int = 5 } } };
try std.testing.expect(try globalMatchesHaving(allocator, &specs, values, passing));
const failing = Predicate{ .gt = .{ .field = "sum_x", .value = .{ .int = 1000 } } };
try std.testing.expect(!try globalMatchesHaving(allocator, &specs, values, failing));
}
test "sortGroupsBy orders by the group key ascending by default" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
defer freeGroupRows(allocator, groups);
const order = [_]querymod.OrderBy{.{ .field = "city" }};
try sortGroupsBy(allocator, groups, "city", &.{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
}, &order);
try std.testing.expectEqualStrings("gdansk", groups[0].group.text);
try std.testing.expectEqualStrings("krakow", groups[1].group.text);
try std.testing.expectEqualStrings("warsaw", groups[2].group.text);
}
test "sortGroupsBy orders by the group key descending when asked" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
defer freeGroupRows(allocator, groups);
const order = [_]querymod.OrderBy{.{ .field = "city", .descending = true }};
try sortGroupsBy(allocator, groups, "city", &.{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
}, &order);
try std.testing.expectEqualStrings("warsaw", groups[0].group.text);
try std.testing.expectEqualStrings("krakow", groups[1].group.text);
try std.testing.expectEqualStrings("gdansk", groups[2].group.text);
}
test "sortGroupsBy orders by an aggregate result label, not just the group key" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
defer freeGroupRows(allocator, groups);
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
// warsaw sum=40, krakow sum=5, gdansk sum=300 - ascending by sum_score
// is a completely different order than ascending by city.
const order = [_]querymod.OrderBy{.{ .field = "sum_score" }};
try sortGroupsBy(allocator, groups, "city", &specs, &order);
try std.testing.expectEqualStrings("krakow", groups[0].group.text);
try std.testing.expectEqualStrings("warsaw", groups[1].group.text);
try std.testing.expectEqualStrings("gdansk", groups[2].group.text);
}
test "sortGroupsBy breaks ties left to right across multiple order columns" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
// Every group has count == 1 (a full tie on the first order column), so
// only the second column (the group key itself) can decide the order -
// built out of first-seen order d,b,a,c specifically so an
// alphabetical result proves the second column actually ran, rather
// than the sort coincidentally matching input order.
const groupNames = [_][]const u8{ "d", "b", "a", "c" };
for (groupNames) |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);
const order = [_]querymod.OrderBy{
.{ .field = "count" },
.{ .field = "g" },
};
try sortGroupsBy(allocator, groups, "g", &specs, &order);
try std.testing.expectEqualStrings("a", groups[0].group.text);
try std.testing.expectEqualStrings("b", groups[1].group.text);
try std.testing.expectEqualStrings("c", groups[2].group.text);
try std.testing.expectEqualStrings("d", groups[3].group.text);
}
test "sortGroupsBy with an empty order list leaves groups in first-seen order" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
defer freeGroupRows(allocator, groups);
try sortGroupsBy(allocator, groups, "city", &.{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
}, &.{});
try std.testing.expectEqualStrings("warsaw", groups[0].group.text);
try std.testing.expectEqualStrings("krakow", groups[1].group.text);
try std.testing.expectEqualStrings("gdansk", groups[2].group.text);
}
test "sortGroupsBy rejects a field that names neither the group key nor a known aggregate label" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
defer freeGroupRows(allocator, groups);
const order = [_]querymod.OrderBy{.{ .field = "not_a_real_field" }};
try std.testing.expectError(error.UnknownOrderField, sortGroupsBy(allocator, groups, "city", &.{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
}, &order));
}
test "sortGroupsBy composes with filterHaving and applyPageGroups: ORDER BY reorders before LIMIT/OFFSET windows" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
// HAVING keeps only warsaw (count=2, sum=40) and gdansk (count=3,
// sum=300); descending sum_score then puts gdansk first even though
// warsaw was first-seen - a limit of 1 must keep gdansk, not warsaw.
const having = Predicate{ .gt = .{ .field = "count", .value = .{ .int = 1 } } };
const filtered = try filterHaving(allocator, groups, "city", &specs, having);
const order = [_]querymod.OrderBy{.{ .field = "sum_score", .descending = true }};
try sortGroupsBy(allocator, filtered, "city", &specs, &order);
const paged = try applyPageGroups(allocator, filtered, .{ .limit = 1 });
defer freeGroupRows(allocator, paged);
try std.testing.expectEqual(@as(usize, 1), paged.len);
try std.testing.expectEqualStrings("gdansk", paged[0].group.text);
}
test "sortGroupsBy propagates OutOfMemory without leaking or freeing the caller's groups" {
const allocator = std.testing.allocator;
const groups = try threeCityGroups(allocator);
defer freeGroupRows(allocator, groups);
const specs = [_]AggSpec{
.{ .field = "", .kind = .count },
.{ .field = "score", .kind = .sum },
};
var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });
const order = [_]querymod.OrderBy{.{ .field = "city" }};
try std.testing.expectError(error.OutOfMemory, sortGroupsBy(failing.allocator(), groups, "city", &specs, &order));
// `sortGroupsBy` never took ownership of `groups` (unlike
// `filterHaving`), so the outer `defer freeGroupRows(allocator, ...)`
// above must still see every group intact and still first-seen order
// (untouched, not partially sorted) - both the leak detector on
// `std.testing.allocator` and this ordering check are the real
// assertions here.
try std.testing.expectEqualStrings("warsaw", groups[0].group.text);
try std.testing.expectEqualStrings("krakow", groups[1].group.text);
try std.testing.expectEqualStrings("gdansk", groups[2].group.text);
}