From 933ba332982fd3c7b031fa945c6349460dbc5c0d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 20:50:37 +0200 Subject: [PATCH] feat: extend main with runCollTtl and parseValue --- src/main.zig | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/main.zig b/src/main.zig index 6464a38..885dab8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1003,6 +1003,179 @@ fn runColl(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, db: * if (args.len < 1) return fail(stderr, "list len needs "); const n = try cobalt.List.len(db, allocator, args[0]); try stdout.print("{d}\n", .{n}); + } else if (std.mem.eql(u8, op, "range")) { + if (args.len < 3) return fail(stderr, "list range needs "); + const start = std.fmt.parseInt(i64, args[1], 10) catch return fail(stderr, "invalid start index"); + const stop = std.fmt.parseInt(i64, args[2], 10) catch return fail(stderr, "invalid stop index"); + const vals = try cobalt.List.range(db, allocator, args[0], start, stop); + defer { + for (vals) |v| allocator.free(v); + allocator.free(vals); + } + for (vals) |v| try stdout.print("{s}\n", .{v}); + } else { + return fail(stderr, "unknown list operation"); + } + } else if (std.mem.eql(u8, kind, "zset")) { + if (std.mem.eql(u8, op, "add")) { + if (args.len < 3) return fail(stderr, "zset add needs "); + const score = std.fmt.parseFloat(f64, args[2]) catch return fail(stderr, "invalid score"); + try cobalt.ZSet.add(db, allocator, args[0], args[1], score); + try stdout.writeAll("OK\n"); + } else if (std.mem.eql(u8, op, "score")) { + if (args.len < 2) return fail(stderr, "zset score needs "); + if (try cobalt.ZSet.score(db, allocator, args[0], args[1])) |s| { + try stdout.print("{d}\n", .{s}); + } else { + try stdout.writeAll("(nil)\n"); + } + } else if (std.mem.eql(u8, op, "incrby")) { + if (args.len < 3) return fail(stderr, "zset incrby needs "); + const delta = std.fmt.parseFloat(f64, args[2]) catch return fail(stderr, "invalid delta"); + const s = try cobalt.ZSet.incrBy(db, allocator, args[0], args[1], delta); + try stdout.print("{d}\n", .{s}); + } else if (std.mem.eql(u8, op, "rem")) { + if (args.len < 2) return fail(stderr, "zset rem needs "); + const removed = try cobalt.ZSet.remove(db, allocator, args[0], args[1]); + try stdout.print("{d}\n", .{boolFlag(removed)}); + } else if (std.mem.eql(u8, op, "card")) { + if (args.len < 1) return fail(stderr, "zset card needs "); + const n = try cobalt.ZSet.card(db, allocator, args[0]); + try stdout.print("{d}\n", .{n}); + } else if (std.mem.eql(u8, op, "rangebyrank")) { + if (args.len < 3) return fail(stderr, "zset rangebyrank needs "); + const start = std.fmt.parseInt(i64, args[1], 10) catch return fail(stderr, "invalid start rank"); + const stop = std.fmt.parseInt(i64, args[2], 10) catch return fail(stderr, "invalid stop rank"); + const zms = try cobalt.ZSet.rangeByRank(db, allocator, args[0], start, stop); + defer { + for (zms) |zm| zm.free(allocator); + allocator.free(zms); + } + for (zms) |zm| try stdout.print("{s}\t{d}\n", .{ zm.member, zm.score }); + } else if (std.mem.eql(u8, op, "rangebyscore")) { + if (args.len < 3) return fail(stderr, "zset rangebyscore needs "); + const min = std.fmt.parseFloat(f64, args[1]) catch return fail(stderr, "invalid min score"); + const max = std.fmt.parseFloat(f64, args[2]) catch return fail(stderr, "invalid max score"); + const zms = try cobalt.ZSet.rangeByScore(db, allocator, args[0], min, max); + defer { + for (zms) |zm| zm.free(allocator); + allocator.free(zms); + } + for (zms) |zm| try stdout.print("{s}\t{d}\n", .{ zm.member, zm.score }); + } else if (std.mem.eql(u8, op, "rank")) { + if (args.len < 2) return fail(stderr, "zset rank needs "); + if (try cobalt.ZSet.rank(db, allocator, args[0], args[1])) |r| { + try stdout.print("{d}\n", .{r}); + } else { + try stdout.writeAll("(nil)\n"); + } + } else { + return fail(stderr, "unknown zset operation"); + } + } else { + return fail(stderr, "unknown collection kind (expected counter/hash/set/list/zset)"); + } +} + +/// Dispatches `coll expire|ttl|persist [seconds]` onto +/// collttl.zig - the whole-collection counterpart of the flat-key +/// `expire`/`ttl`/`persist` commands above. `expire` mirrors Redis-strict +/// EXPIRE by checking the collection currently exists first (same shape as +/// the flat-key `expire` handler checking db.get first). +fn runCollTtl(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, db: *cobalt.Db, k: cobalt.CollectionKind, op: []const u8, args: []const []const u8) !void { + if (std.mem.eql(u8, op, "expire")) { + if (args.len < 2) return fail(stderr, "coll expire needs "); + const seconds = std.fmt.parseInt(u64, args[1], 10) catch return fail(stderr, "expire needs a whole number of seconds"); + if (!try cobalt.collections.collectionExists(db, allocator, k, args[0])) return fail(stderr, "no such collection"); + try cobalt.collttl.setExpireSeconds(db, allocator, k, args[0], std.time.nanoTimestamp(), seconds); + try stdout.writeAll("OK\n"); + } else if (std.mem.eql(u8, op, "persist")) { + if (args.len < 1) return fail(stderr, "coll persist needs "); + try cobalt.collttl.clearExpire(db, allocator, k, args[0]); + try stdout.writeAll("OK\n"); + } else if (std.mem.eql(u8, op, "ttl")) { + if (args.len < 1) return fail(stderr, "coll ttl needs "); + switch (try cobalt.collttl.remaining(db, allocator, k, args[0], std.time.nanoTimestamp())) { + .noKey => try stdout.writeAll("-2\n"), + .noExpiry => try stdout.writeAll("-1\n"), + .seconds => |s| try stdout.print("{d}\n", .{s}), + } + } else { + return fail(stderr, "unknown coll ttl operation"); + } +} + +/// Parses a CLI-supplied scalar into a typed row Value. `i:`/`f:`/`b:`/`s:` +/// prefixes pick int/float/bool/text explicitly; unprefixed text defaults to +/// `.text`. The returned Value borrows `text` directly (no allocation), so it +/// is only valid for as long as the argv slice (or script line) it came from. +fn parseValue(text: []const u8) !Value { + return cobalt.row.Value.parseCli(text); +} + +/// Splits `name=value` into a Field, validating the name with the same +/// identifier rule tables and indexes already enforce. +fn parseField(text: []const u8) !Field { + const eq = std.mem.indexOfScalar(u8, text, '=') orelse return error.InvalidField; + const name = text[0..eq]; + if (!cobalt.index.validIdentifier(name)) return error.InvalidField; + const value = try parseValue(text[eq + 1 ..]); + return .{ .name = name, .value = value }; +} + +const PageFlags = struct { limit: u32 = 0, offset: u32 = 0 }; + +/// Pulls `--limit N` / `--offset N` out of a `table select`/`remote-select` +/// argument list wherever they appear, returning the remaining tokens +/// (destined to be joined into the expr text) alongside the parsed page +/// window. Caller owns and frees `rest`. +fn extractPageFlags(allocator: std.mem.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/ +/// `remote-window`'s trailing args, requesting `window.FrameMode.range` +/// (SQL's actual `RANGE` frame default for running aggregates) instead of +/// this CLI's `.rows` default - see `window.zig`'s `compute` doc comment +/// for the semantic difference. Kept separate from `extractPageFlags` +/// (same reason `extractSelectFlags`'s `--order-by` is kept separate: +/// `extractPageFlags` is also shared by `table join`/`table aggregate`, +/// neither of which has a frame concept to silently swallow the flag into). +/// Caller owns and frees `rest`. +fn extractRangeFlag(allocator: std.mem.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 cobalt.query.OrderBy = &.{} }; + +/// `table select`/`remote-select`-only counterpart of `extractPageFlags`: } fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {