diff --git a/src/repl.zig b/src/repl.zig index 89b1b5a..b43078f 100644 --- a/src/repl.zig +++ b/src/repl.zig @@ -937,3 +937,169 @@ pub const Repl = struct { } } + fn cmdCollZSet(self: *Repl, op: []const u8, args: []const []const u8, writer: anytype) !void { + const ZSet = collectionsmod.ZSet; + if (std.mem.eql(u8, op, "add")) { + if (args.len < 3) return ReplError.WrongArgCount; + const scoreVal = std.fmt.parseFloat(f64, args[2]) catch return ReplError.InvalidArgument; + try ZSet.add(self.db, self.allocator, args[0], args[1], scoreVal); + try writer.writeAll("OK\n"); + } else if (std.mem.eql(u8, op, "score")) { + if (args.len < 2) return ReplError.WrongArgCount; + if (try ZSet.score(self.db, self.allocator, args[0], args[1])) |s| { + try writer.print("{d}\n", .{s}); + } else { + try writer.writeAll("(nil)\n"); + } + } else if (std.mem.eql(u8, op, "incrby")) { + if (args.len < 3) return ReplError.WrongArgCount; + const delta = std.fmt.parseFloat(f64, args[2]) catch return ReplError.InvalidArgument; + const s = try ZSet.incrBy(self.db, self.allocator, args[0], args[1], delta); + try writer.print("{d}\n", .{s}); + } else if (std.mem.eql(u8, op, "rem")) { + if (args.len < 2) return ReplError.WrongArgCount; + const removed = try ZSet.remove(self.db, self.allocator, args[0], args[1]); + try writer.print("{d}\n", .{@intFromBool(removed)}); + } else if (std.mem.eql(u8, op, "card")) { + if (args.len < 1) return ReplError.WrongArgCount; + const n = try ZSet.card(self.db, self.allocator, args[0]); + try writer.print("{d}\n", .{n}); + } else if (std.mem.eql(u8, op, "rangebyrank") or std.mem.eql(u8, op, "rangebyscore")) { + if (args.len < 3) return ReplError.WrongArgCount; + const zms = if (std.mem.eql(u8, op, "rangebyrank")) blk: { + const start = std.fmt.parseInt(i64, args[1], 10) catch return ReplError.InvalidArgument; + const stop = std.fmt.parseInt(i64, args[2], 10) catch return ReplError.InvalidArgument; + break :blk try ZSet.rangeByRank(self.db, self.allocator, args[0], start, stop); + } else blk: { + const min = std.fmt.parseFloat(f64, args[1]) catch return ReplError.InvalidArgument; + const max = std.fmt.parseFloat(f64, args[2]) catch return ReplError.InvalidArgument; + break :blk try ZSet.rangeByScore(self.db, self.allocator, args[0], min, max); + }; + defer { + for (zms) |zm| zm.free(self.allocator); + self.allocator.free(zms); + } + for (zms) |zm| try writer.print("{s}\t{d}\n", .{ zm.member, zm.score }); + } else if (std.mem.eql(u8, op, "rank")) { + if (args.len < 2) return ReplError.WrongArgCount; + if (try ZSet.rank(self.db, self.allocator, args[0], args[1])) |r| { + try writer.print("{d}\n", .{r}); + } else { + try writer.writeAll("(nil)\n"); + } + } else { + return ReplError.UnknownCollectionOp; + } + } + + fn cmdHelp(self: *Repl, writer: anytype) !void { + _ = self; + try writer.writeAll( + \\commands: + \\ put table put [field=value ...] + \\ get table get
+ \\ del table del
+ \\ scan [prefix] begin / commit / rollback + \\ stats / flush / compact exit / quit + \\ table select
[expr...] [--limit N] [--offset N] + \\ [--order-by f[:desc][,f[:desc]...]] + \\ table join [--left|--right|--full] [--limit N] [--offset N] + \\ table aggregate
[,...] [expr...] [--limit N] [--offset N] + \\ table window
[,...] [expr...] [partition f[,f...]] + \\ [order f[:desc][,f...]] [--limit N] [--offset N] [--range] + \\ coll [args...] kind: counter/hash/set/list/zset + \\ expire/persist/ttl coll expire/persist/ttl + \\ expire-sweep coll expire-sweep + \\ + ); + } +}; + +const PageFlags = struct { limit: u32 = 0, offset: u32 = 0 }; + +/// Pulls `--limit N` / `--offset N` out of a `table select` argument list +/// wherever they appear, returning the remaining tokens (destined to be +/// joined into the expr text) alongside the parsed page window. Same shape +/// as main.zig's identically-named helper for `table select`/`remote-select` +/// on the one-shot CLI - kept as its own small copy here rather than shared, +/// since main.zig is the binary entry point and not a module repl.zig can +/// import. Caller owns and frees `rest`. +fn extractPageFlags(allocator: Allocator, args: []const []const u8) !struct { page: PageFlags, rest: [][]const u8 } { + var page = PageFlags{}; + var rest = std.ArrayList([]const u8).init(allocator); + errdefer rest.deinit(); + + var i: usize = 0; + while (i < args.len) : (i += 1) { + const isLimit = std.mem.eql(u8, args[i], "--limit"); + const isOffset = std.mem.eql(u8, args[i], "--offset"); + if (isLimit or isOffset) { + i += 1; + if (i >= args.len) return error.InvalidPageFlag; + const n = std.fmt.parseInt(u32, args[i], 10) catch return error.InvalidPageFlag; + if (isLimit) page.limit = n else page.offset = n; + } else { + try rest.append(args[i]); + } + } + return .{ .page = page, .rest = try rest.toOwnedSlice() }; +} + +/// Pulls a lone `--range` flag (no value) out of `table window`'s trailing +/// args, requesting `window.FrameMode.range` instead of the default +/// `.rows` running frame - see `window.zig`'s `compute` doc comment. Same +/// shape as main.zig's identically-named helper, kept as its own small copy +/// here for the same reason `extractPageFlags` above is. Caller owns and +/// frees `rest`. +fn extractRangeFlag(allocator: Allocator, args: []const []const u8) !struct { range: bool, rest: [][]const u8 } { + var range = false; + var rest = std.ArrayList([]const u8).init(allocator); + errdefer rest.deinit(); + for (args) |a| { + if (std.mem.eql(u8, a, "--range")) { + range = true; + } else { + try rest.append(a); + } + } + return .{ .range = range, .rest = try rest.toOwnedSlice() }; +} + +const SelectFlags = struct { page: PageFlags = .{}, orderBy: []const querymod.OrderBy = &.{} }; + +/// `table select`-only counterpart of `extractPageFlags` above, mirroring +/// main.zig's identically-shaped `extractSelectFlags`/`SelectFlags`: same +/// `--limit`/`--offset` handling plus `--order-by +/// field[:desc][,field[:desc]...]` (comma-separated, applied left to +/// right), kept SEPARATE from `extractPageFlags` rather than folded into +/// it because `extractPageFlags` is also used by `table join`/`table +/// aggregate` here, neither of which sorts its result - recognizing +/// `--order-by` there too would silently consume the flag without +/// honoring it. Caller owns and frees `rest` AND `flags.orderBy`. +fn extractSelectFlags(allocator: Allocator, args: []const []const u8) !struct { flags: SelectFlags, rest: [][]const u8 } { + var flags = SelectFlags{}; + var rest = std.ArrayList([]const u8).init(allocator); + errdefer rest.deinit(); + + var i: usize = 0; + while (i < args.len) : (i += 1) { + const isLimit = std.mem.eql(u8, args[i], "--limit"); + const isOffset = std.mem.eql(u8, args[i], "--offset"); + const isOrderBy = std.mem.eql(u8, args[i], "--order-by"); + if (isLimit or isOffset) { + i += 1; + if (i >= args.len) return error.InvalidPageFlag; + const n = std.fmt.parseInt(u32, args[i], 10) catch return error.InvalidPageFlag; + if (isLimit) flags.page.limit = n else flags.page.offset = n; + } else if (isOrderBy) { + i += 1; + if (i >= args.len) return error.InvalidPageFlag; + allocator.free(flags.orderBy); + flags.orderBy = try parseOrderByList(allocator, args[i]); + } else { + try rest.append(args[i]); + } + } + return .{ .flags = flags, .rest = try rest.toOwnedSlice() }; +} +