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