//! Redis hash commands //! //! Hashes are maps of field-value pairs, ideal for representing objects. //! Each hash can store up to 2^32 - 1 field-value pairs. //! //! ## Use Cases //! //! - User profiles: `HSET user:1000 name "Alice" email "alice@example.com"` //! - Session data: `HMSET session:abc user_id 1000 created 1640000000` //! - Counters per entity: `HINCRBY stats:page:home views 1` //! //! ## Memory Efficiency //! //! Small hashes (configurable, default <512 fields with values <64 bytes) //! use ziplist encoding, which is very memory efficient. //! //! ## Examples //! //! ```zig //! try client.hashes().hset("user:1", "name", "alice"); //! try client.hashes().hset("user:1", "email", "alice@example.com"); //! //! const name = try client.hashes().hget("user:1", "name"); // "alice" //! const all = try client.hashes().hgetAll("user:1"); // [name, alice, email, ...] //! //! _ = try client.hashes().hincrBy("user:1", "visits", 1); //! ``` const std = @import("std"); const Client = @import("../client.zig").Client; const Value = @import("../resp.zig").Value; const CommandError = @import("../resp.zig").CommandError; const ClientError = @import("../resp.zig").ClientError; /// Hash command implementations. pub const HashCommands = struct { client: *Client, pub fn init(client: *Client) HashCommands { return .{ .client = client }; } // ======================================================================== // Single Field Operations // ======================================================================== /// HSET key field value [field value ...] - set field(s) /// Returns number of fields added (not updated). pub fn hset(self: *HashCommands, key: []const u8, field: []const u8, value: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "HSET", key, field, value }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => 0, }; } /// HSET with multiple field-value pairs pub fn hsetMulti(self: *HashCommands, key: []const u8, pairs: []const [2][]const u8) ClientError!i64 { const arg_count = 2 + pairs.len * 2; var args = try self.client.allocator.alloc([]const u8, arg_count); defer self.client.allocator.free(args); args[0] = "HSET"; args[1] = key; for (pairs, 0..) |pair, i| { args[2 + i * 2] = pair[0]; args[2 + i * 2 + 1] = pair[1]; } const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => 0, }; } /// HSETNX key field value - set field only if it doesn't exist /// Returns true if field was set. pub fn hsetnx(self: *HashCommands, key: []const u8, field: []const u8, value: []const u8) ClientError!bool { const result = try self.client.sendCommand(&.{ "HSETNX", key, field, value }); return switch (result) { .integer => |i| i == 1, .err => return CommandError.WrongType, else => false, }; } /// HGET key field - get field value pub fn hget(self: *HashCommands, key: []const u8, field: []const u8) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{ "HGET", key, field }); return switch (result) { .bulk => |b| b, .nil => null, .err => return CommandError.WrongType, else => null, }; } /// HMGET key field [field ...] - get multiple fields /// Returns array of values (null for missing fields). pub fn hmget(self: *HashCommands, key: []const u8, fields: []const []const u8) ClientError![]const Value { var args = try self.client.allocator.alloc([]const u8, fields.len + 2); defer self.client.allocator.free(args); args[0] = "HMGET"; args[1] = key; @memcpy(args[2..], fields); const result = try self.client.sendCommand(args); return switch (result) { .array => |a| a, else => &.{}, }; } /// HDEL key field [field ...] - delete fields /// Returns number of fields deleted. pub fn hdel(self: *HashCommands, key: []const u8, fields: []const []const u8) ClientError!i64 { var args = try self.client.allocator.alloc([]const u8, fields.len + 2); defer self.client.allocator.free(args); args[0] = "HDEL"; args[1] = key; @memcpy(args[2..], fields); const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, else => 0, }; } /// HEXISTS key field - check if field exists pub fn hexists(self: *HashCommands, key: []const u8, field: []const u8) ClientError!bool { const result = try self.client.sendCommand(&.{ "HEXISTS", key, field }); return switch (result) { .integer => |i| i == 1, else => false, }; } // ======================================================================== // Numeric Operations // ======================================================================== /// HINCRBY key field increment - increment integer field pub fn hincrBy(self: *HashCommands, key: []const u8, field: []const u8, increment: i64) ClientError!i64 { var buf: [24]u8 = undefined; const inc_str = std.fmt.bufPrint(&buf, "{d}", .{increment}) catch unreachable; const result = try self.client.sendCommand(&.{ "HINCRBY", key, field, inc_str }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => return CommandError.RedisError, }; } /// HINCRBYFLOAT key field increment - increment float field pub fn hincrByFloat(self: *HashCommands, key: []const u8, field: []const u8, increment: f64) ClientError!f64 { var buf: [32]u8 = undefined; const inc_str = std.fmt.bufPrint(&buf, "{d}", .{increment}) catch unreachable; const result = try self.client.sendCommand(&.{ "HINCRBYFLOAT", key, field, inc_str }); return switch (result) { .bulk => |b| if (b) |s| std.fmt.parseFloat(f64, s) catch return CommandError.RedisError else return CommandError.RedisError, .err => return CommandError.WrongType, else => return CommandError.RedisError, }; } // ======================================================================== // Bulk Operations // ======================================================================== /// HGETALL key - get all field-value pairs /// Returns flat array: [field1, value1, field2, value2, ...] pub fn hgetAll(self: *HashCommands, key: []const u8) ClientError![]const Value { const result = try self.client.sendCommand(&.{ "HGETALL", key }); return switch (result) { .array => |a| a, .err => return CommandError.WrongType, else => &.{}, }; } /// HKEYS key - get all field names pub fn hkeys(self: *HashCommands, key: []const u8) ClientError![]const Value { const result = try self.client.sendCommand(&.{ "HKEYS", key }); return switch (result) { .array => |a| a, else => &.{}, }; } /// HVALS key - get all values pub fn hvals(self: *HashCommands, key: []const u8) ClientError![]const Value { const result = try self.client.sendCommand(&.{ "HVALS", key }); return switch (result) { .array => |a| a, else => &.{}, }; } /// HLEN key - get number of fields pub fn hlen(self: *HashCommands, key: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "HLEN", key }); return switch (result) { .integer => |i| i, else => 0, }; } /// HSTRLEN key field - get string length of field value pub fn hstrlen(self: *HashCommands, key: []const u8, field: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "HSTRLEN", key, field }); return switch (result) { .integer => |i| i, else => 0, }; } // ======================================================================== // Scanning // ======================================================================== /// Result from HSCAN command pub const ScanResult = struct { cursor: []const u8, /// Flat array: [field1, value1, field2, value2, ...] pairs: []const Value, }; /// HSCAN key cursor [MATCH pattern] [COUNT count] - iterate fields pub fn hscan(self: *HashCommands, key: []const u8, cursor: []const u8, pattern: ?[]const u8, count: ?u32) ClientError!ScanResult { var args_buf: [8][]const u8 = undefined; var arg_count: usize = 3; args_buf[0] = "HSCAN"; args_buf[1] = key; args_buf[2] = cursor; if (pattern) |p| { args_buf[arg_count] = "MATCH"; args_buf[arg_count + 1] = p; arg_count += 2; } var count_buf: [16]u8 = undefined; if (count) |c| { args_buf[arg_count] = "COUNT"; args_buf[arg_count + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable; arg_count += 2; } const result = try self.client.sendCommand(args_buf[0..arg_count]); return switch (result) { .array => |arr| { if (arr.len < 2) return ScanResult{ .cursor = "0", .pairs = &.{} }; return ScanResult{ .cursor = arr[0].asString() orelse "0", .pairs = arr[1].asArray() orelse &.{}, }; }, else => ScanResult{ .cursor = "0", .pairs = &.{} }, }; } // ======================================================================== // Random Field (Redis 6.2+) // ======================================================================== /// HRANDFIELD key [count] - get random field(s) pub fn hrandfield(self: *HashCommands, key: []const u8, count: ?i32) ClientError![]const Value { if (count) |c| { var buf: [16]u8 = undefined; const count_str = std.fmt.bufPrint(&buf, "{d}", .{c}) catch unreachable; const result = try self.client.sendCommand(&.{ "HRANDFIELD", key, count_str }); return switch (result) { .array => |a| a, else => &.{}, }; } else { const result = try self.client.sendCommand(&.{ "HRANDFIELD", key }); return switch (result) { .bulk => |b| if (b != null) &.{Value{ .bulk = b }} else &.{}, else => &.{}, }; } } /// HRANDFIELD key count WITHVALUES - get random field-value pairs pub fn hrandfieldWithValues(self: *HashCommands, key: []const u8, count: i32) ClientError![]const Value { var buf: [16]u8 = undefined; const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; const result = try self.client.sendCommand(&.{ "HRANDFIELD", key, count_str, "WITHVALUES" }); return switch (result) { .array => |a| a, else => &.{}, }; } }; /// Extend Client with hash commands. pub fn hashes(client: *Client) HashCommands { return HashCommands.init(client); }