From ea08f11a80279605ea657900904d5b3886b1b677 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 22:47:43 +0200 Subject: [PATCH] feat: grow server --- src/server.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/server.zig b/src/server.zig index 69f9833..b25a23f 100644 --- a/src/server.zig +++ b/src/server.zig @@ -4535,3 +4535,162 @@ test "server table_window rejects a RANGE frame combined with lag as bad_request 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 orderBy = [_]querymod.OrderBy{.{ .field = "total", .descending = false }}; + const specs = [_]windowmod.WindowSpec{.{ .field = "total", .kind = .lead }}; + const err = client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .range); + try std.testing.expectError(client_mod.ClientError.ServerBadRequest, err); +} + +test "server table_window first_value/last_value read the current RANGE tie group's edges, 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); + + 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 orderBy = [_]querymod.OrderBy{.{ .field = "total", .descending = false }}; + const specs = [_]windowmod.WindowSpec{ + .{ .field = "total", .kind = .first_value }, + .{ .field = "total", .kind = .last_value }, + }; + const results = try client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .range); + defer protocol.freeWindowRows(allocator, results); + + // Sorted by total ascending: krakow/40, warsaw/100, warsaw/250 - no ties, + // so RANGE reduces to one tie group per row (same as ROWS): first_value + // is always the partition's first row (40, frame start is unaffected by + // ties), but last_value is each row's OWN value since it is its own + // singleton tie group. + try std.testing.expectEqual(@as(usize, 3), results.len); + for (results) |r| try std.testing.expectEqual(@as(i64, 40), r.values[0].int); + try std.testing.expectEqual(@as(i64, 40), results[0].values[1].int); + try std.testing.expectEqual(@as(i64, 100), results[1].values[1].int); + try std.testing.expectEqual(@as(i64, 250), results[2].values[1].int); +} + +test "server table_window nth_value reads a fixed frame position, default at the sequence edge, 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); + + 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 orderBy = [_]querymod.OrderBy{.{ .field = "total", .descending = false }}; + const specs = [_]windowmod.WindowSpec{.{ .field = "total", .kind = .nth_value, .offset = 2, .default = .{ .int = -1 } }}; + const results = try client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .rows); + defer protocol.freeWindowRows(allocator, results); + + // Sorted: krakow/40, warsaw/100, warsaw/250. Position 2 (n=2) needs a + // frame of at least size 2 - the first row's ROWS frame is still just + // itself (size 1), so it falls back to the caller's default (-1); the + // remaining two rows both see position 2's value, 100. + try std.testing.expectEqual(@as(usize, 3), results.len); + try std.testing.expectEqual(@as(i64, -1), results[0].values[0].int); + try std.testing.expectEqual(@as(i64, 100), results[1].values[0].int); + try std.testing.expectEqual(@as(i64, 100), results[2].values[0].int); +} + +test "server table_window count_distinct runs a distinct-city total over real TCP, matching local semantics" { + 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(); + + // Sorted by total ascending: krakow/40, warsaw/100, warsaw/250 - distinct + // "city" running count grows 1 (krakow), 2 (warsaw joins), then stays 2 + // (warsaw repeats). + const orderBy = [_]querymod.OrderBy{.{ .field = "total", .descending = false }}; + const specs = [_]windowmod.WindowSpec{.{ .field = "city", .kind = .count_distinct }}; + const results = try client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .rows); + defer protocol.freeWindowRows(allocator, results); + + try std.testing.expectEqual(@as(usize, 3), results.len); + try std.testing.expectEqual(@as(i64, 1), results[0].values[0].int); + try std.testing.expectEqual(@as(i64, 2), results[1].values[0].int); + try std.testing.expectEqual(@as(i64, 2), results[2].values[0].int); +} + +test "server table_window applies the expr filter before annotating, 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); + + 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 = [_]windowmod.WindowSpec{.{ .field = "", .kind = .count }}; + const results = try client.tableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "city eq s:warsaw", .rows); + defer protocol.freeWindowRows(allocator, results); + + try std.testing.expectEqual(@as(usize, 2), results.len); + for (results) |wr| try std.testing.expectEqual(@as(i64, 2), wr.values[0].int); +} + +test "server table_window reports a malformed expr as 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); +