feat: add ReaderCtx to server
This commit is contained in:
1 parent
b056b0901e
commit
eee83a0ec7
1 file changed
+154
+154
@@ -5015,3 +5015,157 @@ test "server table_join runs left, right, and full outer variants with predicate
|
||||
}
|
||||
try std.testing.expect(carolUnmatched);
|
||||
|
||||
const right = try client.tableJoin(allocator, .right, "customers", querymod.pkField, "orders", "customer_id", 0, 0, "", "");
|
||||
defer protocol.freeJoinPairs(allocator, right);
|
||||
try std.testing.expectEqual(@as(usize, 3), right.len);
|
||||
var orphanOrderUnmatched = false;
|
||||
for (right) |p| {
|
||||
if (p.left == null) orphanOrderUnmatched = true;
|
||||
}
|
||||
try std.testing.expect(orphanOrderUnmatched);
|
||||
|
||||
const full = try client.tableJoin(allocator, .full, "customers", querymod.pkField, "orders", "customer_id", 0, 0, "", "");
|
||||
defer protocol.freeJoinPairs(allocator, full);
|
||||
try std.testing.expectEqual(@as(usize, 5), full.len);
|
||||
|
||||
// rightExprText filters the orders side before the join runs, same as
|
||||
// `innerJoin`'s `rightPred` - narrowing to `total gt i:40` drops one of
|
||||
// alice's two orders.
|
||||
const filtered = try client.tableJoin(allocator, .inner, "customers", querymod.pkField, "orders", "customer_id", 0, 0, "", "total gt i:40");
|
||||
defer protocol.freeJoinPairs(allocator, filtered);
|
||||
try std.testing.expectEqual(@as(usize, 1), filtered.len);
|
||||
try std.testing.expectEqual(@as(i64, 50), filtered[0].right.?.get("total").?.int);
|
||||
}
|
||||
|
||||
test "server table_join windows a real TCP result through offset and limit" {
|
||||
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 seedJoinTables(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();
|
||||
|
||||
// Unpaged left outer join order: (alice,101) (alice,102) (bob,nil) (carol,nil).
|
||||
const unpaged = try client.tableJoin(allocator, .left, "customers", querymod.pkField, "orders", "customer_id", 0, 0, "", "");
|
||||
defer protocol.freeJoinPairs(allocator, unpaged);
|
||||
try std.testing.expectEqual(@as(usize, 4), unpaged.len);
|
||||
|
||||
const windowed = try client.tableJoin(allocator, .left, "customers", querymod.pkField, "orders", "customer_id", 1, 2, "", "");
|
||||
defer protocol.freeJoinPairs(allocator, windowed);
|
||||
try std.testing.expectEqual(@as(usize, 2), windowed.len);
|
||||
try std.testing.expectEqual(@as(i64, 30), windowed[0].right.?.get("total").?.int);
|
||||
try std.testing.expectEqualStrings("bob", windowed[1].left.?.get("name").?.text);
|
||||
try std.testing.expect(windowed[1].right == null);
|
||||
|
||||
const pastEnd = try client.tableJoin(allocator, .left, "customers", querymod.pkField, "orders", "customer_id", 50, 0, "", "");
|
||||
defer protocol.freeJoinPairs(allocator, pastEnd);
|
||||
try std.testing.expectEqual(@as(usize, 0), pastEnd.len);
|
||||
}
|
||||
|
||||
test "server table_join 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();
|
||||
try seedJoinTables(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 err = client.tableJoin(allocator, .inner, "customers", querymod.pkField, "orders", "customer_id", 0, 0, "name bogus s:alice", "");
|
||||
try std.testing.expectError(client_mod.ClientError.ServerBadRequest, err);
|
||||
}
|
||||
|
||||
// `table_select` is the only networked table opcode - rows can only be
|
||||
// written locally (CLI `table put`), never over the wire - so this cannot
|
||||
// reuse the coll_op "many writers, one shared counter" shape. What it CAN
|
||||
// share with a live server is the sstable storage underneath: `flush` and
|
||||
// `compact` (both real networked opcodes) mutate `Db.sstables` in place,
|
||||
// closing and destroying old `Reader`s and swapping in new ones while
|
||||
// `compact` in particular collapses the whole list down to a single file.
|
||||
// A `table_select` cursor concurrently mid-scan over those same readers is
|
||||
// exactly the use-after-free/torn-read shape a missing lock around
|
||||
// `Server.dispatch` would produce - `self.mutex` must serialize every
|
||||
// request regardless of which opcode it is, not just same-opcode traffic.
|
||||
test "table_select survives concurrent flush and compact reshaping the underlying sstables" {
|
||||
const allocator = std.testing.allocator;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
const path = try tmp.dir.realpathAlloc(allocator, ".");
|
||||
defer allocator.free(path);
|
||||
|
||||
// A small target size so a handful of `flush`/`compact` calls do real
|
||||
// structural work instead of needing megabytes of data first.
|
||||
var db = try Db.open(allocator, path, .{ .sstableTargetBytes = 512 });
|
||||
defer db.close();
|
||||
|
||||
const rowCount = 30;
|
||||
{
|
||||
var table = try querymod.Table.open(&db, allocator, "people");
|
||||
defer table.deinit();
|
||||
var pk: i64 = 1;
|
||||
while (pk <= rowCount) : (pk += 1) {
|
||||
const city: []const u8 = switch (@mod(pk, 3)) {
|
||||
0 => "warsaw",
|
||||
1 => "krakow",
|
||||
else => "gdansk",
|
||||
};
|
||||
try table.put(.{ .int = pk }, &.{
|
||||
.{ .name = "city", .value = .{ .text = city } },
|
||||
.{ .name = "age", .value = .{ .int = pk * 2 } },
|
||||
});
|
||||
}
|
||||
try db.flush();
|
||||
}
|
||||
|
||||
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");
|
||||
const readerThreads = 4;
|
||||
const readerIters = 40;
|
||||
const mutatorThreads = 2;
|
||||
const mutatorIters = 15;
|
||||
|
||||
const ReaderCtx = struct {
|
||||
allocator: Allocator,
|
||||
addr: std.net.Address,
|
||||
ok: bool = false,
|
||||
};
|
||||
// A multi-element order-by (city ascending, then age descending as a
|
||||
// tiebreak within each city) - `handleTableSelect` decodes a wire
|
||||
// orderBy count > 1 into `Table.selectPageOrdered`, which cannot
|
||||
// early-stop mid-scan the way an unordered `selectPage` can, so it reads
|
||||
// more of the table before sorting. This proves that path survives real
|
||||
// `flush`/`compact` reshaping the underlying sstables mid-scan, not just
|
||||
// the single-column order-by already covered by
|
||||
// "orders a remote result by a field before windowing it" above.
|
||||
const cityOrderBy = [_]querymod.OrderBy{
|
||||
.{ .field = "city" },
|
||||
.{ .field = "age", .descending = true },
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user