From d83f4f9ad16f4c13d951f8de43826c2bcfe4c8f3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 20:31:39 +0200 Subject: [PATCH] feat: fill in server --- src/server.zig | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/server.zig b/src/server.zig index 48ec55a..4bfa863 100644 --- a/src/server.zig +++ b/src/server.zig @@ -3901,3 +3901,163 @@ test "server table_aggregate groups remote rows by field with count/sum, matchin const groups = try client.tableAggregateGroups(allocator, "orders", "city", &specs, 0, 0, &.{}, "", ""); defer protocol.freeAggGroupRows(allocator, groups); + try std.testing.expectEqual(@as(usize, 2), groups.len); + try std.testing.expectEqualStrings("warsaw", groups[0].group.text); + try std.testing.expectEqual(@as(i64, 2), groups[0].values[0].int); + try std.testing.expectEqual(@as(i64, 350), groups[0].values[1].int); + try std.testing.expectEqualStrings("krakow", groups[1].group.text); + try std.testing.expectEqual(@as(i64, 1), groups[1].values[0].int); + try std.testing.expectEqual(@as(i64, 40), groups[1].values[1].int); +} + +test "server table_aggregate honors offset/limit to window the result groups server-side" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + try seedAggregateOrders(allocator, &db); + var orders = try querymod.Table.open(&db, allocator, "orders"); + try orders.put(.{ .int = 4 }, &.{ + .{ .name = "city", .value = .{ .text = "gdansk" } }, + .{ .name = "total", .value = .{ .int = 5 } }, + }); + orders.deinit(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + const specs = [_]aggregatemod.AggSpec{.{ .field = "", .kind = .count }}; + + // Unpaged baseline: 3 distinct cities in first-seen order (warsaw twice + // seeded first, then krakow, then gdansk added above). + const all = try client.tableAggregateGroups(allocator, "orders", "city", &specs, 0, 0, &.{}, "", ""); + defer protocol.freeAggGroupRows(allocator, all); + try std.testing.expectEqual(@as(usize, 3), all.len); + + const paged = try client.tableAggregateGroups(allocator, "orders", "city", &specs, 1, 1, &.{}, "", ""); + defer protocol.freeAggGroupRows(allocator, paged); + try std.testing.expectEqual(@as(usize, 1), paged.len); + try std.testing.expectEqualStrings(all[1].group.text, paged[0].group.text); +} + +test "server table_aggregate with no groupField runs a single global aggregate and honors the expr filter" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + try seedAggregateOrders(allocator, &db); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + const specs = [_]aggregatemod.AggSpec{ + .{ .field = "", .kind = .count }, + .{ .field = "total", .kind = .sum }, + }; + const all = try client.tableAggregateGlobal(allocator, "orders", &specs, "", ""); + defer protocol.freeAggValues(allocator, all); + try std.testing.expectEqual(@as(i64, 3), all[0].int); + try std.testing.expectEqual(@as(i64, 390), all[1].int); + + const filtered = try client.tableAggregateGlobal(allocator, "orders", &specs, "", "total gt i:50"); + defer protocol.freeAggValues(allocator, filtered); + try std.testing.expectEqual(@as(i64, 2), filtered[0].int); + try std.testing.expectEqual(@as(i64, 350), filtered[1].int); +} + +test "server table_aggregate applies a HAVING clause to result groups after aggregation, over real TCP" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + try seedAggregateOrders(allocator, &db); // warsaw: count=2 sum=350, krakow: count=1 sum=40 + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + const specs = [_]aggregatemod.AggSpec{ + .{ .field = "", .kind = .count }, + .{ .field = "total", .kind = .sum }, + }; + + // Referencing the printed aggregate label (sum_total): only warsaw's + // group sum clears 100. + const bySum = try client.tableAggregateGroups(allocator, "orders", "city", &specs, 0, 0, &.{}, "sum_total gt i:100", ""); + defer protocol.freeAggGroupRows(allocator, bySum); + try std.testing.expectEqual(@as(usize, 1), bySum.len); + try std.testing.expectEqualStrings("warsaw", bySum[0].group.text); + + // Referencing the group key itself (city), not just an aggregate result. + const byGroup = try client.tableAggregateGroups(allocator, "orders", "city", &specs, 0, 0, &.{}, "city eq s:krakow", ""); + defer protocol.freeAggGroupRows(allocator, byGroup); + try std.testing.expectEqual(@as(usize, 1), byGroup.len); + try std.testing.expectEqualStrings("krakow", byGroup[0].group.text); + + // A HAVING clause that excludes every group comes back as zero groups, + // not an error. + const none = try client.tableAggregateGroups(allocator, "orders", "city", &specs, 0, 0, &.{}, "count gt i:1000", ""); + defer protocol.freeAggGroupRows(allocator, none); + try std.testing.expectEqual(@as(usize, 0), none.len); +} + +test "server table_aggregate HAVING narrows groups before LIMIT/OFFSET windows them, over real TCP" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + try seedAggregateOrders(allocator, &db); + var orders = try querymod.Table.open(&db, allocator, "orders"); + try orders.put(.{ .int = 4 }, &.{ + .{ .name = "city", .value = .{ .text = "gdansk" } }, + .{ .name = "total", .value = .{ .int = 500 } }, + }); + orders.deinit(); + // Groups in first-seen order: warsaw(count=2 sum=350), krakow(count=1 + // sum=40), gdansk(count=1 sum=500). HAVING count gt 0 keeps all three, + // but HAVING sum_total gt i:100 keeps only warsaw and gdansk (in that + // order) - a limit of 1 must then return warsaw, not krakow (which + // would be first if the window applied before HAVING). + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); +