From cac096469e703799a9724c1c5303a15179923975 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 17:48:55 +0200 Subject: [PATCH] feat: expand server --- src/server.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/server.zig b/src/server.zig index 4b90135..108c3cd 100644 --- a/src/server.zig +++ b/src/server.zig @@ -2330,3 +2330,162 @@ test "per-connection rate limit gives each connection its own budget" { var clientB = try client_mod.Client.connect(allocator, server.boundAddress()); defer clientB.close(); + // Exhaust A's private budget first: B must be unaffected because each + // connection got its own bucket, not a bucket shared across the server. + try clientA.ping(); + try clientA.ping(); + try std.testing.expectError(client_mod.ClientError.ServerRateLimited, clientA.ping()); + + try clientB.ping(); + try clientB.ping(); + try std.testing.expectError(client_mod.ClientError.ServerRateLimited, clientB.ping()); + + try std.testing.expectEqual(@as(u64, 2), server.connRateLimitedTotal.get()); + try std.testing.expectEqual(@as(u64, 0), server.rateLimitedTotal.get()); +} + +test "per-connection and global rate limits both apply, connection budget checked first" { + 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(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enablePerConnectionRateLimit(10, 1); + server.enableRateLimit(2, 1); + 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(); + + try client.ping(); + try client.ping(); + try std.testing.expectError(client_mod.ClientError.ServerRateLimited, client.ping()); + + // The global bucket was the one that actually ran out, not the + // per-connection one (which had plenty of headroom left). + try std.testing.expectEqual(@as(u64, 1), server.rateLimitedTotal.get()); + try std.testing.expectEqual(@as(u64, 0), server.connRateLimitedTotal.get()); +} + +test "server table_select executes a remote expr against real rows, using an index when available" { + 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.createIndex("city"); + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 20 } }, + }); + try table.put(.{ .int = 2 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 40 } }, + }); + try table.put(.{ .int = 3 }, &.{ + .{ .name = "city", .value = .{ .text = "krakow" } }, + .{ .name = "age", .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, "users", "city eq s:warsaw and age gte i:30", 0, 0, &.{}); + defer protocol.freeRows(allocator, rows); + try std.testing.expectEqual(@as(usize, 1), rows.len); + try std.testing.expectEqual(@as(i64, 2), rows[0].get(querymod.pkField).?.int); + + const all = try client.tableSelect(allocator, "users", "", 0, 0, &.{}); + defer protocol.freeRows(allocator, all); + try std.testing.expectEqual(@as(usize, 3), all.len); +} + +test "server table_explain reports index_eq, composite_eq, and full_scan over real TCP, matching what table_select would actually use" { + 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.createIndex("age"); + try table.createCompositeIndex("city_age", &.{ "city", "age" }); + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + }); + } + + 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(); + + // Single-field index eq on `age`. + var agePlan = try client.tableExplain(allocator, "users", "age eq i:30"); + defer agePlan.deinit(allocator); + try std.testing.expectEqual(querymod.PlanKind.index_eq, agePlan.kind); + try std.testing.expectEqualStrings("age", agePlan.indexName.?); + + // Composite index, full prefix matched by equality on both columns. + var compositePlan = try client.tableExplain(allocator, "users", "city eq s:warsaw and age eq i:30"); + defer compositePlan.deinit(allocator); + try std.testing.expectEqual(querymod.PlanKind.composite_eq, compositePlan.kind); + try std.testing.expectEqualStrings("city_age", compositePlan.indexName.?); + try std.testing.expectEqual(@as(usize, 2), compositePlan.matchedFields); + try std.testing.expectEqual(@as(usize, 2), compositePlan.totalFields); + + // No predicate on an indexed field anywhere -> full scan. + var scanPlan = try client.tableExplain(allocator, "users", "city eq s:warsaw"); + defer scanPlan.deinit(allocator); + try std.testing.expectEqual(querymod.PlanKind.full_scan, scanPlan.kind); + + // Cross-check: what table_explain reports for the composite query is + // exactly what table_select actually finds when it runs it - the whole + // point of an explain feature is not lying about what execution does. + const rows = try client.tableSelect(allocator, "users", "city eq s:warsaw and age eq i:30", 0, 0, &.{}); + defer protocol.freeRows(allocator, rows); + try std.testing.expectEqual(@as(usize, 1), rows.len); +} + +test "server table_explain 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(); +