feat: add handleTableAggregate to server

This commit is contained in:
dualfroz committed 2026-09-11 15:01:46 +02:00
1 parent 6cd6ec5574
commit a7551a7923
1 file changed
+140
+140
View File
@@ -1243,3 +1243,143 @@ pub const Server = struct {
}; };
defer right.deinit(); defer right.deinit();
var leftParsed: ?exprmod.Parsed = null;
defer if (leftParsed) |*p| p.deinit();
if (args.leftExprText.len > 0) {
leftParsed = exprmod.parse(self.allocator, args.leftExprText) catch |err| {
return writeErr(writer, .bad_request, @errorName(err));
};
}
const leftPred: ?querymod.Predicate = if (leftParsed) |p| p.predicate else null;
var rightParsed: ?exprmod.Parsed = null;
defer if (rightParsed) |*p| p.deinit();
if (args.rightExprText.len > 0) {
rightParsed = exprmod.parse(self.allocator, args.rightExprText) catch |err| {
return writeErr(writer, .bad_request, @errorName(err));
};
}
const rightPred: ?querymod.Predicate = if (rightParsed) |p| p.predicate else null;
var pairs = std.ArrayList(protocol.JoinPair).init(self.allocator);
defer {
for (pairs.items) |*p| {
if (p.left) |*l| l.deinit();
if (p.right) |*r| r.deinit();
}
pairs.deinit();
}
const page = querymod.Page{ .offset = args.offset, .limit = args.limit };
switch (args.mode) {
.inner => {
const joined = joinmod.innerJoin(self.allocator, &left, args.leftField, leftPred, &right, args.rightField, rightPred) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
const paged = joinmod.applyPage(joinmod.JoinedRow, self.allocator, joined, page) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
defer self.allocator.free(paged);
for (paged) |p| try pairs.append(.{ .left = p.left, .right = p.right });
},
.left => {
const joined = joinmod.leftOuterJoin(self.allocator, &left, args.leftField, leftPred, &right, args.rightField, rightPred) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
const paged = joinmod.applyPage(joinmod.OuterJoinedRow, self.allocator, joined, page) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
defer self.allocator.free(paged);
for (paged) |p| try pairs.append(.{ .left = p.left, .right = p.right });
},
.right => {
const joined = joinmod.rightOuterJoin(self.allocator, &left, args.leftField, leftPred, &right, args.rightField, rightPred) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
const paged = joinmod.applyPage(joinmod.RightOuterJoinedRow, self.allocator, joined, page) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
defer self.allocator.free(paged);
for (paged) |p| try pairs.append(.{ .left = p.left, .right = p.right });
},
.full => {
const joined = joinmod.fullOuterJoin(self.allocator, &left, args.leftField, leftPred, &right, args.rightField, rightPred) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
const paged = joinmod.applyPage(joinmod.FullOuterJoinedRow, self.allocator, joined, page) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
defer self.allocator.free(paged);
for (paged) |p| try pairs.append(.{ .left = p.left, .right = p.right });
},
}
const out = protocol.encodeJoinedRows(self.allocator, pairs.items) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
defer self.allocator.free(out);
try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
}
/// Executes a `table_aggregate` request: opens the named table over the
/// same `*Db` the rest of the server serves, filters rows with the same
/// `expr.zig` grammar `table_select`/`table_join` already use (empty
/// text selects every row), then hands the filtered rows to
/// `aggregate.zig` - `runGlobal` when `args.groupField` is empty (the
/// network counterpart of the CLI's `*` sentinel for "no GROUP BY"),
/// `run` otherwise. When `args.havingText` is non-empty, it is parsed
/// with the same `expr.zig` grammar and applied AFTER aggregation but
/// BEFORE paging - `globalMatchesHaving` on the `runGlobal` path (an
/// excluded single result comes back as zero values, not an error) or
/// `filterHaving` on the grouped path - then, on the grouped path only,
/// a non-empty `args.orderBy` sorts the HAVING-narrowed groups via
/// `aggregate.sortGroupsBy` (an unknown order field is `bad_request`,
/// same treatment as a malformed HAVING expr) - then windowed by
/// `args.offset`/`args.limit` through `aggregate.applyPageGroups` (0/0 =
/// unlimited, ignored for the `runGlobal` path since that always
/// produces at most one result row, same as `args.orderBy`). Two distinct wire shapes come back
/// (`encodeAggGlobalResult` vs `encodeAggGroupRows`) because the client
/// already knows which one it asked for (it is the one that chose
/// `groupField`), so no extra discriminator byte is needed on the wire -
/// the client just decodes with whichever matches its own request, the
/// same reasoning `Client.tableAggregateGlobal`/`tableAggregateGroups`
/// document. A bad table name, malformed expr, or a spec referencing a
/// non-numeric field (`error.NonNumericField`/`error.Overflow` from
/// `aggregate.zig`) is a client mistake (`bad_request`), not a server
/// fault.
fn handleTableAggregate(self: *Server, payload: []const u8, writer: anytype) !void {
const args = protocol.decodeTableAggregate(self.allocator, payload) catch {
return writeErr(writer, .bad_request, "malformed table_aggregate payload");
};
defer self.allocator.free(args.specs);
defer self.allocator.free(args.orderBy);
var table = querymod.Table.open(self.db, self.allocator, args.table) catch |err| {
return writeErr(writer, .bad_request, @errorName(err));
};
defer table.deinit();
var parsed: ?exprmod.Parsed = null;
defer if (parsed) |*p| p.deinit();
if (args.exprText.len > 0) {
parsed = exprmod.parse(self.allocator, args.exprText) catch |err| {
return writeErr(writer, .bad_request, @errorName(err));
};
}
const pred: ?querymod.Predicate = if (parsed) |p| p.predicate else null;
var havingParsed: ?exprmod.Parsed = null;
defer if (havingParsed) |*p| p.deinit();
if (args.havingText.len > 0) {
havingParsed = exprmod.parse(self.allocator, args.havingText) catch |err| {
return writeErr(writer, .bad_request, @errorName(err));
};
}
const havingPred: ?querymod.Predicate = if (havingParsed) |p| p.predicate else null;
const rows = table.select(pred) catch |err| {
return writeErr(writer, .internal, @errorName(err));
};
defer querymod.freeRows(self.allocator, rows);