feat: extend main with runCollTtl and parseValue

This commit is contained in:
dualfroz committed 2026-09-06 20:50:37 +02:00
1 parent bdeec4bfdf
commit 933ba33298
1 file changed
+173
+173
View File
@@ -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 <name>"); if (args.len < 1) return fail(stderr, "list len needs <name>");
const n = try cobalt.List.len(db, allocator, args[0]); const n = try cobalt.List.len(db, allocator, args[0]);
try stdout.print("{d}\n", .{n}); try stdout.print("{d}\n", .{n});
} else if (std.mem.eql(u8, op, "range")) {
if (args.len < 3) return fail(stderr, "list range needs <name> <start> <stop>");
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 <name> <member> <score>");
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 <name> <member>");
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 <name> <member> <delta>");
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 <name> <member>");
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 <name>");
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 <name> <start> <stop>");
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 <name> <min> <max>");
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 <name> <member>");
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 <kind> expire|ttl|persist <name> [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 <kind> expire needs <name> <seconds>");
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 <kind> persist needs <name>");
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 <kind> ttl needs <name>");
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 { fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {