this repo has no description
at main 354 lines 14 kB view raw
1//! Redis list commands 2//! 3//! Lists are linked lists of strings, optimized for pushing and popping 4//! at both ends. They support blocking operations for queue patterns. 5//! 6//! ## Use Cases 7//! 8//! - Message queues: LPUSH + BRPOP 9//! - Activity feeds: LPUSH + LRANGE 10//! - Bounded collections: LPUSH + LTRIM 11//! - Background job queues: RPUSH + BLPOP 12//! 13//! ## Performance 14//! 15//! - Push/pop at ends: O(1) 16//! - Access by index: O(n) where n is distance from nearest end 17//! - Range queries: O(s+n) where s is start offset and n is elements returned 18//! 19//! ## Examples 20//! 21//! ```zig 22//! // Simple queue 23//! _ = try client.lists().rpush("queue", &.{"job1", "job2"}); 24//! const job = try client.lists().lpop("queue"); // "job1" 25//! 26//! // Recent items with bounded size 27//! _ = try client.lists().lpush("recent", &.{item}); 28//! try client.lists().ltrim("recent", 0, 99); // keep only 100 most recent 29//! 30//! // Blocking pop (for workers) 31//! const item = try client.lists().blpop(&.{"queue"}, 30); // wait up to 30s 32//! ``` 33 34const std = @import("std"); 35const Client = @import("../client.zig").Client; 36const Value = @import("../resp.zig").Value; 37const CommandError = @import("../resp.zig").CommandError; 38const ClientError = @import("../resp.zig").ClientError; 39 40/// List command implementations. 41pub const ListCommands = struct { 42 client: *Client, 43 44 pub fn init(client: *Client) ListCommands { 45 return .{ .client = client }; 46 } 47 48 // ======================================================================== 49 // Push Operations 50 // ======================================================================== 51 52 /// LPUSH key value [value ...] - prepend to list 53 /// Returns new list length. 54 pub fn lpush(self: *ListCommands, key: []const u8, values: []const []const u8) ClientError!i64 { 55 var args = try self.client.allocator.alloc([]const u8, values.len + 2); 56 defer self.client.allocator.free(args); 57 args[0] = "LPUSH"; 58 args[1] = key; 59 @memcpy(args[2..], values); 60 61 const result = try self.client.sendCommand(args); 62 return switch (result) { 63 .integer => |i| i, 64 .err => return CommandError.WrongType, 65 else => 0, 66 }; 67 } 68 69 /// RPUSH key value [value ...] - append to list 70 /// Returns new list length. 71 pub fn rpush(self: *ListCommands, key: []const u8, values: []const []const u8) ClientError!i64 { 72 var args = try self.client.allocator.alloc([]const u8, values.len + 2); 73 defer self.client.allocator.free(args); 74 args[0] = "RPUSH"; 75 args[1] = key; 76 @memcpy(args[2..], values); 77 78 const result = try self.client.sendCommand(args); 79 return switch (result) { 80 .integer => |i| i, 81 .err => return CommandError.WrongType, 82 else => 0, 83 }; 84 } 85 86 /// LPUSHX key value - prepend only if list exists 87 pub fn lpushx(self: *ListCommands, key: []const u8, value: []const u8) ClientError!i64 { 88 const result = try self.client.sendCommand(&.{ "LPUSHX", key, value }); 89 return switch (result) { 90 .integer => |i| i, 91 else => 0, 92 }; 93 } 94 95 /// RPUSHX key value - append only if list exists 96 pub fn rpushx(self: *ListCommands, key: []const u8, value: []const u8) ClientError!i64 { 97 const result = try self.client.sendCommand(&.{ "RPUSHX", key, value }); 98 return switch (result) { 99 .integer => |i| i, 100 else => 0, 101 }; 102 } 103 104 // ======================================================================== 105 // Pop Operations 106 // ======================================================================== 107 108 /// LPOP key [count] - remove and return first element(s) 109 pub fn lpop(self: *ListCommands, key: []const u8) ClientError!?[]const u8 { 110 const result = try self.client.sendCommand(&.{ "LPOP", key }); 111 return switch (result) { 112 .bulk => |b| b, 113 .nil => null, 114 else => null, 115 }; 116 } 117 118 /// LPOP key count - pop multiple elements 119 pub fn lpopCount(self: *ListCommands, key: []const u8, count: u32) ClientError![]const Value { 120 var buf: [16]u8 = undefined; 121 const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; 122 const result = try self.client.sendCommand(&.{ "LPOP", key, count_str }); 123 return switch (result) { 124 .array => |a| a, 125 .nil => &.{}, 126 else => &.{}, 127 }; 128 } 129 130 /// RPOP key - remove and return last element 131 pub fn rpop(self: *ListCommands, key: []const u8) ClientError!?[]const u8 { 132 const result = try self.client.sendCommand(&.{ "RPOP", key }); 133 return switch (result) { 134 .bulk => |b| b, 135 .nil => null, 136 else => null, 137 }; 138 } 139 140 /// RPOP key count - pop multiple elements from end 141 pub fn rpopCount(self: *ListCommands, key: []const u8, count: u32) ClientError![]const Value { 142 var buf: [16]u8 = undefined; 143 const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; 144 const result = try self.client.sendCommand(&.{ "RPOP", key, count_str }); 145 return switch (result) { 146 .array => |a| a, 147 .nil => &.{}, 148 else => &.{}, 149 }; 150 } 151 152 // ======================================================================== 153 // Blocking Pop Operations 154 // ======================================================================== 155 156 /// Result from blocking pop operations 157 pub const BlockingPopResult = struct { 158 key: []const u8, 159 value: []const u8, 160 }; 161 162 /// BLPOP key [key ...] timeout - blocking left pop 163 /// Timeout in seconds (0 = block forever). Returns null on timeout. 164 pub fn blpop(self: *ListCommands, keys: []const []const u8, timeout: f64) ClientError!?BlockingPopResult { 165 var args = try self.client.allocator.alloc([]const u8, keys.len + 2); 166 defer self.client.allocator.free(args); 167 168 args[0] = "BLPOP"; 169 @memcpy(args[1..][0..keys.len], keys); 170 171 var buf: [32]u8 = undefined; 172 args[keys.len + 1] = std.fmt.bufPrint(&buf, "{d}", .{timeout}) catch unreachable; 173 174 const result = try self.client.sendCommand(args); 175 return switch (result) { 176 .array => |arr| { 177 if (arr.len < 2) return null; 178 const k = arr[0].asString() orelse return null; 179 const v = arr[1].asString() orelse return null; 180 return BlockingPopResult{ .key = k, .value = v }; 181 }, 182 .nil => null, 183 else => null, 184 }; 185 } 186 187 /// BRPOP key [key ...] timeout - blocking right pop 188 pub fn brpop(self: *ListCommands, keys: []const []const u8, timeout: f64) ClientError!?BlockingPopResult { 189 var args = try self.client.allocator.alloc([]const u8, keys.len + 2); 190 defer self.client.allocator.free(args); 191 192 args[0] = "BRPOP"; 193 @memcpy(args[1..][0..keys.len], keys); 194 195 var buf: [32]u8 = undefined; 196 args[keys.len + 1] = std.fmt.bufPrint(&buf, "{d}", .{timeout}) catch unreachable; 197 198 const result = try self.client.sendCommand(args); 199 return switch (result) { 200 .array => |arr| { 201 if (arr.len < 2) return null; 202 const k = arr[0].asString() orelse return null; 203 const v = arr[1].asString() orelse return null; 204 return BlockingPopResult{ .key = k, .value = v }; 205 }, 206 .nil => null, 207 else => null, 208 }; 209 } 210 211 /// LMPOP numkeys key [key ...] LEFT|RIGHT [COUNT count] (Redis 7+) 212 pub fn lmpop(self: *ListCommands, keys: []const []const u8, direction: enum { left, right }, count: ?u32) ClientError!?struct { key: []const u8, values: []const Value } { 213 const base_args = 3 + keys.len + if (count != null) @as(usize, 2) else @as(usize, 0); 214 var args = try self.client.allocator.alloc([]const u8, base_args); 215 defer self.client.allocator.free(args); 216 217 var numkeys_buf: [16]u8 = undefined; 218 args[0] = "LMPOP"; 219 args[1] = std.fmt.bufPrint(&numkeys_buf, "{d}", .{keys.len}) catch unreachable; 220 @memcpy(args[2..][0..keys.len], keys); 221 args[2 + keys.len] = if (direction == .left) "LEFT" else "RIGHT"; 222 223 const arg_idx = 3 + keys.len; 224 var count_buf: [16]u8 = undefined; 225 if (count) |c| { 226 args[arg_idx] = "COUNT"; 227 args[arg_idx + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable; 228 } 229 230 const result = try self.client.sendCommand(args[0..arg_idx]); 231 return switch (result) { 232 .array => |arr| { 233 if (arr.len < 2) return null; 234 const key = arr[0].asString() orelse return null; 235 const values = arr[1].asArray() orelse return null; 236 return .{ .key = key, .values = values }; 237 }, 238 .nil => null, 239 else => null, 240 }; 241 } 242 243 // ======================================================================== 244 // Access Operations 245 // ======================================================================== 246 247 /// LINDEX key index - get element by index 248 pub fn lindex(self: *ListCommands, key: []const u8, index: i64) ClientError!?[]const u8 { 249 var buf: [24]u8 = undefined; 250 const idx_str = std.fmt.bufPrint(&buf, "{d}", .{index}) catch unreachable; 251 const result = try self.client.sendCommand(&.{ "LINDEX", key, idx_str }); 252 return switch (result) { 253 .bulk => |b| b, 254 else => null, 255 }; 256 } 257 258 /// LRANGE key start stop - get range of elements 259 /// Supports negative indices (-1 = last element). 260 pub fn lrange(self: *ListCommands, key: []const u8, start: i64, stop: i64) ClientError![]const Value { 261 var start_buf: [24]u8 = undefined; 262 var stop_buf: [24]u8 = undefined; 263 const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable; 264 const stop_str = std.fmt.bufPrint(&stop_buf, "{d}", .{stop}) catch unreachable; 265 266 const result = try self.client.sendCommand(&.{ "LRANGE", key, start_str, stop_str }); 267 return switch (result) { 268 .array => |a| a, 269 else => &.{}, 270 }; 271 } 272 273 /// LLEN key - get list length 274 pub fn llen(self: *ListCommands, key: []const u8) ClientError!i64 { 275 const result = try self.client.sendCommand(&.{ "LLEN", key }); 276 return switch (result) { 277 .integer => |i| i, 278 else => 0, 279 }; 280 } 281 282 // ======================================================================== 283 // Modification Operations 284 // ======================================================================== 285 286 /// LSET key index value - set element at index 287 pub fn lset(self: *ListCommands, key: []const u8, index: i64, value: []const u8) ClientError!void { 288 var buf: [24]u8 = undefined; 289 const idx_str = std.fmt.bufPrint(&buf, "{d}", .{index}) catch unreachable; 290 const result = try self.client.sendCommand(&.{ "LSET", key, idx_str, value }); 291 if (result.isError()) return CommandError.RedisError; 292 } 293 294 /// LTRIM key start stop - trim list to specified range 295 pub fn ltrim(self: *ListCommands, key: []const u8, start: i64, stop: i64) ClientError!void { 296 var start_buf: [24]u8 = undefined; 297 var stop_buf: [24]u8 = undefined; 298 const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable; 299 const stop_str = std.fmt.bufPrint(&stop_buf, "{d}", .{stop}) catch unreachable; 300 301 const result = try self.client.sendCommand(&.{ "LTRIM", key, start_str, stop_str }); 302 if (result.isError()) return CommandError.RedisError; 303 } 304 305 /// LREM key count value - remove elements 306 /// count > 0: remove first count matches from head 307 /// count < 0: remove first |count| matches from tail 308 /// count = 0: remove all matches 309 pub fn lrem(self: *ListCommands, key: []const u8, count: i64, value: []const u8) ClientError!i64 { 310 var buf: [24]u8 = undefined; 311 const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; 312 const result = try self.client.sendCommand(&.{ "LREM", key, count_str, value }); 313 return switch (result) { 314 .integer => |i| i, 315 else => 0, 316 }; 317 } 318 319 /// LINSERT key BEFORE|AFTER pivot value - insert before/after element 320 pub fn linsert(self: *ListCommands, key: []const u8, position: enum { before, after }, pivot: []const u8, value: []const u8) ClientError!i64 { 321 const pos_str = if (position == .before) "BEFORE" else "AFTER"; 322 const result = try self.client.sendCommand(&.{ "LINSERT", key, pos_str, pivot, value }); 323 return switch (result) { 324 .integer => |i| i, 325 else => -1, 326 }; 327 } 328 329 /// LPOS key element [RANK rank] [COUNT count] [MAXLEN len] - find element position (Redis 6.0.6+) 330 pub fn lpos(self: *ListCommands, key: []const u8, element: []const u8) ClientError!?i64 { 331 const result = try self.client.sendCommand(&.{ "LPOS", key, element }); 332 return switch (result) { 333 .integer => |i| i, 334 .nil => null, 335 else => null, 336 }; 337 } 338 339 /// LMOVE source destination LEFT|RIGHT LEFT|RIGHT - atomically move element 340 pub fn lmove(self: *ListCommands, source: []const u8, dest: []const u8, src_dir: enum { left, right }, dst_dir: enum { left, right }) ClientError!?[]const u8 { 341 const src_str = if (src_dir == .left) "LEFT" else "RIGHT"; 342 const dst_str = if (dst_dir == .left) "LEFT" else "RIGHT"; 343 const result = try self.client.sendCommand(&.{ "LMOVE", source, dest, src_str, dst_str }); 344 return switch (result) { 345 .bulk => |b| b, 346 else => null, 347 }; 348 } 349}; 350 351/// Extend Client with list commands. 352pub fn lists(client: *Client) ListCommands { 353 return ListCommands.init(client); 354}