diff --git a/src/server.zig b/src/server.zig index fe258ad..69f9833 100644 --- a/src/server.zig +++ b/src/server.zig @@ -4375,3 +4375,163 @@ test "server table_window RANGE frame groups tied rows by day, ROWS gives each r const path = try tmp.dir.realpathAlloc(allocator, "."); defer allocator.free(path); + var db = try testDb(allocator, path); + defer db.close(); + { + var orders = try querymod.Table.open(&db, allocator, "orders"); + defer orders.deinit(); + // Two rows tied at day=1 (10, 20), then one row at day=2 (5). + try orders.put(.{ .int = 1 }, &.{ + .{ .name = "day", .value = .{ .int = 1 } }, + .{ .name = "amount", .value = .{ .int = 10 } }, + }); + try orders.put(.{ .int = 2 }, &.{ + .{ .name = "day", .value = .{ .int = 1 } }, + .{ .name = "amount", .value = .{ .int = 20 } }, + }); + try orders.put(.{ .int = 3 }, &.{ + .{ .name = "day", .value = .{ .int = 2 } }, + .{ .name = "amount", .value = .{ .int = 5 } }, + }); + } + + 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 = "day", .descending = false }}; + const specs = [_]windowmod.WindowSpec{.{ .field = "amount", .kind = .sum }}; + + const rangeResults = try client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .range); + defer protocol.freeWindowRows(allocator, rangeResults); + try std.testing.expectEqual(@as(usize, 3), rangeResults.len); + // Both day=1 rows share the tie group's combined total (10+20=30). + try std.testing.expectEqual(@as(i64, 30), rangeResults[0].values[0].int); + try std.testing.expectEqual(@as(i64, 30), rangeResults[1].values[0].int); + try std.testing.expectEqual(@as(i64, 35), rangeResults[2].values[0].int); + + const rowsResults = try client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .rows); + defer protocol.freeWindowRows(allocator, rowsResults); + try std.testing.expectEqual(@as(usize, 3), rowsResults.len); + // ROWS: each physical row gets its own running total, even tied ones. + try std.testing.expectEqual(@as(i64, 10), rowsResults[0].values[0].int); + try std.testing.expectEqual(@as(i64, 30), rowsResults[1].values[0].int); + try std.testing.expectEqual(@as(i64, 35), rowsResults[2].values[0].int); +} + +test "server table_window rejects a RANGE frame combined with row_number 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); + + 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 = "", .kind = .row_number }}; + const err = client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .range); + try std.testing.expectError(client_mod.ClientError.ServerBadRequest, err); +} + +test "server table_window lag/lead read the adjacent row's value along order, NULL 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 = .lag }, + .{ .field = "total", .kind = .lead }, + }; + const results = try client.tableWindow(allocator, "orders", &specs, &.{}, &orderBy, 0, 0, "", .rows); + defer protocol.freeWindowRows(allocator, results); + + // Sorted by total ascending: krakow/40, warsaw/100, warsaw/250. + try std.testing.expectEqual(@as(usize, 3), results.len); + try std.testing.expectEqual(@as(i64, 40), results[0].row.get("total").?.int); + try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, results[0].values[0])); + try std.testing.expectEqual(@as(i64, 100), results[0].values[1].int); + try std.testing.expectEqual(@as(i64, 100), results[1].row.get("total").?.int); + try std.testing.expectEqual(@as(i64, 40), results[1].values[0].int); + try std.testing.expectEqual(@as(i64, 250), results[1].values[1].int); + try std.testing.expectEqual(@as(i64, 250), results[2].row.get("total").?.int); + try std.testing.expectEqual(@as(i64, 100), results[2].values[0].int); + try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, results[2].values[1])); +} + +test "server table_window lag with an explicit offset and a caller-supplied default, 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 = .lag, .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 - lag offset 2 reaches back + // two physical positions, so only the LAST row has a real source (40); + // the first two fall outside the sequence and get the -1 default. + try std.testing.expectEqual(@as(i64, -1), results[0].values[0].int); + try std.testing.expectEqual(@as(i64, -1), results[1].values[0].int); + try std.testing.expectEqual(@as(i64, 40), results[2].values[0].int); +} + +test "server table_window rejects a RANGE frame combined with lag 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); + + var db = try testDb(allocator, path); + defer db.close(); + try seedAggregateOrders(allocator, &db); +