this repo has no description
at main 352 lines 13 kB view raw
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}