//! Redis generic key commands //! //! These commands work on keys regardless of their value type. //! Common uses: //! - Key existence checks (EXISTS) //! - Key deletion (DEL, UNLINK) //! - TTL management (EXPIRE, TTL, PERSIST) //! - Key scanning (SCAN, KEYS) //! - Key type inspection (TYPE) //! //! ## Examples //! //! ```zig //! const exists = try client.keys().exists(&.{"key1", "key2"}); // count of existing //! const deleted = try client.keys().del(&.{"key1", "key2"}); // count deleted //! //! _ = try client.keys().expire("session", 3600); //! const ttl = try client.keys().ttl("session"); // seconds remaining //! ``` const std = @import("std"); const Client = @import("../client.zig").Client; const Value = @import("../resp.zig").Value; const CommandError = @import("../resp.zig").CommandError; const ClientError = @import("../resp.zig").ClientError; /// Generic key command implementations. pub const KeyCommands = struct { client: *Client, pub fn init(client: *Client) KeyCommands { return .{ .client = client }; } // ======================================================================== // Key Existence and Deletion // ======================================================================== /// DEL key [key ...] - delete keys /// Returns count of keys deleted. pub fn del(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); defer self.client.allocator.free(args); args[0] = "DEL"; @memcpy(args[1..], key_list); const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, else => 0, }; } /// UNLINK key [key ...] - delete keys asynchronously /// Like DEL but non-blocking. Returns count of keys unlinked. pub fn unlink(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); defer self.client.allocator.free(args); args[0] = "UNLINK"; @memcpy(args[1..], key_list); const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, else => 0, }; } /// EXISTS key [key ...] - check if keys exist /// Returns count of keys that exist. pub fn exists(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); defer self.client.allocator.free(args); args[0] = "EXISTS"; @memcpy(args[1..], key_list); const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, else => 0, }; } /// TYPE key - get key's value type /// Returns "string", "list", "set", "zset", "hash", "stream", or "none". pub fn keyType(self: *KeyCommands, key: []const u8) ClientError![]const u8 { const result = try self.client.sendCommand(&.{ "TYPE", key }); return switch (result) { .string => |s| s, else => "none", }; } // ======================================================================== // TTL Management // ======================================================================== /// EXPIRE key seconds - set TTL in seconds /// Returns true if timeout was set. pub fn expire(self: *KeyCommands, key: []const u8, seconds: u32) ClientError!bool { var buf: [16]u8 = undefined; const sec_str = std.fmt.bufPrint(&buf, "{d}", .{seconds}) catch unreachable; const result = try self.client.sendCommand(&.{ "EXPIRE", key, sec_str }); return switch (result) { .integer => |i| i == 1, else => false, }; } /// EXPIREAT key unix-timestamp - set expiration as Unix timestamp pub fn expireAt(self: *KeyCommands, key: []const u8, timestamp: i64) ClientError!bool { var buf: [24]u8 = undefined; const ts_str = std.fmt.bufPrint(&buf, "{d}", .{timestamp}) catch unreachable; const result = try self.client.sendCommand(&.{ "EXPIREAT", key, ts_str }); return switch (result) { .integer => |i| i == 1, else => false, }; } /// PEXPIRE key milliseconds - set TTL in milliseconds pub fn pexpire(self: *KeyCommands, key: []const u8, millis: u64) ClientError!bool { var buf: [24]u8 = undefined; const ms_str = std.fmt.bufPrint(&buf, "{d}", .{millis}) catch unreachable; const result = try self.client.sendCommand(&.{ "PEXPIRE", key, ms_str }); return switch (result) { .integer => |i| i == 1, else => false, }; } /// TTL key - get remaining TTL in seconds /// Returns null if key doesn't exist or has no TTL. pub fn ttl(self: *KeyCommands, key: []const u8) ClientError!?i64 { const result = try self.client.sendCommand(&.{ "TTL", key }); return switch (result) { .integer => |i| if (i < 0) null else i, else => null, }; } /// PTTL key - get remaining TTL in milliseconds pub fn pttl(self: *KeyCommands, key: []const u8) ClientError!?i64 { const result = try self.client.sendCommand(&.{ "PTTL", key }); return switch (result) { .integer => |i| if (i < 0) null else i, else => null, }; } /// PERSIST key - remove TTL /// Returns true if timeout was removed. pub fn persist(self: *KeyCommands, key: []const u8) ClientError!bool { const result = try self.client.sendCommand(&.{ "PERSIST", key }); return switch (result) { .integer => |i| i == 1, else => false, }; } /// EXPIRETIME key - get Unix timestamp when key will expire (Redis 7+) pub fn expireTime(self: *KeyCommands, key: []const u8) ClientError!?i64 { const result = try self.client.sendCommand(&.{ "EXPIRETIME", key }); return switch (result) { .integer => |i| if (i < 0) null else i, else => null, }; } // ======================================================================== // Key Manipulation // ======================================================================== /// RENAME key newkey - rename a key pub fn rename(self: *KeyCommands, key: []const u8, new_key: []const u8) ClientError!void { const result = try self.client.sendCommand(&.{ "RENAME", key, new_key }); if (result.isError()) return CommandError.KeyNotFound; } /// RENAMENX key newkey - rename only if newkey doesn't exist /// Returns true if renamed. pub fn renameNx(self: *KeyCommands, key: []const u8, new_key: []const u8) ClientError!bool { const result = try self.client.sendCommand(&.{ "RENAMENX", key, new_key }); return switch (result) { .integer => |i| i == 1, else => false, }; } /// COPY source destination [REPLACE] - copy key to another key (Redis 6.2+) pub fn copy(self: *KeyCommands, source: []const u8, dest: []const u8, replace: bool) ClientError!bool { const result = if (replace) try self.client.sendCommand(&.{ "COPY", source, dest, "REPLACE" }) else try self.client.sendCommand(&.{ "COPY", source, dest }); return switch (result) { .integer => |i| i == 1, else => false, }; } // ======================================================================== // Key Scanning // ======================================================================== /// Result from SCAN command pub const ScanResult = struct { cursor: []const u8, keys: []const Value, }; /// SCAN cursor [MATCH pattern] [COUNT count] - iterate keys pub fn scan(self: *KeyCommands, cursor: []const u8, pattern: ?[]const u8, count: ?u32) ClientError!ScanResult { // Build args dynamically var args_buf: [7][]const u8 = undefined; var arg_count: usize = 2; args_buf[0] = "SCAN"; args_buf[1] = cursor; if (pattern) |p| { args_buf[arg_count] = "MATCH"; args_buf[arg_count + 1] = p; arg_count += 2; } var count_buf: [16]u8 = undefined; if (count) |c| { args_buf[arg_count] = "COUNT"; args_buf[arg_count + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable; arg_count += 2; } const result = try self.client.sendCommand(args_buf[0..arg_count]); return switch (result) { .array => |arr| { if (arr.len < 2) return ScanResult{ .cursor = "0", .keys = &.{} }; return ScanResult{ .cursor = arr[0].asString() orelse "0", .keys = arr[1].asArray() orelse &.{}, }; }, else => ScanResult{ .cursor = "0", .keys = &.{} }, }; } /// KEYS pattern - find keys matching pattern /// Warning: can be slow on large databases. Prefer SCAN for production. pub fn keys(self: *KeyCommands, pattern: []const u8) ClientError![]const Value { const result = try self.client.sendCommand(&.{ "KEYS", pattern }); return switch (result) { .array => |a| a, else => &.{}, }; } /// RANDOMKEY - return a random key pub fn randomKey(self: *KeyCommands) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{"RANDOMKEY"}); return switch (result) { .bulk => |b| b, else => null, }; } // ======================================================================== // Serialization // ======================================================================== /// DUMP key - serialize key's value /// Returns serialized value or null if key doesn't exist. pub fn dump(self: *KeyCommands, key: []const u8) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{ "DUMP", key }); return switch (result) { .bulk => |b| b, else => null, }; } /// RESTORE key ttl serialized-value - restore from DUMP pub fn restore(self: *KeyCommands, key: []const u8, ttl_ms: u64, serialized: []const u8, replace: bool) ClientError!void { var ttl_buf: [24]u8 = undefined; const ttl_str = std.fmt.bufPrint(&ttl_buf, "{d}", .{ttl_ms}) catch unreachable; const result = if (replace) try self.client.sendCommand(&.{ "RESTORE", key, ttl_str, serialized, "REPLACE" }) else try self.client.sendCommand(&.{ "RESTORE", key, ttl_str, serialized }); if (result.isError()) return CommandError.RedisError; } /// OBJECT ENCODING key - get internal encoding of key's value pub fn objectEncoding(self: *KeyCommands, key: []const u8) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{ "OBJECT", "ENCODING", key }); return result.asString(); } /// TOUCH key [key ...] - alters last access time, returns count touched pub fn touch(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); defer self.client.allocator.free(args); args[0] = "TOUCH"; @memcpy(args[1..], key_list); const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, else => 0, }; } }; /// Extend Client with key commands. pub fn keyOps(client: *Client) KeyCommands { return KeyCommands.init(client); }