feat: flesh out server
This commit is contained in:
1 parent
13d3b3e43b
commit
794a2465f5
1 file changed
+161
+161
@@ -7552,3 +7552,164 @@ test "server txn_op rejects a CHECK constraint violation over the wire and commi
|
||||
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 entries = [_]protocol.TxnOpEntry{
|
||||
.{ .verb = .put, .table = "orders", .pk = .{ .int = 1 }, .fields = &.{.{ .name = "qty", .value = .{ .int = 5 } }} },
|
||||
.{ .verb = .put, .table = "orders", .pk = .{ .int = 2 }, .fields = &.{.{ .name = "qty", .value = .{ .int = -1 } }} },
|
||||
};
|
||||
try std.testing.expectError(client_mod.ClientError.ServerInternal, client.txnOp(&entries));
|
||||
|
||||
// Whole transaction is atomic across both entries: the earlier,
|
||||
// individually-valid row 1 must not have been committed either.
|
||||
var orders = try querymod.Table.open(&db, allocator, "orders");
|
||||
defer orders.deinit();
|
||||
try std.testing.expect((try orders.get(.{ .int = 1 })) == null);
|
||||
try std.testing.expect((try orders.get(.{ .int = 2 })) == null);
|
||||
}
|
||||
|
||||
// `handleTxnOp` runs every entry through `TableTxn.put`/`TableTxn.delete`
|
||||
// (see txn_table.zig), which now enforce ON DELETE RESTRICT exactly like
|
||||
// CHECK/UNIQUE/FOREIGN KEY on put above - never separately wired for this
|
||||
// opcode either. The interesting case a plain `table_delete` test cannot
|
||||
// cover: both the child insert AND the restricted parent delete are only
|
||||
// staged within this ONE `txn_op` batch, never durable on their own -
|
||||
// `TableTxn.delete`'s RESTRICT check has to see the child through
|
||||
// `Transaction.scanPrefix`'s same-transaction visibility (see
|
||||
// txn_table.zig's own unit test for this), not a plain Db scan, or this
|
||||
// would wrongly let the delete through.
|
||||
test "server txn_op rejects an ON DELETE RESTRICT violation over the wire, seeing a child staged earlier in the same batch" {
|
||||
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);
|
||||
}
|
||||
|
||||
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 entries = [_]protocol.TxnOpEntry{
|
||||
.{ .verb = .put, .table = "orders", .pk = .{ .int = 100 }, .fields = &.{.{ .name = "customer_id", .value = .{ .int = 1 } }} },
|
||||
.{ .verb = .del, .table = "customers", .pk = .{ .int = 1 } },
|
||||
};
|
||||
try std.testing.expectError(client_mod.ClientError.ServerInternal, client.txnOp(&entries));
|
||||
|
||||
// Nothing committed: neither the staged order nor the rejected delete.
|
||||
var orders = try querymod.Table.open(&db, allocator, "orders");
|
||||
defer orders.deinit();
|
||||
try std.testing.expect((try orders.get(.{ .int = 100 })) == null);
|
||||
|
||||
var customers = try querymod.Table.open(&db, allocator, "customers");
|
||||
defer customers.deinit();
|
||||
var stillThere = (try customers.get(.{ .int = 1 })).?;
|
||||
stillThere.deinit();
|
||||
}
|
||||
|
||||
test "server txn_op rejects an empty operation list with bad_request" {
|
||||
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 client_mod = @import("client.zig");
|
||||
var client = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||
defer client.close();
|
||||
|
||||
try std.testing.expectError(client_mod.ClientError.ServerBadRequest, client.txnOp(&.{}));
|
||||
}
|
||||
|
||||
test "server rejects a malformed txn_op frame 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 u32 entry count of 1 promises one full entry, but the payload ends
|
||||
// right after it - not even a verb byte follows.
|
||||
var payload: [4]u8 = undefined;
|
||||
std.mem.writeInt(u32, &payload, 1, .little);
|
||||
try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.txn_op), &payload);
|
||||
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 - a malformed frame is
|
||||
// a client mistake, not a fatal error for the server.
|
||||
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);
|
||||
}
|
||||
|
||||
// Proves the `txn_op` code path (decode -> `Transaction`/`TableTxn` ->
|
||||
// index maintenance -> commit, all under `Server.dispatch`'s single
|
||||
// `self.mutex`) holds up under real concurrent OS threads each on their
|
||||
// own real TCP connection - the same rigor `coll_op`'s/`table_select`'s
|
||||
// own concurrency tests apply, but for the table+index layer instead of
|
||||
// the flat keyspace or the Redis-like collections, which is a genuinely
|
||||
// different code path (this is the first concurrency test to drive
|
||||
// `TableTxn`/index maintenance through a live socket at all;
|
||||
// `concurrent.zig`'s own `TableTxn` concurrency test bypasses the network
|
||||
// entirely via `GuardedDb`). Each worker writes into two disjoint rows per
|
||||
// round (its own row in `widgets`, indexed by `owner`, and a companion row
|
||||
// in `tags`) so a lost or duplicated write from bad interleaving would
|
||||
// show up as a wrong row count or a wrong index membership, not just a
|
||||
// wrong number.
|
||||
test "server txn_op survives many real concurrent connections writing disjoint rows across two tables" {
|
||||
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 widgets = try querymod.Table.open(&db, allocator, "widgets");
|
||||
defer widgets.deinit();
|
||||
try widgets.createIndex("owner");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user