this repo has no description
1//! Redis sorted set (zset) commands
2//!
3//! Sorted sets combine the uniqueness of sets with a score for each member,
4//! keeping members sorted by score. This enables efficient range queries,
5//! rankings, and priority queues.
6//!
7//! ## Use Cases
8//!
9//! - Leaderboards: `ZADD leaderboard score player`
10//! - Time series (by timestamp): `ZADD events timestamp event_id`
11//! - Priority queues: `ZADD tasks priority task_id`
12//! - Rate limiting: `ZADD requests:user timestamp request_id`
13//! - Autocomplete: `ZADD suggestions 0 "search term"`
14//!
15//! ## Complexity
16//!
17//! - Add/remove/score lookup: O(log n)
18//! - Range by rank/score: O(log n + m) where m is elements returned
19//! - Cardinality: O(1)
20//!
21//! ## Examples
22//!
23//! ```zig
24//! // Leaderboard
25//! _ = try client.zsets().zadd("leaderboard", 100, "alice");
26//! _ = try client.zsets().zadd("leaderboard", 95, "bob");
27//! _ = try client.zsets().zincrBy("leaderboard", 10, "bob"); // bob now at 105
28//!
29//! // Get top 10
30//! const top = try client.zsets().zrangeWithScores("leaderboard", 0, 9, .rev);
31//!
32//! // Time-windowed events
33//! const now = std.time.timestamp();
34//! _ = try client.zsets().zrangeByScore("events", now - 3600, now); // last hour
35//! ```
36
37const std = @import("std");
38const Client = @import("../client.zig").Client;
39const Value = @import("../resp.zig").Value;
40const CommandError = @import("../resp.zig").CommandError;
41const ClientError = @import("../resp.zig").ClientError;
42
43/// Sorted set command implementations.
44pub const SortedSetCommands = struct {
45 client: *Client,
46
47 pub fn init(client: *Client) SortedSetCommands {
48 return .{ .client = client };
49 }
50
51 /// Score-member pair returned from WITHSCORES queries
52 pub const ScoredMember = struct {
53 member: []const u8,
54 score: f64,
55 };
56
57 /// Options for ZADD command
58 pub const AddOptions = struct {
59 /// NX: only add new members (don't update existing)
60 nx: bool = false,
61 /// XX: only update existing members (don't add new)
62 xx: bool = false,
63 /// GT: only update if new score > current (Redis 6.2+)
64 gt: bool = false,
65 /// LT: only update if new score < current (Redis 6.2+)
66 lt: bool = false,
67 /// CH: return count of changed members (added + updated)
68 ch: bool = false,
69 };
70
71 // ========================================================================
72 // Basic Operations
73 // ========================================================================
74
75 /// ZADD key score member [score member ...] - add member with score
76 /// Returns number of elements added.
77 pub fn zadd(self: *SortedSetCommands, key: []const u8, score: f64, member: []const u8) ClientError!i64 {
78 var buf: [32]u8 = undefined;
79 const score_str = std.fmt.bufPrint(&buf, "{d}", .{score}) catch unreachable;
80 const result = try self.client.sendCommand(&.{ "ZADD", key, score_str, member });
81 return switch (result) {
82 .integer => |i| i,
83 .err => return CommandError.WrongType,
84 else => 0,
85 };
86 }
87
88 /// ZADD with options and multiple members
89 pub fn zaddMulti(self: *SortedSetCommands, key: []const u8, options: AddOptions, pairs: []const struct { score: f64, member: []const u8 }) ClientError!i64 {
90 // Calculate arg count: ZADD key [options] score member [score member ...]
91 var opt_count: usize = 0;
92 if (options.nx) opt_count += 1;
93 if (options.xx) opt_count += 1;
94 if (options.gt) opt_count += 1;
95 if (options.lt) opt_count += 1;
96 if (options.ch) opt_count += 1;
97
98 const arg_count = 2 + opt_count + pairs.len * 2;
99 var args = try self.client.allocator.alloc([]const u8, arg_count);
100 defer self.client.allocator.free(args);
101
102 // We need to allocate score strings
103 var score_bufs = try self.client.allocator.alloc([32]u8, pairs.len);
104 defer self.client.allocator.free(score_bufs);
105
106 args[0] = "ZADD";
107 args[1] = key;
108
109 var idx: usize = 2;
110 if (options.nx) {
111 args[idx] = "NX";
112 idx += 1;
113 }
114 if (options.xx) {
115 args[idx] = "XX";
116 idx += 1;
117 }
118 if (options.gt) {
119 args[idx] = "GT";
120 idx += 1;
121 }
122 if (options.lt) {
123 args[idx] = "LT";
124 idx += 1;
125 }
126 if (options.ch) {
127 args[idx] = "CH";
128 idx += 1;
129 }
130
131 for (pairs, 0..) |pair, i| {
132 const score_str = std.fmt.bufPrint(&score_bufs[i], "{d}", .{pair.score}) catch unreachable;
133 args[idx] = score_str;
134 args[idx + 1] = pair.member;
135 idx += 2;
136 }
137
138 const result = try self.client.sendCommand(args);
139 return switch (result) {
140 .integer => |i| i,
141 .err => return CommandError.WrongType,
142 else => 0,
143 };
144 }
145
146 /// ZREM key member [member ...] - remove members
147 pub fn zrem(self: *SortedSetCommands, key: []const u8, members: []const []const u8) ClientError!i64 {
148 var args = try self.client.allocator.alloc([]const u8, members.len + 2);
149 defer self.client.allocator.free(args);
150 args[0] = "ZREM";
151 args[1] = key;
152 @memcpy(args[2..], members);
153
154 const result = try self.client.sendCommand(args);
155 return switch (result) {
156 .integer => |i| i,
157 else => 0,
158 };
159 }
160
161 /// ZSCORE key member - get member's score
162 pub fn zscore(self: *SortedSetCommands, key: []const u8, member: []const u8) ClientError!?f64 {
163 const result = try self.client.sendCommand(&.{ "ZSCORE", key, member });
164 return switch (result) {
165 .bulk => |b| if (b) |s| std.fmt.parseFloat(f64, s) catch null else null,
166 else => null,
167 };
168 }
169
170 /// ZMSCORE key member [member ...] - get multiple scores (Redis 6.2+)
171 pub fn zmscore(self: *SortedSetCommands, key: []const u8, members: []const []const u8) ClientError![]const Value {
172 var args = try self.client.allocator.alloc([]const u8, members.len + 2);
173 defer self.client.allocator.free(args);
174 args[0] = "ZMSCORE";
175 args[1] = key;
176 @memcpy(args[2..], members);
177
178 const result = try self.client.sendCommand(args);
179 return switch (result) {
180 .array => |a| a,
181 else => &.{},
182 };
183 }
184
185 /// ZINCRBY key increment member - increment score
186 pub fn zincrBy(self: *SortedSetCommands, key: []const u8, increment: f64, member: []const u8) ClientError!f64 {
187 var buf: [32]u8 = undefined;
188 const inc_str = std.fmt.bufPrint(&buf, "{d}", .{increment}) catch unreachable;
189 const result = try self.client.sendCommand(&.{ "ZINCRBY", key, inc_str, member });
190 return switch (result) {
191 .bulk => |b| if (b) |s| std.fmt.parseFloat(f64, s) catch return CommandError.RedisError else return CommandError.RedisError,
192 .err => return CommandError.WrongType,
193 else => return CommandError.RedisError,
194 };
195 }
196
197 /// ZCARD key - get cardinality
198 pub fn zcard(self: *SortedSetCommands, key: []const u8) ClientError!i64 {
199 const result = try self.client.sendCommand(&.{ "ZCARD", key });
200 return switch (result) {
201 .integer => |i| i,
202 else => 0,
203 };
204 }
205
206 /// ZCOUNT key min max - count members with score in range
207 pub fn zcount(self: *SortedSetCommands, key: []const u8, min: f64, max: f64) ClientError!i64 {
208 var min_buf: [32]u8 = undefined;
209 var max_buf: [32]u8 = undefined;
210 const min_str = std.fmt.bufPrint(&min_buf, "{d}", .{min}) catch unreachable;
211 const max_str = std.fmt.bufPrint(&max_buf, "{d}", .{max}) catch unreachable;
212 const result = try self.client.sendCommand(&.{ "ZCOUNT", key, min_str, max_str });
213 return switch (result) {
214 .integer => |i| i,
215 else => 0,
216 };
217 }
218
219 // ========================================================================
220 // Rank Operations
221 // ========================================================================
222
223 /// ZRANK key member - get rank (0-indexed, low to high)
224 pub fn zrank(self: *SortedSetCommands, key: []const u8, member: []const u8) ClientError!?i64 {
225 const result = try self.client.sendCommand(&.{ "ZRANK", key, member });
226 return switch (result) {
227 .integer => |i| i,
228 .nil => null,
229 else => null,
230 };
231 }
232
233 /// ZREVRANK key member - get reverse rank (0 = highest score)
234 pub fn zrevrank(self: *SortedSetCommands, key: []const u8, member: []const u8) ClientError!?i64 {
235 const result = try self.client.sendCommand(&.{ "ZREVRANK", key, member });
236 return switch (result) {
237 .integer => |i| i,
238 .nil => null,
239 else => null,
240 };
241 }
242
243 // ========================================================================
244 // Range Queries
245 // ========================================================================
246
247 /// Sort order for range queries
248 pub const Order = enum { asc, desc };
249
250 /// ZRANGE key start stop [REV] - get members by rank
251 pub fn zrange(self: *SortedSetCommands, key: []const u8, start: i64, stop: i64, order: Order) ClientError![]const Value {
252 var start_buf: [24]u8 = undefined;
253 var stop_buf: [24]u8 = undefined;
254 const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable;
255 const stop_str = std.fmt.bufPrint(&stop_buf, "{d}", .{stop}) catch unreachable;
256
257 const result = if (order == .desc)
258 try self.client.sendCommand(&.{ "ZRANGE", key, start_str, stop_str, "REV" })
259 else
260 try self.client.sendCommand(&.{ "ZRANGE", key, start_str, stop_str });
261
262 return switch (result) {
263 .array => |a| a,
264 else => &.{},
265 };
266 }
267
268 /// ZRANGE key start stop WITHSCORES - get members with scores
269 pub fn zrangeWithScores(self: *SortedSetCommands, key: []const u8, start: i64, stop: i64, order: Order) ClientError![]const Value {
270 var start_buf: [24]u8 = undefined;
271 var stop_buf: [24]u8 = undefined;
272 const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable;
273 const stop_str = std.fmt.bufPrint(&stop_buf, "{d}", .{stop}) catch unreachable;
274
275 const result = if (order == .desc)
276 try self.client.sendCommand(&.{ "ZRANGE", key, start_str, stop_str, "REV", "WITHSCORES" })
277 else
278 try self.client.sendCommand(&.{ "ZRANGE", key, start_str, stop_str, "WITHSCORES" });
279
280 return switch (result) {
281 .array => |a| a,
282 else => &.{},
283 };
284 }
285
286 /// ZRANGEBYSCORE key min max - get members by score range
287 pub fn zrangeByScore(self: *SortedSetCommands, key: []const u8, min: f64, max: f64) ClientError![]const Value {
288 var min_buf: [32]u8 = undefined;
289 var max_buf: [32]u8 = undefined;
290 const min_str = std.fmt.bufPrint(&min_buf, "{d}", .{min}) catch unreachable;
291 const max_str = std.fmt.bufPrint(&max_buf, "{d}", .{max}) catch unreachable;
292
293 // ZRANGE with BYSCORE (Redis 6.2+)
294 const result = try self.client.sendCommand(&.{ "ZRANGE", key, min_str, max_str, "BYSCORE" });
295 return switch (result) {
296 .array => |a| a,
297 else => &.{},
298 };
299 }
300
301 // ========================================================================
302 // Pop Operations
303 // ========================================================================
304
305 /// ZPOPMIN key [count] - remove and return lowest scored members
306 pub fn zpopmin(self: *SortedSetCommands, key: []const u8, count: ?u32) ClientError![]const Value {
307 if (count) |c| {
308 var buf: [16]u8 = undefined;
309 const count_str = std.fmt.bufPrint(&buf, "{d}", .{c}) catch unreachable;
310 const result = try self.client.sendCommand(&.{ "ZPOPMIN", key, count_str });
311 return switch (result) {
312 .array => |a| a,
313 else => &.{},
314 };
315 } else {
316 const result = try self.client.sendCommand(&.{ "ZPOPMIN", key });
317 return switch (result) {
318 .array => |a| a,
319 else => &.{},
320 };
321 }
322 }
323
324 /// ZPOPMAX key [count] - remove and return highest scored members
325 pub fn zpopmax(self: *SortedSetCommands, key: []const u8, count: ?u32) ClientError![]const Value {
326 if (count) |c| {
327 var buf: [16]u8 = undefined;
328 const count_str = std.fmt.bufPrint(&buf, "{d}", .{c}) catch unreachable;
329 const result = try self.client.sendCommand(&.{ "ZPOPMAX", key, count_str });
330 return switch (result) {
331 .array => |a| a,
332 else => &.{},
333 };
334 } else {
335 const result = try self.client.sendCommand(&.{ "ZPOPMAX", key });
336 return switch (result) {
337 .array => |a| a,
338 else => &.{},
339 };
340 }
341 }
342
343 /// BZPOPMIN key [key ...] timeout - blocking pop min
344 pub fn bzpopmin(self: *SortedSetCommands, keys: []const []const u8, timeout: f64) ClientError!?struct { key: []const u8, member: []const u8, score: f64 } {
345 var args = try self.client.allocator.alloc([]const u8, keys.len + 2);
346 defer self.client.allocator.free(args);
347
348 args[0] = "BZPOPMIN";
349 @memcpy(args[1..][0..keys.len], keys);
350
351 var buf: [32]u8 = undefined;
352 args[keys.len + 1] = std.fmt.bufPrint(&buf, "{d}", .{timeout}) catch unreachable;
353
354 const result = try self.client.sendCommand(args);
355 return switch (result) {
356 .array => |arr| {
357 if (arr.len < 3) return null;
358 const k = arr[0].asString() orelse return null;
359 const m = arr[1].asString() orelse return null;
360 const s = if (arr[2].asString()) |str| std.fmt.parseFloat(f64, str) catch return null else return null;
361 return .{ .key = k, .member = m, .score = s };
362 },
363 .nil => null,
364 else => null,
365 };
366 }
367
368 // ========================================================================
369 // Removal by Range
370 // ========================================================================
371
372 /// ZREMRANGEBYRANK key start stop - remove by rank range
373 pub fn zremrangeByRank(self: *SortedSetCommands, key: []const u8, start: i64, stop: i64) ClientError!i64 {
374 var start_buf: [24]u8 = undefined;
375 var stop_buf: [24]u8 = undefined;
376 const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable;
377 const stop_str = std.fmt.bufPrint(&stop_buf, "{d}", .{stop}) catch unreachable;
378 const result = try self.client.sendCommand(&.{ "ZREMRANGEBYRANK", key, start_str, stop_str });
379 return switch (result) {
380 .integer => |i| i,
381 else => 0,
382 };
383 }
384
385 /// ZREMRANGEBYSCORE key min max - remove by score range
386 pub fn zremrangeByScore(self: *SortedSetCommands, key: []const u8, min: f64, max: f64) ClientError!i64 {
387 var min_buf: [32]u8 = undefined;
388 var max_buf: [32]u8 = undefined;
389 const min_str = std.fmt.bufPrint(&min_buf, "{d}", .{min}) catch unreachable;
390 const max_str = std.fmt.bufPrint(&max_buf, "{d}", .{max}) catch unreachable;
391 const result = try self.client.sendCommand(&.{ "ZREMRANGEBYSCORE", key, min_str, max_str });
392 return switch (result) {
393 .integer => |i| i,
394 else => 0,
395 };
396 }
397
398 // ========================================================================
399 // Scanning
400 // ========================================================================
401
402 /// Result from ZSCAN command
403 pub const ScanResult = struct {
404 cursor: []const u8,
405 /// Flat array: [member1, score1, member2, score2, ...]
406 pairs: []const Value,
407 };
408
409 /// ZSCAN key cursor [MATCH pattern] [COUNT count] - iterate members
410 pub fn zscan(self: *SortedSetCommands, key: []const u8, cursor: []const u8, pattern: ?[]const u8, count: ?u32) ClientError!ScanResult {
411 var args_buf: [8][]const u8 = undefined;
412 var arg_count: usize = 3;
413 args_buf[0] = "ZSCAN";
414 args_buf[1] = key;
415 args_buf[2] = cursor;
416
417 if (pattern) |p| {
418 args_buf[arg_count] = "MATCH";
419 args_buf[arg_count + 1] = p;
420 arg_count += 2;
421 }
422
423 var count_buf: [16]u8 = undefined;
424 if (count) |c| {
425 args_buf[arg_count] = "COUNT";
426 args_buf[arg_count + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable;
427 arg_count += 2;
428 }
429
430 const result = try self.client.sendCommand(args_buf[0..arg_count]);
431 return switch (result) {
432 .array => |arr| {
433 if (arr.len < 2) return ScanResult{ .cursor = "0", .pairs = &.{} };
434 return ScanResult{
435 .cursor = arr[0].asString() orelse "0",
436 .pairs = arr[1].asArray() orelse &.{},
437 };
438 },
439 else => ScanResult{ .cursor = "0", .pairs = &.{} },
440 };
441 }
442};
443
444/// Extend Client with sorted set commands.
445pub fn zsets(client: *Client) SortedSetCommands {
446 return SortedSetCommands.init(client);
447}