feat: add seedAggregateOrders to server

This commit is contained in:
dualfroz committed 2026-09-11 20:26:27 +02:00
1 parent 977e1b0363
commit 28fa271324
1 file changed
+154
+154
View File
@@ -3747,3 +3747,157 @@ test "server table_select orders a remote result by a field before windowing it,
try std.testing.expectEqual(@as(i64, 1), asc[4].get(querymod.pkField).?.int);
try std.testing.expectEqual(@as(i64, 50), asc[4].get("age").?.int);
// Descending, windowed to the middle page (offset 1, limit 2). Age
// decreases as pk increases here, so descending-by-age happens to land
// on the same row order as ascending pk (pk1..5 = ages 50,40,30,20,10)
// - the offset/limit window (pk 2, pk 3) is still only correct if the
// full 5-row set was sorted first and THEN windowed, not windowed off
// the raw scan and left unsorted (which would coincidentally return
// the same two rows here, but in the ascending-age windowed case below
// would not).
const midDesc = try client.tableSelect(allocator, "users", "", 1, 2, &.{.{ .field = "age", .descending = true }});
defer protocol.freeRows(allocator, midDesc);
try std.testing.expectEqual(@as(usize, 2), midDesc.len);
try std.testing.expectEqual(@as(i64, 2), midDesc[0].get(querymod.pkField).?.int);
try std.testing.expectEqual(@as(i64, 40), midDesc[0].get("age").?.int);
try std.testing.expectEqual(@as(i64, 3), midDesc[1].get(querymod.pkField).?.int);
try std.testing.expectEqual(@as(i64, 30), midDesc[1].get("age").?.int);
// Ascending, same window (offset 1, limit 2): ascending-by-age order is
// pk 5,4,3,2,1 (ages 10,20,30,40,50), so offset 1..3 is pk 4 then pk 3
// - a DIFFERENT pair than the descending window above, which a
// window-before-sort bug (paging the raw pk scan, then sorting only
// the already-cut window) would fail to distinguish.
const midAsc = try client.tableSelect(allocator, "users", "", 1, 2, &.{.{ .field = "age" }});
defer protocol.freeRows(allocator, midAsc);
try std.testing.expectEqual(@as(usize, 2), midAsc.len);
try std.testing.expectEqual(@as(i64, 4), midAsc[0].get(querymod.pkField).?.int);
try std.testing.expectEqual(@as(i64, 20), midAsc[0].get("age").?.int);
try std.testing.expectEqual(@as(i64, 3), midAsc[1].get(querymod.pkField).?.int);
try std.testing.expectEqual(@as(i64, 30), midAsc[1].get("age").?.int);
// Cross-check: table_explain reports the `age` index is used for the
// predicate that would narrow this query, same as the ordered path's
// `selectAllMatches` internally reuses.
var plan = try client.tableExplain(allocator, "users", "age gte i:0");
defer plan.deinit(allocator);
try std.testing.expectEqual(querymod.PlanKind.index_range, plan.kind);
}
// A second, INDEPENDENT `orderBy` column exercises a path the single-column
// test above cannot: `handleTableSelect` decoding a wire count > 1 and
// `sortRowsBy`'s tie-breaking chain, both new with multi-column support.
test "server table_select orders a remote result by two columns, sorting then breaking ties" {
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();
{
var table = try querymod.Table.open(&db, allocator, "people");
defer table.deinit();
// Two cities, scores deliberately NOT already grouped by city in
// scan order, so a single-column `city` sort alone would leave
// each city's rows unsorted by `score` - only a real tiebreak
// fixes that.
try table.put(.{ .int = 1 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "score", .value = .{ .int = 30 } } });
try table.put(.{ .int = 2 }, &.{ .{ .name = "city", .value = .{ .text = "krakow" } }, .{ .name = "score", .value = .{ .int = 20 } } });
try table.put(.{ .int = 3 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "score", .value = .{ .int = 10 } } });
try table.put(.{ .int = 4 }, &.{ .{ .name = "city", .value = .{ .text = "krakow" } }, .{ .name = "score", .value = .{ .int = 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 rows = try client.tableSelect(allocator, "people", "", 0, 0, &.{ .{ .field = "city" }, .{ .field = "score" } });
defer protocol.freeRows(allocator, rows);
try std.testing.expectEqual(@as(usize, 4), rows.len);
try std.testing.expectEqual(@as(i64, 2), rows[0].get(querymod.pkField).?.int); // krakow/20
try std.testing.expectEqual(@as(i64, 4), rows[1].get(querymod.pkField).?.int); // krakow/40
try std.testing.expectEqual(@as(i64, 3), rows[2].get(querymod.pkField).?.int); // warsaw/10
try std.testing.expectEqual(@as(i64, 1), rows[3].get(querymod.pkField).?.int); // warsaw/30
}
test "server table_select rejects a malformed expr with bad_request, not a crash" {
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();
{
var table = try querymod.Table.open(&db, allocator, "users");
defer table.deinit();
try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }});
}
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 err = client.tableSelect(allocator, "users", "city bogus s:warsaw", 0, 0, &.{});
try std.testing.expectError(client_mod.ClientError.ServerBadRequest, err);
}
fn seedAggregateOrders(allocator: Allocator, db: *Db) !void {
var orders = try querymod.Table.open(db, allocator, "orders");
defer orders.deinit();
try orders.put(.{ .int = 1 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "total", .value = .{ .int = 100 } },
});
try orders.put(.{ .int = 2 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "total", .value = .{ .int = 250 } },
});
try orders.put(.{ .int = 3 }, &.{
.{ .name = "city", .value = .{ .text = "krakow" } },
.{ .name = "total", .value = .{ .int = 40 } },
});
}
test "server table_aggregate groups remote rows by field with count/sum, matching the local library result" {
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 groups = try client.tableAggregateGroups(allocator, "orders", "city", &specs, 0, 0, &.{}, "", "");
defer protocol.freeAggGroupRows(allocator, groups);