feat: add globalMatchesHaving and SortKey to aggregate
This commit is contained in:
1 parent
c69db89e14
commit
dd8d35b953
1 file changed
+183
@@ -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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user