diff --git a/src/main.zig b/src/main.zig
index 302d26d..e3ee9e6 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -2387,6 +2387,179 @@ fn runRemoteSelect(allocator: std.mem.Allocator, stdout: anytype, stderr: anytyp
try authenticateIfRequested(&client, stderr, authFlags.auth);
const sel = extractSelectFlags(allocator, authFlags.rest) catch return fail(stderr, "invalid --limit/--offset/--order-by value");
+ defer allocator.free(sel.rest);
+ defer allocator.free(sel.flags.orderBy);
+
+ const exprText = try std.mem.join(allocator, " ", sel.rest);
+ defer allocator.free(exprText);
+
+ const rows = client.tableSelect(allocator, rest[0], exprText, sel.flags.page.offset, sel.flags.page.limit, sel.flags.orderBy) catch |err| {
+ try stderr.print("error: {s}\n", .{@errorName(err)});
+ std.process.exit(1);
+ };
+ defer cobalt.protocol.freeRows(allocator, rows);
+ for (rows) |r| try printRow(stdout, r);
+}
+
+/// Connects to a remote `cobalt serve` instance and runs `table explain`
+/// there instead of against a local Db - the client-side counterpart of
+/// `server.zig`'s `table_explain` opcode, same shape as `runRemoteSelect`
+/// but reporting a `query.Plan` instead of rows. `--user`/`--pass`/
+/// `--token` authenticate first, needed only against a server started with
+/// `serve --auth`.
+fn runRemoteExplain(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
+ if (rest.len < 1) return fail(stderr, "remote-explain needs
[expr...]");
+ const address = try parseHostPort(stdout, addressSpec);
+
+ var client = cobalt.Client.connect(allocator, address) catch |err| {
+ try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
+ std.process.exit(1);
+ };
+ defer client.close();
+
+ const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
+ defer allocator.free(authFlags.rest);
+
+ try authenticateIfRequested(&client, stderr, authFlags.auth);
+
+ const exprText = try std.mem.join(allocator, " ", authFlags.rest);
+ defer allocator.free(exprText);
+
+ var plan = client.tableExplain(allocator, rest[0], exprText) catch |err| {
+ try stderr.print("error: {s}\n", .{@errorName(err)});
+ std.process.exit(1);
+ };
+ defer plan.deinit(allocator);
+ try printPlan(stdout, plan);
+}
+
+/// Connects to a remote `cobalt serve` instance and runs `table update`
+/// there instead of against a local Db - the client-side counterpart of
+/// `server.zig`'s `table_update` opcode, same shape as `runRemoteExplain`
+/// but taking a `--set` field list and printing the number of rows updated
+/// instead of a plan. `--user`/`--pass`/`--token` authenticate first,
+/// needed only against a server started with `serve --auth`.
+fn runRemoteUpdate(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
+ if (rest.len < 1) return fail(stderr, "remote-update needs --set field=value[,field=value...] [expr...]");
+ const address = try parseHostPort(stdout, addressSpec);
+
+ var client = cobalt.Client.connect(allocator, address) catch |err| {
+ try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
+ std.process.exit(1);
+ };
+ defer client.close();
+
+ const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
+ defer allocator.free(authFlags.rest);
+
+ try authenticateIfRequested(&client, stderr, authFlags.auth);
+
+ const setf = extractSetFlag(allocator, authFlags.rest) catch |err| return fail(stderr, if (err == error.MissingSetFlag)
+ "remote-update requires --set field=value[,field=value...]"
+ else
+ "invalid --set value");
+ defer allocator.free(setf.sets);
+ defer allocator.free(setf.rest);
+
+ const exprText = try std.mem.join(allocator, " ", setf.rest);
+ defer allocator.free(exprText);
+
+ const updated = client.tableUpdate(rest[0], setf.sets, exprText) catch |err| {
+ try stderr.print("error: {s}\n", .{@errorName(err)});
+ std.process.exit(1);
+ };
+ try stdout.print("{d}\n", .{updated});
+}
+
+/// Connects to a remote `cobalt serve` instance and runs `table delete`
+/// there instead of against a local Db - the client-side counterpart of
+/// `server.zig`'s `table_delete` opcode, same shape as `runRemoteExplain`
+/// (no `--set` flag, since a delete has no `SET` clause) but printing the
+/// number of rows deleted instead of a plan. `--user`/`--pass`/`--token`
+/// authenticate first, needed only against a server started with
+/// `serve --auth`.
+fn runRemoteDelete(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
+ if (rest.len < 1) return fail(stderr, "remote-delete needs [expr...]");
+ const address = try parseHostPort(stdout, addressSpec);
+
+ var client = cobalt.Client.connect(allocator, address) catch |err| {
+ try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
+ std.process.exit(1);
+ };
+ defer client.close();
+
+ const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
+ defer allocator.free(authFlags.rest);
+
+ try authenticateIfRequested(&client, stderr, authFlags.auth);
+
+ const exprText = try std.mem.join(allocator, " ", authFlags.rest);
+ defer allocator.free(exprText);
+
+ const deleted = client.tableDelete(rest[0], exprText) catch |err| {
+ try stderr.print("error: {s}\n", .{@errorName(err)});
+ std.process.exit(1);
+ };
+ try stdout.print("{d}\n", .{deleted});
+}
+
+fn parseDumpFormat(stderr: anytype, text: []const u8) !cobalt.protocol.DumpFormat {
+ if (std.mem.eql(u8, text, "csv")) return .csv;
+ if (std.mem.eql(u8, text, "json")) return .json;
+ try fail(stderr, "format must be csv or json");
+ unreachable;
+}
+
+/// Connects to a remote `cobalt serve` instance and runs `table export`
+/// there instead of against a local Db - the client-side counterpart of
+/// `server.zig`'s `table_export` opcode. Unlike every other remote-* op,
+/// the destination `` names a file on THIS machine, not the server:
+/// the server renders the whole table into memory and sends the bytes
+/// back in one frame, and this function is the one that actually creates
+/// `` and writes them - the local `table export` writes straight to
+/// disk itself since it already has a `*Db` handle in-process, but a
+/// remote client only ever gets bytes over the wire. `--user`/`--pass`/
+/// `--token` authenticate first, needed only against a server started
+/// with `serve --auth`.
+fn runRemoteExport(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
+ if (rest.len < 1) return fail(stderr, "remote-export needs csv|json ");
+ const address = try parseHostPort(stdout, addressSpec);
+
+ var client = cobalt.Client.connect(allocator, address) catch |err| {
+ try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
+ std.process.exit(1);
+ };
+ defer client.close();
+
+ const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
+ defer allocator.free(authFlags.rest);
+
+ try authenticateIfRequested(&client, stderr, authFlags.auth);
+
+ const args = authFlags.rest;
+ if (args.len < 2) return fail(stderr, "remote-export needs csv|json ");
+ const format = try parseDumpFormat(stderr, args[0]);
+
+ var result = client.tableExport(allocator, rest[0], format) catch |err| {
+ try stderr.print("error: {s}\n", .{@errorName(err)});
+ std.process.exit(1);
+ };
+ defer result.deinit(allocator);
+
+ const file = std.fs.cwd().createFile(args[1], .{ .truncate = true }) catch |err| {
+ try stderr.print("failed to write '{s}': {s}\n", .{ args[1], @errorName(err) });
+ std.process.exit(1);
+ };
+ defer file.close();
+ try file.writeAll(result.content);
+
+ try stdout.print("{d}\n", .{result.rowCount});
+}
+
+/// Connects to a remote `cobalt serve` instance and runs `table import`
+/// there instead of against a local Db - the client-side counterpart of
+/// `server.zig`'s `table_import` opcode. Mirrors `runRemoteExport`: the
+/// source `` is read from THIS machine (the same way `runTxn` reads
}
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {