diff --git a/src/main.zig b/src/main.zig index 6e5dfa2..6464a38 100644 --- a/src/main.zig +++ b/src/main.zig @@ -832,6 +832,179 @@ fn fail(stderr: anytype, message: []const u8) !void { std.process.exit(2); } +fn boolFlag(v: bool) u8 { + return if (v) 1 else 0; +} + +/// Dispatches `coll [args...]` onto the Redis-like typed +/// collections in collections.zig (Counter/Hash/Set/List/ZSet), which until +/// now were only reachable through the library API, never a CLI. +fn runColl(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, db: *cobalt.Db, rest: []const []const u8) !void { + if (rest.len >= 1 and std.mem.eql(u8, rest[0], "expire-sweep")) { + const reaped = try cobalt.collttl.sweepExpired(db, allocator, std.time.nanoTimestamp()); + try stdout.print("reaped: {d}\n", .{reaped}); + return; + } + if (rest.len < 2) return fail(stderr, "coll needs ..."); + const kind = rest[0]; + const op = rest[1]; + const args = rest[2..]; + + if (std.mem.eql(u8, op, "expire") or std.mem.eql(u8, op, "ttl") or std.mem.eql(u8, op, "persist")) { + const k = cobalt.CollectionKind.parse(kind) orelse return fail(stderr, "unknown collection kind (expected counter/hash/set/list/zset)"); + return runCollTtl(allocator, stdout, stderr, db, k, op, args); + } + + // Lazy expiry on the read/write path: every remaining op below takes the + // collection name as args[0], so this is what keeps a bare + // `coll hash get ...` from ever reading a collection that is + // logically already gone. `set inter|union|diff` accept further names + // past args[0] - those get their own expireIfDueMany call right before + // dispatch, so this single-name check only needs to cover args[0]. + if (args.len >= 1) { + if (cobalt.CollectionKind.parse(kind)) |k| { + _ = try cobalt.collttl.expireIfDue(db, allocator, k, args[0], std.time.nanoTimestamp()); + } + } + + if (std.mem.eql(u8, kind, "counter")) { + if (std.mem.eql(u8, op, "get")) { + if (args.len < 1) return fail(stderr, "counter get needs "); + const v = try cobalt.Counter.get(db, allocator, args[0]); + try stdout.print("{d}\n", .{v}); + } else if (std.mem.eql(u8, op, "set")) { + if (args.len < 2) return fail(stderr, "counter set needs "); + const v = std.fmt.parseInt(i64, args[1], 10) catch return fail(stderr, "invalid integer value"); + try cobalt.Counter.set(db, allocator, args[0], v); + try stdout.writeAll("OK\n"); + } else if (std.mem.eql(u8, op, "incrby")) { + if (args.len < 2) return fail(stderr, "counter incrby needs "); + const delta = std.fmt.parseInt(i64, args[1], 10) catch return fail(stderr, "invalid integer delta"); + const v = try cobalt.Counter.incrBy(db, allocator, args[0], delta); + try stdout.print("{d}\n", .{v}); + } else { + return fail(stderr, "unknown counter operation"); + } + } else if (std.mem.eql(u8, kind, "hash")) { + if (std.mem.eql(u8, op, "set")) { + if (args.len < 3) return fail(stderr, "hash set needs "); + const created = try cobalt.Hash.set(db, allocator, args[0], args[1], args[2]); + try stdout.print("{s}\n", .{if (created) "created" else "updated"}); + } else if (std.mem.eql(u8, op, "get")) { + if (args.len < 2) return fail(stderr, "hash get needs "); + if (try cobalt.Hash.get(db, allocator, args[0], args[1])) |v| { + defer allocator.free(v); + try stdout.print("{s}\n", .{v}); + } else { + try stdout.writeAll("(nil)\n"); + } + } else if (std.mem.eql(u8, op, "del")) { + if (args.len < 2) return fail(stderr, "hash del needs "); + const existed = try cobalt.Hash.del(db, allocator, args[0], args[1]); + try stdout.print("{d}\n", .{boolFlag(existed)}); + } else if (std.mem.eql(u8, op, "len")) { + if (args.len < 1) return fail(stderr, "hash len needs "); + const n = try cobalt.Hash.len(db, allocator, args[0]); + try stdout.print("{d}\n", .{n}); + } else if (std.mem.eql(u8, op, "getall")) { + if (args.len < 1) return fail(stderr, "hash getall needs "); + const fields = try cobalt.Hash.getAll(db, allocator, args[0]); + defer { + for (fields) |f| f.free(allocator); + allocator.free(fields); + } + for (fields) |f| try stdout.print("{s}\t{s}\n", .{ f.field, f.value }); + } else { + return fail(stderr, "unknown hash operation"); + } + } else if (std.mem.eql(u8, kind, "set")) { + if (std.mem.eql(u8, op, "add")) { + if (args.len < 2) return fail(stderr, "set add needs "); + const added = try cobalt.Set.add(db, allocator, args[0], args[1]); + try stdout.print("{d}\n", .{boolFlag(added)}); + } else if (std.mem.eql(u8, op, "rem")) { + if (args.len < 2) return fail(stderr, "set rem needs "); + const removed = try cobalt.Set.remove(db, allocator, args[0], args[1]); + try stdout.print("{d}\n", .{boolFlag(removed)}); + } else if (std.mem.eql(u8, op, "ismember")) { + if (args.len < 2) return fail(stderr, "set ismember needs "); + const is = try cobalt.Set.isMember(db, allocator, args[0], args[1]); + try stdout.print("{d}\n", .{boolFlag(is)}); + } else if (std.mem.eql(u8, op, "card")) { + if (args.len < 1) return fail(stderr, "set card needs "); + const n = try cobalt.Set.card(db, allocator, args[0]); + try stdout.print("{d}\n", .{n}); + } else if (std.mem.eql(u8, op, "members")) { + if (args.len < 1) return fail(stderr, "set members needs "); + const ms = try cobalt.Set.members(db, allocator, args[0]); + defer { + for (ms) |m| allocator.free(m); + allocator.free(ms); + } + for (ms) |m| try stdout.print("{s}\n", .{m}); + } else if (std.mem.eql(u8, op, "inter")) { + if (args.len < 1) return fail(stderr, "set inter needs at least one "); + // The blanket lazy check above only covered args[0]; args[1..] + // are additional set names this multi-set op reads directly. + _ = try cobalt.collttl.expireIfDueMany(db, allocator, .set, args[1..], std.time.nanoTimestamp()); + const ms = try cobalt.Set.intersect(db, allocator, args); + defer { + for (ms) |m| allocator.free(m); + allocator.free(ms); + } + for (ms) |m| try stdout.print("{s}\n", .{m}); + } else if (std.mem.eql(u8, op, "union")) { + if (args.len < 1) return fail(stderr, "set union needs at least one "); + _ = try cobalt.collttl.expireIfDueMany(db, allocator, .set, args[1..], std.time.nanoTimestamp()); + const ms = try cobalt.Set.unionOf(db, allocator, args); + defer { + for (ms) |m| allocator.free(m); + allocator.free(ms); + } + for (ms) |m| try stdout.print("{s}\n", .{m}); + } else if (std.mem.eql(u8, op, "diff")) { + if (args.len < 2) return fail(stderr, "set diff needs "); + _ = try cobalt.collttl.expireIfDueMany(db, allocator, .set, args[1..], std.time.nanoTimestamp()); + const ms = try cobalt.Set.difference(db, allocator, args[0], args[1..]); + defer { + for (ms) |m| allocator.free(m); + allocator.free(ms); + } + for (ms) |m| try stdout.print("{s}\n", .{m}); + } else { + return fail(stderr, "unknown set operation"); + } + } else if (std.mem.eql(u8, kind, "list")) { + if (std.mem.eql(u8, op, "lpush")) { + if (args.len < 2) return fail(stderr, "list lpush needs "); + try cobalt.List.pushLeft(db, allocator, args[0], args[1]); + try stdout.writeAll("OK\n"); + } else if (std.mem.eql(u8, op, "rpush")) { + if (args.len < 2) return fail(stderr, "list rpush needs "); + try cobalt.List.pushRight(db, allocator, args[0], args[1]); + try stdout.writeAll("OK\n"); + } else if (std.mem.eql(u8, op, "lpop")) { + if (args.len < 1) return fail(stderr, "list lpop needs "); + if (try cobalt.List.popLeft(db, allocator, args[0])) |v| { + defer allocator.free(v); + try stdout.print("{s}\n", .{v}); + } else { + try stdout.writeAll("(nil)\n"); + } + } else if (std.mem.eql(u8, op, "rpop")) { + if (args.len < 1) return fail(stderr, "list rpop needs "); + if (try cobalt.List.popRight(db, allocator, args[0])) |v| { + defer allocator.free(v); + try stdout.print("{s}\n", .{v}); + } else { + try stdout.writeAll("(nil)\n"); + } + } else if (std.mem.eql(u8, op, "len")) { + 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}); +} + fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void { std.fs.cwd().deleteTree(dir) catch {};