this repo has no description
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}