mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 08:58:52 +00:00
Compare commits
15 Commits
ciro/case-
...
claude/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
720967be9c | ||
|
|
55bb15e793 | ||
|
|
83ee713c14 | ||
|
|
c4c2f0fb3a | ||
|
|
fdf1ee9dae | ||
|
|
d1d0c382a5 | ||
|
|
3497d7c0e8 | ||
|
|
712a645d0f | ||
|
|
56f1e4f19c | ||
|
|
c245820479 | ||
|
|
8d129212e1 | ||
|
|
ffe100963d | ||
|
|
25af820727 | ||
|
|
a44b634a22 | ||
|
|
65c5ee3d94 |
32
packages/bun-types/redis.d.ts
vendored
32
packages/bun-types/redis.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -84,6 +84,10 @@ export default [
|
||||
fn: "hget",
|
||||
length: 2,
|
||||
},
|
||||
hset: {
|
||||
fn: "hset",
|
||||
length: 3,
|
||||
},
|
||||
hmget: {
|
||||
fn: "hmget",
|
||||
length: 2,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
151
test/js/valkey/hset-error-handling.test.ts
Normal file
151
test/js/valkey/hset-error-handling.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user