feat: add globalMatchesHaving and SortKey to aggregate

This commit is contained in:
dualfroz committed 2026-09-01 13:24:21 +02:00
1 parent c69db89e14
commit dd8d35b953
1 file changed
+183
+183
View File
@@ -161,3 +161,186 @@ pub fn filterHaving(allocator: Allocator, groups: []GroupRow, groupField: []cons
allocator.free(groups);
}
while (i < groups.len) : (i += 1) {
var probe = try buildHavingProbe(allocator, groupField, groups[i].group, specs, groups[i].values);
defer probe.deinit();
if (having.evaluate(&probe)) {
try kept.append(allocator, groups[i]);
} else {
groups[i].deinit(allocator);
}
}
allocator.free(groups);
return kept.toOwnedSlice(allocator);
}
/// HAVING for the ungrouped (`runGlobal`, CLI's `*` sentinel) aggregate path:
/// evaluates `having` against the same kind of synthetic probe row
/// `filterHaving` builds, minus any group-key field (there is no group).
/// Reports only whether the single global result should be kept - unlike
/// `filterHaving` it never frees `values`, since `runGlobal` always produces
/// exactly one result and the caller (which already owns it via
/// `freeGlobalResult`) decides whether to print it or not.
pub fn globalMatchesHaving(allocator: Allocator, specs: []const AggSpec, values: []const Value, having: Predicate) AggregateError!bool {
var probe = try buildHavingProbe(allocator, "", null, specs, values);
defer probe.deinit();
return having.evaluate(&probe);
}
/// Which part of a `GroupRow` a single `OrderBy` column reads from -
/// resolved once per column by `resolveSortKeys` rather than re-matching
/// `ob.field` against `groupField`/every `specLabel` on every comparison
/// `sortGroupsBy`'s sort makes.
const SortKey = union(enum) {
group,
value: usize,
};
/// Resolves each `order[i].field` to either the group key itself
/// (`groupField`, e.g. `city`) or the index of the `specs` entry whose
/// `specLabel` matches (e.g. `sum_score`) - the same two-namespace lookup
/// `buildHavingProbe` exposes to a HAVING predicate, done once up front
/// instead of building a probe `Row` per comparison. An `order[i].field`
/// matching neither is `error.UnknownOrderField` rather than silently
/// sorting as if every row tied on that column. Caller owns the returned
/// slice.
fn resolveSortKeys(allocator: Allocator, groupField: []const u8, specs: []const AggSpec, order: []const querymod.OrderBy) AggregateError![]SortKey {
var keys = try allocator.alloc(SortKey, order.len);
errdefer allocator.free(keys);
for (order, 0..) |ob, i| {
if (groupField.len > 0 and std.mem.eql(u8, ob.field, groupField)) {
keys[i] = .group;
continue;
}
var found: ?usize = null;
for (specs, 0..) |spec, j| {
const label = try specLabel(allocator, spec);
defer allocator.free(label);
if (std.mem.eql(u8, label, ob.field)) {
found = j;
break;
}
}
keys[i] = .{ .value = found orelse return error.UnknownOrderField };
}
return keys;
}
/// Stable in-place sort of `groups` by `order` - the GROUP BY counterpart of
/// `query.zig`'s `sortRowsBy`, applied to already-aggregated `GroupRow`s
/// instead of plain `Row`s so an ORDER BY column can name either the group
/// key (`city`) or an aggregate result (`sum_score`), the same two things a
/// HAVING predicate can reference. Columns compare left to right exactly
/// like `sortRowsBy`/SQL's `ORDER BY a, b`: the first entry decides unless
/// it ties, in which case the next breaks it. `order.len == 0` is a no-op
/// (the common case - most `table aggregate` calls have no ORDER BY).
/// Does not take ownership of `groups` - unlike `filterHaving`/
/// `applyPageGroups`, this never frees or reallocates any group, only
/// reorders the slice in place, so callers keep using their own `defer
/// freeGroupRows(...)` around the call exactly as if it were never made.
/// Typically run after `filterHaving` and before `applyPageGroups` - ORDER
/// BY sees the HAVING-narrowed set and decides what LIMIT/OFFSET's window
/// actually keeps, the same clause order SQL evaluates.
pub fn sortGroupsBy(allocator: Allocator, groups: []GroupRow, groupField: []const u8, specs: []const AggSpec, order: []const querymod.OrderBy) AggregateError!void {
if (order.len == 0) return;
const keys = try resolveSortKeys(allocator, groupField, specs, order);
defer allocator.free(keys);
const Ctx = struct {
order: []const querymod.OrderBy,
keys: []const SortKey,
fn lessThan(ctx: @This(), a: GroupRow, b: GroupRow) bool {
for (ctx.order, ctx.keys) |ob, key| {
const av = switch (key) {
.group => a.group,
.value => |j| a.values[j],
};
const bv = switch (key) {
.group => b.group,
.value => |j| b.values[j],
};
const cmp = rowmod.compare(av, bv);
if (cmp == .eq) continue;
return if (ob.descending) cmp == .gt else cmp == .lt;
}
return false;
}
};
std.sort.block(GroupRow, groups, Ctx{ .order = order, .keys = keys }, Ctx.lessThan);
}
const AccState = struct {
kind: AggKind,
field: []const u8,
n: u64 = 0,
sumInt: i64 = 0,
sumFloat: f64 = 0,
allInt: bool = true,
minv: ?Value = null,
maxv: ?Value = null,
/// `.count_distinct` only: encoded-`Value` bytes seen so far, one entry
/// per distinct value (owns each key, same encode-as-hashmap-key trick
/// `GroupBy.observe` uses for the group key itself).
distinct: std.StringHashMapUnmanaged(void) = .{},
fn numericFieldValue(self: *const AccState, row: *const Row) ?Value {
return row.get(self.field);
}
fn observe(self: *AccState, allocator: Allocator, row: *const Row) AggregateError!void {
switch (self.kind) {
.count => {
if (self.field.len == 0) {
self.n += 1;
} else if (row.get(self.field) != null) {
self.n += 1;
}
},
.sum, .avg => {
const v = self.numericFieldValue(row) orelse return;
switch (v) {
.int => |i| {
if (self.allInt) {
self.sumInt = std.math.add(i64, self.sumInt, i) catch return error.Overflow;
} else {
self.sumFloat += @floatFromInt(i);
}
},
.float => |f| {
if (self.allInt) {
// First float observed after an int-only run:
// migrate the exact integer sum so far into the
// float accumulator once, then continue in float.
self.sumFloat = @floatFromInt(self.sumInt);
self.allInt = false;
}
self.sumFloat += f;
},
else => return error.NonNumericField,
}
self.n += 1;
},
.min => {
const v = row.get(self.field) orelse return;
if (self.minv == null or rowmod.compare(v, self.minv.?) == .lt) {
if (self.minv) |old| old.free(allocator);
self.minv = try v.dupe(allocator);
}
},
.max => {
const v = row.get(self.field) orelse return;
if (self.maxv == null or rowmod.compare(v, self.maxv.?) == .gt) {
if (self.maxv) |old| old.free(allocator);
self.maxv = try v.dupe(allocator);
}
},
.count_distinct => {
const v = row.get(self.field) orelse return;
const key = try rowmod.encodeValue(allocator, v);
const res = try self.distinct.getOrPut(allocator, key);
if (res.found_existing) allocator.free(key);
},
}
}