From 7bd98b058003495705c9406a0373f5b89a0a6daf Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 17:51:00 +0200 Subject: [PATCH] feat: build out server --- src/server.zig | 156 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/server.zig b/src/server.zig index 108c3cd..28d2912 100644 --- a/src/server.zig +++ b/src/server.zig @@ -2489,3 +2489,159 @@ test "server table_explain rejects a malformed expr with bad_request, not a cras 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 }, &.{}); + } + + 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.tableExplain(allocator, "users", "city bogus s:warsaw"); + try std.testing.expectError(client_mod.ClientError.ServerBadRequest, err); + + // The connection survives a bad_request and can still serve a good + // request right after - same expectation `table_select`'s malformed-expr + // test above holds for its own opcode. + var plan = try client.tableExplain(allocator, "users", ""); + defer plan.deinit(allocator); + try std.testing.expectEqual(querymod.PlanKind.full_scan, plan.kind); +} + +test "server table_update applies a set clause to matching rows over real TCP, keeping an index in sync" { + 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 = "active", .value = .{ .int = 0 } } }); + try table.put(.{ .int = 2 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "active", .value = .{ .int = 0 } } }); + try table.put(.{ .int = 3 }, &.{ .{ .name = "city", .value = .{ .text = "krakow" } }, .{ .name = "active", .value = .{ .int = 0 } } }); + } + + 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 sets = [_]rowmod.Field{.{ .name = "city", .value = .{ .text = "gdansk" } }}; + const updated = try client.tableUpdate("users", &sets, "city eq s:warsaw"); + try std.testing.expectEqual(@as(u64, 2), updated); + + // The old index entry is gone for both moved rows... + const stale = try client.tableSelect(allocator, "users", "city eq s:warsaw", 0, 0, &.{}); + defer protocol.freeRows(allocator, stale); + try std.testing.expectEqual(@as(usize, 0), stale.len); + + // ...a fresh one under the new value finds them, and the row not + // matched by the predicate (pk 3, krakow) was left untouched. + const moved = try client.tableSelect(allocator, "users", "city eq s:gdansk", 0, 0, &.{}); + defer protocol.freeRows(allocator, moved); + try std.testing.expectEqual(@as(usize, 2), moved.len); + + const untouched = try client.tableSelect(allocator, "users", "city eq s:krakow", 0, 0, &.{}); + defer protocol.freeRows(allocator, untouched); + try std.testing.expectEqual(@as(usize, 1), untouched.len); +} + +test "server table_update 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 }, &.{}); + } + + 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 sets = [_]rowmod.Field{.{ .name = "active", .value = .{ .int = 1 } }}; + const err = client.tableUpdate("users", &sets, "city bogus s:warsaw"); + try std.testing.expectError(client_mod.ClientError.ServerBadRequest, err); + + // The connection survives a bad_request and can still serve a good + // request right after - same expectation `table_explain`'s malformed + // test above holds for its own opcode. + const updated = try client.tableUpdate("users", &sets, ""); + try std.testing.expectEqual(@as(u64, 1), updated); +} + +test "server rejects a malformed table_update frame (bad set fields) with bad_request without crashing" { + 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); + try server.start(); + defer server.stop(); + + const stream = try std.net.tcpConnectToAddress(server.boundAddress()); + defer stream.close(); + + // A well-formed table name ("users"), then a "set fields" byte string + // that decodeTableUpdate happily hands back as opaque bytes but + // Row.decode rejects (a field count of 0xFFFFFFFF with nothing behind + // it) - the decode failure `handleTableUpdate` itself must catch, + // unlike `decodeTableUpdate`'s own malformed-frame check above it. + var payload = std.ArrayList(u8).init(allocator); + defer payload.deinit(); + var lenBuf: [4]u8 = undefined; + std.mem.writeInt(u32, &lenBuf, 5, .little); + try payload.appendSlice(&lenBuf); + try payload.appendSlice("users"); + std.mem.writeInt(u32, &lenBuf, 4, .little); + try payload.appendSlice(&lenBuf); + std.mem.writeInt(u32, &lenBuf, 0xFFFFFFFF, .little); + try payload.appendSlice(&lenBuf); + + try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.table_update), payload.items); + var frame = try protocol.readFrame(allocator, stream.reader()); + defer frame.deinit(); + try std.testing.expectEqual(@intFromEnum(protocol.Status.bad_request), frame.tag); + + // The connection must still be usable afterward. + try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.ping), ""); + var pingFrame = try protocol.readFrame(allocator, stream.reader()); + defer pingFrame.deinit(); + try std.testing.expectEqual(@intFromEnum(protocol.Status.ok), pingFrame.tag); +} +