this repo has no description
at main 304 lines 12 kB view raw
1//! Redis hash commands 2//! 3//! Hashes are maps of field-value pairs, ideal for representing objects. 4//! Each hash can store up to 2^32 - 1 field-value pairs. 5//! 6//! ## Use Cases 7//! 8//! - User profiles: `HSET user:1000 name "Alice" email "alice@example.com"` 9//! - Session data: `HMSET session:abc user_id 1000 created 1640000000` 10//! - Counters per entity: `HINCRBY stats:page:home views 1` 11//! 12//! ## Memory Efficiency 13//! 14//! Small hashes (configurable, default <512 fields with values <64 bytes) 15//! use ziplist encoding, which is very memory efficient. 16//! 17//! ## Examples 18//! 19//! ```zig 20//! try client.hashes().hset("user:1", "name", "alice"); 21//! try client.hashes().hset("user:1", "email", "alice@example.com"); 22//! 23//! const name = try client.hashes().hget("user:1", "name"); // "alice" 24//! const all = try client.hashes().hgetAll("user:1"); // [name, alice, email, ...] 25//! 26//! _ = try client.hashes().hincrBy("user:1", "visits", 1); 27//! ``` 28 29const std = @import("std"); 30const Client = @import("../client.zig").Client; 31const Value = @import("../resp.zig").Value; 32const CommandError = @import("../resp.zig").CommandError; 33const ClientError = @import("../resp.zig").ClientError; 34 35/// Hash command implementations. 36pub const HashCommands = struct { 37 client: *Client, 38 39 pub fn init(client: *Client) HashCommands { 40 return .{ .client = client }; 41 } 42 43 // ======================================================================== 44 // Single Field Operations 45 // ======================================================================== 46 47 /// HSET key field value [field value ...] - set field(s) 48 /// Returns number of fields added (not updated). 49 pub fn hset(self: *HashCommands, key: []const u8, field: []const u8, value: []const u8) ClientError!i64 { 50 const result = try self.client.sendCommand(&.{ "HSET", key, field, value }); 51 return switch (result) { 52 .integer => |i| i, 53 .err => return CommandError.WrongType, 54 else => 0, 55 }; 56 } 57 58 /// HSET with multiple field-value pairs 59 pub fn hsetMulti(self: *HashCommands, key: []const u8, pairs: []const [2][]const u8) ClientError!i64 { 60 const arg_count = 2 + pairs.len * 2; 61 var args = try self.client.allocator.alloc([]const u8, arg_count); 62 defer self.client.allocator.free(args); 63 64 args[0] = "HSET"; 65 args[1] = key; 66 for (pairs, 0..) |pair, i| { 67 args[2 + i * 2] = pair[0]; 68 args[2 + i * 2 + 1] = pair[1]; 69 } 70 71 const result = try self.client.sendCommand(args); 72 return switch (result) { 73 .integer => |i| i, 74 .err => return CommandError.WrongType, 75 else => 0, 76 }; 77 } 78 79 /// HSETNX key field value - set field only if it doesn't exist 80 /// Returns true if field was set. 81 pub fn hsetnx(self: *HashCommands, key: []const u8, field: []const u8, value: []const u8) ClientError!bool { 82 const result = try self.client.sendCommand(&.{ "HSETNX", key, field, value }); 83 return switch (result) { 84 .integer => |i| i == 1, 85 .err => return CommandError.WrongType, 86 else => false, 87 }; 88 } 89 90 /// HGET key field - get field value 91 pub fn hget(self: *HashCommands, key: []const u8, field: []const u8) ClientError!?[]const u8 { 92 const result = try self.client.sendCommand(&.{ "HGET", key, field }); 93 return switch (result) { 94 .bulk => |b| b, 95 .nil => null, 96 .err => return CommandError.WrongType, 97 else => null, 98 }; 99 } 100 101 /// HMGET key field [field ...] - get multiple fields 102 /// Returns array of values (null for missing fields). 103 pub fn hmget(self: *HashCommands, key: []const u8, fields: []const []const u8) ClientError![]const Value { 104 var args = try self.client.allocator.alloc([]const u8, fields.len + 2); 105 defer self.client.allocator.free(args); 106 args[0] = "HMGET"; 107 args[1] = key; 108 @memcpy(args[2..], fields); 109 110 const result = try self.client.sendCommand(args); 111 return switch (result) { 112 .array => |a| a, 113 else => &.{}, 114 }; 115 } 116 117 /// HDEL key field [field ...] - delete fields 118 /// Returns number of fields deleted. 119 pub fn hdel(self: *HashCommands, key: []const u8, fields: []const []const u8) ClientError!i64 { 120 var args = try self.client.allocator.alloc([]const u8, fields.len + 2); 121 defer self.client.allocator.free(args); 122 args[0] = "HDEL"; 123 args[1] = key; 124 @memcpy(args[2..], fields); 125 126 const result = try self.client.sendCommand(args); 127 return switch (result) { 128 .integer => |i| i, 129 else => 0, 130 }; 131 } 132 133 /// HEXISTS key field - check if field exists 134 pub fn hexists(self: *HashCommands, key: []const u8, field: []const u8) ClientError!bool { 135 const result = try self.client.sendCommand(&.{ "HEXISTS", key, field }); 136 return switch (result) { 137 .integer => |i| i == 1, 138 else => false, 139 }; 140 } 141 142 // ======================================================================== 143 // Numeric Operations 144 // ======================================================================== 145 146 /// HINCRBY key field increment - increment integer field 147 pub fn hincrBy(self: *HashCommands, key: []const u8, field: []const u8, increment: i64) ClientError!i64 { 148 var buf: [24]u8 = undefined; 149 const inc_str = std.fmt.bufPrint(&buf, "{d}", .{increment}) catch unreachable; 150 const result = try self.client.sendCommand(&.{ "HINCRBY", key, field, inc_str }); 151 return switch (result) { 152 .integer => |i| i, 153 .err => return CommandError.WrongType, 154 else => return CommandError.RedisError, 155 }; 156 } 157 158 /// HINCRBYFLOAT key field increment - increment float field 159 pub fn hincrByFloat(self: *HashCommands, key: []const u8, field: []const u8, increment: f64) ClientError!f64 { 160 var buf: [32]u8 = undefined; 161 const inc_str = std.fmt.bufPrint(&buf, "{d}", .{increment}) catch unreachable; 162 const result = try self.client.sendCommand(&.{ "HINCRBYFLOAT", key, field, inc_str }); 163 return switch (result) { 164 .bulk => |b| if (b) |s| std.fmt.parseFloat(f64, s) catch return CommandError.RedisError else return CommandError.RedisError, 165 .err => return CommandError.WrongType, 166 else => return CommandError.RedisError, 167 }; 168 } 169 170 // ======================================================================== 171 // Bulk Operations 172 // ======================================================================== 173 174 /// HGETALL key - get all field-value pairs 175 /// Returns flat array: [field1, value1, field2, value2, ...] 176 pub fn hgetAll(self: *HashCommands, key: []const u8) ClientError![]const Value { 177 const result = try self.client.sendCommand(&.{ "HGETALL", key }); 178 return switch (result) { 179 .array => |a| a, 180 .err => return CommandError.WrongType, 181 else => &.{}, 182 }; 183 } 184 185 /// HKEYS key - get all field names 186 pub fn hkeys(self: *HashCommands, key: []const u8) ClientError![]const Value { 187 const result = try self.client.sendCommand(&.{ "HKEYS", key }); 188 return switch (result) { 189 .array => |a| a, 190 else => &.{}, 191 }; 192 } 193 194 /// HVALS key - get all values 195 pub fn hvals(self: *HashCommands, key: []const u8) ClientError![]const Value { 196 const result = try self.client.sendCommand(&.{ "HVALS", key }); 197 return switch (result) { 198 .array => |a| a, 199 else => &.{}, 200 }; 201 } 202 203 /// HLEN key - get number of fields 204 pub fn hlen(self: *HashCommands, key: []const u8) ClientError!i64 { 205 const result = try self.client.sendCommand(&.{ "HLEN", key }); 206 return switch (result) { 207 .integer => |i| i, 208 else => 0, 209 }; 210 } 211 212 /// HSTRLEN key field - get string length of field value 213 pub fn hstrlen(self: *HashCommands, key: []const u8, field: []const u8) ClientError!i64 { 214 const result = try self.client.sendCommand(&.{ "HSTRLEN", key, field }); 215 return switch (result) { 216 .integer => |i| i, 217 else => 0, 218 }; 219 } 220 221 // ======================================================================== 222 // Scanning 223 // ======================================================================== 224 225 /// Result from HSCAN command 226 pub const ScanResult = struct { 227 cursor: []const u8, 228 /// Flat array: [field1, value1, field2, value2, ...] 229 pairs: []const Value, 230 }; 231 232 /// HSCAN key cursor [MATCH pattern] [COUNT count] - iterate fields 233 pub fn hscan(self: *HashCommands, key: []const u8, cursor: []const u8, pattern: ?[]const u8, count: ?u32) ClientError!ScanResult { 234 var args_buf: [8][]const u8 = undefined; 235 var arg_count: usize = 3; 236 args_buf[0] = "HSCAN"; 237 args_buf[1] = key; 238 args_buf[2] = cursor; 239 240 if (pattern) |p| { 241 args_buf[arg_count] = "MATCH"; 242 args_buf[arg_count + 1] = p; 243 arg_count += 2; 244 } 245 246 var count_buf: [16]u8 = undefined; 247 if (count) |c| { 248 args_buf[arg_count] = "COUNT"; 249 args_buf[arg_count + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable; 250 arg_count += 2; 251 } 252 253 const result = try self.client.sendCommand(args_buf[0..arg_count]); 254 return switch (result) { 255 .array => |arr| { 256 if (arr.len < 2) return ScanResult{ .cursor = "0", .pairs = &.{} }; 257 return ScanResult{ 258 .cursor = arr[0].asString() orelse "0", 259 .pairs = arr[1].asArray() orelse &.{}, 260 }; 261 }, 262 else => ScanResult{ .cursor = "0", .pairs = &.{} }, 263 }; 264 } 265 266 // ======================================================================== 267 // Random Field (Redis 6.2+) 268 // ======================================================================== 269 270 /// HRANDFIELD key [count] - get random field(s) 271 pub fn hrandfield(self: *HashCommands, key: []const u8, count: ?i32) ClientError![]const Value { 272 if (count) |c| { 273 var buf: [16]u8 = undefined; 274 const count_str = std.fmt.bufPrint(&buf, "{d}", .{c}) catch unreachable; 275 const result = try self.client.sendCommand(&.{ "HRANDFIELD", key, count_str }); 276 return switch (result) { 277 .array => |a| a, 278 else => &.{}, 279 }; 280 } else { 281 const result = try self.client.sendCommand(&.{ "HRANDFIELD", key }); 282 return switch (result) { 283 .bulk => |b| if (b != null) &.{Value{ .bulk = b }} else &.{}, 284 else => &.{}, 285 }; 286 } 287 } 288 289 /// HRANDFIELD key count WITHVALUES - get random field-value pairs 290 pub fn hrandfieldWithValues(self: *HashCommands, key: []const u8, count: i32) ClientError![]const Value { 291 var buf: [16]u8 = undefined; 292 const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; 293 const result = try self.client.sendCommand(&.{ "HRANDFIELD", key, count_str, "WITHVALUES" }); 294 return switch (result) { 295 .array => |a| a, 296 else => &.{}, 297 }; 298 } 299}; 300 301/// Extend Client with hash commands. 302pub fn hashes(client: *Client) HashCommands { 303 return HashCommands.init(client); 304}