feat: flesh out server
This commit is contained in:
1 parent
6fb0088939
commit
2a7fee541b
1 file changed
+159
+159
@@ -3125,3 +3125,162 @@ test "server table_delete rejects a malformed expr with bad_request, not a crash
|
|||||||
try table.put(.{ .int = 1 }, &.{});
|
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.tableDelete("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_update`'s malformed
|
||||||
|
// test above holds for its own opcode.
|
||||||
|
const deleted = try client.tableDelete("users", "");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `handleTableDelete` calls `Table.deleteWhere` directly (see its own
|
||||||
|
// source), the exact function `Table.delete`'s ON DELETE RESTRICT check
|
||||||
|
// (query.zig, backed by `ReferencedBySet`) is wired into - so this was
|
||||||
|
// never a separate opcode to teach about FOREIGN KEY, only a round trip to
|
||||||
|
// prove the wire path actually reaches it, the same "real dispatch, not
|
||||||
|
// just the in-process unit test" shape the CHECK/FK/UNIQUE `txn_op` tests
|
||||||
|
// elsewhere in this file already are.
|
||||||
|
test "server table_delete rejects a FOREIGN KEY RESTRICT violation over the wire and leaves the row in place" {
|
||||||
|
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 customers = try querymod.Table.open(&db, allocator, "customers");
|
||||||
|
defer customers.deinit();
|
||||||
|
try customers.put(.{ .int = 1 }, &.{});
|
||||||
|
|
||||||
|
var orders = try querymod.Table.open(&db, allocator, "orders");
|
||||||
|
defer orders.deinit();
|
||||||
|
try orders.createForeignKey("customer_id", "customers", querymod.pkField, .restrict);
|
||||||
|
try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .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.tableDelete("customers", "");
|
||||||
|
try std.testing.expectError(client_mod.ClientError.ServerInternal, err);
|
||||||
|
|
||||||
|
var customers = try querymod.Table.open(&db, allocator, "customers");
|
||||||
|
defer customers.deinit();
|
||||||
|
var stillThere = (try customers.get(.{ .int = 1 })).?;
|
||||||
|
defer stillThere.deinit();
|
||||||
|
|
||||||
|
// Once the referencing order is gone, the same request succeeds.
|
||||||
|
_ = try client.tableDelete("orders", "");
|
||||||
|
const deleted = try client.tableDelete("customers", "");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server rejects a malformed table_delete frame (bad table name) 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 length prefix (0xFFFFFFFF) claiming far more table-name bytes than
|
||||||
|
// the payload actually carries - `decodeTableDelete`'s own malformed-frame
|
||||||
|
// check must catch this before `Table.open` is ever reached.
|
||||||
|
var payload = std.ArrayList(u8).init(allocator);
|
||||||
|
defer payload.deinit();
|
||||||
|
var lenBuf: [4]u8 = undefined;
|
||||||
|
std.mem.writeInt(u32, &lenBuf, 0xFFFFFFFF, .little);
|
||||||
|
try payload.appendSlice(&lenBuf);
|
||||||
|
|
||||||
|
try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.table_delete), 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server table_export renders a real table to csv and json over TCP, matching the local dump.zig output" {
|
||||||
|
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();
|
||||||
|
try table.put(.{ .int = 1 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "age", .value = .{ .int = 30 } } });
|
||||||
|
try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "krakow" } }});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
var csvResult = try client.tableExport(allocator, "people", .csv);
|
||||||
|
defer csvResult.deinit(allocator);
|
||||||
|
try std.testing.expectEqual(@as(u64, 2), csvResult.rowCount);
|
||||||
|
|
||||||
|
var localCsv = std.ArrayList(u8).init(allocator);
|
||||||
|
defer localCsv.deinit();
|
||||||
|
{
|
||||||
|
var table = try querymod.Table.open(&db, allocator, "people");
|
||||||
|
defer table.deinit();
|
||||||
|
const n = try dumpmod.exportCsvWriter(allocator, &table, localCsv.writer());
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), n);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqualStrings(localCsv.items, csvResult.content);
|
||||||
|
|
||||||
|
var jsonResult = try client.tableExport(allocator, "people", .json);
|
||||||
|
defer jsonResult.deinit(allocator);
|
||||||
|
try std.testing.expectEqual(@as(u64, 2), jsonResult.rowCount);
|
||||||
|
// Every row's primary key shows up somewhere in the rendered JSON -
|
||||||
|
// enough to prove the server actually rendered real content instead
|
||||||
|
// of an empty/placeholder body, without re-parsing JSON here too.
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, jsonResult.content, "warsaw") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, jsonResult.content, "krakow") != null);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user