this repo has no description
at main 316 lines 12 kB view raw
1//! Redis generic key commands 2//! 3//! These commands work on keys regardless of their value type. 4//! Common uses: 5//! - Key existence checks (EXISTS) 6//! - Key deletion (DEL, UNLINK) 7//! - TTL management (EXPIRE, TTL, PERSIST) 8//! - Key scanning (SCAN, KEYS) 9//! - Key type inspection (TYPE) 10//! 11//! ## Examples 12//! 13//! ```zig 14//! const exists = try client.keys().exists(&.{"key1", "key2"}); // count of existing 15//! const deleted = try client.keys().del(&.{"key1", "key2"}); // count deleted 16//! 17//! _ = try client.keys().expire("session", 3600); 18//! const ttl = try client.keys().ttl("session"); // seconds remaining 19//! ``` 20 21const std = @import("std"); 22const Client = @import("../client.zig").Client; 23const Value = @import("../resp.zig").Value; 24const CommandError = @import("../resp.zig").CommandError; 25const ClientError = @import("../resp.zig").ClientError; 26 27/// Generic key command implementations. 28pub const KeyCommands = struct { 29 client: *Client, 30 31 pub fn init(client: *Client) KeyCommands { 32 return .{ .client = client }; 33 } 34 35 // ======================================================================== 36 // Key Existence and Deletion 37 // ======================================================================== 38 39 /// DEL key [key ...] - delete keys 40 /// Returns count of keys deleted. 41 pub fn del(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { 42 var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); 43 defer self.client.allocator.free(args); 44 args[0] = "DEL"; 45 @memcpy(args[1..], key_list); 46 47 const result = try self.client.sendCommand(args); 48 return switch (result) { 49 .integer => |i| i, 50 else => 0, 51 }; 52 } 53 54 /// UNLINK key [key ...] - delete keys asynchronously 55 /// Like DEL but non-blocking. Returns count of keys unlinked. 56 pub fn unlink(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { 57 var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); 58 defer self.client.allocator.free(args); 59 args[0] = "UNLINK"; 60 @memcpy(args[1..], key_list); 61 62 const result = try self.client.sendCommand(args); 63 return switch (result) { 64 .integer => |i| i, 65 else => 0, 66 }; 67 } 68 69 /// EXISTS key [key ...] - check if keys exist 70 /// Returns count of keys that exist. 71 pub fn exists(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { 72 var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); 73 defer self.client.allocator.free(args); 74 args[0] = "EXISTS"; 75 @memcpy(args[1..], key_list); 76 77 const result = try self.client.sendCommand(args); 78 return switch (result) { 79 .integer => |i| i, 80 else => 0, 81 }; 82 } 83 84 /// TYPE key - get key's value type 85 /// Returns "string", "list", "set", "zset", "hash", "stream", or "none". 86 pub fn keyType(self: *KeyCommands, key: []const u8) ClientError![]const u8 { 87 const result = try self.client.sendCommand(&.{ "TYPE", key }); 88 return switch (result) { 89 .string => |s| s, 90 else => "none", 91 }; 92 } 93 94 // ======================================================================== 95 // TTL Management 96 // ======================================================================== 97 98 /// EXPIRE key seconds - set TTL in seconds 99 /// Returns true if timeout was set. 100 pub fn expire(self: *KeyCommands, key: []const u8, seconds: u32) ClientError!bool { 101 var buf: [16]u8 = undefined; 102 const sec_str = std.fmt.bufPrint(&buf, "{d}", .{seconds}) catch unreachable; 103 const result = try self.client.sendCommand(&.{ "EXPIRE", key, sec_str }); 104 return switch (result) { 105 .integer => |i| i == 1, 106 else => false, 107 }; 108 } 109 110 /// EXPIREAT key unix-timestamp - set expiration as Unix timestamp 111 pub fn expireAt(self: *KeyCommands, key: []const u8, timestamp: i64) ClientError!bool { 112 var buf: [24]u8 = undefined; 113 const ts_str = std.fmt.bufPrint(&buf, "{d}", .{timestamp}) catch unreachable; 114 const result = try self.client.sendCommand(&.{ "EXPIREAT", key, ts_str }); 115 return switch (result) { 116 .integer => |i| i == 1, 117 else => false, 118 }; 119 } 120 121 /// PEXPIRE key milliseconds - set TTL in milliseconds 122 pub fn pexpire(self: *KeyCommands, key: []const u8, millis: u64) ClientError!bool { 123 var buf: [24]u8 = undefined; 124 const ms_str = std.fmt.bufPrint(&buf, "{d}", .{millis}) catch unreachable; 125 const result = try self.client.sendCommand(&.{ "PEXPIRE", key, ms_str }); 126 return switch (result) { 127 .integer => |i| i == 1, 128 else => false, 129 }; 130 } 131 132 /// TTL key - get remaining TTL in seconds 133 /// Returns null if key doesn't exist or has no TTL. 134 pub fn ttl(self: *KeyCommands, key: []const u8) ClientError!?i64 { 135 const result = try self.client.sendCommand(&.{ "TTL", key }); 136 return switch (result) { 137 .integer => |i| if (i < 0) null else i, 138 else => null, 139 }; 140 } 141 142 /// PTTL key - get remaining TTL in milliseconds 143 pub fn pttl(self: *KeyCommands, key: []const u8) ClientError!?i64 { 144 const result = try self.client.sendCommand(&.{ "PTTL", key }); 145 return switch (result) { 146 .integer => |i| if (i < 0) null else i, 147 else => null, 148 }; 149 } 150 151 /// PERSIST key - remove TTL 152 /// Returns true if timeout was removed. 153 pub fn persist(self: *KeyCommands, key: []const u8) ClientError!bool { 154 const result = try self.client.sendCommand(&.{ "PERSIST", key }); 155 return switch (result) { 156 .integer => |i| i == 1, 157 else => false, 158 }; 159 } 160 161 /// EXPIRETIME key - get Unix timestamp when key will expire (Redis 7+) 162 pub fn expireTime(self: *KeyCommands, key: []const u8) ClientError!?i64 { 163 const result = try self.client.sendCommand(&.{ "EXPIRETIME", key }); 164 return switch (result) { 165 .integer => |i| if (i < 0) null else i, 166 else => null, 167 }; 168 } 169 170 // ======================================================================== 171 // Key Manipulation 172 // ======================================================================== 173 174 /// RENAME key newkey - rename a key 175 pub fn rename(self: *KeyCommands, key: []const u8, new_key: []const u8) ClientError!void { 176 const result = try self.client.sendCommand(&.{ "RENAME", key, new_key }); 177 if (result.isError()) return CommandError.KeyNotFound; 178 } 179 180 /// RENAMENX key newkey - rename only if newkey doesn't exist 181 /// Returns true if renamed. 182 pub fn renameNx(self: *KeyCommands, key: []const u8, new_key: []const u8) ClientError!bool { 183 const result = try self.client.sendCommand(&.{ "RENAMENX", key, new_key }); 184 return switch (result) { 185 .integer => |i| i == 1, 186 else => false, 187 }; 188 } 189 190 /// COPY source destination [REPLACE] - copy key to another key (Redis 6.2+) 191 pub fn copy(self: *KeyCommands, source: []const u8, dest: []const u8, replace: bool) ClientError!bool { 192 const result = if (replace) 193 try self.client.sendCommand(&.{ "COPY", source, dest, "REPLACE" }) 194 else 195 try self.client.sendCommand(&.{ "COPY", source, dest }); 196 return switch (result) { 197 .integer => |i| i == 1, 198 else => false, 199 }; 200 } 201 202 // ======================================================================== 203 // Key Scanning 204 // ======================================================================== 205 206 /// Result from SCAN command 207 pub const ScanResult = struct { 208 cursor: []const u8, 209 keys: []const Value, 210 }; 211 212 /// SCAN cursor [MATCH pattern] [COUNT count] - iterate keys 213 pub fn scan(self: *KeyCommands, cursor: []const u8, pattern: ?[]const u8, count: ?u32) ClientError!ScanResult { 214 // Build args dynamically 215 var args_buf: [7][]const u8 = undefined; 216 var arg_count: usize = 2; 217 args_buf[0] = "SCAN"; 218 args_buf[1] = cursor; 219 220 if (pattern) |p| { 221 args_buf[arg_count] = "MATCH"; 222 args_buf[arg_count + 1] = p; 223 arg_count += 2; 224 } 225 226 var count_buf: [16]u8 = undefined; 227 if (count) |c| { 228 args_buf[arg_count] = "COUNT"; 229 args_buf[arg_count + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable; 230 arg_count += 2; 231 } 232 233 const result = try self.client.sendCommand(args_buf[0..arg_count]); 234 return switch (result) { 235 .array => |arr| { 236 if (arr.len < 2) return ScanResult{ .cursor = "0", .keys = &.{} }; 237 return ScanResult{ 238 .cursor = arr[0].asString() orelse "0", 239 .keys = arr[1].asArray() orelse &.{}, 240 }; 241 }, 242 else => ScanResult{ .cursor = "0", .keys = &.{} }, 243 }; 244 } 245 246 /// KEYS pattern - find keys matching pattern 247 /// Warning: can be slow on large databases. Prefer SCAN for production. 248 pub fn keys(self: *KeyCommands, pattern: []const u8) ClientError![]const Value { 249 const result = try self.client.sendCommand(&.{ "KEYS", pattern }); 250 return switch (result) { 251 .array => |a| a, 252 else => &.{}, 253 }; 254 } 255 256 /// RANDOMKEY - return a random key 257 pub fn randomKey(self: *KeyCommands) ClientError!?[]const u8 { 258 const result = try self.client.sendCommand(&.{"RANDOMKEY"}); 259 return switch (result) { 260 .bulk => |b| b, 261 else => null, 262 }; 263 } 264 265 // ======================================================================== 266 // Serialization 267 // ======================================================================== 268 269 /// DUMP key - serialize key's value 270 /// Returns serialized value or null if key doesn't exist. 271 pub fn dump(self: *KeyCommands, key: []const u8) ClientError!?[]const u8 { 272 const result = try self.client.sendCommand(&.{ "DUMP", key }); 273 return switch (result) { 274 .bulk => |b| b, 275 else => null, 276 }; 277 } 278 279 /// RESTORE key ttl serialized-value - restore from DUMP 280 pub fn restore(self: *KeyCommands, key: []const u8, ttl_ms: u64, serialized: []const u8, replace: bool) ClientError!void { 281 var ttl_buf: [24]u8 = undefined; 282 const ttl_str = std.fmt.bufPrint(&ttl_buf, "{d}", .{ttl_ms}) catch unreachable; 283 284 const result = if (replace) 285 try self.client.sendCommand(&.{ "RESTORE", key, ttl_str, serialized, "REPLACE" }) 286 else 287 try self.client.sendCommand(&.{ "RESTORE", key, ttl_str, serialized }); 288 289 if (result.isError()) return CommandError.RedisError; 290 } 291 292 /// OBJECT ENCODING key - get internal encoding of key's value 293 pub fn objectEncoding(self: *KeyCommands, key: []const u8) ClientError!?[]const u8 { 294 const result = try self.client.sendCommand(&.{ "OBJECT", "ENCODING", key }); 295 return result.asString(); 296 } 297 298 /// TOUCH key [key ...] - alters last access time, returns count touched 299 pub fn touch(self: *KeyCommands, key_list: []const []const u8) ClientError!i64 { 300 var args = try self.client.allocator.alloc([]const u8, key_list.len + 1); 301 defer self.client.allocator.free(args); 302 args[0] = "TOUCH"; 303 @memcpy(args[1..], key_list); 304 305 const result = try self.client.sendCommand(args); 306 return switch (result) { 307 .integer => |i| i, 308 else => 0, 309 }; 310 } 311}; 312 313/// Extend Client with key commands. 314pub fn keyOps(client: *Client) KeyCommands { 315 return KeyCommands.init(client); 316}