feat: expand aggregate
This commit is contained in:
1 parent
a3efd074a7
commit
4bb136858e
1 file changed
+202
@@ -1134,3 +1134,205 @@ test "filterHaving composes with applyPageGroups: HAVING narrows before LIMIT/OF
|
|||||||
const paged = try applyPageGroups(allocator, filtered, .{ .limit = 1 });
|
const paged = try applyPageGroups(allocator, filtered, .{ .limit = 1 });
|
||||||
defer freeGroupRows(allocator, paged);
|
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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user