diff --git a/src/main.zig b/src/main.zig index e3ee9e6..202e0fc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2560,6 +2560,179 @@ fn runRemoteExport(allocator: std.mem.Allocator, stdout: anytype, stderr: anytyp /// 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 +/// a local script file), and its bytes are sent to the server in one +/// frame. `--user`/`--pass`/`--token` authenticate first, needed only +/// against a server started with `serve --auth`. +fn runRemoteImport(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void { + if (rest.len < 1) return fail(stderr, "remote-import 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-import needs
csv|json "); + const format = try parseDumpFormat(stderr, args[0]); + + const content = std.fs.cwd().readFileAlloc(allocator, args[1], 1 << 30) catch |err| { + try stderr.print("failed to read '{s}': {s}\n", .{ args[1], @errorName(err) }); + std.process.exit(1); + }; + defer allocator.free(content); + + const imported = client.tableImport(rest[0], format, content) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("{d}\n", .{imported}); +} + +const JoinCliMode = enum { inner, left, right, full }; + +/// Pulls `--left`/`--right`/`--full` (mutually exclusive, last one wins, +/// same as local `table join`'s parsing loop) and `--left-expr E`/ +/// `--right-expr E` out of a `remote-join` argument list, same "strip +/// recognized flags, keep the rest" shape as `extractPageFlags`/ +/// `extractAuthFlags`. An expr not given defaults to the empty string, +/// meaning "select every row on that side" - the same convention +/// `tableSelect`'s empty `exprText` already uses. Caller owns and frees +/// `rest`. +fn extractJoinFlags(allocator: std.mem.Allocator, args: []const []const u8) !struct { + mode: JoinCliMode, + leftExpr: []const u8, + rightExpr: []const u8, + rest: [][]const u8, +} { + var mode: JoinCliMode = .inner; + var leftExpr: []const u8 = ""; + var rightExpr: []const u8 = ""; + var rest = std.ArrayList([]const u8).init(allocator); + errdefer rest.deinit(); + + var i: usize = 0; + while (i < args.len) : (i += 1) { + if (std.mem.eql(u8, args[i], "--left")) { + mode = .left; + } else if (std.mem.eql(u8, args[i], "--right")) { + mode = .right; + } else if (std.mem.eql(u8, args[i], "--full")) { + mode = .full; + } else if (std.mem.eql(u8, args[i], "--left-expr")) { + i += 1; + if (i >= args.len) return error.InvalidJoinFlag; + leftExpr = args[i]; + } else if (std.mem.eql(u8, args[i], "--right-expr")) { + i += 1; + if (i >= args.len) return error.InvalidJoinFlag; + rightExpr = args[i]; + } else { + try rest.append(args[i]); + } + } + return .{ .mode = mode, .leftExpr = leftExpr, .rightExpr = rightExpr, .rest = try rest.toOwnedSlice() }; +} + +/// Connects to a remote `cobalt serve` instance and runs `table join` +/// there instead of against a local Db - the client-side counterpart of +/// `server.zig`'s `table_join` opcode, mirroring `runRemoteSelect`'s shape +/// but for all four of `join.zig`'s equi-join variants instead of a plain +/// select. `--user`/`--pass`/`--token` authenticate first, needed only +/// against a server started with `serve --auth`; printing is identical to +/// the local `table join` command (`L ` / `R `, or `L (nil)` / +/// `R (nil)` for the unmatched side of an outer join). +fn runRemoteJoin(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void { + 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) catch return fail(stderr, "invalid --user/--pass/--token value"); + defer allocator.free(authFlags.rest); + + try authenticateIfRequested(&client, stderr, authFlags.auth); + + const jf = extractJoinFlags(allocator, authFlags.rest) catch return fail(stderr, "invalid --left-expr/--right-expr value"); + defer allocator.free(jf.rest); + + const pf = extractPageFlags(allocator, jf.rest) catch return fail(stderr, "invalid --limit/--offset value"); + defer allocator.free(pf.rest); + + if (pf.rest.len < 4) return fail(stderr, "remote-join needs [--left|--right|--full] [--left-expr E] [--right-expr E] [--limit N] [--offset N]"); + + const mode: cobalt.protocol.JoinMode = switch (jf.mode) { + .inner => .inner, + .left => .left, + .right => .right, + .full => .full, + }; + + const pairs = client.tableJoin(allocator, mode, pf.rest[0], pf.rest[1], pf.rest[2], pf.rest[3], pf.page.offset, pf.page.limit, jf.leftExpr, jf.rightExpr) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + defer cobalt.protocol.freeJoinPairs(allocator, pairs); + + for (pairs) |p| { + if (p.left) |l| { + try stdout.writeAll("L "); + try printRow(stdout, l); + } else { + try stdout.writeAll("L (nil)\n"); + } + if (p.right) |r| { + try stdout.writeAll("R "); + try printRow(stdout, r); + } else { + try stdout.writeAll("R (nil)\n"); + } + } +} + +/// Connects to a remote `cobalt serve` instance and runs `table aggregate` +/// there instead of against a local Db - the client-side counterpart of +/// `server.zig`'s `table_aggregate` opcode, mirroring `runRemoteSelect`'s +/// shape. Same `*` sentinel as local `table aggregate` for "no GROUP BY" +/// (dispatches to `client.tableAggregateGlobal` instead of +/// `tableAggregateGroups`), same comma-separated spec grammar +/// (`parseAggSpecs`), and the same output format as the local command +/// (`printAggLabel`/`printValue`). `--limit`/`--offset` window the result +/// groups server-side (ignored for the `*` global path, same as local +/// `table aggregate`), same convention as `remote-join`. +/// `--user`/`--pass`/`--token` authenticate first, needed only against a +/// server started with `serve --auth`. An optional literal `having ` +/// after the WHERE-style expr (split out by `splitAggFilterArgs`, the same +/// helper the local `table aggregate` handler uses) filters the result +/// groups server-side after aggregation, before `--limit`/`--offset` window +/// them. An optional literal `order [,field...]` after that +/// sorts the (HAVING-narrowed) result groups server-side before that same +/// window, ignored for the `*` global path exactly like `--limit`/`--offset` +/// already are - see `Client.tableAggregateGroups`/`tableAggregateGlobal`. +fn runRemoteAggregate(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void { + if (rest.len < 3) return fail(stderr, "remote-aggregate needs
[,...] [expr...] [having ] [order [,field...]] [--limit N] [--offset N]"); + 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 table = rest[0]; + const groupField = rest[1]; + const specs = parseAggSpecs(allocator, rest[2]) catch return fail(stderr, "invalid aggregate spec (expected count | count:field | sum:field | avg:field | min:field | max:field | distinct:field, comma-separated)"); + defer allocator.free(specs); + } fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {