Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
e4a945603f [autofix.ci] apply automated fixes 2025-08-19 22:54:17 +00:00
Claude Bot
c6343a9acc implement record for hmset 2025-08-19 22:52:01 +00:00
3 changed files with 154 additions and 28 deletions

View File

@@ -265,10 +265,10 @@ declare module "bun" {
/**
* Set multiple hash fields to multiple values
* @param key The hash key
* @param fieldValues An array of alternating field names and values
* @param fieldValues An array of alternating field names and values, or an object with field-value pairs
* @returns Promise that resolves with "OK" on success
*/
hmset(key: RedisClient.KeyLike, fieldValues: string[]): Promise<string>;
hmset(key: RedisClient.KeyLike, fieldValues: string[] | Record<string, string>): Promise<string>;
/**
* Get the values of all the given hash fields

View File

@@ -484,23 +484,59 @@ pub fn hincrbyfloat(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, ca
return promise.toJS();
}
// Context for collecting object properties during iteration
const ObjectIteratorContext = struct {
args: *std.ArrayList(jsc.ZigString.Slice),
globalObject: *jsc.JSGlobalObject,
has_error: bool = false,
};
// Callback function for iterating over object properties
fn collectObjectProperty(
globalThis: *jsc.JSGlobalObject,
ctx_ptr: ?*anyopaque,
key: *jsc.ZigString,
value: jsc.JSValue,
is_symbol: bool,
is_private_symbol: bool,
) callconv(.C) void {
_ = is_symbol;
_ = is_private_symbol;
var ctx: *ObjectIteratorContext = bun.cast(*ObjectIteratorContext, ctx_ptr orelse return);
if (ctx.has_error) return;
// Add field name
const field_slice = key.toSliceFast(bun.default_allocator);
ctx.args.append(field_slice) catch {
ctx.has_error = true;
return;
};
// Add field value
const value_str = value.toBunString(globalThis) catch {
ctx.has_error = true;
return;
};
defer value_str.deref();
const value_slice = value_str.toUTF8WithoutRef(bun.default_allocator);
ctx.args.append(value_slice) catch {
ctx.has_error = true;
return;
};
}
// Implement hmset (set multiple values in hash)
pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const key = try callframe.argument(0).toBunString(globalObject);
defer key.deref();
// For simplicity, let's accept a list of alternating keys and values
const array_arg = callframe.argument(1);
if (!array_arg.isObject() or !array_arg.isArray()) {
return globalObject.throw("Arguments must be an array of alternating field names and values", .{});
const field_values_arg = callframe.argument(1);
if (!field_values_arg.isObject()) {
return globalObject.throw("Arguments must be an array of alternating field names and values or an object", .{});
}
var iter = try array_arg.arrayIterator(globalObject);
if (iter.len % 2 != 0) {
return globalObject.throw("Arguments must be an array of alternating field names and values", .{});
}
var args = try std.ArrayList(jsc.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1);
var args = std.ArrayList(jsc.ZigString.Slice).init(bun.default_allocator);
defer {
for (args.items) |item| {
item.deinit();
@@ -511,25 +547,50 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe
// Add key as first argument
const key_slice = key.toUTF8WithoutRef(bun.default_allocator);
defer key_slice.deinit();
args.appendAssumeCapacity(key_slice);
try args.append(key_slice);
// Add field-value pairs
while (try iter.next()) |field_js| {
// Add field name
const field_str = try field_js.toBunString(globalObject);
defer field_str.deref();
const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator);
args.appendAssumeCapacity(field_slice);
// Add value
if (try iter.next()) |value_js| {
const value_str = try value_js.toBunString(globalObject);
defer value_str.deref();
const value_slice = value_str.toUTF8WithoutRef(bun.default_allocator);
args.appendAssumeCapacity(value_slice);
} else {
if (field_values_arg.isArray()) {
// Handle array case (existing behavior)
var iter = try field_values_arg.arrayIterator(globalObject);
if (iter.len % 2 != 0) {
return globalObject.throw("Arguments must be an array of alternating field names and values", .{});
}
// Add field-value pairs from array
while (try iter.next()) |field_js| {
// Add field name
const field_str = try field_js.toBunString(globalObject);
defer field_str.deref();
const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator);
try args.append(field_slice);
// Add value
if (try iter.next()) |value_js| {
const value_str = try value_js.toBunString(globalObject);
defer value_str.deref();
const value_slice = value_str.toUTF8WithoutRef(bun.default_allocator);
try args.append(value_slice);
} else {
return globalObject.throw("Arguments must be an array of alternating field names and values", .{});
}
}
} else {
// Handle object case (new behavior)
var ctx = ObjectIteratorContext{
.args = &args,
.globalObject = globalObject,
};
try field_values_arg.forEachProperty(globalObject, &ctx, collectObjectProperty);
if (ctx.has_error) {
return globalObject.throw("Failed to process object properties", .{});
}
// Check if the object was empty (Redis requires at least one field-value pair)
if (args.items.len == 1) { // Only the key was added
return globalObject.throw("Object must have at least one property", .{});
}
}
// Send HMSET command

View File

@@ -0,0 +1,65 @@
import { randomUUIDv7 } from "bun";
import { beforeEach, describe, expect, test } from "bun:test";
import { ConnectionType, createClient, ctx, isEnabled } from "./test-utils";
describe.skipIf(!isEnabled)("HMSET object support", () => {
beforeEach(() => {
if (ctx.redis?.connected) {
ctx.redis.close?.();
}
ctx.redis = createClient(ConnectionType.TCP);
});
test("array format (existing)", async () => {
const key = "test:array:" + randomUUIDv7().substring(0, 8);
await ctx.redis.hmset(key, ["field1", "value1", "field2", "value2"]);
const result = await ctx.redis.hgetall(key);
expect(result).toEqual({ field1: "value1", field2: "value2" });
});
test("object format (new)", async () => {
const key = "test:object:" + randomUUIDv7().substring(0, 8);
const data = { name: "John", age: "30", active: "true" };
await ctx.redis.hmset(key, data);
const result = await ctx.redis.hgetall(key);
expect(result).toEqual(data);
});
test("empty object should error", async () => {
const key = "test:empty:" + randomUUIDv7().substring(0, 8);
await expect(() => ctx.redis.hmset(key, {})).toThrow("Object must have at least one property");
});
test("complex field names", async () => {
const key = "test:complex:" + randomUUIDv7().substring(0, 8);
const data = {
"field:colons": "value1",
"field spaces": "value2",
"unicode_🔑": "value3",
};
await ctx.redis.hmset(key, data);
const result = await ctx.redis.hgetall(key);
expect(result).toEqual(data);
});
test("invalid arguments", async () => {
const key = "test:invalid:" + randomUUIDv7().substring(0, 8);
await expect(() => ctx.redis.hmset(key, null)).toThrow();
await expect(() => ctx.redis.hmset(key, "string")).toThrow();
await expect(() => ctx.redis.hmset(key, 123)).toThrow();
});
test("odd array length should error", async () => {
const key = "test:odd:" + randomUUIDv7().substring(0, 8);
await expect(() => ctx.redis.hmset(key, ["field1", "value1", "field2"])).toThrow();
});
});