From 7424bb6ed8ee71c8c4e9f273e0f56becd94aff23 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 15:30:22 +0200 Subject: [PATCH] feat: extend main with runRemoteExplain and runRemoteUpdate --- src/main.zig | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) 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 {