Compare commits

...

15 Commits

Author SHA1 Message Date
Alistair Smith
720967be9c fix: update hset argument error message 2025-09-29 15:51:15 -07:00
autofix-ci[bot]
55bb15e793 [autofix.ci] apply automated fixes 2025-09-29 05:09:59 +00:00
Claude Bot
83ee713c14 Address CodeRabbit review feedback
- Reject arrays in object syntax path for hset
- Pre-size argument list based on expected capacity
- Remove redundant bounds check in variadic path
- Fix test issues: use ctx.redis instead of client
- Use array syntax for HMGET calls
- Add test case mixing field updates and additions
- Make stress tests more deterministic with environment flag
- Fix Buffer value assertions in tests
2025-09-29 05:06:56 +00:00
autofix-ci[bot]
c4c2f0fb3a [autofix.ci] apply automated fixes 2025-09-29 04:50:17 +00:00
Claude Bot
fdf1ee9dae Add object syntax support for redis.hset
Adds support for passing an object as the second argument to hset:
- hset(key, {field1: value1, field2: value2})
- Maintains backward compatibility with variadic arguments
- Added comprehensive tests for both syntaxes
- Added TypeScript overloads for type safety
2025-09-29 04:47:33 +00:00
Claude Bot
d1d0c382a5 test(valkey): add tests for invalid large argument counts in hset
- Test with 100 arguments (key + 99 = missing last value)
- Test with 1001 arguments (key + 1000 = missing last value)
- Verify validation correctly catches uneven field-value pairs even with many arguments

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 04:14:02 +00:00
autofix-ci[bot]
3497d7c0e8 [autofix.ci] apply automated fixes 2025-09-29 04:13:11 +00:00
Claude Bot
712a645d0f test(valkey): add stress tests for hset with many field-value pairs
- Add test for 8 field-value pairs (17 total arguments)
- Add stress test for 1000 field-value pairs (2001 total arguments)
- Add extreme stress test for 10000 field-value pairs (20001 total arguments)
- Verify implementation handles large numbers of arguments correctly

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 04:11:34 +00:00
Claude Bot
56f1e4f19c fix(test): properly verify errors are thrown in hset tests
- Fix test pattern to verify errors are actually thrown
- Use thrown variable pattern to ensure test fails if no error occurs
- Remove invalid number type tests as Redis coerces numbers to strings

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 04:02:34 +00:00
autofix-ci[bot]
c245820479 [autofix.ci] apply automated fixes 2025-09-29 03:54:44 +00:00
Claude Bot
8d129212e1 refactor: remove unnecessary comments from hset implementation
Remove redundant and obvious comments per code review feedback.
Keep code clean and self-documenting.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 03:53:06 +00:00
Claude Bot
ffe100963d types(valkey): add TypeScript definitions for hset method
Add complete TypeScript type definitions for the new hset method including:
- Method signature with required key, field, value parameters
- Support for variadic arguments to set multiple field-value pairs
- JSDoc documentation with examples
- Return type as Promise<number> for number of fields added

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 03:51:04 +00:00
autofix-ci[bot]
25af820727 [autofix.ci] apply automated fixes 2025-09-29 03:42:51 +00:00
Claude Bot
a44b634a22 test(valkey): add comprehensive error handling tests for hset
Add tests to ensure hset properly validates arguments and throws appropriate errors:
- Test missing arguments (less than 3 args)
- Test uneven field-value pairs
- Test invalid types for key, field, and value
- Test null/undefined values
- Verify errors are thrown before attempting network operations

This ensures the implementation won't crash on invalid input and provides
clear error messages to users.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 03:40:09 +00:00
Claude Bot
65c5ee3d94 feat(valkey): implement redis.hset method
Add native hset method to Valkey/Redis client supporting both single and multiple field-value pairs.
The HSET command sets the value of a field in a hash and returns the number of fields that were added.

- Add hset function declaration in valkey.classes.ts
- Implement hset handler in js_valkey_functions.zig supporting variadic arguments
- Export hset in js_valkey.zig
- Add comprehensive tests for hset method including single/multiple field-value pairs

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 03:33:44 +00:00
9 changed files with 707 additions and 14 deletions

View File

@@ -292,6 +292,38 @@ declare module "bun" {
*/
hget(key: RedisClient.KeyLike, field: RedisClient.KeyLike): Promise<string | null>;
/**
* Set the value of a hash field
* @param key The hash key
* @param field The field to set
* @param value The value to set
* @param args Additional field-value pairs (optional)
* @returns Promise that resolves with the number of fields that were added (not updated)
* @example
* // Set a single field
* await redis.hset("user:1", "name", "John");
*
* // Set multiple fields (Redis 4.0.0+)
* await redis.hset("user:1", "name", "John", "age", "30", "email", "john@example.com");
*/
hset(
key: RedisClient.KeyLike,
field: RedisClient.KeyLike,
value: RedisClient.KeyLike,
...args: RedisClient.KeyLike[]
): Promise<number>;
/**
* Set multiple hash fields using an object
* @param key The hash key
* @param fields An object containing field-value pairs
* @returns Promise that resolves with the number of fields that were added (not updated)
* @example
* // Set multiple fields using object syntax
* await redis.hset("user:1", { name: "John", age: "30", email: "john@example.com" });
*/
hset(key: RedisClient.KeyLike, fields: Record<string, RedisClient.KeyLike>): Promise<number>;
/**
* Get the values of all the given hash fields
* @param key The hash key

View File

@@ -84,6 +84,10 @@ export default [
fn: "hget",
length: 2,
},
hset: {
fn: "hset",
length: 3,
},
hmget: {
fn: "hmget",
length: 2,

View File

@@ -1257,6 +1257,7 @@ pub const JSValkeyClient = struct {
pub const getset = fns.getset;
pub const hgetall = fns.hgetall;
pub const hget = fns.hget;
pub const hset = fns.hset;
pub const hincrby = fns.hincrby;
pub const hincrbyfloat = fns.hincrbyfloat;
pub const hkeys = fns.hkeys;

View File

@@ -598,6 +598,105 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe
return promise.toJS();
}
pub fn hset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
const args_view = callframe.arguments();
if (args_view.len < 2) {
return globalObject.throwInvalidArguments("hset requires at least 3 arguments: key, field, value", .{});
}
// Pre-size based on expected argument count
const estimated_capacity: usize = if (args_view.len == 2) 64 else args_view.len;
var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), estimated_capacity);
defer {
for (args.items) |*item| {
item.deinit();
}
args.deinit();
}
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("hset", "key", "string or buffer");
};
args.appendAssumeCapacity(key);
// Check if second argument is an object (for object syntax)
if (args_view.len == 2) {
const obj_arg = callframe.argument(1);
// Reject arrays and null values, only accept plain objects
if (!obj_arg.isObject() or obj_arg.isNull() or obj_arg.isArray()) {
return globalObject.throwInvalidArgumentType("hset", "fields", "object or field-value pairs");
}
// Iterate over object properties
const obj_ref = obj_arg.asObjectRef() orelse unreachable;
var iter = try jsc.JSPropertyIterator(.{
.skip_empty_name = false,
.include_value = true,
}).init(globalObject, @as(*jsc.JSObject, @ptrCast(obj_ref)));
defer iter.deinit();
var count: usize = 0;
while (try iter.next()) |prop_name| {
// Convert property name to JSValue for conversion
const name_js = prop_name.toJS(globalObject);
const field = (try fromJS(globalObject, name_js)) orelse {
return globalObject.throwInvalidArgumentType("hset", "field name", "string or buffer");
};
try args.append(field);
// iter.value contains the property value when include_value is true
const value = (try fromJS(globalObject, iter.value)) orelse {
return globalObject.throwInvalidArgumentType("hset", "field value", "string or buffer");
};
try args.append(value);
count += 1;
}
if (count == 0) {
return globalObject.throwInvalidArguments("hset requires at least one field-value pair in the object", .{});
}
} else {
// Original syntax: key, field1, value1, field2, value2, ...
if (args_view.len < 3) {
return globalObject.throwInvalidArguments("hset requires at least 3 arguments: key, field, value", .{});
}
if ((args_view.len - 1) % 2 != 0) {
return globalObject.throwInvalidArguments("hset requires field-value pairs after the key", .{});
}
var i: usize = 1;
while (i < args_view.len) : (i += 2) {
const field = (try fromJS(globalObject, args_view[i])) orelse {
return globalObject.throwInvalidArgumentType("hset", "field", "string or buffer");
};
try args.append(field);
// No bounds check needed - we verified parity above
const value = (try fromJS(globalObject, args_view[i + 1])) orelse {
return globalObject.throwInvalidArgumentType("hset", "value", "string or buffer");
};
try args.append(value);
}
}
const promise = this.send(
globalObject,
callframe.this(),
&.{
.command = "HSET",
.args = .{ .args = args.items },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send HSET command", err);
};
return promise.toJS();
}
// Implement ping (send a PING command with an optional message)
pub fn ping(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
var message_buf: [1]JSArgument = undefined;

View File

@@ -5,6 +5,7 @@
* without always needing to run `bun install` in development.
*/
import * as numeric from "_util/numeric.ts";
import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun";
import { heapStats } from "bun:jsc";
import { beforeAll, describe, expect } from "bun:test";
@@ -13,7 +14,6 @@ import { readdir, readFile, readlink, rm, writeFile } from "fs/promises";
import fs, { closeSync, openSync, rmSync } from "node:fs";
import os from "node:os";
import { dirname, isAbsolute, join } from "path";
import * as numeric from "_util/numeric.ts";
type Awaitable<T> = T | Promise<T>;
@@ -31,9 +31,9 @@ export const libcFamily: "glibc" | "musl" =
process.platform !== "linux"
? "glibc"
: // process.report.getReport() has incorrect type definitions.
(process.report.getReport() as { header: { glibcVersionRuntime: boolean } }).header.glibcVersionRuntime
? "glibc"
: "musl";
(process.report.getReport() as { header: { glibcVersionRuntime: boolean } }).header.glibcVersionRuntime
? "glibc"
: "musl";
export const isMusl = isLinux && libcFamily === "musl";
export const isGlibc = isLinux && libcFamily === "glibc";

View File

@@ -0,0 +1,151 @@
import { RedisClient } from "bun";
import { describe, expect, test } from "bun:test";
describe("HSET error handling without server", () => {
test("hset validates arguments at runtime", async () => {
const client = new RedisClient({ socket: { host: "localhost", port: 6379 } });
let thrown;
try {
// @ts-expect-error
await client.hset();
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires at least 2 arguments");
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key");
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires at least 2 arguments");
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", "field");
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("object or field-value pairs");
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", "field1", "value1", "field2");
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires field-value pairs");
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", "f1", "v1", "f2", "v2", "f3");
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires field-value pairs");
// Numbers are coerced to strings by Redis, which is valid behavior
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", "field", null);
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/value.*string or buffer/i);
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", "field1", "value1", undefined, "value2");
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/field.*string or buffer/i);
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", { field: "value" }, "value");
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/field.*string or buffer/i);
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", "field", { nested: "object" });
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/value.*string or buffer/i);
});
test("hset with object syntax - invalid values", async () => {
const client = new RedisClient({ socket: { host: "localhost", port: 6379 } });
let thrown;
try {
// @ts-expect-error
await client.hset("key", { field: null });
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/value.*string or buffer/i);
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", { field: undefined });
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/value.*string or buffer/i);
thrown = undefined;
try {
// @ts-expect-error
await client.hset("key", null);
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/object or field-value pairs/i);
thrown = undefined;
try {
await client.hset("key", {});
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("at least one field-value pair");
});
test("hset method signature is correct", () => {
const client = new RedisClient({ socket: { host: "localhost", port: 6379 } });
expect(typeof client.hset).toBe("function");
expect(client.hset.length).toBe(3);
expect(client.hset.name).toBe("hset");
});
});

View File

@@ -1,6 +1,6 @@
import { RedisClient, type SpawnOptions } from "bun";
import { RedisClient } from "bun";
import { afterAll, beforeAll, expect } from "bun:test";
import { bunEnv, dockerExe, isCI, randomPort, tempDirWithFiles } from "harness";
import { bunEnv, dockerExe, randomPort, tempDirWithFiles } from "harness";
import path from "path";
import * as dockerCompose from "../../docker/index.ts";
@@ -220,9 +220,6 @@ export function testKey(name: string): string {
return `${context.id}:${TEST_KEY_PREFIX}${name}`;
}
// Import needed functions from Bun
import { tmpdir } from "os";
/**
* Create a new client with specific connection type
*/
@@ -490,13 +487,28 @@ if (!isEnabled) {
console.warn("Redis is not enabled, skipping tests");
}
type TypeofString<T> = T extends string
? "string"
: T extends number
? "number"
: T extends bigint
? "bigint"
: T extends boolean
? "boolean"
: T extends symbol
? "symbol"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: T extends {}
? "object"
: never;
/**
* Verify that a value is of a specific type
*/
export function expectType<T>(
value: any,
expectedType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function",
): asserts value is T {
export function expectType<T>(value: T, expectedType: TypeofString<T>): asserts value is T {
expect(value).toBeTypeOf(expectedType);
}

View File

@@ -229,7 +229,7 @@ describe.skipIf(!isEnabled)("Valkey: Basic String Operations", () => {
// INCR should increment and return new value
const incremented = await ctx.redis.incr(key);
expectType<number>(incremented, "number");
expectType(incremented, "number");
expect(incremented).toBe(11);
// DECR should decrement and return new value

View File

@@ -36,6 +36,400 @@ describe.skipIf(!isEnabled)("Valkey: Hash Data Type Operations", () => {
expect(nonExistentField).toBeNull();
});
test("HSET native method", async () => {
const key = ctx.generateKey("hset-native-test");
const setResult = await ctx.redis.hset(key, "username", "johndoe");
expectType<number>(setResult, "number");
expect(setResult).toBe(1);
const updateResult = await ctx.redis.hset(key, "username", "janedoe");
expectType<number>(updateResult, "number");
expect(updateResult).toBe(0);
const getValue = await ctx.redis.hget(key, "username");
expect(getValue).toBe("janedoe");
const multiSetResult = await ctx.redis.hset(key, "email", "jane@example.com", "age", "25");
expectType<number>(multiSetResult, "number");
expect(multiSetResult).toBe(2);
const allFields = await ctx.redis.hgetall(key);
expect(allFields).toEqual({
username: "janedoe",
email: "jane@example.com",
age: "25",
});
const bufferKey = ctx.generateKey("hset-buffer-test");
const bufferValue = Buffer.from("binary data");
const bufferSetResult = await ctx.redis.hset(bufferKey, "data", bufferValue);
expect(bufferSetResult).toBe(1);
const retrievedBuffer = await ctx.redis.hget(bufferKey, "data");
expect(retrievedBuffer).toBe("binary data");
});
test("HSET with object syntax", async () => {
const key = ctx.generateKey("hset-object-test");
// Test with simple object
const result = await ctx.redis.hset(key, {
name: "Alice",
age: "25",
email: "alice@example.com",
});
expectType<number>(result, "number");
expect(result).toBe(3);
// Verify fields were set
const name = await ctx.redis.hget(key, "name");
expect(name).toBe("Alice");
const age = await ctx.redis.hget(key, "age");
expect(age).toBe("25");
// Test updating with object (some new, some existing)
const updateResult = await ctx.redis.hset(key, {
name: "Alice Smith", // Update existing
city: "New York", // Add new
country: "USA", // Add new
});
expect(updateResult).toBe(2); // Only 2 new fields added
// Verify all fields
const allFields = await ctx.redis.hgetall(key);
expect(allFields).toEqual({
name: "Alice Smith",
age: "25",
email: "alice@example.com",
city: "New York",
country: "USA",
});
// Test with large object
const largeKey = ctx.generateKey("hset-large-object");
const largeObject: Record<string, string> = {};
for (let i = 0; i < 100; i++) {
largeObject[`field_${i}`] = `value_${i}`;
}
const largeResult = await ctx.redis.hset(largeKey, largeObject);
expect(largeResult).toBe(100);
const size = await ctx.redis.hlen(largeKey);
expect(size).toBe(100);
// Test with buffer values in object
const bufferKey = ctx.generateKey("hset-object-buffer");
const bufferResult = await ctx.redis.hset(bufferKey, {
text: "plain text",
binary: Buffer.from("binary data"),
number: 42, // Numbers should be coerced to strings
});
expect(bufferResult).toBe(3);
const retrievedBinary = await ctx.redis.hget(bufferKey, "binary");
expect(retrievedBinary).toBe("binary data");
const retrievedNumber = await ctx.redis.hget(bufferKey, "number");
expect(retrievedNumber).toBe("42");
// Test empty object should throw
let thrown;
try {
await ctx.redis.hset(key, {});
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("at least one field-value pair");
});
test("HSET with 8 field-value pairs", async () => {
const key = ctx.generateKey("hset-8-pairs-test");
const result = await ctx.redis.hset(
key,
"field1",
"value1",
"field2",
"value2",
"field3",
"value3",
"field4",
"value4",
"field5",
"value5",
"field6",
"value6",
"field7",
"value7",
"field8",
"value8",
);
expectType<number>(result, "number");
expect(result).toBe(8);
const allFields = await ctx.redis.hgetall(key);
expect(allFields).toEqual({
field1: "value1",
field2: "value2",
field3: "value3",
field4: "value4",
field5: "value5",
field6: "value6",
field7: "value7",
field8: "value8",
});
});
test("HSET stress test with 1000 field-value pairs", async () => {
const key = ctx.generateKey("hset-stress-test");
const args = [key];
const expectedObject: Record<string, string> = {};
for (let i = 0; i < 1000; i++) {
const field = `field_${i}`;
const value = `value_${i}_${Math.random().toString(36).substring(2, 15)}`;
args.push(field, value);
expectedObject[field] = value;
}
const result = await ctx.redis.hset(...args);
expectType<number>(result, "number");
expect(result).toBe(1000);
const size = await ctx.redis.hlen(key);
expect(size).toBe(1000);
const value500 = await ctx.redis.hget(key, "field_500");
expect(value500).toBe(expectedObject.field_500);
const value999 = await ctx.redis.hget(key, "field_999");
expect(value999).toBe(expectedObject.field_999);
const allFields = await ctx.redis.hgetall(key);
expect(Object.keys(allFields).length).toBe(1000);
expect(allFields.field_0).toBe(expectedObject.field_0);
expect(allFields.field_999).toBe(expectedObject.field_999);
});
test("hset with object syntax (single field)", async () => {
const key = ctx.generateKey("hset-obj-single");
const result = await ctx.redis.hset(key, { field1: "value1" });
expect(result).toBe(1);
const value = await ctx.redis.hget(key, "field1");
expect(value).toBe("value1");
});
test("hset with object syntax (multiple fields)", async () => {
const key = ctx.generateKey("hset-obj-multiple");
const result = await ctx.redis.hset(key, {
field1: "value1",
field2: "value2",
field3: "value3",
});
expect(result).toBe(3);
const values = await ctx.redis.hmget(key, ["field1", "field2", "field3"]);
expect(values).toEqual(["value1", "value2", "value3"]);
});
test("hset with object syntax (numbers as values)", async () => {
const key = ctx.generateKey("hset-obj-numbers");
const result = await ctx.redis.hset(key, {
count: 42,
price: "19.99",
stock: 100,
});
expect(result).toBe(3);
const values = await ctx.redis.hmget(key, ["count", "price", "stock"]);
expect(values).toEqual(["42", "19.99", "100"]);
});
test("hset with object syntax (Buffer values)", async () => {
const key = ctx.generateKey("hset-obj-buffer");
const result = await ctx.redis.hset(key, {
binary: Buffer.from("binary data"),
text: "plain text",
});
expect(result).toBe(2);
const values = await ctx.redis.hmget(key, ["binary", "text"]);
expect(values[0].toString()).toBe("binary data");
expect(values[1]).toBe("plain text");
});
test("hset with object syntax (empty object)", async () => {
const key = ctx.generateKey("hset-obj-empty");
let thrown;
try {
await ctx.redis.hset(key, {});
} catch (error: any) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("at least one field-value pair");
});
test("hset with object syntax (mixing updates and additions)", async () => {
const key = ctx.generateKey("hset-obj-mixed");
// First add some initial fields
const result1 = await ctx.redis.hset(key, {
existing1: "old1",
existing2: "old2",
});
expect(result1).toBe(2);
// Now mix updates with new fields
const result2 = await ctx.redis.hset(key, {
existing1: "updated1", // Update
existing2: "updated2", // Update
new1: "value1", // Add
new2: "value2", // Add
});
// HSET returns number of fields added, not updated
expect(result2).toBe(2);
// Verify all values
const values = await ctx.redis.hmget(key, ["existing1", "existing2", "new1", "new2"]);
expect(values).toEqual(["updated1", "updated2", "value1", "value2"]);
});
test("hset with object syntax (stress test 100 fields)", async () => {
const key = ctx.generateKey("hset-obj-stress");
const fields: Record<string, string> = {};
for (let i = 0; i < 100; i++) {
fields[`field${i}`] = `value${i}`;
}
const result = await ctx.redis.hset(key, fields);
expect(result).toBe(100);
const value50 = await ctx.redis.hget(key, "field50");
expect(value50).toBe("value50");
});
(process.env.RUN_EXTREME_TESTS ? test : test.skip)(
"HSET extreme stress test with 10000 field-value pairs",
async () => {
const key = ctx.generateKey("hset-extreme-stress-test");
const fieldCount = 10000;
// Build arguments programmatically for better readability
const args: any[] = [key];
for (let i = 0; i < fieldCount; i++) {
args.push(`f${i}`, `v${i}`);
}
const result = await ctx.redis.hset(...args);
expectType<number>(result, "number");
expect(result).toBe(fieldCount);
const size = await ctx.redis.hlen(key);
expect(size).toBe(fieldCount);
// Test specific known field values for determinism
const testIndices = [0, Math.floor(fieldCount / 2), fieldCount - 1];
for (const index of testIndices) {
const value = await ctx.redis.hget(key, `f${index}`);
expect(value).toBe(`v${index}`);
}
},
);
test("HSET error handling", async () => {
const key = ctx.generateKey("hset-error-test");
let thrown;
try {
// @ts-expect-error
await ctx.redis.hset(key);
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires at least 3 arguments");
thrown = undefined;
try {
// @ts-expect-error
await ctx.redis.hset(key, "field1");
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("object or field-value pairs");
thrown = undefined;
try {
// @ts-expect-error
await ctx.redis.hset(key, "field1", "value1", "field2");
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires field-value pairs");
// Test with many arguments but missing the last value (100 total = key + 99 args)
thrown = undefined;
try {
const args = [key];
for (let i = 0; i < 49; i++) {
args.push(`field${i}`, `value${i}`);
}
args.push("field49"); // Missing value for this field
// @ts-expect-error
await ctx.redis.hset(...args);
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires field-value pairs");
// Test with 1001 arguments (key + 1000 args = 500 pairs missing last value)
thrown = undefined;
try {
const args = [key];
for (let i = 0; i < 500; i++) {
args.push(`f${i}`, `v${i}`);
}
args.push("f500"); // Missing value for this field
// @ts-expect-error
await ctx.redis.hset(...args);
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toContain("hset requires field-value pairs");
// Numbers are coerced to strings by Redis, which is valid behavior
thrown = undefined;
try {
// @ts-expect-error
await ctx.redis.hset(key, "field", null);
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/value.*string or buffer/i);
thrown = undefined;
try {
// @ts-expect-error
await ctx.redis.hset(key, "field1", "value1", undefined, "value2");
} catch (error) {
thrown = error;
}
expect(thrown).toBeDefined();
expect(thrown.message).toMatch(/field.*string or buffer/i);
});
test("HGET native method", async () => {
const key = ctx.generateKey("hget-native-test");