this repo has no description
1//! Redis set commands
2//!
3//! Sets are unordered collections of unique strings. Operations on sets
4//! are O(1) for add/remove/membership, making them ideal for:
5//!
6//! ## Use Cases
7//!
8//! - Tags: `SADD post:1:tags "redis" "database" "nosql"`
9//! - Unique visitors: `SADD visitors:2024-01-15 user_id`
10//! - Online users: `SADD online user_id` + `SREM online user_id`
11//! - Permissions: `SADD user:1:roles "admin" "editor"`
12//! - Relationships: `SADD user:1:following user:2`
13//!
14//! ## Set Operations
15//!
16//! - SUNION: combine sets
17//! - SINTER: find common elements
18//! - SDIFF: elements in first set but not others
19//!
20//! ## Examples
21//!
22//! ```zig
23//! _ = try client.sets().sadd("tags", &.{"redis", "fast", "database"});
24//! const is_tagged = try client.sets().sismember("tags", "redis"); // true
25//! const all_tags = try client.sets().smembers("tags");
26//!
27//! // Set operations
28//! const common = try client.sets().sinter(&.{"set1", "set2"});
29//! const combined = try client.sets().sunion(&.{"set1", "set2", "set3"});
30//! ```
31
32const std = @import("std");
33const Client = @import("../client.zig").Client;
34const Value = @import("../resp.zig").Value;
35const CommandError = @import("../resp.zig").CommandError;
36const ClientError = @import("../resp.zig").ClientError;
37
38/// Set command implementations.
39pub const SetCommands = struct {
40 client: *Client,
41
42 pub fn init(client: *Client) SetCommands {
43 return .{ .client = client };
44 }
45
46 // ========================================================================
47 // Basic Operations
48 // ========================================================================
49
50 /// SADD key member [member ...] - add members to set
51 /// Returns number of members added (not already present).
52 pub fn sadd(self: *SetCommands, key: []const u8, members: []const []const u8) ClientError!i64 {
53 var args = try self.client.allocator.alloc([]const u8, members.len + 2);
54 defer self.client.allocator.free(args);
55 args[0] = "SADD";
56 args[1] = key;
57 @memcpy(args[2..], members);
58
59 const result = try self.client.sendCommand(args);
60 return switch (result) {
61 .integer => |i| i,
62 .err => return CommandError.WrongType,
63 else => 0,
64 };
65 }
66
67 /// SREM key member [member ...] - remove members from set
68 /// Returns number of members removed.
69 pub fn srem(self: *SetCommands, key: []const u8, members: []const []const u8) ClientError!i64 {
70 var args = try self.client.allocator.alloc([]const u8, members.len + 2);
71 defer self.client.allocator.free(args);
72 args[0] = "SREM";
73 args[1] = key;
74 @memcpy(args[2..], members);
75
76 const result = try self.client.sendCommand(args);
77 return switch (result) {
78 .integer => |i| i,
79 else => 0,
80 };
81 }
82
83 /// SISMEMBER key member - check if member exists
84 pub fn sismember(self: *SetCommands, key: []const u8, member: []const u8) ClientError!bool {
85 const result = try self.client.sendCommand(&.{ "SISMEMBER", key, member });
86 return switch (result) {
87 .integer => |i| i == 1,
88 else => false,
89 };
90 }
91
92 /// SMISMEMBER key member [member ...] - check multiple members (Redis 6.2+)
93 /// Returns array of 0/1 for each member.
94 pub fn smismember(self: *SetCommands, key: []const u8, members: []const []const u8) ClientError![]const Value {
95 var args = try self.client.allocator.alloc([]const u8, members.len + 2);
96 defer self.client.allocator.free(args);
97 args[0] = "SMISMEMBER";
98 args[1] = key;
99 @memcpy(args[2..], members);
100
101 const result = try self.client.sendCommand(args);
102 return switch (result) {
103 .array => |a| a,
104 else => &.{},
105 };
106 }
107
108 /// SMEMBERS key - get all members
109 pub fn smembers(self: *SetCommands, key: []const u8) ClientError![]const Value {
110 const result = try self.client.sendCommand(&.{ "SMEMBERS", key });
111 return switch (result) {
112 .array => |a| a,
113 else => &.{},
114 };
115 }
116
117 /// SCARD key - get set cardinality (size)
118 pub fn scard(self: *SetCommands, key: []const u8) ClientError!i64 {
119 const result = try self.client.sendCommand(&.{ "SCARD", key });
120 return switch (result) {
121 .integer => |i| i,
122 else => 0,
123 };
124 }
125
126 // ========================================================================
127 // Pop and Random
128 // ========================================================================
129
130 /// SPOP key [count] - remove and return random member(s)
131 pub fn spop(self: *SetCommands, key: []const u8) ClientError!?[]const u8 {
132 const result = try self.client.sendCommand(&.{ "SPOP", key });
133 return switch (result) {
134 .bulk => |b| b,
135 .nil => null,
136 else => null,
137 };
138 }
139
140 /// SPOP key count - remove and return multiple random members
141 pub fn spopCount(self: *SetCommands, 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(&.{ "SPOP", key, count_str });
145 return switch (result) {
146 .array => |a| a,
147 else => &.{},
148 };
149 }
150
151 /// SRANDMEMBER key [count] - get random member(s) without removing
152 pub fn srandmember(self: *SetCommands, key: []const u8) ClientError!?[]const u8 {
153 const result = try self.client.sendCommand(&.{ "SRANDMEMBER", key });
154 return switch (result) {
155 .bulk => |b| b,
156 else => null,
157 };
158 }
159
160 /// SRANDMEMBER key count - get multiple random members
161 /// If count is negative, allows duplicates.
162 pub fn srandmemberCount(self: *SetCommands, key: []const u8, count: i32) ClientError![]const Value {
163 var buf: [16]u8 = undefined;
164 const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable;
165 const result = try self.client.sendCommand(&.{ "SRANDMEMBER", key, count_str });
166 return switch (result) {
167 .array => |a| a,
168 else => &.{},
169 };
170 }
171
172 // ========================================================================
173 // Set Operations
174 // ========================================================================
175
176 /// SUNION key [key ...] - union of sets
177 pub fn sunion(self: *SetCommands, keys: []const []const u8) ClientError![]const Value {
178 var args = try self.client.allocator.alloc([]const u8, keys.len + 1);
179 defer self.client.allocator.free(args);
180 args[0] = "SUNION";
181 @memcpy(args[1..], keys);
182
183 const result = try self.client.sendCommand(args);
184 return switch (result) {
185 .array => |a| a,
186 else => &.{},
187 };
188 }
189
190 /// SUNIONSTORE destination key [key ...] - store union in destination
191 /// Returns size of resulting set.
192 pub fn sunionstore(self: *SetCommands, dest: []const u8, keys: []const []const u8) ClientError!i64 {
193 var args = try self.client.allocator.alloc([]const u8, keys.len + 2);
194 defer self.client.allocator.free(args);
195 args[0] = "SUNIONSTORE";
196 args[1] = dest;
197 @memcpy(args[2..], keys);
198
199 const result = try self.client.sendCommand(args);
200 return switch (result) {
201 .integer => |i| i,
202 else => 0,
203 };
204 }
205
206 /// SINTER key [key ...] - intersection of sets
207 pub fn sinter(self: *SetCommands, keys: []const []const u8) ClientError![]const Value {
208 var args = try self.client.allocator.alloc([]const u8, keys.len + 1);
209 defer self.client.allocator.free(args);
210 args[0] = "SINTER";
211 @memcpy(args[1..], keys);
212
213 const result = try self.client.sendCommand(args);
214 return switch (result) {
215 .array => |a| a,
216 else => &.{},
217 };
218 }
219
220 /// SINTERSTORE destination key [key ...] - store intersection
221 pub fn sinterstore(self: *SetCommands, dest: []const u8, keys: []const []const u8) ClientError!i64 {
222 var args = try self.client.allocator.alloc([]const u8, keys.len + 2);
223 defer self.client.allocator.free(args);
224 args[0] = "SINTERSTORE";
225 args[1] = dest;
226 @memcpy(args[2..], keys);
227
228 const result = try self.client.sendCommand(args);
229 return switch (result) {
230 .integer => |i| i,
231 else => 0,
232 };
233 }
234
235 /// SINTERCARD numkeys key [key ...] [LIMIT limit] - cardinality of intersection (Redis 7+)
236 pub fn sintercard(self: *SetCommands, keys: []const []const u8, limit: ?u32) ClientError!i64 {
237 const base_len = 2 + keys.len + if (limit != null) @as(usize, 2) else @as(usize, 0);
238 var args = try self.client.allocator.alloc([]const u8, base_len);
239 defer self.client.allocator.free(args);
240
241 var numkeys_buf: [16]u8 = undefined;
242 args[0] = "SINTERCARD";
243 args[1] = std.fmt.bufPrint(&numkeys_buf, "{d}", .{keys.len}) catch unreachable;
244 @memcpy(args[2..][0..keys.len], keys);
245
246 var arg_idx = 2 + keys.len;
247 var limit_buf: [16]u8 = undefined;
248 if (limit) |l| {
249 args[arg_idx] = "LIMIT";
250 args[arg_idx + 1] = std.fmt.bufPrint(&limit_buf, "{d}", .{l}) catch unreachable;
251 arg_idx += 2;
252 }
253
254 const result = try self.client.sendCommand(args[0..arg_idx]);
255 return switch (result) {
256 .integer => |i| i,
257 else => 0,
258 };
259 }
260
261 /// SDIFF key [key ...] - difference of sets (first minus others)
262 pub fn sdiff(self: *SetCommands, keys: []const []const u8) ClientError![]const Value {
263 var args = try self.client.allocator.alloc([]const u8, keys.len + 1);
264 defer self.client.allocator.free(args);
265 args[0] = "SDIFF";
266 @memcpy(args[1..], keys);
267
268 const result = try self.client.sendCommand(args);
269 return switch (result) {
270 .array => |a| a,
271 else => &.{},
272 };
273 }
274
275 /// SDIFFSTORE destination key [key ...] - store difference
276 pub fn sdiffstore(self: *SetCommands, dest: []const u8, keys: []const []const u8) ClientError!i64 {
277 var args = try self.client.allocator.alloc([]const u8, keys.len + 2);
278 defer self.client.allocator.free(args);
279 args[0] = "SDIFFSTORE";
280 args[1] = dest;
281 @memcpy(args[2..], keys);
282
283 const result = try self.client.sendCommand(args);
284 return switch (result) {
285 .integer => |i| i,
286 else => 0,
287 };
288 }
289
290 // ========================================================================
291 // Move
292 // ========================================================================
293
294 /// SMOVE source destination member - move member between sets
295 /// Returns true if moved (member existed in source).
296 pub fn smove(self: *SetCommands, source: []const u8, dest: []const u8, member: []const u8) ClientError!bool {
297 const result = try self.client.sendCommand(&.{ "SMOVE", source, dest, member });
298 return switch (result) {
299 .integer => |i| i == 1,
300 else => false,
301 };
302 }
303
304 // ========================================================================
305 // Scanning
306 // ========================================================================
307
308 /// Result from SSCAN command
309 pub const ScanResult = struct {
310 cursor: []const u8,
311 members: []const Value,
312 };
313
314 /// SSCAN key cursor [MATCH pattern] [COUNT count] - iterate members
315 pub fn sscan(self: *SetCommands, key: []const u8, cursor: []const u8, pattern: ?[]const u8, count: ?u32) ClientError!ScanResult {
316 var args_buf: [8][]const u8 = undefined;
317 var arg_count: usize = 3;
318 args_buf[0] = "SSCAN";
319 args_buf[1] = key;
320 args_buf[2] = cursor;
321
322 if (pattern) |p| {
323 args_buf[arg_count] = "MATCH";
324 args_buf[arg_count + 1] = p;
325 arg_count += 2;
326 }
327
328 var count_buf: [16]u8 = undefined;
329 if (count) |c| {
330 args_buf[arg_count] = "COUNT";
331 args_buf[arg_count + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable;
332 arg_count += 2;
333 }
334
335 const result = try self.client.sendCommand(args_buf[0..arg_count]);
336 return switch (result) {
337 .array => |arr| {
338 if (arr.len < 2) return ScanResult{ .cursor = "0", .members = &.{} };
339 return ScanResult{
340 .cursor = arr[0].asString() orelse "0",
341 .members = arr[1].asArray() orelse &.{},
342 };
343 },
344 else => ScanResult{ .cursor = "0", .members = &.{} },
345 };
346 }
347};
348
349/// Extend Client with set commands.
350pub fn sets(client: *Client) SetCommands {
351 return SetCommands.init(client);
352}